Skip to content

Commit cfb6abb

Browse files
authored
Merge pull request #1006 from oceanprotocol/feature/add_grants_swap
Feature/add grants swap
2 parents 18aee48 + dce752b commit cfb6abb

4 files changed

Lines changed: 950 additions & 2 deletions

File tree

addresses/address.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -311,7 +311,8 @@
311311
"AccessListFactory": "0x43eC0a34E1b70C7f8E579ab866F37642777727E7",
312312
"EnterpriseEscrow": "0x49E35cd2bAE043Abd9074B6e5a649a5AdEB05C33",
313313
"COMPY":"0x973e69303259B0c2543a38665122b773D28405fB",
314-
"COMPYFaucet":"0x3EFDD8f728c8e774aB81D14d0B2F07a8238960f4"
314+
"COMPYFaucet":"0x3EFDD8f728c8e774aB81D14d0B2F07a8238960f4",
315+
"COMPYSwap":"0x8B8E187CF9c551e63f54AA04E21F48CDAF2296aE"
315316
},
316317
"oasis_sapphire": {
317318
"chainId": 23294,
@@ -420,6 +421,7 @@
420421
"Escrow": "0xf0c7A31D7Ee26bEBfb4BAD8e37490bEadE3F846f",
421422
"AccessListFactory": "0xE5aa2C9B551aFcA4C0A98BB3B37D7A43084d0a66",
422423
"COMPY":"0x298f163244e0c8cc9316D6E97162e5792ac5d410",
423-
"COMPYFaucet":"0x23A8b2D7176485a6349e4830605F323f31019333"
424+
"COMPYFaucet":"0x23A8b2D7176485a6349e4830605F323f31019333",
425+
"COMPYSwap":"0xb65F19225fEBb650Fcc211dC9F18FEC6f4a328D5"
424426
}
425427
}

contracts/grants/GrantsSwap.sol

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
pragma solidity 0.8.12;
2+
// Copyright Ocean Protocol contributors
3+
// SPDX-License-Identifier: Apache-2.0
4+
5+
import '../interfaces/IERC20.sol';
6+
import '../utils/SafeERC20.sol';
7+
import '@openzeppelin/contracts/security/ReentrancyGuard.sol';
8+
import '@openzeppelin/contracts/token/ERC20/extensions/IERC20Permit.sol';
9+
import '@openzeppelin/contracts/access/Ownable.sol';
10+
11+
/**
12+
* @title GrantsSwap
13+
* @dev Contract that allows swapping input tokens for COMPY tokens at a 1:1 ratio.
14+
* Users can swap the input token for COMPY (one-way swap only).
15+
* The swap maintains a 1:1 ratio in token units (accounting for decimals).
16+
*/
17+
contract GrantsSwap is ReentrancyGuard, Ownable {
18+
using SafeERC20 for IERC20;
19+
20+
// The COMPY token address
21+
IERC20 public immutable compyToken;
22+
23+
// The input token address (the token that can be swapped with COMPY)
24+
IERC20 public immutable inputToken;
25+
26+
// Decimals for COMPY token (6)
27+
uint8 private immutable compyDecimals;
28+
29+
// Decimals for input token
30+
uint8 private immutable inputDecimals;
31+
32+
// Events
33+
event Swap(
34+
address indexed user,
35+
uint256 inputTokenAmount,
36+
uint256 compyAmount
37+
);
38+
39+
event Withdraw(
40+
address indexed token,
41+
address indexed to,
42+
uint256 amount
43+
);
44+
45+
/**
46+
* @dev Constructor for GrantsSwap
47+
* @param _compyToken Address of the COMPY token
48+
* @param _inputToken Address of the input token that can be swapped with COMPY
49+
*/
50+
constructor(address _compyToken, address _inputToken) {
51+
require(_compyToken != address(0), "GrantsSwap: COMPY token cannot be zero address");
52+
require(_inputToken != address(0), "GrantsSwap: input token cannot be zero address");
53+
require(_compyToken != _inputToken, "GrantsSwap: tokens must be different");
54+
55+
compyToken = IERC20(_compyToken);
56+
inputToken = IERC20(_inputToken);
57+
58+
// Get decimals from tokens
59+
compyDecimals = IERC20(_compyToken).decimals();
60+
inputDecimals = IERC20(_inputToken).decimals();
61+
}
62+
63+
/**
64+
* @dev Swap input tokens for COMPY tokens at 1:1 ratio (accounting for decimals)
65+
* @param amount Amount of input tokens to swap (in input token's smallest unit)
66+
*/
67+
function swapToCOMPY(uint256 amount) external nonReentrant {
68+
require(amount > 0, "GrantsSwap: amount must be greater than zero");
69+
70+
// Calculate equivalent amount in COMPY's smallest unit (1:1 ratio)
71+
uint256 compyAmount = convertAmount(amount, inputDecimals, compyDecimals);
72+
73+
// Transfer input tokens from user to this contract
74+
inputToken.safeTransferFrom(msg.sender, address(this), amount);
75+
76+
// Transfer COMPY from this contract to user (1:1 ratio)
77+
compyToken.safeTransfer(msg.sender, compyAmount);
78+
79+
emit Swap(msg.sender, amount, compyAmount);
80+
}
81+
82+
/**
83+
* @dev Swap input tokens for COMPY tokens at 1:1 ratio using ERC20Permit (accounting for decimals)
84+
* This function allows users to swap without a separate approval transaction by using a permit signature.
85+
* @param amount Amount of input tokens to swap (in input token's smallest unit)
86+
* @param deadline The time at which the permit expires (unix timestamp)
87+
* @param v The recovery byte of the signature
88+
* @param r Half of the ECDSA signature pair
89+
* @param s Half of the ECDSA signature pair
90+
*/
91+
function swapToCOMPYwithPermit(
92+
uint256 amount,
93+
uint256 deadline,
94+
uint8 v,
95+
bytes32 r,
96+
bytes32 s
97+
) external nonReentrant {
98+
require(amount > 0, "GrantsSwap: amount must be greater than zero");
99+
100+
// Use permit to approve this contract to spend user's input tokens
101+
IERC20Permit(address(inputToken)).permit(
102+
msg.sender,
103+
address(this),
104+
amount,
105+
deadline,
106+
v,
107+
r,
108+
s
109+
);
110+
111+
// Calculate equivalent amount in COMPY's smallest unit (1:1 ratio)
112+
uint256 compyAmount = convertAmount(amount, inputDecimals, compyDecimals);
113+
114+
// Transfer input tokens from user to this contract
115+
inputToken.safeTransferFrom(msg.sender, address(this), amount);
116+
117+
// Transfer COMPY from this contract to user (1:1 ratio)
118+
compyToken.safeTransfer(msg.sender, compyAmount);
119+
120+
emit Swap(msg.sender, amount, compyAmount);
121+
}
122+
123+
/**
124+
* @dev Withdraw tokens from the contract (only owner)
125+
* @param token Address of the token to withdraw (address(0) for native ETH, but not supported in this contract)
126+
* @param to Address to send the tokens to
127+
* @param amount Amount of tokens to withdraw
128+
*/
129+
function withdrawTokens(
130+
address token,
131+
address to,
132+
uint256 amount
133+
) external onlyOwner {
134+
require(to != address(0), "GrantsSwap: cannot withdraw to zero address");
135+
require(amount > 0, "GrantsSwap: amount must be greater than zero");
136+
require(token != address(0), "GrantsSwap: token address cannot be zero");
137+
138+
IERC20(token).safeTransfer(to, amount);
139+
140+
emit Withdraw(token, to, amount);
141+
}
142+
143+
/**
144+
* @dev Convert amount from one token's decimals to another (for 1:1 token unit ratio)
145+
* @param amount Amount in source token's smallest unit
146+
* @param sourceDecimals Decimals of source token
147+
* @param targetDecimals Decimals of target token
148+
* @return Converted amount in target token's smallest unit
149+
*/
150+
function convertAmount(uint256 amount, uint8 sourceDecimals, uint8 targetDecimals) internal pure returns (uint256) {
151+
if (sourceDecimals == targetDecimals) {
152+
return amount;
153+
} else if (sourceDecimals < targetDecimals) {
154+
// Multiply by 10^(targetDecimals - sourceDecimals)
155+
return amount * (10 ** (targetDecimals - sourceDecimals));
156+
} else {
157+
// Divide by 10^(sourceDecimals - targetDecimals)
158+
return amount / (10 ** (sourceDecimals - targetDecimals));
159+
}
160+
}
161+
162+
/**
163+
* @dev Get the balance of COMPY tokens held by this contract
164+
* @return uint256 Balance of COMPY tokens
165+
*/
166+
function getCOMPYBalance() external view returns (uint256) {
167+
return compyToken.balanceOf(address(this));
168+
}
169+
170+
/**
171+
* @dev Get the balance of input tokens held by this contract
172+
* @return uint256 Balance of input tokens
173+
*/
174+
function getInputTokenBalance() external view returns (uint256) {
175+
return inputToken.balanceOf(address(this));
176+
}
177+
}

scripts/deploy_grants_swap.js

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
// We require the Hardhat Runtime Environment explicitly here. This is optional
2+
// but useful for running the script in a standalone fashion through `node <script>`.
3+
//
4+
// When running the script with `hardhat run <script>` you'll find the Hardhat
5+
// Runtime Environment's members available in the global scope.
6+
const hre = require("hardhat");
7+
const fs = require("fs");
8+
const { address } = require("../test/helpers/constants");
9+
const { Wallet } = require("ethers");
10+
const { UV_FS_O_FILEMAP } = require("constants");
11+
const ethers = hre.ethers;
12+
require("dotenv").config();
13+
const logging = true;
14+
const show_verify = true;
15+
async function main() {
16+
const url = process.env.NETWORK_RPC_URL;
17+
console.log("Using RPC: " + url);
18+
if (!url) {
19+
console.error("Missing NETWORK_RPC_URL. Aborting..");
20+
return null;
21+
}
22+
23+
const provider = new ethers.providers.JsonRpcProvider(url);
24+
const network = provider.getNetwork();
25+
// utils
26+
const networkDetails = await network;
27+
28+
let wallet;
29+
if (process.env.MNEMONIC)
30+
wallet = new Wallet.fromMnemonic(process.env.MNEMONIC);
31+
if (process.env.PRIVATE_KEY) wallet = new Wallet(process.env.PRIVATE_KEY);
32+
if (!wallet) {
33+
console.error("Missing MNEMONIC or PRIVATE_KEY. Aborting..");
34+
return null;
35+
}
36+
owner = wallet.connect(provider);
37+
let gasLimit = 3000000;
38+
let gasPrice = null;
39+
let sleepAmount = 10;
40+
let OPFOwner = null;
41+
let RouterAddress = null;
42+
let grantsTokenAddress = null;
43+
let compyAddress = null;
44+
const grantsOwner = "0x09b575B5eC7Fff24cbccC092DE9E36eADdDbEe71";
45+
switch (networkDetails.chainId) {
46+
case 11155111:
47+
networkName = "sepolia";
48+
gasPrice = ethers.utils.parseUnits("12", "gwei");
49+
gasLimit = 6000000;
50+
compyAddress = "0x973e69303259B0c2543a38665122b773D28405fB";
51+
usdcAddress = "0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238";
52+
break;
53+
case 8453:
54+
networkName = "base";
55+
gasPrice = ethers.utils.parseUnits("0.02", "gwei");
56+
gasLimit = 5000000;
57+
compyAddress = "0x298f163244e0c8cc9316D6E97162e5792ac5d410";
58+
usdcAddress = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
59+
break;
60+
}
61+
if (!compyAddress) {
62+
console.error("Invalid network. Aborting..");
63+
return null;
64+
}
65+
let options;
66+
if (gasPrice) {
67+
options = { gasLimit: gasLimit, gasPrice: gasPrice };
68+
} else {
69+
options = { gasLimit };
70+
}
71+
console.log("Deploying contracts with the account:", owner.address);
72+
console.log("Deployer nonce:", await owner.getTransactionCount());
73+
74+
if (logging) console.info("Deploying GrantsSwap");
75+
const GrantsTokenSwap = await ethers.getContractFactory("GrantsSwap", owner);
76+
const deployGrantsTokenSwap = await GrantsTokenSwap.connect(owner).deploy(
77+
compyAddress,
78+
usdcAddress,
79+
options
80+
);
81+
await deployGrantsTokenSwap.deployTransaction.wait(1);
82+
83+
if (logging) console.info("GrantsTokenSwap deployed at:", deployGrantsTokenSwap.address);
84+
85+
if (show_verify) {
86+
console.log("\tRun the following to verify on etherscan");
87+
console.log(
88+
"\tnpx hardhat verify --network " +
89+
networkName +
90+
" " +
91+
deployGrantsTokenSwap.address +
92+
" " +
93+
compyAddress +
94+
" " +
95+
usdcAddress
96+
);
97+
}
98+
// Transfer ownership if GRANTS_OWNER is set
99+
if (grantsOwner) {
100+
if (logging) console.info("Transferring ownership to:", grantsOwner);
101+
const transferTx = await deployGrantsTokenSwap.transferOwnership(grantsOwner, options);
102+
await transferTx.wait(1);
103+
if (logging) console.info("Tokens transferred successfully");
104+
} else {
105+
if (logging) console.warn("GRANTS_OWNER not set. Ownership remains with deployer:", owner.address);
106+
}
107+
}
108+
109+
// We recommend this pattern to be able to use async/await everywhere
110+
// and properly handle errors.
111+
main()
112+
.then(() => process.exit(0))
113+
.catch((error) => {
114+
console.error(error);
115+
process.exit(1);
116+
});

0 commit comments

Comments
 (0)