├── .env.example ├── .eslintignore ├── .eslintrc.js ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .npmignore ├── .prettierignore ├── .prettierrc ├── .solhint.json ├── .solhintignore ├── README.md ├── contracts ├── OffsetHelper.sol ├── OffsetHelperStorage.sol ├── interfaces │ ├── IRetirementCertificates.sol │ ├── IToucanCarbonOffsets.sol │ ├── IToucanContractRegistry.sol │ └── IToucanPoolToken.sol ├── test │ └── Swapper.sol └── types │ ├── CarbonProjectTypes.sol │ └── CarbonProjectVintageTypes.sol ├── docs ├── OffsetHelper.md └── OffsetHelperStorage.md ├── git-workflow.md ├── hardhat.config.ts ├── package.json ├── test └── OffsetHelper.test.ts ├── tsconfig.json ├── utils ├── ABIs.ts ├── addresses.ts ├── getTotalTCO2sHeld.ts ├── impersonateAccount.ts ├── tokens.ts └── toucanContracts.json └── yarn.lock /.env.example: -------------------------------------------------------------------------------- 1 | POLYGON_URL=https://matic-mainnet.chainstacklabs.com 2 | PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 3 | POLYGONSCAN_KEY=abc123abc123abc123abc123abc123abc123abc 4 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | artifacts 3 | cache 4 | coverage 5 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: false, 4 | es2021: true, 5 | mocha: true, 6 | node: true, 7 | }, 8 | plugins: ["@typescript-eslint"], 9 | extends: [ 10 | "standard", 11 | "plugin:prettier/recommended", 12 | "plugin:node/recommended", 13 | ], 14 | parser: "@typescript-eslint/parser", 15 | parserOptions: { 16 | ecmaVersion: 12, 17 | }, 18 | rules: { 19 | "node/no-unsupported-features/es-syntax": [ 20 | "error", 21 | { ignores: ["modules"] }, 22 | ], 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Smart contracts 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | strategy: 14 | matrix: 15 | node-version: [16.x] 16 | 17 | steps: 18 | - name: Checkout code 19 | uses: actions/checkout@v2 20 | 21 | - name: Use Node.js ${{ matrix.node-version }} 22 | uses: actions/setup-node@v2 23 | with: 24 | node-version: ${{ matrix.node-version }} 25 | 26 | - name: Get yarn cache directory path 27 | id: yarn-cache-dir-path 28 | run: echo "::set-output name=dir::$(yarn cache dir)" 29 | 30 | - name: Check yarn cache 31 | uses: actions/cache@v2 32 | id: cache-yarn-cache 33 | with: 34 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }} 35 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 36 | restore-keys: | 37 | ${{ runner.os }}-yarn- 38 | 39 | - name: Check node_modules cache 40 | id: cache-node-modules 41 | uses: actions/cache@v2 42 | with: 43 | path: | 44 | node_modules 45 | */*/node_modules 46 | key: ${{ runner.os }}-${{ matrix.node-version }}-nodemodules-${{ hashFiles('**/yarn.lock') }} 47 | restore-keys: | 48 | ${{ runner.os }}-${{ matrix.node-version }}-nodemodules- 49 | 50 | - name: Install packages 51 | run: yarn 52 | 53 | - name: Prepare environment 54 | run: cp .env.example .env 55 | 56 | - name: Build contract 57 | run: yarn compile 58 | 59 | - name: Test contract 60 | run: yarn test 61 | env: 62 | POLYGON_URL: ${{ secrets.POLYGON_URL }} 63 | 64 | - name: Test solidity-docgen builds 65 | run: yarn doc 66 | 67 | - name: Deploy contract 68 | run: yarn hardhat --network hardhat deployOffsetHelper --verify false 69 | env: 70 | POLYGON_URL: ${{ secrets.POLYGON_URL }} 71 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | coverage 4 | coverage.json 5 | typechain 6 | 7 | #Hardhat files 8 | cache 9 | /artifacts 10 | 11 | #IDE files 12 | .idea -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | hardhat.config.ts 2 | scripts 3 | test 4 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | artifacts 3 | cache 4 | coverage* 5 | gasReporterOutput.json 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /.solhint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "solhint:recommended", 3 | "rules": { 4 | "compiler-version": ["error", "^0.8.0"], 5 | "func-visibility": ["warn", { "ignoreConstructors": true }] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.solhintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Example Implementations 2 | 3 | A hardhat repo that is a collection of Solidity contract **examples** that integrate with or demonstrate the use of Toucan's contracts and infrastructure. Some of these may be used in production and we'll provide links to their prod repos below. 4 | 5 | ## OffsetHelper 6 | 7 | The `OffsetHelper` contract implements helper functions that simplify the carbon offsetting (retirement) process. It's production version (with actualized related documentation) which is actively maintained can be found [here](https://github.com/ToucanProtocol/OffsetHelper). You can still use the version found in this repo as an example for inspiration purposes. 8 | 9 | ### Development 10 | 11 | ## Preqrequisites 12 | 13 | 1. Install the required packages: 14 | ``` 15 | yarn 16 | ``` 17 | 2. Copy `.env.example` to `.env` and modify values of the required environment variables: 18 | 1. `POLYGON_URL`/`MUMBAI_URL` to specify custom RPC endpoints for Polygon Mainnet, respectively, the Mumbai Testnet. 19 | 2. `PRIVATE_KEY` and `POLYGONSCAN_KEY` in order to deploy contract and publish source code on [polygonscan](https://polygonscan.com). 20 | 21 | ## Commands 22 | 23 | Use the following commands to compile, test and deploy the contracts: 24 | 25 | ``` 26 | yarn compile 27 | yarn test # test using a polygon fork 28 | yarn coverage # test using a polygon fork with coverage report 29 | yarn deploy 30 | ``` 31 | 32 | Documentation can be auto-generated from the contract's [natspec](https://docs.soliditylang.org/en/latest/natspec-format.html) in [./docs/](./docs/) using 33 | 34 | ``` 35 | yarn doc 36 | ``` 37 | 38 | Deploy the contract locally with: 39 | 40 | ``` 41 | yarn hardhat --network hardhat deployOffsetHelper --verify false 42 | ``` 43 | -------------------------------------------------------------------------------- /contracts/OffsetHelper.sol: -------------------------------------------------------------------------------- 1 | //SPDX-License-Identifier: Unlicense 2 | pragma solidity ^0.8.0; 3 | 4 | import "hardhat/console.sol"; 5 | import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; 6 | import "@uniswap/v2-periphery/contracts/interfaces/IUniswapV2Router02.sol"; 7 | import "./OffsetHelperStorage.sol"; 8 | import "./interfaces/IToucanPoolToken.sol"; 9 | import "./interfaces/IToucanCarbonOffsets.sol"; 10 | import "./interfaces/IToucanContractRegistry.sol"; 11 | import "hardhat/console.sol"; 12 | 13 | /** 14 | * @title Toucan Protocol Offset Helpers 15 | * @notice Helper functions that simplify the carbon offsetting (retirement) 16 | * process. 17 | * 18 | * Retiring carbon tokens requires multiple steps and interactions with 19 | * Toucan Protocol's main contracts: 20 | * 1. Obtain a Toucan pool token such as BCT or NCT (by performing a token 21 | * swap). 22 | * 2. Redeem the pool token for a TCO2 token. 23 | * 3. Retire the TCO2 token. 24 | * 25 | * These steps are combined in each of the following "auto offset" methods 26 | * implemented in `OffsetHelper` to allow a retirement within one transaction: 27 | * - `autoOffsetUsingPoolToken()` if the user already owns a Toucan pool 28 | * token such as BCT or NCT, 29 | * - `autoOffsetUsingETH()` if the user would like to perform a retirement 30 | * using MATIC, 31 | * - `autoOffsetUsingToken()` if the user would like to perform a retirement 32 | * using an ERC20 token: USDC, WETH or WMATIC. 33 | * 34 | * In these methods, "auto" refers to the fact that these methods use 35 | * `autoRedeem()` in order to automatically choose a TCO2 token corresponding 36 | * to the oldest tokenized carbon project in the specfified token pool. 37 | * There are no fees incurred by the user when using `autoRedeem()`, i.e., the 38 | * user receives 1 TCO2 token for each pool token (BCT/NCT) redeemed. 39 | * 40 | * There are two `view` helper functions `calculateNeededETHAmount()` and 41 | * `calculateNeededTokenAmount()` that should be called before using 42 | * `autoOffsetUsingETH()` and `autoOffsetUsingToken()`, to determine how much 43 | * MATIC, respectively how much of the ERC20 token must be sent to the 44 | * `OffsetHelper` contract in order to retire the specified amount of carbon. 45 | */ 46 | contract OffsetHelper is OffsetHelperStorage { 47 | using SafeERC20 for IERC20; 48 | 49 | /** 50 | * @notice Contract constructor. Should specify arrays of ERC20 symbols and 51 | * addresses that can used by the contract. 52 | * 53 | * @dev See `isEligible()` for a list of tokens that can be used in the 54 | * contract. These can be modified after deployment by the contract owner 55 | * using `setEligibleTokenAddress()` and `deleteEligibleTokenAddress()`. 56 | * 57 | * @param _eligibleTokenSymbols A list of token symbols. 58 | * @param _eligibleTokenAddresses A list of token addresses corresponding 59 | * to the provided token symbols. 60 | */ 61 | constructor( 62 | string[] memory _eligibleTokenSymbols, 63 | address[] memory _eligibleTokenAddresses 64 | ) { 65 | uint256 i = 0; 66 | uint256 eligibleTokenSymbolsLen = _eligibleTokenSymbols.length; 67 | while (i < eligibleTokenSymbolsLen) { 68 | eligibleTokenAddresses[ 69 | _eligibleTokenSymbols[i] 70 | ] = _eligibleTokenAddresses[i]; 71 | i += 1; 72 | } 73 | } 74 | 75 | /** 76 | * @notice Emitted upon successful redemption of TCO2 tokens from a Toucan 77 | * pool token such as BCT or NCT. 78 | * 79 | * @param who The sender of the transaction 80 | * @param poolToken The address of the Toucan pool token used in the 81 | * redemption, for example, NCT or BCT 82 | * @param tco2s An array of the TCO2 addresses that were redeemed 83 | * @param amounts An array of the amounts of each TCO2 that were redeemed 84 | */ 85 | event Redeemed( 86 | address who, 87 | address poolToken, 88 | address[] tco2s, 89 | uint256[] amounts 90 | ); 91 | 92 | /** 93 | * @notice Retire carbon credits using the lowest quality (oldest) TCO2 94 | * tokens available from the specified Toucan token pool by sending ERC20 95 | * tokens (USDC, WETH, WMATIC). Use `calculateNeededTokenAmount` first in 96 | * order to find out how much of the ERC20 token is required to retire the 97 | * specified quantity of TCO2. 98 | * 99 | * This function: 100 | * 1. Swaps the ERC20 token sent to the contract for the specified pool token. 101 | * 2. Redeems the pool token for the poorest quality TCO2 tokens available. 102 | * 3. Retires the TCO2 tokens. 103 | * 104 | * Note: The client must approve the ERC20 token that is sent to the contract. 105 | * 106 | * @dev When automatically redeeming pool tokens for the lowest quality 107 | * TCO2s there are no fees and you receive exactly 1 TCO2 token for 1 pool 108 | * token. 109 | * 110 | * @param _depositedToken The address of the ERC20 token that the user sends 111 | * (must be one of USDC, WETH, WMATIC) 112 | * @param _poolToken The address of the Toucan pool token that the 113 | * user wants to use, for example, NCT or BCT 114 | * @param _amountToOffset The amount of TCO2 to offset 115 | * 116 | * @return tco2s An array of the TCO2 addresses that were redeemed 117 | * @return amounts An array of the amounts of each TCO2 that were redeemed 118 | */ 119 | function autoOffsetUsingToken( 120 | address _depositedToken, 121 | address _poolToken, 122 | uint256 _amountToOffset 123 | ) public returns (address[] memory tco2s, uint256[] memory amounts) { 124 | // swap input token for BCT / NCT 125 | swap(_depositedToken, _poolToken, _amountToOffset); 126 | 127 | // redeem BCT / NCT for TCO2s 128 | (tco2s, amounts) = autoRedeem(_poolToken, _amountToOffset); 129 | 130 | // retire the TCO2s to achieve offset 131 | autoRetire(tco2s, amounts); 132 | } 133 | 134 | /** 135 | * @notice Retire carbon credits using the lowest quality (oldest) TCO2 136 | * tokens available from the specified Toucan token pool by sending MATIC. 137 | * Use `calculateNeededETHAmount()` first in order to find out how much 138 | * MATIC is required to retire the specified quantity of TCO2. 139 | * 140 | * This function: 141 | * 1. Swaps the Matic sent to the contract for the specified pool token. 142 | * 2. Redeems the pool token for the poorest quality TCO2 tokens available. 143 | * 3. Retires the TCO2 tokens. 144 | * 145 | * @dev If the user sends much MATIC, the leftover amount will be sent back 146 | * to the user. 147 | * 148 | * @param _poolToken The address of the Toucan pool token that the 149 | * user wants to use, for example, NCT or BCT. 150 | * @param _amountToOffset The amount of TCO2 to offset. 151 | * 152 | * @return tco2s An array of the TCO2 addresses that were redeemed 153 | * @return amounts An array of the amounts of each TCO2 that were redeemed 154 | */ 155 | function autoOffsetUsingETH(address _poolToken, uint256 _amountToOffset) 156 | public 157 | payable 158 | returns (address[] memory tco2s, uint256[] memory amounts) 159 | { 160 | // swap MATIC for BCT / NCT 161 | swap(_poolToken, _amountToOffset); 162 | 163 | // redeem BCT / NCT for TCO2s 164 | (tco2s, amounts) = autoRedeem(_poolToken, _amountToOffset); 165 | 166 | // retire the TCO2s to achieve offset 167 | autoRetire(tco2s, amounts); 168 | } 169 | 170 | /** 171 | * @notice Retire carbon credits using the lowest quality (oldest) TCO2 172 | * tokens available by sending Toucan pool tokens, for example, BCT or NCT. 173 | * 174 | * This function: 175 | * 1. Redeems the pool token for the poorest quality TCO2 tokens available. 176 | * 2. Retires the TCO2 tokens. 177 | * 178 | * Note: The client must approve the pool token that is sent. 179 | * 180 | * @param _poolToken The address of the Toucan pool token that the 181 | * user wants to use, for example, NCT or BCT. 182 | * @param _amountToOffset The amount of TCO2 to offset. 183 | * 184 | * @return tco2s An array of the TCO2 addresses that were redeemed 185 | * @return amounts An array of the amounts of each TCO2 that were redeemed 186 | */ 187 | function autoOffsetUsingPoolToken( 188 | address _poolToken, 189 | uint256 _amountToOffset 190 | ) public returns (address[] memory tco2s, uint256[] memory amounts) { 191 | // deposit pool token from user to this contract 192 | deposit(_poolToken, _amountToOffset); 193 | 194 | // redeem BCT / NCT for TCO2s 195 | (tco2s, amounts) = autoRedeem(_poolToken, _amountToOffset); 196 | 197 | // retire the TCO2s to achieve offset 198 | autoRetire(tco2s, amounts); 199 | } 200 | 201 | /** 202 | * @notice Checks whether an address can be used by the contract. 203 | * @param _erc20Address address of the ERC20 token to be checked 204 | * @return True if the address can be used by the contract 205 | */ 206 | function isEligible(address _erc20Address) private view returns (bool) { 207 | bool isToucanContract = IToucanContractRegistry(contractRegistryAddress) 208 | .checkERC20(_erc20Address); 209 | if (isToucanContract) return true; 210 | if (_erc20Address == eligibleTokenAddresses["BCT"]) return true; 211 | if (_erc20Address == eligibleTokenAddresses["NCT"]) return true; 212 | if (_erc20Address == eligibleTokenAddresses["USDC"]) return true; 213 | if (_erc20Address == eligibleTokenAddresses["WETH"]) return true; 214 | if (_erc20Address == eligibleTokenAddresses["WMATIC"]) return true; 215 | return false; 216 | } 217 | 218 | /** 219 | * @notice Checks whether an address can be used in a token swap 220 | * @param _erc20Address address of token to be checked 221 | * @return True if the specified address can be used in a swap 222 | */ 223 | function isSwapable(address _erc20Address) private view returns (bool) { 224 | if (_erc20Address == eligibleTokenAddresses["USDC"]) return true; 225 | if (_erc20Address == eligibleTokenAddresses["WETH"]) return true; 226 | if (_erc20Address == eligibleTokenAddresses["WMATIC"]) return true; 227 | return false; 228 | } 229 | 230 | /** 231 | * @notice Checks whether an address is a Toucan pool token address 232 | * @param _erc20Address address of token to be checked 233 | * @return True if the address is a Toucan pool token address 234 | */ 235 | function isRedeemable(address _erc20Address) private view returns (bool) { 236 | if (_erc20Address == eligibleTokenAddresses["BCT"]) return true; 237 | if (_erc20Address == eligibleTokenAddresses["NCT"]) return true; 238 | return false; 239 | } 240 | 241 | /** 242 | * @notice Return how much of the specified ERC20 token is required in 243 | * order to swap for the desired amount of a Toucan pool token, for 244 | * example, BCT or NCT. 245 | * 246 | * @param _fromToken The address of the ERC20 token used for the swap 247 | * @param _toToken The address of the pool token to swap for, 248 | * for example, NCT or BCT 249 | * @param _amount The desired amount of pool token to receive 250 | * @return amountsIn The amount of the ERC20 token required in order to 251 | * swap for the specified amount of the pool token 252 | */ 253 | function calculateNeededTokenAmount( 254 | address _fromToken, 255 | address _toToken, 256 | uint256 _amount 257 | ) public view returns (uint256) { 258 | // check tokens 259 | require( 260 | isSwapable(_fromToken) && isRedeemable(_toToken), 261 | "Token not eligible" 262 | ); 263 | 264 | // instantiate router 265 | IUniswapV2Router02 routerSushi = IUniswapV2Router02(sushiRouterAddress); 266 | 267 | // generate path 268 | address[] memory path = generatePath(_fromToken, _toToken); 269 | 270 | // get expected amountsIn 271 | uint256[] memory amountsIn = routerSushi.getAmountsIn(_amount, path); 272 | return amountsIn[0]; 273 | } 274 | 275 | /** 276 | * @notice Swap eligible ERC20 tokens for Toucan pool tokens (BCT/NCT) on SushiSwap 277 | * @dev Needs to be approved on the client side 278 | * @param _fromToken The ERC20 oken to deposit and swap 279 | * @param _toToken The token to swap for (will be held within contract) 280 | * @param _amount The required amount of the Toucan pool token (NCT/BCT) 281 | */ 282 | function swap( 283 | address _fromToken, 284 | address _toToken, 285 | uint256 _amount 286 | ) public { 287 | // check tokens 288 | require( 289 | isSwapable(_fromToken) && isRedeemable(_toToken), 290 | "Token not eligible" 291 | ); 292 | 293 | // instantiate router 294 | IUniswapV2Router02 routerSushi = IUniswapV2Router02(sushiRouterAddress); 295 | 296 | // generate path 297 | address[] memory path = generatePath(_fromToken, _toToken); 298 | 299 | // estimate amountsIn 300 | uint256[] memory expectedAmountsIn = routerSushi.getAmountsIn( 301 | _amount, 302 | path 303 | ); 304 | 305 | // transfer tokens 306 | IERC20(_fromToken).safeTransferFrom( 307 | msg.sender, 308 | address(this), 309 | expectedAmountsIn[0] 310 | ); 311 | 312 | // approve router 313 | IERC20(_fromToken).approve(sushiRouterAddress, expectedAmountsIn[0]); 314 | 315 | // swap 316 | routerSushi.swapTokensForExactTokens( 317 | _amount, 318 | expectedAmountsIn[0], 319 | path, 320 | address(this), 321 | block.timestamp 322 | ); 323 | 324 | // update balances 325 | balances[msg.sender][_toToken] += _amount; 326 | } 327 | 328 | // apparently I need a fallback and a receive method to fix the situation where transfering dust MATIC 329 | // in the MATIC to token swap fails 330 | fallback() external payable {} 331 | 332 | receive() external payable {} 333 | 334 | /** 335 | * @notice Return how much MATIC is required in order to swap for the 336 | * desired amount of a Toucan pool token, for example, BCT or NCT. 337 | * 338 | * @param _toToken The address of the pool token to swap for, for 339 | * example, NCT or BCT 340 | * @param _amount The desired amount of pool token to receive 341 | * @return amounts The amount of MATIC required in order to swap for 342 | * the specified amount of the pool token 343 | */ 344 | function calculateNeededETHAmount(address _toToken, uint256 _amount) 345 | public 346 | view 347 | returns (uint256) 348 | { 349 | // check token 350 | require(isRedeemable(_toToken), "Token not eligible"); 351 | 352 | // instantiate router 353 | IUniswapV2Router02 routerSushi = IUniswapV2Router02(sushiRouterAddress); 354 | 355 | // generate path 356 | address[] memory path = generatePath( 357 | eligibleTokenAddresses["WMATIC"], 358 | _toToken 359 | ); 360 | 361 | // get expectedAmountsIn 362 | uint256[] memory amounts = routerSushi.getAmountsIn(_amount, path); 363 | return amounts[0]; 364 | } 365 | 366 | /** 367 | * @notice Swap MATIC for Toucan pool tokens (BCT/NCT) on SushiSwap 368 | * @param _toToken Token to swap for (will be held within contract) 369 | * @param _amount Amount of NCT / BCT wanted 370 | */ 371 | function swap(address _toToken, uint256 _amount) public payable { 372 | // check tokens 373 | require(isRedeemable(_toToken), "Token not eligible"); 374 | 375 | // instantiate router 376 | IUniswapV2Router02 routerSushi = IUniswapV2Router02(sushiRouterAddress); 377 | 378 | // generate path 379 | address[] memory path = generatePath( 380 | eligibleTokenAddresses["WMATIC"], 381 | _toToken 382 | ); 383 | 384 | // estimate amountsIn 385 | uint256[] memory expectedAmountsIn = routerSushi.getAmountsIn( 386 | _amount, 387 | path 388 | ); 389 | 390 | // check user sent enough ETH/MATIC 391 | require(msg.value >= expectedAmountsIn[0], "Didn't send enough MATIC"); 392 | 393 | // swap 394 | uint256[] memory amountsIn = routerSushi.swapETHForExactTokens{ 395 | value: msg.value 396 | }(_amount, path, address(this), block.timestamp); 397 | 398 | // send surplus back 399 | if (msg.value > amountsIn[0]) { 400 | uint256 leftoverETH = msg.value - amountsIn[0]; 401 | (bool success, ) = msg.sender.call{value: leftoverETH}( 402 | new bytes(0) 403 | ); 404 | 405 | require(success, "Failed to send surplus back"); 406 | } 407 | 408 | // update balances 409 | balances[msg.sender][_toToken] += _amount; 410 | } 411 | 412 | /** 413 | * @notice Allow users to withdraw tokens they have deposited. 414 | */ 415 | function withdraw(address _erc20Addr, uint256 _amount) public { 416 | require( 417 | balances[msg.sender][_erc20Addr] >= _amount, 418 | "Insufficient balance" 419 | ); 420 | 421 | IERC20(_erc20Addr).safeTransfer(msg.sender, _amount); 422 | balances[msg.sender][_erc20Addr] -= _amount; 423 | } 424 | 425 | /** 426 | * @notice Allow users to deposit BCT / NCT. 427 | * @dev Needs to be approved 428 | */ 429 | function deposit(address _erc20Addr, uint256 _amount) public { 430 | require(isRedeemable(_erc20Addr), "Token not eligible"); 431 | 432 | IERC20(_erc20Addr).safeTransferFrom(msg.sender, address(this), _amount); 433 | balances[msg.sender][_erc20Addr] += _amount; 434 | } 435 | 436 | /** 437 | * @notice Redeems the specified amount of NCT / BCT for TCO2. 438 | * @dev Needs to be approved on the client side 439 | * @param _fromToken Could be the address of NCT or BCT 440 | * @param _amount Amount to redeem 441 | * @return tco2s An array of the TCO2 addresses that were redeemed 442 | * @return amounts An array of the amounts of each TCO2 that were redeemed 443 | */ 444 | function autoRedeem(address _fromToken, uint256 _amount) 445 | public 446 | returns (address[] memory tco2s, uint256[] memory amounts) 447 | { 448 | require(isRedeemable(_fromToken), "Token not eligible"); 449 | 450 | require( 451 | balances[msg.sender][_fromToken] >= _amount, 452 | "Insufficient NCT/BCT balance" 453 | ); 454 | 455 | // instantiate pool token (NCT or BCT) 456 | IToucanPoolToken PoolTokenImplementation = IToucanPoolToken(_fromToken); 457 | 458 | // auto redeem pool token for TCO2; will transfer automatically picked TCO2 to this contract 459 | (tco2s, amounts) = PoolTokenImplementation.redeemAuto2(_amount); 460 | 461 | // update balances 462 | balances[msg.sender][_fromToken] -= _amount; 463 | uint256 tco2sLen = tco2s.length; 464 | for (uint256 index = 0; index < tco2sLen; index++) { 465 | balances[msg.sender][tco2s[index]] += amounts[index]; 466 | } 467 | 468 | emit Redeemed(msg.sender, _fromToken, tco2s, amounts); 469 | } 470 | 471 | /** 472 | * @notice Retire the specified TCO2 tokens. 473 | * @param _tco2s The addresses of the TCO2s to retire 474 | * @param _amounts The amounts to retire from each of the corresponding 475 | * TCO2 addresses 476 | */ 477 | function autoRetire(address[] memory _tco2s, uint256[] memory _amounts) 478 | public 479 | { 480 | uint256 tco2sLen = _tco2s.length; 481 | require(tco2sLen != 0, "Array empty"); 482 | 483 | require(tco2sLen == _amounts.length, "Arrays unequal"); 484 | 485 | uint256 i = 0; 486 | while (i < tco2sLen) { 487 | require( 488 | balances[msg.sender][_tco2s[i]] >= _amounts[i], 489 | "Insufficient TCO2 balance" 490 | ); 491 | 492 | balances[msg.sender][_tco2s[i]] -= _amounts[i]; 493 | 494 | IToucanCarbonOffsets(_tco2s[i]).retire(_amounts[i]); 495 | 496 | unchecked { 497 | ++i; 498 | } 499 | } 500 | } 501 | 502 | function generatePath(address _fromToken, address _toToken) 503 | internal 504 | view 505 | returns (address[] memory) 506 | { 507 | if (_fromToken == eligibleTokenAddresses["USDC"]) { 508 | address[] memory path = new address[](2); 509 | path[0] = _fromToken; 510 | path[1] = _toToken; 511 | return path; 512 | } else { 513 | address[] memory path = new address[](3); 514 | path[0] = _fromToken; 515 | path[1] = eligibleTokenAddresses["USDC"]; 516 | path[2] = _toToken; 517 | return path; 518 | } 519 | } 520 | 521 | // ---------------------------------------- 522 | // Admin methods 523 | // ---------------------------------------- 524 | 525 | /** 526 | * @notice Change or add eligible tokens and their addresses. 527 | * @param _tokenSymbol The symbol of the token to add 528 | * @param _address The address of the token to add 529 | */ 530 | function setEligibleTokenAddress( 531 | string memory _tokenSymbol, 532 | address _address 533 | ) public virtual onlyOwner { 534 | eligibleTokenAddresses[_tokenSymbol] = _address; 535 | } 536 | 537 | /** 538 | * @notice Delete eligible tokens stored in the contract. 539 | * @param _tokenSymbol The symbol of the token to remove 540 | */ 541 | function deleteEligibleTokenAddress(string memory _tokenSymbol) 542 | public 543 | virtual 544 | onlyOwner 545 | { 546 | delete eligibleTokenAddresses[_tokenSymbol]; 547 | } 548 | 549 | /** 550 | * @notice Change the TCO2 contracts registry. 551 | * @param _address The address of the Toucan contract registry to use 552 | */ 553 | function setToucanContractRegistry(address _address) 554 | public 555 | virtual 556 | onlyOwner 557 | { 558 | contractRegistryAddress = _address; 559 | } 560 | } 561 | -------------------------------------------------------------------------------- /contracts/OffsetHelperStorage.sol: -------------------------------------------------------------------------------- 1 | //SPDX-License-Identifier: Unlicense 2 | pragma solidity ^0.8.0; 3 | 4 | import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; 5 | 6 | contract OffsetHelperStorage is OwnableUpgradeable { 7 | // token symbol => token address 8 | mapping(string => address) public eligibleTokenAddresses; 9 | address public contractRegistryAddress = 10 | 0x263fA1c180889b3a3f46330F32a4a23287E99FC9; 11 | address public sushiRouterAddress = 12 | 0x1b02dA8Cb0d097eB8D57A175b88c7D8b47997506; 13 | // user => (token => amount) 14 | mapping(address => mapping(address => uint256)) public balances; 15 | } 16 | -------------------------------------------------------------------------------- /contracts/interfaces/IRetirementCertificates.sol: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 Toucan Labs 2 | // 3 | // SPDX-License-Identifier: UNLICENSED 4 | 5 | // If you encounter a vulnerability or an issue, please contact or visit security.toucan.earth 6 | pragma solidity ^0.8.0; 7 | 8 | import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; 9 | import "@openzeppelin/contracts-upgradeable/token/ERC721/IERC721Upgradeable.sol"; 10 | import "@openzeppelin/contracts-upgradeable/utils/AddressUpgradeable.sol"; 11 | import "@openzeppelin/contracts-upgradeable/utils/StringsUpgradeable.sol"; 12 | import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; 13 | 14 | import "./IToucanContractRegistry.sol"; 15 | 16 | interface RetirementCertificates is IERC721Upgradeable { 17 | /// @dev Returns the Uniform Resource Identifier (URI) for `tokenId` token. 18 | function tokenURI(uint256 tokenId) external returns (string memory); 19 | 20 | /// @notice Update retirementMessage, beneficiary, and beneficiaryString of a NFT 21 | /// within 24h of creation. Empty values are ignored, ie., will not overwrite the 22 | /// existing stored values in the NFT. 23 | /// @param tokenId The id of the NFT to update 24 | /// @param beneficiary The new beneficiary to set in the NFT 25 | /// @param beneficiaryString The new beneficiaryString to set in the NFT 26 | /// @param retirementMessage The new retirementMessage to set in the NFT 27 | /// @dev The function can only be called by a the NFT owner 28 | function updateCertificate( 29 | uint256 tokenId, 30 | address beneficiary, 31 | string calldata beneficiaryString, 32 | string calldata retirementMessage 33 | ) external; 34 | } 35 | -------------------------------------------------------------------------------- /contracts/interfaces/IToucanCarbonOffsets.sol: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 Toucan Labs 2 | // 3 | // SPDX-License-Identifier: UNLICENSED 4 | 5 | // If you encounter a vulnerability or an issue, please contact or visit security.toucan.earth 6 | pragma solidity ^0.8.0; 7 | 8 | import "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; 9 | import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol"; 10 | 11 | import "../types/CarbonProjectTypes.sol"; 12 | import "../types/CarbonProjectVintageTypes.sol"; 13 | 14 | interface IToucanCarbonOffsets is IERC20Upgradeable, IERC721Receiver { 15 | function getGlobalProjectVintageIdentifiers() 16 | external 17 | view 18 | returns (string memory, string memory); 19 | 20 | function getAttributes() 21 | external 22 | view 23 | returns (ProjectData memory, VintageData memory); 24 | 25 | function getRemaining() external view returns (uint256 remaining); 26 | 27 | function getDepositCap() external view returns (uint256); 28 | 29 | function retire(uint256 amount) external; 30 | 31 | function retireFrom(address account, uint256 amount) external; 32 | 33 | function mintCertificateLegacy( 34 | string calldata retiringEntityString, 35 | address beneficiary, 36 | string calldata beneficiaryString, 37 | string calldata retirementMessage, 38 | uint256 amount 39 | ) external; 40 | 41 | function retireAndMintCertificate( 42 | string calldata retiringEntityString, 43 | address beneficiary, 44 | string calldata beneficiaryString, 45 | string calldata retirementMessage, 46 | uint256 amount 47 | ) external; 48 | } 49 | -------------------------------------------------------------------------------- /contracts/interfaces/IToucanContractRegistry.sol: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 Toucan Labs 2 | // 3 | // SPDX-License-Identifier: UNLICENSED 4 | 5 | // If you encounter a vulnerability or an issue, please contact or visit security.toucan.earth 6 | pragma solidity ^0.8.0; 7 | 8 | interface IToucanContractRegistry { 9 | function carbonOffsetBatchesAddress() external view returns (address); 10 | 11 | function carbonProjectsAddress() external view returns (address); 12 | 13 | function carbonProjectVintagesAddress() external view returns (address); 14 | 15 | function toucanCarbonOffsetsFactoryAddress() 16 | external 17 | view 18 | returns (address); 19 | 20 | function carbonOffsetBadgesAddress() external view returns (address); 21 | 22 | function checkERC20(address _address) external view returns (bool); 23 | } 24 | -------------------------------------------------------------------------------- /contracts/interfaces/IToucanPoolToken.sol: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 Toucan Labs 2 | // 3 | // SPDX-License-Identifier: UNLICENSED 4 | 5 | // If you encounter a vulnerability or an issue, please contact or visit security.toucan.earth 6 | pragma solidity ^0.8.0; 7 | 8 | import "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; 9 | 10 | interface IToucanPoolToken is IERC20Upgradeable { 11 | function deposit(address erc20Addr, uint256 amount) external; 12 | 13 | function checkEligible(address erc20Addr) external view returns (bool); 14 | 15 | function checkAttributeMatching(address erc20Addr) 16 | external 17 | view 18 | returns (bool); 19 | 20 | function calculateRedeemFees( 21 | address[] memory tco2s, 22 | uint256[] memory amounts 23 | ) external view returns (uint256); 24 | 25 | function redeemMany(address[] memory tco2s, uint256[] memory amounts) 26 | external; 27 | 28 | function redeemAuto(uint256 amount) external; 29 | 30 | function redeemAuto2(uint256 amount) 31 | external 32 | returns (address[] memory tco2s, uint256[] memory amounts); 33 | 34 | function getRemaining() external view returns (uint256); 35 | 36 | function getScoredTCO2s() external view returns (address[] memory); 37 | } 38 | -------------------------------------------------------------------------------- /contracts/test/Swapper.sol: -------------------------------------------------------------------------------- 1 | //SPDX-License-Identifier: Unlicense 2 | pragma solidity ^0.8.0; 3 | 4 | import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; 5 | import "@uniswap/v2-periphery/contracts/interfaces/IUniswapV2Router02.sol"; 6 | 7 | contract Swapper { 8 | using SafeERC20 for IERC20; 9 | 10 | address public sushiRouterAddress = 11 | 0x1b02dA8Cb0d097eB8D57A175b88c7D8b47997506; 12 | mapping(string => address) public tokenAddresses; 13 | 14 | constructor(string[] memory _tokenSymbols, address[] memory _tokenAddresses) 15 | { 16 | uint256 i = 0; 17 | while (i < _tokenSymbols.length) { 18 | tokenAddresses[_tokenSymbols[i]] = _tokenAddresses[i]; 19 | i += 1; 20 | } 21 | } 22 | 23 | function calculateNeededETHAmount(address _toToken, uint256 _amount) 24 | public 25 | view 26 | returns (uint256) 27 | { 28 | IUniswapV2Router02 routerSushi = IUniswapV2Router02(sushiRouterAddress); 29 | 30 | address[] memory path = generatePath( 31 | tokenAddresses["WMATIC"], 32 | _toToken 33 | ); 34 | 35 | uint256[] memory amounts = routerSushi.getAmountsIn(_amount, path); 36 | return amounts[0]; 37 | } 38 | 39 | function swap(address _toToken, uint256 _amount) public payable { 40 | IUniswapV2Router02 routerSushi = IUniswapV2Router02(sushiRouterAddress); 41 | 42 | address[] memory path = generatePath( 43 | tokenAddresses["WMATIC"], 44 | _toToken 45 | ); 46 | 47 | uint256[] memory amounts = routerSushi.swapETHForExactTokens{ 48 | value: msg.value 49 | }(_amount, path, address(this), block.timestamp); 50 | 51 | IERC20(_toToken).transfer(msg.sender, _amount); 52 | 53 | if (msg.value > amounts[0]) { 54 | uint256 leftoverETH = msg.value - amounts[0]; 55 | (bool success, ) = msg.sender.call{value: leftoverETH}( 56 | new bytes(0) 57 | ); 58 | 59 | require(success, "Failed to send surplus ETH back to user."); 60 | } 61 | } 62 | 63 | function generatePath(address _fromToken, address _toToken) 64 | internal 65 | view 66 | returns (address[] memory) 67 | { 68 | if (_toToken == tokenAddresses["USDC"]) { 69 | address[] memory path = new address[](2); 70 | path[0] = _fromToken; 71 | path[1] = _toToken; 72 | return path; 73 | } else { 74 | address[] memory path = new address[](3); 75 | path[0] = _fromToken; 76 | path[1] = tokenAddresses["USDC"]; 77 | path[2] = _toToken; 78 | return path; 79 | } 80 | } 81 | 82 | fallback() external payable {} 83 | 84 | receive() external payable {} 85 | } 86 | -------------------------------------------------------------------------------- /contracts/types/CarbonProjectTypes.sol: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 Toucan Labs 2 | // 3 | // SPDX-License-Identifier: UNLICENSED 4 | 5 | // If you encounter a vulnerability or an issue, please contact or visit security.toucan.earth 6 | 7 | pragma solidity >=0.8.4 <0.9.0; 8 | 9 | /// @dev CarbonProject related data and attributes 10 | struct ProjectData { 11 | string projectId; 12 | string standard; 13 | string methodology; 14 | string region; 15 | string storageMethod; 16 | string method; 17 | string emissionType; 18 | string category; 19 | string uri; 20 | address controller; 21 | } 22 | -------------------------------------------------------------------------------- /contracts/types/CarbonProjectVintageTypes.sol: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 Toucan Labs 2 | // 3 | // SPDX-License-Identifier: UNLICENSED 4 | 5 | // If you encounter a vulnerability or an issue, please contact or visit security.toucan.earth 6 | 7 | pragma solidity >=0.8.4 <0.9.0; 8 | 9 | struct VintageData { 10 | /// @dev A human-readable string which differentiates this from other vintages in 11 | /// the same project, and helps build the corresponding TCO2 name and symbol. 12 | string name; 13 | uint64 startTime; // UNIX timestamp 14 | uint64 endTime; // UNIX timestamp 15 | uint256 projectTokenId; 16 | uint64 totalVintageQuantity; 17 | bool isCorsiaCompliant; 18 | bool isCCPcompliant; 19 | string coBenefits; 20 | string correspAdjustment; 21 | string additionalCertification; 22 | string uri; 23 | } 24 | -------------------------------------------------------------------------------- /docs/OffsetHelper.md: -------------------------------------------------------------------------------- 1 | # Solidity API 2 | 3 | ## OffsetHelper 4 | 5 | Helper functions that simplify the carbon offsetting (retirement) 6 | process. 7 | 8 | Retiring carbon tokens requires multiple steps and interactions with 9 | Toucan Protocol's main contracts: 10 | 1. Obtain a Toucan pool token such as BCT or NCT (by performing a token 11 | swap). 12 | 2. Redeem the pool token for a TCO2 token. 13 | 3. Retire the TCO2 token. 14 | 15 | These steps are combined in each of the following "auto offset" methods 16 | implemented in `OffsetHelper` to allow a retirement within one transaction: 17 | - `autoOffsetUsingPoolToken()` if the user already owns a Toucan pool 18 | token such as BCT or NCT, 19 | - `autoOffsetUsingETH()` if the user would like to perform a retirement 20 | using MATIC, 21 | - `autoOffsetUsingToken()` if the user would like to perform a retirement 22 | using an ERC20 token: USDC, WETH or WMATIC. 23 | 24 | In these methods, "auto" refers to the fact that these methods use 25 | `autoRedeem()` in order to automatically choose a TCO2 token corresponding 26 | to the oldest tokenized carbon project in the specfified token pool. 27 | There are no fees incurred by the user when using `autoRedeem()`, i.e., the 28 | user receives 1 TCO2 token for each pool token (BCT/NCT) redeemed. 29 | 30 | There are two `view` helper functions `calculateNeededETHAmount()` and 31 | `calculateNeededTokenAmount()` that should be called before using 32 | `autoOffsetUsingETH()` and `autoOffsetUsingToken()`, to determine how much 33 | MATIC, respectively how much of the ERC20 token must be sent to the 34 | `OffsetHelper` contract in order to retire the specified amount of carbon. 35 | 36 | ### constructor 37 | 38 | ```solidity 39 | constructor(string[] _eligibleTokenSymbols, address[] _eligibleTokenAddresses) public 40 | ``` 41 | 42 | Contract constructor. Should specify arrays of ERC20 symbols and 43 | addresses that can used by the contract. 44 | 45 | _See `isEligible()` for a list of tokens that can be used in the 46 | contract. These can be modified after deployment by the contract owner 47 | using `setEligibleTokenAddress()` and `deleteEligibleTokenAddress()`._ 48 | 49 | | Name | Type | Description | 50 | | ---- | ---- | ----------- | 51 | | _eligibleTokenSymbols | string[] | A list of token symbols. | 52 | | _eligibleTokenAddresses | address[] | A list of token addresses corresponding to the provided token symbols. | 53 | 54 | ### Redeemed 55 | 56 | ```solidity 57 | event Redeemed(address who, address poolToken, address[] tco2s, uint256[] amounts) 58 | ``` 59 | 60 | Emitted upon successful redemption of TCO2 tokens from a Toucan 61 | pool token such as BCT or NCT. 62 | 63 | | Name | Type | Description | 64 | | ---- | ---- | ----------- | 65 | | who | address | The sender of the transaction | 66 | | poolToken | address | The address of the Toucan pool token used in the redemption, for example, NCT or BCT | 67 | | tco2s | address[] | An array of the TCO2 addresses that were redeemed | 68 | | amounts | uint256[] | An array of the amounts of each TCO2 that were redeemed | 69 | 70 | ### autoOffsetUsingToken 71 | 72 | ```solidity 73 | function autoOffsetUsingToken(address _depositedToken, address _poolToken, uint256 _amountToOffset) public returns (address[] tco2s, uint256[] amounts) 74 | ``` 75 | 76 | Retire carbon credits using the lowest quality (oldest) TCO2 77 | tokens available from the specified Toucan token pool by sending ERC20 78 | tokens (USDC, WETH, WMATIC). Use `calculateNeededTokenAmount` first in 79 | order to find out how much of the ERC20 token is required to retire the 80 | specified quantity of TCO2. 81 | 82 | This function: 83 | 1. Swaps the ERC20 token sent to the contract for the specified pool token. 84 | 2. Redeems the pool token for the poorest quality TCO2 tokens available. 85 | 3. Retires the TCO2 tokens. 86 | 87 | Note: The client must approve the ERC20 token that is sent to the contract. 88 | 89 | _When automatically redeeming pool tokens for the lowest quality 90 | TCO2s there are no fees and you receive exactly 1 TCO2 token for 1 pool 91 | token._ 92 | 93 | | Name | Type | Description | 94 | | ---- | ---- | ----------- | 95 | | _depositedToken | address | The address of the ERC20 token that the user sends (must be one of USDC, WETH, WMATIC) | 96 | | _poolToken | address | The address of the Toucan pool token that the user wants to use, for example, NCT or BCT | 97 | | _amountToOffset | uint256 | The amount of TCO2 to offset | 98 | 99 | | Name | Type | Description | 100 | | ---- | ---- | ----------- | 101 | | tco2s | address[] | An array of the TCO2 addresses that were redeemed | 102 | | amounts | uint256[] | An array of the amounts of each TCO2 that were redeemed | 103 | 104 | ### autoOffsetUsingETH 105 | 106 | ```solidity 107 | function autoOffsetUsingETH(address _poolToken, uint256 _amountToOffset) public payable returns (address[] tco2s, uint256[] amounts) 108 | ``` 109 | 110 | Retire carbon credits using the lowest quality (oldest) TCO2 111 | tokens available from the specified Toucan token pool by sending MATIC. 112 | Use `calculateNeededETHAmount()` first in order to find out how much 113 | MATIC is required to retire the specified quantity of TCO2. 114 | 115 | This function: 116 | 1. Swaps the Matic sent to the contract for the specified pool token. 117 | 2. Redeems the pool token for the poorest quality TCO2 tokens available. 118 | 3. Retires the TCO2 tokens. 119 | 120 | _If the user sends much MATIC, the leftover amount will be sent back 121 | to the user._ 122 | 123 | | Name | Type | Description | 124 | | ---- | ---- | ----------- | 125 | | _poolToken | address | The address of the Toucan pool token that the user wants to use, for example, NCT or BCT. | 126 | | _amountToOffset | uint256 | The amount of TCO2 to offset. | 127 | 128 | | Name | Type | Description | 129 | | ---- | ---- | ----------- | 130 | | tco2s | address[] | An array of the TCO2 addresses that were redeemed | 131 | | amounts | uint256[] | An array of the amounts of each TCO2 that were redeemed | 132 | 133 | ### autoOffsetUsingPoolToken 134 | 135 | ```solidity 136 | function autoOffsetUsingPoolToken(address _poolToken, uint256 _amountToOffset) public returns (address[] tco2s, uint256[] amounts) 137 | ``` 138 | 139 | Retire carbon credits using the lowest quality (oldest) TCO2 140 | tokens available by sending Toucan pool tokens, for example, BCT or NCT. 141 | 142 | This function: 143 | 1. Redeems the pool token for the poorest quality TCO2 tokens available. 144 | 2. Retires the TCO2 tokens. 145 | 146 | Note: The client must approve the pool token that is sent. 147 | 148 | | Name | Type | Description | 149 | | ---- | ---- | ----------- | 150 | | _poolToken | address | The address of the Toucan pool token that the user wants to use, for example, NCT or BCT. | 151 | | _amountToOffset | uint256 | The amount of TCO2 to offset. | 152 | 153 | | Name | Type | Description | 154 | | ---- | ---- | ----------- | 155 | | tco2s | address[] | An array of the TCO2 addresses that were redeemed | 156 | | amounts | uint256[] | An array of the amounts of each TCO2 that were redeemed | 157 | 158 | ### isEligible 159 | 160 | ```solidity 161 | function isEligible(address _erc20Address) private view returns (bool) 162 | ``` 163 | 164 | Checks whether an address can be used by the contract. 165 | 166 | | Name | Type | Description | 167 | | ---- | ---- | ----------- | 168 | | _erc20Address | address | address of the ERC20 token to be checked | 169 | 170 | | Name | Type | Description | 171 | | ---- | ---- | ----------- | 172 | | [0] | bool | True if the address can be used by the contract | 173 | 174 | ### isSwapable 175 | 176 | ```solidity 177 | function isSwapable(address _erc20Address) private view returns (bool) 178 | ``` 179 | 180 | Checks whether an address can be used in a token swap 181 | 182 | | Name | Type | Description | 183 | | ---- | ---- | ----------- | 184 | | _erc20Address | address | address of token to be checked | 185 | 186 | | Name | Type | Description | 187 | | ---- | ---- | ----------- | 188 | | [0] | bool | True if the specified address can be used in a swap | 189 | 190 | ### isRedeemable 191 | 192 | ```solidity 193 | function isRedeemable(address _erc20Address) private view returns (bool) 194 | ``` 195 | 196 | Checks whether an address is a Toucan pool token address 197 | 198 | | Name | Type | Description | 199 | | ---- | ---- | ----------- | 200 | | _erc20Address | address | address of token to be checked | 201 | 202 | | Name | Type | Description | 203 | | ---- | ---- | ----------- | 204 | | [0] | bool | True if the address is a Toucan pool token address | 205 | 206 | ### calculateNeededTokenAmount 207 | 208 | ```solidity 209 | function calculateNeededTokenAmount(address _fromToken, address _toToken, uint256 _amount) public view returns (uint256) 210 | ``` 211 | 212 | Return how much of the specified ERC20 token is required in 213 | order to swap for the desired amount of a Toucan pool token, for 214 | example, BCT or NCT. 215 | 216 | | Name | Type | Description | 217 | | ---- | ---- | ----------- | 218 | | _fromToken | address | The address of the ERC20 token used for the swap | 219 | | _toToken | address | The address of the pool token to swap for, for example, NCT or BCT | 220 | | _amount | uint256 | The desired amount of pool token to receive | 221 | 222 | | Name | Type | Description | 223 | | ---- | ---- | ----------- | 224 | | [0] | uint256 | amountsIn The amount of the ERC20 token required in order to swap for the specified amount of the pool token | 225 | 226 | ### swap 227 | 228 | ```solidity 229 | function swap(address _fromToken, address _toToken, uint256 _amount) public 230 | ``` 231 | 232 | Swap eligible ERC20 tokens for Toucan pool tokens (BCT/NCT) on SushiSwap 233 | 234 | _Needs to be approved on the client side_ 235 | 236 | | Name | Type | Description | 237 | | ---- | ---- | ----------- | 238 | | _fromToken | address | The ERC20 oken to deposit and swap | 239 | | _toToken | address | The token to swap for (will be held within contract) | 240 | | _amount | uint256 | The required amount of the Toucan pool token (NCT/BCT) | 241 | 242 | ### fallback 243 | 244 | ```solidity 245 | fallback() external payable 246 | ``` 247 | 248 | ### receive 249 | 250 | ```solidity 251 | receive() external payable 252 | ``` 253 | 254 | ### calculateNeededETHAmount 255 | 256 | ```solidity 257 | function calculateNeededETHAmount(address _toToken, uint256 _amount) public view returns (uint256) 258 | ``` 259 | 260 | Return how much MATIC is required in order to swap for the 261 | desired amount of a Toucan pool token, for example, BCT or NCT. 262 | 263 | | Name | Type | Description | 264 | | ---- | ---- | ----------- | 265 | | _toToken | address | The address of the pool token to swap for, for example, NCT or BCT | 266 | | _amount | uint256 | The desired amount of pool token to receive | 267 | 268 | | Name | Type | Description | 269 | | ---- | ---- | ----------- | 270 | | [0] | uint256 | amounts The amount of MATIC required in order to swap for the specified amount of the pool token | 271 | 272 | ### swap 273 | 274 | ```solidity 275 | function swap(address _toToken, uint256 _amount) public payable 276 | ``` 277 | 278 | Swap MATIC for Toucan pool tokens (BCT/NCT) on SushiSwap 279 | 280 | | Name | Type | Description | 281 | | ---- | ---- | ----------- | 282 | | _toToken | address | Token to swap for (will be held within contract) | 283 | | _amount | uint256 | Amount of NCT / BCT wanted | 284 | 285 | ### withdraw 286 | 287 | ```solidity 288 | function withdraw(address _erc20Addr, uint256 _amount) public 289 | ``` 290 | 291 | Allow users to withdraw tokens they have deposited. 292 | 293 | ### deposit 294 | 295 | ```solidity 296 | function deposit(address _erc20Addr, uint256 _amount) public 297 | ``` 298 | 299 | Allow users to deposit BCT / NCT. 300 | 301 | _Needs to be approved_ 302 | 303 | ### autoRedeem 304 | 305 | ```solidity 306 | function autoRedeem(address _fromToken, uint256 _amount) public returns (address[] tco2s, uint256[] amounts) 307 | ``` 308 | 309 | Redeems the specified amount of NCT / BCT for TCO2. 310 | 311 | _Needs to be approved on the client side_ 312 | 313 | | Name | Type | Description | 314 | | ---- | ---- | ----------- | 315 | | _fromToken | address | Could be the address of NCT or BCT | 316 | | _amount | uint256 | Amount to redeem | 317 | 318 | | Name | Type | Description | 319 | | ---- | ---- | ----------- | 320 | | tco2s | address[] | An array of the TCO2 addresses that were redeemed | 321 | | amounts | uint256[] | An array of the amounts of each TCO2 that were redeemed | 322 | 323 | ### autoRetire 324 | 325 | ```solidity 326 | function autoRetire(address[] _tco2s, uint256[] _amounts) public 327 | ``` 328 | 329 | Retire the specified TCO2 tokens. 330 | 331 | | Name | Type | Description | 332 | | ---- | ---- | ----------- | 333 | | _tco2s | address[] | The addresses of the TCO2s to retire | 334 | | _amounts | uint256[] | The amounts to retire from each of the corresponding TCO2 addresses | 335 | 336 | ### setEligibleTokenAddress 337 | 338 | ```solidity 339 | function setEligibleTokenAddress(string _tokenSymbol, address _address) public virtual 340 | ``` 341 | 342 | Change or add eligible tokens and their addresses. 343 | 344 | | Name | Type | Description | 345 | | ---- | ---- | ----------- | 346 | | _tokenSymbol | string | The symbol of the token to add | 347 | | _address | address | The address of the token to add | 348 | 349 | ### deleteEligibleTokenAddress 350 | 351 | ```solidity 352 | function deleteEligibleTokenAddress(string _tokenSymbol) public virtual 353 | ``` 354 | 355 | Delete eligible tokens stored in the contract. 356 | 357 | | Name | Type | Description | 358 | | ---- | ---- | ----------- | 359 | | _tokenSymbol | string | The symbol of the token to remove | 360 | 361 | ### setToucanContractRegistry 362 | 363 | ```solidity 364 | function setToucanContractRegistry(address _address) public virtual 365 | ``` 366 | 367 | Change the TCO2 contracts registry. 368 | 369 | | Name | Type | Description | 370 | | ---- | ---- | ----------- | 371 | | _address | address | The address of the Toucan contract registry to use | 372 | 373 | -------------------------------------------------------------------------------- /docs/OffsetHelperStorage.md: -------------------------------------------------------------------------------- 1 | # Solidity API 2 | 3 | ## OffsetHelperStorage 4 | 5 | ### eligibleTokenAddresses 6 | 7 | ```solidity 8 | mapping(string => address) eligibleTokenAddresses 9 | ``` 10 | 11 | ### contractRegistryAddress 12 | 13 | ```solidity 14 | address contractRegistryAddress 15 | ``` 16 | 17 | ### sushiRouterAddress 18 | 19 | ```solidity 20 | address sushiRouterAddress 21 | ``` 22 | 23 | ### balances 24 | 25 | ```solidity 26 | mapping(address => mapping(address => uint256)) balances 27 | ``` 28 | 29 | -------------------------------------------------------------------------------- /git-workflow.md: -------------------------------------------------------------------------------- 1 | # git contribution workflow 2 | 3 | We develop using [triangular workflows](https://github.blog/2015-07-29-git-2-5-including-multiple-worktrees-and-triangular-workflows/#improved-support-for-triangular-workflows) on git. 4 | 5 | For local deployment please create a fork of the [Toucan Protocol example-implementations repo](https://github.com/CO2ken/example-implementations). 6 | 7 | Clone your fork: 8 | 9 | ```bash 10 | git clone git@github.com:YOURUSERNAME/example-implementations.git 11 | git config remote.pushdefault origin 12 | git config push.default current 13 | ``` 14 | 15 | next add the root repository as `upstream`: 16 | 17 | ```bash 18 | git remote add upstream git@github.com:CO2ken/example-implementations.git 19 | git fetch upstream 20 | ``` 21 | 22 | Run: 23 | 24 | ```bash 25 | git remote -v 26 | ``` 27 | 28 | and you should see something like: 29 | 30 | ```bash 31 | origin git@github.com:YOURUSERNAME/example-implementations.git (fetch) 32 | origin git@github.com:YOURUSERNAME/example-implementations.git (push) 33 | upstream git@github.com:CO2ken/example-implementations.git (fetch) 34 | upstream git@github.com:CO2ken/example-implementations.git (push) 35 | ``` 36 | 37 | Feature flow: 38 | 39 | ```bash 40 | git checkout main && git pull --ff-only upstream main 41 | ``` 42 | 43 | Cmd+shift+. to copy your issue number from linear, and use it as your branch name: 44 | 45 | ```bash 46 | git checkout -b co2-190-small-edits-on-the-home-page 47 | ``` 48 | 49 | To open a PR push to your fork and open your PR to `CO2ken/example-implementations`. 50 | -------------------------------------------------------------------------------- /hardhat.config.ts: -------------------------------------------------------------------------------- 1 | import * as dotenv from "dotenv"; 2 | 3 | import { HardhatUserConfig, subtask, task } from "hardhat/config"; 4 | import "@nomiclabs/hardhat-etherscan"; 5 | import "@nomiclabs/hardhat-waffle"; 6 | import "@typechain/hardhat"; 7 | import "hardhat-gas-reporter"; 8 | import "solidity-coverage"; 9 | import "solidity-docgen"; 10 | import { tokens } from "./utils/tokens"; 11 | import addresses, { mumbaiAddresses } from "./utils/addresses"; 12 | import { network } from "hardhat"; 13 | import { boolean } from "hardhat/internal/core/params/argumentTypes"; 14 | import { relative } from "path"; 15 | 16 | dotenv.config(); 17 | 18 | task("accounts", "Prints the list of accounts", async (taskArgs, hre) => { 19 | const accounts = await hre.ethers.getSigners(); 20 | 21 | for (const account of accounts) { 22 | console.log(account.address); 23 | } 24 | }); 25 | 26 | task("deployOffsetHelper", "Deploys and verifies OffsetHelper") 27 | .addOptionalParam( 28 | "verify", 29 | "Set false to not verify the OffsetHelper after deployment", 30 | true, 31 | boolean 32 | ) 33 | .setAction(async (taskArgs, hre) => { 34 | const OffsetHelper = await hre.ethers.getContractFactory("OffsetHelper"); 35 | 36 | const addressesToUse = 37 | hre.network.name == "mumbai" ? mumbaiAddresses : addresses; 38 | 39 | const oh = await OffsetHelper.deploy(tokens, [ 40 | addressesToUse.bct, 41 | addressesToUse.nct, 42 | addressesToUse.usdc, 43 | addressesToUse.weth, 44 | addressesToUse.wmatic, 45 | ]); 46 | await oh.deployed(); 47 | console.log(`OffsetHelper deployed on ${hre.network.name} to:`, oh.address); 48 | 49 | if (taskArgs.verify === true) { 50 | await oh.deployTransaction.wait(5); 51 | await hre.run("verify:verify", { 52 | address: oh.address, 53 | constructorArguments: [ 54 | tokens, 55 | [ 56 | addressesToUse.bct, 57 | addressesToUse.nct, 58 | addressesToUse.usdc, 59 | addressesToUse.weth, 60 | addressesToUse.wmatic, 61 | ], 62 | ], 63 | }); 64 | console.log( 65 | `OffsetHelper verified on ${hre.network.name} to:`, 66 | oh.address 67 | ); 68 | } 69 | }); 70 | 71 | task("deploySwapper", "Deploys and verifies Swapper") 72 | .addOptionalParam( 73 | "verify", 74 | "Set false to not verify the Swapper after deployment", 75 | true, 76 | boolean 77 | ) 78 | .setAction(async (taskArgs, hre) => { 79 | const Swapper = await hre.ethers.getContractFactory("Swapper"); 80 | 81 | const addressesToUse = 82 | hre.network.name == "mumbai" ? mumbaiAddresses : addresses; 83 | 84 | const swapper = await Swapper.deploy(tokens, [ 85 | addressesToUse.bct, 86 | addressesToUse.nct, 87 | addressesToUse.usdc, 88 | addressesToUse.weth, 89 | addressesToUse.wmatic, 90 | ]); 91 | await swapper.deployed(); 92 | console.log(`Swapper deployed on ${hre.network.name} to:`, swapper.address); 93 | 94 | if (taskArgs.verify === true) { 95 | await swapper.deployTransaction.wait(5); 96 | await hre.run("verify:verify", { 97 | address: swapper.address, 98 | constructorArguments: [ 99 | tokens, 100 | [ 101 | addressesToUse.bct, 102 | addressesToUse.nct, 103 | addressesToUse.usdc, 104 | addressesToUse.weth, 105 | addressesToUse.wmatic, 106 | ], 107 | ], 108 | }); 109 | console.log( 110 | `Swapper verified on ${hre.network.name} to:`, 111 | swapper.address 112 | ); 113 | } 114 | }); 115 | 116 | const config: HardhatUserConfig = { 117 | defaultNetwork: "hardhat", 118 | solidity: { 119 | version: "0.8.4", 120 | settings: { 121 | optimizer: { 122 | enabled: true, 123 | runs: 200, 124 | }, 125 | }, 126 | }, 127 | networks: { 128 | polygon: { 129 | url: 130 | process.env.POLYGON_URL || "https://matic-mainnet.chainstacklabs.com", 131 | accounts: 132 | process.env.PRIVATE_KEY !== undefined ? [process.env.PRIVATE_KEY] : [], 133 | }, 134 | mumbai: { 135 | url: process.env.MUMBAI_URL || "https://matic-mumbai.chainstacklabs.com", 136 | accounts: 137 | process.env.PRIVATE_KEY !== undefined ? [process.env.PRIVATE_KEY] : [], 138 | }, 139 | hardhat: { 140 | forking: { 141 | url: 142 | process.env.POLYGON_URL || 143 | "https://polygon-mainnet.g.alchemy.com/v2/4rzRS2MH5LIunV6cejmLhQelv_Vd82rq", 144 | }, 145 | }, 146 | }, 147 | mocha: { 148 | timeout: 150000, 149 | }, 150 | etherscan: { 151 | apiKey: process.env.POLYGONSCAN_API_KEY || "", 152 | }, 153 | docgen: { 154 | pages: (item: any, file: any) => 155 | file.absolutePath.startsWith("contracts/OffsetHelper") 156 | ? relative("contracts", file.absolutePath).replace(".sol", ".md") 157 | : undefined, 158 | }, 159 | }; 160 | 161 | export default config; 162 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example-implementations", 3 | "scripts": { 4 | "chain": "yarn hardhat node --network hardhat", 5 | "compile": "yarn hardhat compile", 6 | "console": "yarn hardhat console", 7 | "coverage": "yarn hardhat coverage --network hardhat", 8 | "doc": "yarn hardhat docgen", 9 | "deploy": "yarn hardhat deploy", 10 | "test": "yarn hardhat test --network hardhat" 11 | }, 12 | "devDependencies": { 13 | "@nomiclabs/hardhat-ethers": "^2.0.5", 14 | "@nomiclabs/hardhat-etherscan": "^3.0.3", 15 | "@nomiclabs/hardhat-waffle": "^2.0.3", 16 | "@typechain/ethers-v5": "^7.2.0", 17 | "@typechain/hardhat": "^2.3.1", 18 | "@types/chai": "^4.3.0", 19 | "@types/mocha": "^9.1.0", 20 | "@types/node": "^12.20.47", 21 | "@typescript-eslint/eslint-plugin": "^4.33.0", 22 | "@typescript-eslint/parser": "^4.33.0", 23 | "@uniswap/sdk-core": "^3.0.1", 24 | "@uniswap/v3-sdk": "^3.8.2", 25 | "chai": "^4.3.6", 26 | "dotenv": "^10.0.0", 27 | "eslint": "^7.32.0", 28 | "eslint-config-prettier": "^8.4.0", 29 | "eslint-config-standard": "^16.0.3", 30 | "eslint-plugin-import": "^2.25.4", 31 | "eslint-plugin-node": "^11.1.0", 32 | "eslint-plugin-prettier": "^3.4.1", 33 | "eslint-plugin-promise": "^5.2.0", 34 | "ethereum-waffle": "^3.4.0", 35 | "ethers": "^5.6.0", 36 | "hardhat": "^2.9.1", 37 | "hardhat-gas-reporter": "^1.0.8", 38 | "prettier": "^2.5.1", 39 | "prettier-plugin-solidity": "^1.0.0-beta.13", 40 | "solhint": "^3.3.7", 41 | "solidity-coverage": "^0.7.20", 42 | "solidity-docgen": "^0.6.0-beta.16", 43 | "ts-node": "^10.7.0", 44 | "tslib": "^2.3.1", 45 | "typechain": "^5.2.0", 46 | "typescript": "^4.6.2" 47 | }, 48 | "dependencies": { 49 | "@openzeppelin/contracts": "^4.5.0", 50 | "@openzeppelin/contracts-upgradeable": "^4.5.1", 51 | "@uniswap/v2-periphery": "^1.1.0-beta.0", 52 | "i": "^0.3.7", 53 | "solc": "^0.8.12", 54 | "urql": "^2.2.0" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /test/OffsetHelper.test.ts: -------------------------------------------------------------------------------- 1 | import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; 2 | import { expect } from "chai"; 3 | import { ethers } from "hardhat"; 4 | import { formatEther, parseEther, parseUnits } from "ethers/lib/utils"; 5 | 6 | import * as hardhatContracts from "../utils/toucanContracts.json"; 7 | import * as poolContract from "../artifacts/contracts/interfaces/IToucanPoolToken.sol/IToucanPoolToken.json"; 8 | import { 9 | IToucanPoolToken, 10 | OffsetHelper, 11 | OffsetHelper__factory, 12 | Swapper, 13 | Swapper__factory, 14 | } from "../typechain"; 15 | import addresses from "../utils/addresses"; 16 | import { BigNumber, Contract } from "ethers"; 17 | import { usdcABI, wethABI, wmaticABI } from "../utils/ABIs"; 18 | 19 | const ONE_ETHER = parseEther("1.0"); 20 | 21 | describe("Offset Helper - autoOffset", function () { 22 | let offsetHelper: OffsetHelper; 23 | let swapper: Swapper; 24 | let bct: IToucanPoolToken; 25 | let nct: IToucanPoolToken; 26 | let weth: Contract; 27 | let wmatic: Contract; 28 | let usdc: Contract; 29 | let addr1: SignerWithAddress; 30 | let addr2: SignerWithAddress; 31 | let addrs: SignerWithAddress[]; 32 | 33 | beforeEach(async function () { 34 | [addr1, addr2, ...addrs] = await ethers.getSigners(); 35 | 36 | const offsetHelperFactory = (await ethers.getContractFactory( 37 | "OffsetHelper", 38 | addr2 39 | )) as OffsetHelper__factory; 40 | offsetHelper = await offsetHelperFactory.deploy( 41 | ["BCT", "NCT", "USDC", "WETH", "WMATIC"], 42 | [ 43 | addresses.bct, 44 | addresses.nct, 45 | addresses.usdc, 46 | addresses.weth, 47 | addresses.wmatic, 48 | ] 49 | ); 50 | 51 | weth = new ethers.Contract(addresses.weth, wethABI, addr2); 52 | wmatic = new ethers.Contract(addresses.wmatic, wmaticABI, addr2); 53 | usdc = new ethers.Contract(addresses.usdc, usdcABI, addr2); 54 | 55 | nct = new ethers.Contract( 56 | addresses.nct, 57 | hardhatContracts.contracts.NatureCarbonTonne.abi, 58 | addr2 59 | ) as IToucanPoolToken; 60 | 61 | bct = new ethers.Contract( 62 | addresses.bct, 63 | poolContract.abi, 64 | addr2 65 | ) as IToucanPoolToken; 66 | }); 67 | 68 | before(async () => { 69 | [addr1, addr2, ...addrs] = await ethers.getSigners(); 70 | 71 | const swapperFactory = (await ethers.getContractFactory( 72 | "Swapper", 73 | addr2 74 | )) as Swapper__factory; 75 | swapper = await swapperFactory.deploy( 76 | ["BCT", "NCT", "USDC", "WETH", "WMATIC"], 77 | [ 78 | addresses.bct, 79 | addresses.nct, 80 | addresses.usdc, 81 | addresses.weth, 82 | addresses.wmatic, 83 | ] 84 | ); 85 | 86 | await Promise.all( 87 | addrs.map(async (addr) => { 88 | await addr.sendTransaction({ 89 | to: addr2.address, 90 | value: (await addr.getBalance()).sub(ONE_ETHER), 91 | }); 92 | }) 93 | ); 94 | 95 | await swapper.swap(addresses.weth, parseEther("20.0"), { 96 | value: await swapper.calculateNeededETHAmount( 97 | addresses.weth, 98 | parseEther("20.0") 99 | ), 100 | }); 101 | 102 | await swapper.swap(addresses.usdc, parseUnits("20.0", 6), { 103 | value: await swapper.calculateNeededETHAmount( 104 | addresses.usdc, 105 | parseUnits("20.0", 6) 106 | ), 107 | }); 108 | 109 | await swapper.swap(addresses.bct, parseEther("50.0"), { 110 | value: await swapper.calculateNeededETHAmount( 111 | addresses.bct, 112 | parseEther("50.0") 113 | ), 114 | }); 115 | 116 | await swapper.swap(addresses.nct, parseEther("50.0"), { 117 | value: await swapper.calculateNeededETHAmount( 118 | addresses.nct, 119 | parseEther("50.0") 120 | ), 121 | }); 122 | }); 123 | 124 | describe("Testing autoOffset()", function () { 125 | it("should retire 1.0 TCO2 using a WETH swap and NCT redemption", async function () { 126 | // first we set the initial chain state 127 | const wethBalanceBefore = await weth.balanceOf(addr2.address); 128 | const nctSupplyBefore = await nct.totalSupply(); 129 | 130 | // then we calculate the cost in WETH of retiring 1.0 TCO2 131 | const wethCost = await offsetHelper.calculateNeededTokenAmount( 132 | addresses.weth, 133 | addresses.nct, 134 | ONE_ETHER 135 | ); 136 | 137 | // then we use the autoOffset function to retire 1.0 TCO2 from WETH using NCT 138 | await (await weth.approve(offsetHelper.address, wethCost)).wait(); 139 | await offsetHelper.autoOffsetUsingToken( 140 | addresses.weth, 141 | addresses.nct, 142 | ONE_ETHER 143 | ); 144 | 145 | // then we set the chain state after the transaction 146 | const wethBalanceAfter = await weth.balanceOf(addr2.address); 147 | const nctSupplyAfter = await nct.totalSupply(); 148 | 149 | // and we compare chain states 150 | expect( 151 | formatEther(wethBalanceBefore.sub(wethBalanceAfter)), 152 | `User should have spent ${formatEther(wethCost)}} WETH` 153 | ).to.equal(formatEther(wethCost)); 154 | expect( 155 | formatEther(nctSupplyBefore.sub(nctSupplyAfter)), 156 | "Total supply of NCT should have decreased by 1" 157 | ).to.equal("1.0"); 158 | }); 159 | 160 | it("should retire using a MATIC swap and NCT redemption", async function () { 161 | // first we set the initial chain state 162 | const maticBalanceBefore = await addr2.getBalance(); 163 | const nctSupplyBefore = await nct.totalSupply(); 164 | 165 | // then we calculate the cost in MATIC of retiring 1.0 TCO2 166 | const maticCost = await offsetHelper.calculateNeededETHAmount( 167 | addresses.nct, 168 | ONE_ETHER 169 | ); 170 | 171 | // then we use the autoOffset function to retire 1.0 TCO2 from MATIC using NCT 172 | const tx = await ( 173 | await offsetHelper.autoOffsetUsingETH(addresses.nct, ONE_ETHER, { 174 | value: maticCost, 175 | }) 176 | ).wait(); 177 | 178 | // we calculate the used gas 179 | const txFees = tx.gasUsed.mul(tx.effectiveGasPrice); 180 | 181 | // and we set the chain state after the transaction 182 | const maticBalanceAfter = await addr2.getBalance(); 183 | const nctSupplyAfter = await nct.totalSupply(); 184 | 185 | // lastly we compare chain states 186 | expect( 187 | formatEther(maticBalanceBefore.sub(maticBalanceAfter)), 188 | `User should have spent ${formatEther(maticCost)}} MATIC` 189 | ).to.equal(formatEther(maticCost.add(txFees))); 190 | expect( 191 | formatEther(nctSupplyBefore.sub(nctSupplyAfter)), 192 | "Total supply of NCT should have decreased by 1" 193 | ).to.equal("1.0"); 194 | }); 195 | 196 | it("should retire using a NCT deposit and NCT redemption", async function () { 197 | // first we set the initial chain state 198 | const nctBalanceBefore = await nct.balanceOf(addr2.address); 199 | const nctSupplyBefore = await nct.totalSupply(); 200 | 201 | // then we use the autoOffset function to retire 1.0 TCO2 from NCT 202 | await (await nct.approve(offsetHelper.address, ONE_ETHER)).wait(); 203 | await offsetHelper.autoOffsetUsingPoolToken(addresses.nct, ONE_ETHER); 204 | 205 | // then we set the chain state after the transaction 206 | const nctBalanceAfter = await nct.balanceOf(addr2.address); 207 | const nctSupplyAfter = await nct.totalSupply(); 208 | 209 | // and we compare chain states 210 | expect( 211 | formatEther(nctBalanceBefore.sub(nctBalanceAfter)), 212 | `User should have spent 1.0 NCT` 213 | ).to.equal("1.0"); 214 | expect( 215 | formatEther(nctSupplyBefore.sub(nctSupplyAfter)), 216 | "Total supply of NCT should have decreased by 1" 217 | ).to.equal("1.0"); 218 | }); 219 | 220 | it("should retire using a WETH swap and BCT redemption", async function () { 221 | // first we set the initial chain state 222 | const wethBalanceBefore = await weth.balanceOf(addr2.address); 223 | const bctSupplyBefore = await bct.totalSupply(); 224 | 225 | // then we calculate the cost in WETH of retiring 1.0 TCO2 226 | const wethCost = await offsetHelper.calculateNeededTokenAmount( 227 | addresses.weth, 228 | addresses.bct, 229 | ONE_ETHER 230 | ); 231 | 232 | // then we use the autoOffset function to retire 1.0 TCO2 from WETH using BCT 233 | await (await weth.approve(offsetHelper.address, wethCost)).wait(); 234 | await offsetHelper.autoOffsetUsingToken( 235 | addresses.weth, 236 | addresses.bct, 237 | ONE_ETHER 238 | ); 239 | 240 | // then we set the chain state after the transaction 241 | const wethBalanceAfter = await weth.balanceOf(addr2.address); 242 | const bctSupplyAfter = await bct.totalSupply(); 243 | 244 | // and we compare chain states 245 | expect( 246 | formatEther(wethBalanceBefore.sub(wethBalanceAfter)), 247 | `User should have spent ${formatEther(wethCost)}} WETH` 248 | ).to.equal(formatEther(wethCost)); 249 | expect( 250 | formatEther(bctSupplyBefore.sub(bctSupplyAfter)), 251 | "Total supply of BCT should have decreased by 1" 252 | ).to.equal("1.0"); 253 | }); 254 | 255 | it("should retire using a USDC swap and NCT redemption", async function () { 256 | // first we set the initial chain state 257 | const usdcBalanceBefore = await usdc.balanceOf(addr2.address); 258 | const nctSupplyBefore = await nct.totalSupply(); 259 | 260 | // then we calculate the cost in USDC of retiring 1.0 TCO2 261 | const usdcCost = await offsetHelper.calculateNeededTokenAmount( 262 | addresses.usdc, 263 | addresses.nct, 264 | ONE_ETHER 265 | ); 266 | 267 | // then we use the autoOffset function to retire 1.0 TCO2 from USDC using NCT 268 | await (await usdc.approve(offsetHelper.address, usdcCost)).wait(); 269 | await offsetHelper.autoOffsetUsingToken( 270 | addresses.usdc, 271 | addresses.nct, 272 | ONE_ETHER 273 | ); 274 | 275 | // then we set the chain state after the transaction 276 | const usdcBalanceAfter = await usdc.balanceOf(addr2.address); 277 | const nctSupplyAfter = await nct.totalSupply(); 278 | 279 | // and we compare chain states 280 | expect( 281 | formatEther(usdcBalanceBefore.sub(usdcBalanceAfter)), 282 | `User should have spent ${formatEther(usdcCost)}} USDC` 283 | ).to.equal(formatEther(usdcCost)); 284 | expect( 285 | formatEther(nctSupplyBefore.sub(nctSupplyAfter)), 286 | "Total supply of NCT should have decreased by 1" 287 | ).to.equal("1.0"); 288 | }); 289 | 290 | it("should retire using a WMATIC swap and NCT redemption", async function () { 291 | // first we wrap some matic 292 | await wmatic.deposit({ 293 | value: parseEther("20.0"), 294 | }); 295 | 296 | // then we set the initial chain state 297 | const wmaticBalanceBefore = await wmatic.balanceOf(addr2.address); 298 | const nctSupplyBefore = await nct.totalSupply(); 299 | 300 | // and we calculate the cost in WMATIC of retiring 1.0 TCO2 301 | const wmaticCost = await offsetHelper.calculateNeededTokenAmount( 302 | addresses.wmatic, 303 | addresses.nct, 304 | ONE_ETHER 305 | ); 306 | 307 | // we use the autoOffset function to retire 1.0 TCO2 from WMATIC using NCT 308 | await (await wmatic.approve(offsetHelper.address, wmaticCost)).wait(); 309 | await offsetHelper.autoOffsetUsingToken( 310 | addresses.wmatic, 311 | addresses.nct, 312 | ONE_ETHER 313 | ); 314 | 315 | // then we set the chain state after the transaction 316 | const wmaticBalanceAfter = await wmatic.balanceOf(addr2.address); 317 | const nctSupplyAfter = await nct.totalSupply(); 318 | 319 | // and we compare chain states 320 | expect( 321 | formatEther(wmaticBalanceBefore.sub(wmaticBalanceAfter)), 322 | `User should have spent ${formatEther(wmaticCost)} WMATIC` 323 | ).to.equal(formatEther(wmaticCost)); 324 | expect( 325 | formatEther(nctSupplyBefore.sub(nctSupplyAfter)), 326 | "Total supply of NCT should have decreased by 1" 327 | ).to.equal("1.0"); 328 | }); 329 | }); 330 | 331 | describe("Testing autoRedeem()", function () { 332 | it("should redeem NCT from deposit", async function () { 333 | // first we set the initial chain state 334 | const states: { 335 | userNctBalance: BigNumber; 336 | contractNctBalance: BigNumber; 337 | nctSupply: BigNumber; 338 | }[] = []; 339 | states.push({ 340 | userNctBalance: await nct.balanceOf(addr2.address), 341 | contractNctBalance: await nct.balanceOf(offsetHelper.address), 342 | nctSupply: await nct.totalSupply(), 343 | }); 344 | 345 | // then we deposit 1.0 NCT into the OH contract 346 | await (await nct.approve(offsetHelper.address, ONE_ETHER)).wait(); 347 | await (await offsetHelper.deposit(addresses.nct, ONE_ETHER)).wait(); 348 | 349 | // then we set the chain state after the deposit transaction 350 | states.push({ 351 | userNctBalance: await nct.balanceOf(addr2.address), 352 | contractNctBalance: await nct.balanceOf(offsetHelper.address), 353 | nctSupply: await nct.totalSupply(), 354 | }); 355 | 356 | // and we compare chain states post deposit 357 | expect( 358 | formatEther(states[0].userNctBalance.sub(states[1].userNctBalance)), 359 | "User should have 1 less NCT post deposit" 360 | ).to.equal(formatEther(ONE_ETHER)); 361 | expect( 362 | formatEther( 363 | states[1].contractNctBalance.sub(states[0].contractNctBalance) 364 | ), 365 | "Contract should have 1 more NCT post deposit" 366 | ).to.equal(formatEther(ONE_ETHER)); 367 | expect( 368 | formatEther(states[0].nctSupply), 369 | "NCT supply should be the same post deposit" 370 | ).to.equal(formatEther(states[1].nctSupply)); 371 | 372 | // we redeem 1.0 NCT from the OH contract for TCO2s 373 | await offsetHelper.autoRedeem(addresses.nct, ONE_ETHER); 374 | 375 | // then we set the chain state after the redeem transaction 376 | states.push({ 377 | userNctBalance: await nct.balanceOf(addr2.address), 378 | contractNctBalance: await nct.balanceOf(offsetHelper.address), 379 | nctSupply: await nct.totalSupply(), 380 | }); 381 | 382 | // and we compare chain states post redeem 383 | expect( 384 | formatEther(states[1].userNctBalance), 385 | "User should have the same amount of NCT post redeem" 386 | ).to.equal(formatEther(states[2].userNctBalance)); 387 | expect( 388 | formatEther( 389 | states[1].contractNctBalance.sub(states[2].contractNctBalance) 390 | ), 391 | "Contract should have 1 less NCT post redeem" 392 | ).to.equal(formatEther(ONE_ETHER)); 393 | expect( 394 | formatEther(states[1].nctSupply.sub(states[2].nctSupply)), 395 | "NCT supply should be less by 1 post redeem" 396 | ).to.equal(formatEther(ONE_ETHER)); 397 | }); 398 | 399 | it("Should fail because we haven't deposited NCT", async function () { 400 | await expect( 401 | offsetHelper.autoRedeem(addresses.nct, ONE_ETHER) 402 | ).to.be.revertedWith("Insufficient NCT/BCT balance"); 403 | }); 404 | 405 | it("should redeem BCT from deposit", async function () { 406 | // first we set the initial chain state 407 | const states: { 408 | userBctBalance: BigNumber; 409 | contractBctBalance: BigNumber; 410 | bctSupply: BigNumber; 411 | }[] = []; 412 | states.push({ 413 | userBctBalance: await bct.balanceOf(addr2.address), 414 | contractBctBalance: await bct.balanceOf(offsetHelper.address), 415 | bctSupply: await bct.totalSupply(), 416 | }); 417 | 418 | // then we deposit 1.0 BCT into the OH contract 419 | await (await bct.approve(offsetHelper.address, ONE_ETHER)).wait(); 420 | await (await offsetHelper.deposit(addresses.bct, ONE_ETHER)).wait(); 421 | 422 | // then we set the chain state after the deposit transaction 423 | states.push({ 424 | userBctBalance: await bct.balanceOf(addr2.address), 425 | contractBctBalance: await bct.balanceOf(offsetHelper.address), 426 | bctSupply: await bct.totalSupply(), 427 | }); 428 | 429 | // and we compare chain states post deposit 430 | expect( 431 | formatEther(states[0].userBctBalance.sub(states[1].userBctBalance)), 432 | "User should have 1 less BCT post deposit" 433 | ).to.equal(formatEther(ONE_ETHER)); 434 | expect( 435 | formatEther( 436 | states[1].contractBctBalance.sub(states[0].contractBctBalance) 437 | ), 438 | "Contract should have 1 more BCT post deposit" 439 | ).to.equal(formatEther(ONE_ETHER)); 440 | expect( 441 | formatEther(states[0].bctSupply), 442 | "BCT supply should be the same post deposit" 443 | ).to.equal(formatEther(states[1].bctSupply)); 444 | 445 | // we redeem 1.0 BCT from the OH contract for TCO2s 446 | await offsetHelper.autoRedeem(addresses.bct, ONE_ETHER); 447 | 448 | // then we set the chain state after the redeem transaction 449 | states.push({ 450 | userBctBalance: await bct.balanceOf(addr2.address), 451 | contractBctBalance: await bct.balanceOf(offsetHelper.address), 452 | bctSupply: await bct.totalSupply(), 453 | }); 454 | 455 | // and we compare chain states post redeem 456 | expect( 457 | formatEther(states[1].userBctBalance), 458 | "User should have the same amount of BCT post redeem" 459 | ).to.equal(formatEther(states[2].userBctBalance)); 460 | expect( 461 | formatEther( 462 | states[1].contractBctBalance.sub(states[2].contractBctBalance) 463 | ), 464 | "Contract should have 1 less BCT post redeem" 465 | ).to.equal(formatEther(ONE_ETHER)); 466 | expect( 467 | formatEther(states[1].bctSupply.sub(states[2].bctSupply)), 468 | "BCT supply should be less by 1 post redeem" 469 | ).to.equal(formatEther(ONE_ETHER)); 470 | }); 471 | }); 472 | 473 | describe("Testing autoRetire()", function () { 474 | it("should retire using an NCT deposit", async function () { 475 | // first we set the initial state 476 | const state: { 477 | userNctBalance: BigNumber; 478 | contractNctBalance: BigNumber; 479 | nctSupply: BigNumber; 480 | }[] = []; 481 | state.push({ 482 | userNctBalance: await nct.balanceOf(addr2.address), 483 | contractNctBalance: await nct.balanceOf(offsetHelper.address), 484 | nctSupply: await nct.totalSupply(), 485 | }); 486 | 487 | // we deposit NCT into OH 488 | await (await nct.approve(offsetHelper.address, ONE_ETHER)).wait(); 489 | await (await offsetHelper.deposit(addresses.nct, ONE_ETHER)).wait(); 490 | 491 | // and we check the state after the deposit 492 | state.push({ 493 | userNctBalance: await nct.balanceOf(addr2.address), 494 | contractNctBalance: await nct.balanceOf(offsetHelper.address), 495 | nctSupply: await nct.totalSupply(), 496 | }); 497 | expect( 498 | formatEther(state[0].userNctBalance.sub(state[1].userNctBalance)), 499 | "User should have 1 less NCT post deposit" 500 | ).to.equal(formatEther(ONE_ETHER)); 501 | expect( 502 | formatEther( 503 | state[1].contractNctBalance.sub(state[0].contractNctBalance) 504 | ), 505 | "Contract should have 1 more NCT post deposit" 506 | ).to.equal(formatEther(ONE_ETHER)); 507 | expect( 508 | formatEther(state[0].nctSupply), 509 | "NCT supply should be the same post deposit" 510 | ).to.equal(formatEther(state[1].nctSupply)); 511 | 512 | // we redeem NCT for TCO2 within OH 513 | const redeemReceipt = await ( 514 | await offsetHelper.autoRedeem(addresses.nct, ONE_ETHER) 515 | ).wait(); 516 | 517 | // and we check the state after the redeem 518 | state.push({ 519 | userNctBalance: await nct.balanceOf(addr2.address), 520 | contractNctBalance: await nct.balanceOf(offsetHelper.address), 521 | nctSupply: await nct.totalSupply(), 522 | }); 523 | expect( 524 | formatEther(state[1].userNctBalance), 525 | "User should have the same amount of NCT post redeem" 526 | ).to.equal(formatEther(state[2].userNctBalance)); 527 | expect( 528 | formatEther( 529 | state[1].contractNctBalance.sub(state[2].contractNctBalance) 530 | ), 531 | "Contract should have 1 less NCT post redeem" 532 | ).to.equal(formatEther(ONE_ETHER)); 533 | expect( 534 | formatEther(state[1].nctSupply.sub(state[2].nctSupply)), 535 | "NCT supply should be less by 1 post redeem" 536 | ).to.equal(formatEther(ONE_ETHER)); 537 | 538 | // we get the tco2s and amounts that were redeemed 539 | if (!redeemReceipt.events) throw new Error("No events emitted"); 540 | const tco2s = 541 | redeemReceipt.events[redeemReceipt.events.length - 1].args?.tco2s; 542 | const amounts = 543 | redeemReceipt.events[redeemReceipt.events.length - 1].args?.amounts; 544 | 545 | // we retire the tco2s 546 | await offsetHelper.autoRetire(tco2s, amounts); 547 | 548 | // and we check the state after the retire 549 | state.push({ 550 | userNctBalance: await nct.balanceOf(addr2.address), 551 | contractNctBalance: await nct.balanceOf(offsetHelper.address), 552 | nctSupply: await nct.totalSupply(), 553 | }); 554 | expect( 555 | formatEther(state[2].userNctBalance), 556 | "User should have the same amount of NCT post retire" 557 | ).to.equal(formatEther(state[3].userNctBalance)); 558 | expect( 559 | formatEther(state[2].contractNctBalance), 560 | "Contract should have the same amount of NCT post retire" 561 | ).to.equal(formatEther(state[3].contractNctBalance)); 562 | expect( 563 | formatEther(state[2].nctSupply), 564 | "NCT supply should be the same post retire" 565 | ).to.equal(formatEther(state[3].nctSupply)); 566 | }); 567 | 568 | it("Should fail because we haven't redeemed any TCO2", async function () { 569 | await expect( 570 | offsetHelper.autoRetire( 571 | ["0xb139C4cC9D20A3618E9a2268D73Eff18C496B991"], 572 | [ONE_ETHER] 573 | ) 574 | ).to.be.revertedWith("Insufficient TCO2 balance"); 575 | }); 576 | 577 | it("should retire using an BCT deposit", async function () { 578 | // first we set the initial state 579 | const state: { 580 | userBctBalance: BigNumber; 581 | contractBctBalance: BigNumber; 582 | bctSupply: BigNumber; 583 | }[] = []; 584 | state.push({ 585 | userBctBalance: await bct.balanceOf(addr2.address), 586 | contractBctBalance: await bct.balanceOf(offsetHelper.address), 587 | bctSupply: await bct.totalSupply(), 588 | }); 589 | 590 | // we deposit BCT into OH 591 | await (await bct.approve(offsetHelper.address, ONE_ETHER)).wait(); 592 | await (await offsetHelper.deposit(addresses.bct, ONE_ETHER)).wait(); 593 | 594 | // and we check the state after the deposit 595 | state.push({ 596 | userBctBalance: await bct.balanceOf(addr2.address), 597 | contractBctBalance: await bct.balanceOf(offsetHelper.address), 598 | bctSupply: await bct.totalSupply(), 599 | }); 600 | expect( 601 | formatEther(state[0].userBctBalance.sub(state[1].userBctBalance)), 602 | "User should have 1 less BCT post deposit" 603 | ).to.equal(formatEther(ONE_ETHER)); 604 | expect( 605 | formatEther( 606 | state[1].contractBctBalance.sub(state[0].contractBctBalance) 607 | ), 608 | "Contract should have 1 more BCT post deposit" 609 | ).to.equal(formatEther(ONE_ETHER)); 610 | expect( 611 | formatEther(state[0].bctSupply), 612 | "BCT supply should be the same post deposit" 613 | ).to.equal(formatEther(state[1].bctSupply)); 614 | 615 | // we redeem BCT for TCO2 within OH 616 | const redeemReceipt = await ( 617 | await offsetHelper.autoRedeem(addresses.bct, ONE_ETHER) 618 | ).wait(); 619 | 620 | // and we check the state after the redeem 621 | state.push({ 622 | userBctBalance: await bct.balanceOf(addr2.address), 623 | contractBctBalance: await bct.balanceOf(offsetHelper.address), 624 | bctSupply: await bct.totalSupply(), 625 | }); 626 | expect( 627 | formatEther(state[1].userBctBalance), 628 | "User should have the same amount of BCT post redeem" 629 | ).to.equal(formatEther(state[2].userBctBalance)); 630 | expect( 631 | formatEther( 632 | state[1].contractBctBalance.sub(state[2].contractBctBalance) 633 | ), 634 | "Contract should have 1 less BCT post redeem" 635 | ).to.equal(formatEther(ONE_ETHER)); 636 | expect( 637 | formatEther(state[1].bctSupply.sub(state[2].bctSupply)), 638 | "BCT supply should be less by 1 post redeem" 639 | ).to.equal(formatEther(ONE_ETHER)); 640 | 641 | // we get the tco2s and amounts that were redeemed 642 | if (!redeemReceipt.events) throw new Error("No events emitted"); 643 | const tco2s = 644 | redeemReceipt.events[redeemReceipt.events.length - 1].args?.tco2s; 645 | const amounts = 646 | redeemReceipt.events[redeemReceipt.events.length - 1].args?.amounts; 647 | 648 | // we retire the tco2s 649 | await offsetHelper.autoRetire(tco2s, amounts); 650 | 651 | // and we check the state after the retire 652 | state.push({ 653 | userBctBalance: await bct.balanceOf(addr2.address), 654 | contractBctBalance: await bct.balanceOf(offsetHelper.address), 655 | bctSupply: await bct.totalSupply(), 656 | }); 657 | expect( 658 | formatEther(state[2].userBctBalance), 659 | "User should have the same amount of BCT post retire" 660 | ).to.equal(formatEther(state[3].userBctBalance)); 661 | expect( 662 | formatEther(state[2].contractBctBalance), 663 | "Contract should have the same amount of BCT post retire" 664 | ).to.equal(formatEther(state[3].contractBctBalance)); 665 | expect( 666 | formatEther(state[2].bctSupply), 667 | "BCT supply should be the same post retire" 668 | ).to.equal(formatEther(state[3].bctSupply)); 669 | }); 670 | }); 671 | 672 | describe("Testing deposit() and withdraw()", function () { 673 | it("Should deposit 1.0 NCT", async function () { 674 | await (await nct.approve(offsetHelper.address, ONE_ETHER)).wait(); 675 | 676 | await (await offsetHelper.deposit(addresses.nct, ONE_ETHER)).wait(); 677 | 678 | expect( 679 | formatEther(await offsetHelper.balances(addr2.address, addresses.nct)) 680 | ).to.be.eql("1.0"); 681 | }); 682 | 683 | it("Should fail to deposit because we have no NCT", async function () { 684 | await ( 685 | await nct.connect(addrs[0]).approve(offsetHelper.address, ONE_ETHER) 686 | ).wait(); 687 | 688 | await expect( 689 | offsetHelper.connect(addrs[0]).deposit(addresses.nct, ONE_ETHER) 690 | ).to.be.revertedWith("ERC20: transfer amount exceeds balance"); 691 | }); 692 | 693 | it("Should deposit and withdraw 1.0 NCT", async function () { 694 | const preDepositNCTBalance = await nct.balanceOf(addr2.address); 695 | 696 | await (await nct.approve(offsetHelper.address, ONE_ETHER)).wait(); 697 | 698 | await (await offsetHelper.deposit(addresses.nct, ONE_ETHER)).wait(); 699 | 700 | await (await offsetHelper.withdraw(addresses.nct, ONE_ETHER)).wait(); 701 | 702 | const postWithdrawNCTBalance = await nct.balanceOf(addr2.address); 703 | 704 | expect(formatEther(postWithdrawNCTBalance)).to.be.eql( 705 | formatEther(preDepositNCTBalance) 706 | ); 707 | }); 708 | 709 | it("Should fail to withdraw because we haven't deposited enough NCT", async function () { 710 | await (await nct.approve(offsetHelper.address, ONE_ETHER)).wait(); 711 | 712 | await (await offsetHelper.deposit(addresses.nct, ONE_ETHER)).wait(); 713 | 714 | await expect( 715 | offsetHelper.withdraw(addresses.nct, parseEther("2.0")) 716 | ).to.be.revertedWith("Insufficient balance"); 717 | }); 718 | 719 | it("Should deposit 1.0 BCT", async function () { 720 | await (await bct.approve(offsetHelper.address, ONE_ETHER)).wait(); 721 | 722 | await (await offsetHelper.deposit(addresses.bct, ONE_ETHER)).wait(); 723 | 724 | expect( 725 | formatEther(await offsetHelper.balances(addr2.address, addresses.bct)) 726 | ).to.be.eql("1.0"); 727 | }); 728 | }); 729 | 730 | describe("swap() for NCT", function () { 731 | it("Should swap WETH for 1.0 NCT", async function () { 732 | const initialBalance = await nct.balanceOf(offsetHelper.address); 733 | 734 | await ( 735 | await weth.approve( 736 | offsetHelper.address, 737 | await offsetHelper.calculateNeededTokenAmount( 738 | addresses.weth, 739 | addresses.nct, 740 | ONE_ETHER 741 | ) 742 | ) 743 | ).wait(); 744 | 745 | await ( 746 | await offsetHelper["swap(address,address,uint256)"]( 747 | addresses.weth, 748 | addresses.nct, 749 | ONE_ETHER 750 | ) 751 | ).wait(); 752 | 753 | // I expect the offsetHelper will have 1 extra NCT in its balance 754 | const balance = await nct.balanceOf(offsetHelper.address); 755 | expect(formatEther(balance)).to.be.eql( 756 | formatEther(initialBalance.add(ONE_ETHER)) 757 | ); 758 | 759 | // I expect that the user should have his in-contract balance for NCT to be 1.0 760 | expect( 761 | formatEther(await offsetHelper.balances(addr2.address, addresses.nct)) 762 | ).to.be.eql("1.0"); 763 | }); 764 | 765 | it("Should swap MATIC for 1.0 NCT", async function () { 766 | const maticToSend = await offsetHelper.calculateNeededETHAmount( 767 | addresses.nct, 768 | ONE_ETHER 769 | ); 770 | 771 | await ( 772 | await offsetHelper["swap(address,uint256)"](addresses.nct, ONE_ETHER, { 773 | value: maticToSend, 774 | }) 775 | ).wait(); 776 | 777 | const balance = await nct.balanceOf(offsetHelper.address); 778 | expect(formatEther(balance)).to.be.eql("1.0"); 779 | }); 780 | 781 | it("Should send surplus MATIC to user", async function () { 782 | const preSwapETHBalance = await offsetHelper.provider.getBalance( 783 | offsetHelper.address 784 | ); 785 | 786 | const maticToSend = await offsetHelper.calculateNeededETHAmount( 787 | addresses.nct, 788 | ONE_ETHER 789 | ); 790 | 791 | await ( 792 | await offsetHelper["swap(address,uint256)"](addresses.nct, ONE_ETHER, { 793 | value: maticToSend.add(parseEther("0.5")), 794 | }) 795 | ).wait(); 796 | 797 | const postSwapETHBalance = await offsetHelper.provider.getBalance( 798 | offsetHelper.address 799 | ); 800 | 801 | // I'm expecting that the OffsetHelper doesn't have extra MATIC 802 | // this check is done to ensure any surplus MATIC has been sent to the user, and not to OffsetHelper 803 | expect(formatEther(preSwapETHBalance)).to.be.eql( 804 | formatEther(postSwapETHBalance) 805 | ); 806 | }); 807 | 808 | it("Should fail since we have no WETH", async function () { 809 | await ( 810 | await weth.connect(addrs[0]).approve(offsetHelper.address, ONE_ETHER) 811 | ).wait(); 812 | 813 | await expect( 814 | offsetHelper 815 | .connect(addrs[0]) 816 | ["swap(address,address,uint256)"]( 817 | addresses.weth, 818 | addresses.nct, 819 | ONE_ETHER 820 | ) 821 | ).to.be.revertedWith("ERC20: transfer amount exceeds balance"); 822 | }); 823 | 824 | it("Should swap WETH for 1.0 BCT", async function () { 825 | const initialBalance = await bct.balanceOf(offsetHelper.address); 826 | 827 | const neededAmount = await offsetHelper.calculateNeededTokenAmount( 828 | addresses.weth, 829 | addresses.bct, 830 | ONE_ETHER 831 | ); 832 | 833 | await (await weth.approve(offsetHelper.address, neededAmount)).wait(); 834 | 835 | await ( 836 | await offsetHelper["swap(address,address,uint256)"]( 837 | addresses.weth, 838 | addresses.bct, 839 | ONE_ETHER 840 | ) 841 | ).wait(); 842 | 843 | // I expect the offsetHelper will have 1 extra BCT in its balance 844 | const balance = await bct.balanceOf(offsetHelper.address); 845 | expect(formatEther(balance)).to.be.eql( 846 | formatEther(initialBalance.add(ONE_ETHER)) 847 | ); 848 | 849 | // I expect that the user should have his in-contract balance for BCT to be 1.0 850 | expect( 851 | formatEther(await offsetHelper.balances(addr2.address, addresses.bct)) 852 | ).to.be.eql("1.0"); 853 | }); 854 | }); 855 | }); 856 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2018", 4 | "module": "commonjs", 5 | "strict": true, 6 | "esModuleInterop": true, 7 | "outDir": "dist", 8 | "declaration": true, 9 | "resolveJsonModule": true 10 | }, 11 | "include": ["./scripts", "./test", "./typechain", "./utils"], 12 | "files": ["./hardhat.config.ts"] 13 | } 14 | -------------------------------------------------------------------------------- /utils/ABIs.ts: -------------------------------------------------------------------------------- 1 | export const wethABI = [ 2 | { 3 | inputs: [ 4 | { internalType: "address", name: "childChainManager", type: "address" }, 5 | ], 6 | stateMutability: "nonpayable", 7 | type: "constructor", 8 | }, 9 | { 10 | anonymous: false, 11 | inputs: [ 12 | { 13 | indexed: true, 14 | internalType: "address", 15 | name: "owner", 16 | type: "address", 17 | }, 18 | { 19 | indexed: true, 20 | internalType: "address", 21 | name: "spender", 22 | type: "address", 23 | }, 24 | { 25 | indexed: false, 26 | internalType: "uint256", 27 | name: "value", 28 | type: "uint256", 29 | }, 30 | ], 31 | name: "Approval", 32 | type: "event", 33 | }, 34 | { 35 | anonymous: false, 36 | inputs: [ 37 | { 38 | indexed: false, 39 | internalType: "address", 40 | name: "userAddress", 41 | type: "address", 42 | }, 43 | { 44 | indexed: false, 45 | internalType: "address payable", 46 | name: "relayerAddress", 47 | type: "address", 48 | }, 49 | { 50 | indexed: false, 51 | internalType: "bytes", 52 | name: "functionSignature", 53 | type: "bytes", 54 | }, 55 | ], 56 | name: "MetaTransactionExecuted", 57 | type: "event", 58 | }, 59 | { 60 | anonymous: false, 61 | inputs: [ 62 | { indexed: true, internalType: "bytes32", name: "role", type: "bytes32" }, 63 | { 64 | indexed: true, 65 | internalType: "bytes32", 66 | name: "previousAdminRole", 67 | type: "bytes32", 68 | }, 69 | { 70 | indexed: true, 71 | internalType: "bytes32", 72 | name: "newAdminRole", 73 | type: "bytes32", 74 | }, 75 | ], 76 | name: "RoleAdminChanged", 77 | type: "event", 78 | }, 79 | { 80 | anonymous: false, 81 | inputs: [ 82 | { indexed: true, internalType: "bytes32", name: "role", type: "bytes32" }, 83 | { 84 | indexed: true, 85 | internalType: "address", 86 | name: "account", 87 | type: "address", 88 | }, 89 | { 90 | indexed: true, 91 | internalType: "address", 92 | name: "sender", 93 | type: "address", 94 | }, 95 | ], 96 | name: "RoleGranted", 97 | type: "event", 98 | }, 99 | { 100 | anonymous: false, 101 | inputs: [ 102 | { indexed: true, internalType: "bytes32", name: "role", type: "bytes32" }, 103 | { 104 | indexed: true, 105 | internalType: "address", 106 | name: "account", 107 | type: "address", 108 | }, 109 | { 110 | indexed: true, 111 | internalType: "address", 112 | name: "sender", 113 | type: "address", 114 | }, 115 | ], 116 | name: "RoleRevoked", 117 | type: "event", 118 | }, 119 | { 120 | anonymous: false, 121 | inputs: [ 122 | { indexed: true, internalType: "address", name: "from", type: "address" }, 123 | { indexed: true, internalType: "address", name: "to", type: "address" }, 124 | { 125 | indexed: false, 126 | internalType: "uint256", 127 | name: "value", 128 | type: "uint256", 129 | }, 130 | ], 131 | name: "Transfer", 132 | type: "event", 133 | }, 134 | { 135 | inputs: [], 136 | name: "CHILD_CHAIN_ID", 137 | outputs: [{ internalType: "uint256", name: "", type: "uint256" }], 138 | stateMutability: "view", 139 | type: "function", 140 | }, 141 | { 142 | inputs: [], 143 | name: "CHILD_CHAIN_ID_BYTES", 144 | outputs: [{ internalType: "bytes", name: "", type: "bytes" }], 145 | stateMutability: "view", 146 | type: "function", 147 | }, 148 | { 149 | inputs: [], 150 | name: "DEFAULT_ADMIN_ROLE", 151 | outputs: [{ internalType: "bytes32", name: "", type: "bytes32" }], 152 | stateMutability: "view", 153 | type: "function", 154 | }, 155 | { 156 | inputs: [], 157 | name: "DEPOSITOR_ROLE", 158 | outputs: [{ internalType: "bytes32", name: "", type: "bytes32" }], 159 | stateMutability: "view", 160 | type: "function", 161 | }, 162 | { 163 | inputs: [], 164 | name: "ERC712_VERSION", 165 | outputs: [{ internalType: "string", name: "", type: "string" }], 166 | stateMutability: "view", 167 | type: "function", 168 | }, 169 | { 170 | inputs: [], 171 | name: "ROOT_CHAIN_ID", 172 | outputs: [{ internalType: "uint256", name: "", type: "uint256" }], 173 | stateMutability: "view", 174 | type: "function", 175 | }, 176 | { 177 | inputs: [], 178 | name: "ROOT_CHAIN_ID_BYTES", 179 | outputs: [{ internalType: "bytes", name: "", type: "bytes" }], 180 | stateMutability: "view", 181 | type: "function", 182 | }, 183 | { 184 | inputs: [ 185 | { internalType: "address", name: "owner", type: "address" }, 186 | { internalType: "address", name: "spender", type: "address" }, 187 | ], 188 | name: "allowance", 189 | outputs: [{ internalType: "uint256", name: "", type: "uint256" }], 190 | stateMutability: "view", 191 | type: "function", 192 | }, 193 | { 194 | inputs: [ 195 | { internalType: "address", name: "spender", type: "address" }, 196 | { internalType: "uint256", name: "amount", type: "uint256" }, 197 | ], 198 | name: "approve", 199 | outputs: [{ internalType: "bool", name: "", type: "bool" }], 200 | stateMutability: "nonpayable", 201 | type: "function", 202 | }, 203 | { 204 | inputs: [{ internalType: "address", name: "account", type: "address" }], 205 | name: "balanceOf", 206 | outputs: [{ internalType: "uint256", name: "", type: "uint256" }], 207 | stateMutability: "view", 208 | type: "function", 209 | }, 210 | { 211 | inputs: [], 212 | name: "decimals", 213 | outputs: [{ internalType: "uint8", name: "", type: "uint8" }], 214 | stateMutability: "view", 215 | type: "function", 216 | }, 217 | { 218 | inputs: [ 219 | { internalType: "address", name: "spender", type: "address" }, 220 | { internalType: "uint256", name: "subtractedValue", type: "uint256" }, 221 | ], 222 | name: "decreaseAllowance", 223 | outputs: [{ internalType: "bool", name: "", type: "bool" }], 224 | stateMutability: "nonpayable", 225 | type: "function", 226 | }, 227 | { 228 | inputs: [ 229 | { internalType: "address", name: "user", type: "address" }, 230 | { internalType: "bytes", name: "depositData", type: "bytes" }, 231 | ], 232 | name: "deposit", 233 | outputs: [], 234 | stateMutability: "nonpayable", 235 | type: "function", 236 | }, 237 | { 238 | inputs: [ 239 | { internalType: "address", name: "userAddress", type: "address" }, 240 | { internalType: "bytes", name: "functionSignature", type: "bytes" }, 241 | { internalType: "bytes32", name: "sigR", type: "bytes32" }, 242 | { internalType: "bytes32", name: "sigS", type: "bytes32" }, 243 | { internalType: "uint8", name: "sigV", type: "uint8" }, 244 | ], 245 | name: "executeMetaTransaction", 246 | outputs: [{ internalType: "bytes", name: "", type: "bytes" }], 247 | stateMutability: "payable", 248 | type: "function", 249 | }, 250 | { 251 | inputs: [], 252 | name: "getChainId", 253 | outputs: [{ internalType: "uint256", name: "", type: "uint256" }], 254 | stateMutability: "pure", 255 | type: "function", 256 | }, 257 | { 258 | inputs: [], 259 | name: "getDomainSeperator", 260 | outputs: [{ internalType: "bytes32", name: "", type: "bytes32" }], 261 | stateMutability: "view", 262 | type: "function", 263 | }, 264 | { 265 | inputs: [{ internalType: "address", name: "user", type: "address" }], 266 | name: "getNonce", 267 | outputs: [{ internalType: "uint256", name: "nonce", type: "uint256" }], 268 | stateMutability: "view", 269 | type: "function", 270 | }, 271 | { 272 | inputs: [{ internalType: "bytes32", name: "role", type: "bytes32" }], 273 | name: "getRoleAdmin", 274 | outputs: [{ internalType: "bytes32", name: "", type: "bytes32" }], 275 | stateMutability: "view", 276 | type: "function", 277 | }, 278 | { 279 | inputs: [ 280 | { internalType: "bytes32", name: "role", type: "bytes32" }, 281 | { internalType: "uint256", name: "index", type: "uint256" }, 282 | ], 283 | name: "getRoleMember", 284 | outputs: [{ internalType: "address", name: "", type: "address" }], 285 | stateMutability: "view", 286 | type: "function", 287 | }, 288 | { 289 | inputs: [{ internalType: "bytes32", name: "role", type: "bytes32" }], 290 | name: "getRoleMemberCount", 291 | outputs: [{ internalType: "uint256", name: "", type: "uint256" }], 292 | stateMutability: "view", 293 | type: "function", 294 | }, 295 | { 296 | inputs: [ 297 | { internalType: "bytes32", name: "role", type: "bytes32" }, 298 | { internalType: "address", name: "account", type: "address" }, 299 | ], 300 | name: "grantRole", 301 | outputs: [], 302 | stateMutability: "nonpayable", 303 | type: "function", 304 | }, 305 | { 306 | inputs: [ 307 | { internalType: "bytes32", name: "role", type: "bytes32" }, 308 | { internalType: "address", name: "account", type: "address" }, 309 | ], 310 | name: "hasRole", 311 | outputs: [{ internalType: "bool", name: "", type: "bool" }], 312 | stateMutability: "view", 313 | type: "function", 314 | }, 315 | { 316 | inputs: [ 317 | { internalType: "address", name: "spender", type: "address" }, 318 | { internalType: "uint256", name: "addedValue", type: "uint256" }, 319 | ], 320 | name: "increaseAllowance", 321 | outputs: [{ internalType: "bool", name: "", type: "bool" }], 322 | stateMutability: "nonpayable", 323 | type: "function", 324 | }, 325 | { 326 | inputs: [], 327 | name: "name", 328 | outputs: [{ internalType: "string", name: "", type: "string" }], 329 | stateMutability: "view", 330 | type: "function", 331 | }, 332 | { 333 | inputs: [ 334 | { internalType: "bytes32", name: "role", type: "bytes32" }, 335 | { internalType: "address", name: "account", type: "address" }, 336 | ], 337 | name: "renounceRole", 338 | outputs: [], 339 | stateMutability: "nonpayable", 340 | type: "function", 341 | }, 342 | { 343 | inputs: [ 344 | { internalType: "bytes32", name: "role", type: "bytes32" }, 345 | { internalType: "address", name: "account", type: "address" }, 346 | ], 347 | name: "revokeRole", 348 | outputs: [], 349 | stateMutability: "nonpayable", 350 | type: "function", 351 | }, 352 | { 353 | inputs: [], 354 | name: "symbol", 355 | outputs: [{ internalType: "string", name: "", type: "string" }], 356 | stateMutability: "view", 357 | type: "function", 358 | }, 359 | { 360 | inputs: [], 361 | name: "totalSupply", 362 | outputs: [{ internalType: "uint256", name: "", type: "uint256" }], 363 | stateMutability: "view", 364 | type: "function", 365 | }, 366 | { 367 | inputs: [ 368 | { internalType: "address", name: "recipient", type: "address" }, 369 | { internalType: "uint256", name: "amount", type: "uint256" }, 370 | ], 371 | name: "transfer", 372 | outputs: [{ internalType: "bool", name: "", type: "bool" }], 373 | stateMutability: "nonpayable", 374 | type: "function", 375 | }, 376 | { 377 | inputs: [ 378 | { internalType: "address", name: "sender", type: "address" }, 379 | { internalType: "address", name: "recipient", type: "address" }, 380 | { internalType: "uint256", name: "amount", type: "uint256" }, 381 | ], 382 | name: "transferFrom", 383 | outputs: [{ internalType: "bool", name: "", type: "bool" }], 384 | stateMutability: "nonpayable", 385 | type: "function", 386 | }, 387 | { 388 | inputs: [{ internalType: "uint256", name: "amount", type: "uint256" }], 389 | name: "withdraw", 390 | outputs: [], 391 | stateMutability: "nonpayable", 392 | type: "function", 393 | }, 394 | ]; 395 | 396 | export const wmaticABI = [ 397 | { 398 | constant: true, 399 | inputs: [], 400 | name: "name", 401 | outputs: [{ name: "", type: "string" }], 402 | payable: false, 403 | stateMutability: "view", 404 | type: "function", 405 | }, 406 | { 407 | constant: false, 408 | inputs: [ 409 | { name: "guy", type: "address" }, 410 | { name: "wad", type: "uint256" }, 411 | ], 412 | name: "approve", 413 | outputs: [{ name: "", type: "bool" }], 414 | payable: false, 415 | stateMutability: "nonpayable", 416 | type: "function", 417 | }, 418 | { 419 | constant: true, 420 | inputs: [], 421 | name: "totalSupply", 422 | outputs: [{ name: "", type: "uint256" }], 423 | payable: false, 424 | stateMutability: "view", 425 | type: "function", 426 | }, 427 | { 428 | constant: false, 429 | inputs: [ 430 | { name: "src", type: "address" }, 431 | { name: "dst", type: "address" }, 432 | { name: "wad", type: "uint256" }, 433 | ], 434 | name: "transferFrom", 435 | outputs: [{ name: "", type: "bool" }], 436 | payable: false, 437 | stateMutability: "nonpayable", 438 | type: "function", 439 | }, 440 | { 441 | constant: false, 442 | inputs: [{ name: "wad", type: "uint256" }], 443 | name: "withdraw", 444 | outputs: [], 445 | payable: false, 446 | stateMutability: "nonpayable", 447 | type: "function", 448 | }, 449 | { 450 | constant: true, 451 | inputs: [], 452 | name: "decimals", 453 | outputs: [{ name: "", type: "uint8" }], 454 | payable: false, 455 | stateMutability: "view", 456 | type: "function", 457 | }, 458 | { 459 | constant: true, 460 | inputs: [{ name: "", type: "address" }], 461 | name: "balanceOf", 462 | outputs: [{ name: "", type: "uint256" }], 463 | payable: false, 464 | stateMutability: "view", 465 | type: "function", 466 | }, 467 | { 468 | constant: true, 469 | inputs: [], 470 | name: "symbol", 471 | outputs: [{ name: "", type: "string" }], 472 | payable: false, 473 | stateMutability: "view", 474 | type: "function", 475 | }, 476 | { 477 | constant: false, 478 | inputs: [ 479 | { name: "dst", type: "address" }, 480 | { name: "wad", type: "uint256" }, 481 | ], 482 | name: "transfer", 483 | outputs: [{ name: "", type: "bool" }], 484 | payable: false, 485 | stateMutability: "nonpayable", 486 | type: "function", 487 | }, 488 | { 489 | constant: false, 490 | inputs: [], 491 | name: "deposit", 492 | outputs: [], 493 | payable: true, 494 | stateMutability: "payable", 495 | type: "function", 496 | }, 497 | { 498 | constant: true, 499 | inputs: [ 500 | { name: "", type: "address" }, 501 | { name: "", type: "address" }, 502 | ], 503 | name: "allowance", 504 | outputs: [{ name: "", type: "uint256" }], 505 | payable: false, 506 | stateMutability: "view", 507 | type: "function", 508 | }, 509 | { payable: true, stateMutability: "payable", type: "fallback" }, 510 | { 511 | anonymous: false, 512 | inputs: [ 513 | { indexed: true, name: "src", type: "address" }, 514 | { indexed: true, name: "guy", type: "address" }, 515 | { indexed: false, name: "wad", type: "uint256" }, 516 | ], 517 | name: "Approval", 518 | type: "event", 519 | }, 520 | { 521 | anonymous: false, 522 | inputs: [ 523 | { indexed: true, name: "src", type: "address" }, 524 | { indexed: true, name: "dst", type: "address" }, 525 | { indexed: false, name: "wad", type: "uint256" }, 526 | ], 527 | name: "Transfer", 528 | type: "event", 529 | }, 530 | { 531 | anonymous: false, 532 | inputs: [ 533 | { indexed: true, name: "dst", type: "address" }, 534 | { indexed: false, name: "wad", type: "uint256" }, 535 | ], 536 | name: "Deposit", 537 | type: "event", 538 | }, 539 | { 540 | anonymous: false, 541 | inputs: [ 542 | { indexed: true, name: "src", type: "address" }, 543 | { indexed: false, name: "wad", type: "uint256" }, 544 | ], 545 | name: "Withdrawal", 546 | type: "event", 547 | }, 548 | ]; 549 | 550 | export const usdcABI = [ 551 | { 552 | anonymous: false, 553 | inputs: [ 554 | { 555 | indexed: true, 556 | internalType: "address", 557 | name: "owner", 558 | type: "address", 559 | }, 560 | { 561 | indexed: true, 562 | internalType: "address", 563 | name: "spender", 564 | type: "address", 565 | }, 566 | { 567 | indexed: false, 568 | internalType: "uint256", 569 | name: "value", 570 | type: "uint256", 571 | }, 572 | ], 573 | name: "Approval", 574 | type: "event", 575 | }, 576 | { 577 | anonymous: false, 578 | inputs: [ 579 | { 580 | indexed: true, 581 | internalType: "address", 582 | name: "authorizer", 583 | type: "address", 584 | }, 585 | { 586 | indexed: true, 587 | internalType: "bytes32", 588 | name: "nonce", 589 | type: "bytes32", 590 | }, 591 | ], 592 | name: "AuthorizationCanceled", 593 | type: "event", 594 | }, 595 | { 596 | anonymous: false, 597 | inputs: [ 598 | { 599 | indexed: true, 600 | internalType: "address", 601 | name: "authorizer", 602 | type: "address", 603 | }, 604 | { 605 | indexed: true, 606 | internalType: "bytes32", 607 | name: "nonce", 608 | type: "bytes32", 609 | }, 610 | ], 611 | name: "AuthorizationUsed", 612 | type: "event", 613 | }, 614 | { 615 | anonymous: false, 616 | inputs: [ 617 | { 618 | indexed: true, 619 | internalType: "address", 620 | name: "account", 621 | type: "address", 622 | }, 623 | ], 624 | name: "Blacklisted", 625 | type: "event", 626 | }, 627 | { 628 | anonymous: false, 629 | inputs: [ 630 | { 631 | indexed: false, 632 | internalType: "address", 633 | name: "userAddress", 634 | type: "address", 635 | }, 636 | { 637 | indexed: false, 638 | internalType: "address payable", 639 | name: "relayerAddress", 640 | type: "address", 641 | }, 642 | { 643 | indexed: false, 644 | internalType: "bytes", 645 | name: "functionSignature", 646 | type: "bytes", 647 | }, 648 | ], 649 | name: "MetaTransactionExecuted", 650 | type: "event", 651 | }, 652 | { anonymous: false, inputs: [], name: "Pause", type: "event" }, 653 | { 654 | anonymous: false, 655 | inputs: [ 656 | { 657 | indexed: true, 658 | internalType: "address", 659 | name: "newRescuer", 660 | type: "address", 661 | }, 662 | ], 663 | name: "RescuerChanged", 664 | type: "event", 665 | }, 666 | { 667 | anonymous: false, 668 | inputs: [ 669 | { indexed: true, internalType: "bytes32", name: "role", type: "bytes32" }, 670 | { 671 | indexed: true, 672 | internalType: "bytes32", 673 | name: "previousAdminRole", 674 | type: "bytes32", 675 | }, 676 | { 677 | indexed: true, 678 | internalType: "bytes32", 679 | name: "newAdminRole", 680 | type: "bytes32", 681 | }, 682 | ], 683 | name: "RoleAdminChanged", 684 | type: "event", 685 | }, 686 | { 687 | anonymous: false, 688 | inputs: [ 689 | { indexed: true, internalType: "bytes32", name: "role", type: "bytes32" }, 690 | { 691 | indexed: true, 692 | internalType: "address", 693 | name: "account", 694 | type: "address", 695 | }, 696 | { 697 | indexed: true, 698 | internalType: "address", 699 | name: "sender", 700 | type: "address", 701 | }, 702 | ], 703 | name: "RoleGranted", 704 | type: "event", 705 | }, 706 | { 707 | anonymous: false, 708 | inputs: [ 709 | { indexed: true, internalType: "bytes32", name: "role", type: "bytes32" }, 710 | { 711 | indexed: true, 712 | internalType: "address", 713 | name: "account", 714 | type: "address", 715 | }, 716 | { 717 | indexed: true, 718 | internalType: "address", 719 | name: "sender", 720 | type: "address", 721 | }, 722 | ], 723 | name: "RoleRevoked", 724 | type: "event", 725 | }, 726 | { 727 | anonymous: false, 728 | inputs: [ 729 | { indexed: true, internalType: "address", name: "from", type: "address" }, 730 | { indexed: true, internalType: "address", name: "to", type: "address" }, 731 | { 732 | indexed: false, 733 | internalType: "uint256", 734 | name: "value", 735 | type: "uint256", 736 | }, 737 | ], 738 | name: "Transfer", 739 | type: "event", 740 | }, 741 | { 742 | anonymous: false, 743 | inputs: [ 744 | { 745 | indexed: true, 746 | internalType: "address", 747 | name: "account", 748 | type: "address", 749 | }, 750 | ], 751 | name: "UnBlacklisted", 752 | type: "event", 753 | }, 754 | { anonymous: false, inputs: [], name: "Unpause", type: "event" }, 755 | { 756 | inputs: [], 757 | name: "APPROVE_WITH_AUTHORIZATION_TYPEHASH", 758 | outputs: [{ internalType: "bytes32", name: "", type: "bytes32" }], 759 | stateMutability: "view", 760 | type: "function", 761 | }, 762 | { 763 | inputs: [], 764 | name: "BLACKLISTER_ROLE", 765 | outputs: [{ internalType: "bytes32", name: "", type: "bytes32" }], 766 | stateMutability: "view", 767 | type: "function", 768 | }, 769 | { 770 | inputs: [], 771 | name: "CANCEL_AUTHORIZATION_TYPEHASH", 772 | outputs: [{ internalType: "bytes32", name: "", type: "bytes32" }], 773 | stateMutability: "view", 774 | type: "function", 775 | }, 776 | { 777 | inputs: [], 778 | name: "DECREASE_ALLOWANCE_WITH_AUTHORIZATION_TYPEHASH", 779 | outputs: [{ internalType: "bytes32", name: "", type: "bytes32" }], 780 | stateMutability: "view", 781 | type: "function", 782 | }, 783 | { 784 | inputs: [], 785 | name: "DEFAULT_ADMIN_ROLE", 786 | outputs: [{ internalType: "bytes32", name: "", type: "bytes32" }], 787 | stateMutability: "view", 788 | type: "function", 789 | }, 790 | { 791 | inputs: [], 792 | name: "DEPOSITOR_ROLE", 793 | outputs: [{ internalType: "bytes32", name: "", type: "bytes32" }], 794 | stateMutability: "view", 795 | type: "function", 796 | }, 797 | { 798 | inputs: [], 799 | name: "DOMAIN_SEPARATOR", 800 | outputs: [{ internalType: "bytes32", name: "", type: "bytes32" }], 801 | stateMutability: "view", 802 | type: "function", 803 | }, 804 | { 805 | inputs: [], 806 | name: "EIP712_VERSION", 807 | outputs: [{ internalType: "string", name: "", type: "string" }], 808 | stateMutability: "view", 809 | type: "function", 810 | }, 811 | { 812 | inputs: [], 813 | name: "INCREASE_ALLOWANCE_WITH_AUTHORIZATION_TYPEHASH", 814 | outputs: [{ internalType: "bytes32", name: "", type: "bytes32" }], 815 | stateMutability: "view", 816 | type: "function", 817 | }, 818 | { 819 | inputs: [], 820 | name: "META_TRANSACTION_TYPEHASH", 821 | outputs: [{ internalType: "bytes32", name: "", type: "bytes32" }], 822 | stateMutability: "view", 823 | type: "function", 824 | }, 825 | { 826 | inputs: [], 827 | name: "PAUSER_ROLE", 828 | outputs: [{ internalType: "bytes32", name: "", type: "bytes32" }], 829 | stateMutability: "view", 830 | type: "function", 831 | }, 832 | { 833 | inputs: [], 834 | name: "PERMIT_TYPEHASH", 835 | outputs: [{ internalType: "bytes32", name: "", type: "bytes32" }], 836 | stateMutability: "view", 837 | type: "function", 838 | }, 839 | { 840 | inputs: [], 841 | name: "RESCUER_ROLE", 842 | outputs: [{ internalType: "bytes32", name: "", type: "bytes32" }], 843 | stateMutability: "view", 844 | type: "function", 845 | }, 846 | { 847 | inputs: [], 848 | name: "TRANSFER_WITH_AUTHORIZATION_TYPEHASH", 849 | outputs: [{ internalType: "bytes32", name: "", type: "bytes32" }], 850 | stateMutability: "view", 851 | type: "function", 852 | }, 853 | { 854 | inputs: [], 855 | name: "WITHDRAW_WITH_AUTHORIZATION_TYPEHASH", 856 | outputs: [{ internalType: "bytes32", name: "", type: "bytes32" }], 857 | stateMutability: "view", 858 | type: "function", 859 | }, 860 | { 861 | inputs: [ 862 | { internalType: "address", name: "owner", type: "address" }, 863 | { internalType: "address", name: "spender", type: "address" }, 864 | ], 865 | name: "allowance", 866 | outputs: [{ internalType: "uint256", name: "", type: "uint256" }], 867 | stateMutability: "view", 868 | type: "function", 869 | }, 870 | { 871 | inputs: [ 872 | { internalType: "address", name: "spender", type: "address" }, 873 | { internalType: "uint256", name: "amount", type: "uint256" }, 874 | ], 875 | name: "approve", 876 | outputs: [{ internalType: "bool", name: "", type: "bool" }], 877 | stateMutability: "nonpayable", 878 | type: "function", 879 | }, 880 | { 881 | inputs: [ 882 | { internalType: "address", name: "owner", type: "address" }, 883 | { internalType: "address", name: "spender", type: "address" }, 884 | { internalType: "uint256", name: "value", type: "uint256" }, 885 | { internalType: "uint256", name: "validAfter", type: "uint256" }, 886 | { internalType: "uint256", name: "validBefore", type: "uint256" }, 887 | { internalType: "bytes32", name: "nonce", type: "bytes32" }, 888 | { internalType: "uint8", name: "v", type: "uint8" }, 889 | { internalType: "bytes32", name: "r", type: "bytes32" }, 890 | { internalType: "bytes32", name: "s", type: "bytes32" }, 891 | ], 892 | name: "approveWithAuthorization", 893 | outputs: [], 894 | stateMutability: "nonpayable", 895 | type: "function", 896 | }, 897 | { 898 | inputs: [ 899 | { internalType: "address", name: "authorizer", type: "address" }, 900 | { internalType: "bytes32", name: "nonce", type: "bytes32" }, 901 | ], 902 | name: "authorizationState", 903 | outputs: [ 904 | { 905 | internalType: "enum GasAbstraction.AuthorizationState", 906 | name: "", 907 | type: "uint8", 908 | }, 909 | ], 910 | stateMutability: "view", 911 | type: "function", 912 | }, 913 | { 914 | inputs: [{ internalType: "address", name: "account", type: "address" }], 915 | name: "balanceOf", 916 | outputs: [{ internalType: "uint256", name: "", type: "uint256" }], 917 | stateMutability: "view", 918 | type: "function", 919 | }, 920 | { 921 | inputs: [{ internalType: "address", name: "account", type: "address" }], 922 | name: "blacklist", 923 | outputs: [], 924 | stateMutability: "nonpayable", 925 | type: "function", 926 | }, 927 | { 928 | inputs: [], 929 | name: "blacklisters", 930 | outputs: [{ internalType: "address[]", name: "", type: "address[]" }], 931 | stateMutability: "view", 932 | type: "function", 933 | }, 934 | { 935 | inputs: [ 936 | { internalType: "address", name: "authorizer", type: "address" }, 937 | { internalType: "bytes32", name: "nonce", type: "bytes32" }, 938 | { internalType: "uint8", name: "v", type: "uint8" }, 939 | { internalType: "bytes32", name: "r", type: "bytes32" }, 940 | { internalType: "bytes32", name: "s", type: "bytes32" }, 941 | ], 942 | name: "cancelAuthorization", 943 | outputs: [], 944 | stateMutability: "nonpayable", 945 | type: "function", 946 | }, 947 | { 948 | inputs: [], 949 | name: "decimals", 950 | outputs: [{ internalType: "uint8", name: "", type: "uint8" }], 951 | stateMutability: "view", 952 | type: "function", 953 | }, 954 | { 955 | inputs: [ 956 | { internalType: "address", name: "spender", type: "address" }, 957 | { internalType: "uint256", name: "subtractedValue", type: "uint256" }, 958 | ], 959 | name: "decreaseAllowance", 960 | outputs: [{ internalType: "bool", name: "", type: "bool" }], 961 | stateMutability: "nonpayable", 962 | type: "function", 963 | }, 964 | { 965 | inputs: [ 966 | { internalType: "address", name: "owner", type: "address" }, 967 | { internalType: "address", name: "spender", type: "address" }, 968 | { internalType: "uint256", name: "decrement", type: "uint256" }, 969 | { internalType: "uint256", name: "validAfter", type: "uint256" }, 970 | { internalType: "uint256", name: "validBefore", type: "uint256" }, 971 | { internalType: "bytes32", name: "nonce", type: "bytes32" }, 972 | { internalType: "uint8", name: "v", type: "uint8" }, 973 | { internalType: "bytes32", name: "r", type: "bytes32" }, 974 | { internalType: "bytes32", name: "s", type: "bytes32" }, 975 | ], 976 | name: "decreaseAllowanceWithAuthorization", 977 | outputs: [], 978 | stateMutability: "nonpayable", 979 | type: "function", 980 | }, 981 | { 982 | inputs: [ 983 | { internalType: "address", name: "user", type: "address" }, 984 | { internalType: "bytes", name: "depositData", type: "bytes" }, 985 | ], 986 | name: "deposit", 987 | outputs: [], 988 | stateMutability: "nonpayable", 989 | type: "function", 990 | }, 991 | { 992 | inputs: [ 993 | { internalType: "address", name: "userAddress", type: "address" }, 994 | { internalType: "bytes", name: "functionSignature", type: "bytes" }, 995 | { internalType: "bytes32", name: "sigR", type: "bytes32" }, 996 | { internalType: "bytes32", name: "sigS", type: "bytes32" }, 997 | { internalType: "uint8", name: "sigV", type: "uint8" }, 998 | ], 999 | name: "executeMetaTransaction", 1000 | outputs: [{ internalType: "bytes", name: "", type: "bytes" }], 1001 | stateMutability: "payable", 1002 | type: "function", 1003 | }, 1004 | { 1005 | inputs: [{ internalType: "bytes32", name: "role", type: "bytes32" }], 1006 | name: "getRoleAdmin", 1007 | outputs: [{ internalType: "bytes32", name: "", type: "bytes32" }], 1008 | stateMutability: "view", 1009 | type: "function", 1010 | }, 1011 | { 1012 | inputs: [ 1013 | { internalType: "bytes32", name: "role", type: "bytes32" }, 1014 | { internalType: "uint256", name: "index", type: "uint256" }, 1015 | ], 1016 | name: "getRoleMember", 1017 | outputs: [{ internalType: "address", name: "", type: "address" }], 1018 | stateMutability: "view", 1019 | type: "function", 1020 | }, 1021 | { 1022 | inputs: [{ internalType: "bytes32", name: "role", type: "bytes32" }], 1023 | name: "getRoleMemberCount", 1024 | outputs: [{ internalType: "uint256", name: "", type: "uint256" }], 1025 | stateMutability: "view", 1026 | type: "function", 1027 | }, 1028 | { 1029 | inputs: [ 1030 | { internalType: "bytes32", name: "role", type: "bytes32" }, 1031 | { internalType: "address", name: "account", type: "address" }, 1032 | ], 1033 | name: "grantRole", 1034 | outputs: [], 1035 | stateMutability: "nonpayable", 1036 | type: "function", 1037 | }, 1038 | { 1039 | inputs: [ 1040 | { internalType: "bytes32", name: "role", type: "bytes32" }, 1041 | { internalType: "address", name: "account", type: "address" }, 1042 | ], 1043 | name: "hasRole", 1044 | outputs: [{ internalType: "bool", name: "", type: "bool" }], 1045 | stateMutability: "view", 1046 | type: "function", 1047 | }, 1048 | { 1049 | inputs: [ 1050 | { internalType: "address", name: "spender", type: "address" }, 1051 | { internalType: "uint256", name: "addedValue", type: "uint256" }, 1052 | ], 1053 | name: "increaseAllowance", 1054 | outputs: [{ internalType: "bool", name: "", type: "bool" }], 1055 | stateMutability: "nonpayable", 1056 | type: "function", 1057 | }, 1058 | { 1059 | inputs: [ 1060 | { internalType: "address", name: "owner", type: "address" }, 1061 | { internalType: "address", name: "spender", type: "address" }, 1062 | { internalType: "uint256", name: "increment", type: "uint256" }, 1063 | { internalType: "uint256", name: "validAfter", type: "uint256" }, 1064 | { internalType: "uint256", name: "validBefore", type: "uint256" }, 1065 | { internalType: "bytes32", name: "nonce", type: "bytes32" }, 1066 | { internalType: "uint8", name: "v", type: "uint8" }, 1067 | { internalType: "bytes32", name: "r", type: "bytes32" }, 1068 | { internalType: "bytes32", name: "s", type: "bytes32" }, 1069 | ], 1070 | name: "increaseAllowanceWithAuthorization", 1071 | outputs: [], 1072 | stateMutability: "nonpayable", 1073 | type: "function", 1074 | }, 1075 | { 1076 | inputs: [ 1077 | { internalType: "string", name: "newName", type: "string" }, 1078 | { internalType: "string", name: "newSymbol", type: "string" }, 1079 | { internalType: "uint8", name: "newDecimals", type: "uint8" }, 1080 | { internalType: "address", name: "childChainManager", type: "address" }, 1081 | ], 1082 | name: "initialize", 1083 | outputs: [], 1084 | stateMutability: "nonpayable", 1085 | type: "function", 1086 | }, 1087 | { 1088 | inputs: [], 1089 | name: "initialized", 1090 | outputs: [{ internalType: "bool", name: "", type: "bool" }], 1091 | stateMutability: "view", 1092 | type: "function", 1093 | }, 1094 | { 1095 | inputs: [{ internalType: "address", name: "account", type: "address" }], 1096 | name: "isBlacklisted", 1097 | outputs: [{ internalType: "bool", name: "", type: "bool" }], 1098 | stateMutability: "view", 1099 | type: "function", 1100 | }, 1101 | { 1102 | inputs: [], 1103 | name: "name", 1104 | outputs: [{ internalType: "string", name: "", type: "string" }], 1105 | stateMutability: "view", 1106 | type: "function", 1107 | }, 1108 | { 1109 | inputs: [{ internalType: "address", name: "owner", type: "address" }], 1110 | name: "nonces", 1111 | outputs: [{ internalType: "uint256", name: "", type: "uint256" }], 1112 | stateMutability: "view", 1113 | type: "function", 1114 | }, 1115 | { 1116 | inputs: [], 1117 | name: "pause", 1118 | outputs: [], 1119 | stateMutability: "nonpayable", 1120 | type: "function", 1121 | }, 1122 | { 1123 | inputs: [], 1124 | name: "paused", 1125 | outputs: [{ internalType: "bool", name: "", type: "bool" }], 1126 | stateMutability: "view", 1127 | type: "function", 1128 | }, 1129 | { 1130 | inputs: [], 1131 | name: "pausers", 1132 | outputs: [{ internalType: "address[]", name: "", type: "address[]" }], 1133 | stateMutability: "view", 1134 | type: "function", 1135 | }, 1136 | { 1137 | inputs: [ 1138 | { internalType: "address", name: "owner", type: "address" }, 1139 | { internalType: "address", name: "spender", type: "address" }, 1140 | { internalType: "uint256", name: "value", type: "uint256" }, 1141 | { internalType: "uint256", name: "deadline", type: "uint256" }, 1142 | { internalType: "uint8", name: "v", type: "uint8" }, 1143 | { internalType: "bytes32", name: "r", type: "bytes32" }, 1144 | { internalType: "bytes32", name: "s", type: "bytes32" }, 1145 | ], 1146 | name: "permit", 1147 | outputs: [], 1148 | stateMutability: "nonpayable", 1149 | type: "function", 1150 | }, 1151 | { 1152 | inputs: [ 1153 | { internalType: "bytes32", name: "role", type: "bytes32" }, 1154 | { internalType: "address", name: "account", type: "address" }, 1155 | ], 1156 | name: "renounceRole", 1157 | outputs: [], 1158 | stateMutability: "nonpayable", 1159 | type: "function", 1160 | }, 1161 | { 1162 | inputs: [ 1163 | { 1164 | internalType: "contract IERC20", 1165 | name: "tokenContract", 1166 | type: "address", 1167 | }, 1168 | { internalType: "address", name: "to", type: "address" }, 1169 | { internalType: "uint256", name: "amount", type: "uint256" }, 1170 | ], 1171 | name: "rescueERC20", 1172 | outputs: [], 1173 | stateMutability: "nonpayable", 1174 | type: "function", 1175 | }, 1176 | { 1177 | inputs: [], 1178 | name: "rescuers", 1179 | outputs: [{ internalType: "address[]", name: "", type: "address[]" }], 1180 | stateMutability: "view", 1181 | type: "function", 1182 | }, 1183 | { 1184 | inputs: [ 1185 | { internalType: "bytes32", name: "role", type: "bytes32" }, 1186 | { internalType: "address", name: "account", type: "address" }, 1187 | ], 1188 | name: "revokeRole", 1189 | outputs: [], 1190 | stateMutability: "nonpayable", 1191 | type: "function", 1192 | }, 1193 | { 1194 | inputs: [], 1195 | name: "symbol", 1196 | outputs: [{ internalType: "string", name: "", type: "string" }], 1197 | stateMutability: "view", 1198 | type: "function", 1199 | }, 1200 | { 1201 | inputs: [], 1202 | name: "totalSupply", 1203 | outputs: [{ internalType: "uint256", name: "", type: "uint256" }], 1204 | stateMutability: "view", 1205 | type: "function", 1206 | }, 1207 | { 1208 | inputs: [ 1209 | { internalType: "address", name: "recipient", type: "address" }, 1210 | { internalType: "uint256", name: "amount", type: "uint256" }, 1211 | ], 1212 | name: "transfer", 1213 | outputs: [{ internalType: "bool", name: "", type: "bool" }], 1214 | stateMutability: "nonpayable", 1215 | type: "function", 1216 | }, 1217 | { 1218 | inputs: [ 1219 | { internalType: "address", name: "sender", type: "address" }, 1220 | { internalType: "address", name: "recipient", type: "address" }, 1221 | { internalType: "uint256", name: "amount", type: "uint256" }, 1222 | ], 1223 | name: "transferFrom", 1224 | outputs: [{ internalType: "bool", name: "", type: "bool" }], 1225 | stateMutability: "nonpayable", 1226 | type: "function", 1227 | }, 1228 | { 1229 | inputs: [ 1230 | { internalType: "address", name: "from", type: "address" }, 1231 | { internalType: "address", name: "to", type: "address" }, 1232 | { internalType: "uint256", name: "value", type: "uint256" }, 1233 | { internalType: "uint256", name: "validAfter", type: "uint256" }, 1234 | { internalType: "uint256", name: "validBefore", type: "uint256" }, 1235 | { internalType: "bytes32", name: "nonce", type: "bytes32" }, 1236 | { internalType: "uint8", name: "v", type: "uint8" }, 1237 | { internalType: "bytes32", name: "r", type: "bytes32" }, 1238 | { internalType: "bytes32", name: "s", type: "bytes32" }, 1239 | ], 1240 | name: "transferWithAuthorization", 1241 | outputs: [], 1242 | stateMutability: "nonpayable", 1243 | type: "function", 1244 | }, 1245 | { 1246 | inputs: [{ internalType: "address", name: "account", type: "address" }], 1247 | name: "unBlacklist", 1248 | outputs: [], 1249 | stateMutability: "nonpayable", 1250 | type: "function", 1251 | }, 1252 | { 1253 | inputs: [], 1254 | name: "unpause", 1255 | outputs: [], 1256 | stateMutability: "nonpayable", 1257 | type: "function", 1258 | }, 1259 | { 1260 | inputs: [ 1261 | { internalType: "string", name: "newName", type: "string" }, 1262 | { internalType: "string", name: "newSymbol", type: "string" }, 1263 | ], 1264 | name: "updateMetadata", 1265 | outputs: [], 1266 | stateMutability: "nonpayable", 1267 | type: "function", 1268 | }, 1269 | { 1270 | inputs: [{ internalType: "uint256", name: "amount", type: "uint256" }], 1271 | name: "withdraw", 1272 | outputs: [], 1273 | stateMutability: "nonpayable", 1274 | type: "function", 1275 | }, 1276 | { 1277 | inputs: [ 1278 | { internalType: "address", name: "owner", type: "address" }, 1279 | { internalType: "uint256", name: "value", type: "uint256" }, 1280 | { internalType: "uint256", name: "validAfter", type: "uint256" }, 1281 | { internalType: "uint256", name: "validBefore", type: "uint256" }, 1282 | { internalType: "bytes32", name: "nonce", type: "bytes32" }, 1283 | { internalType: "uint8", name: "v", type: "uint8" }, 1284 | { internalType: "bytes32", name: "r", type: "bytes32" }, 1285 | { internalType: "bytes32", name: "s", type: "bytes32" }, 1286 | ], 1287 | name: "withdrawWithAuthorization", 1288 | outputs: [], 1289 | stateMutability: "nonpayable", 1290 | type: "function", 1291 | }, 1292 | ]; 1293 | -------------------------------------------------------------------------------- /utils/addresses.ts: -------------------------------------------------------------------------------- 1 | interface IfcAddresses { 2 | myAddress: string; 3 | bct: string; 4 | nct: string; 5 | usdc: string; 6 | weth: string; 7 | wmatic: string; 8 | } 9 | 10 | const addresses: IfcAddresses = { 11 | myAddress: "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266", 12 | bct: "0x2F800Db0fdb5223b3C3f354886d907A671414A7F", 13 | nct: "0xD838290e877E0188a4A44700463419ED96c16107", 14 | usdc: "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174", 15 | weth: "0x7ceB23fD6bC0adD59E62ac25578270cFf1b9f619", 16 | wmatic: "0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270", 17 | }; 18 | 19 | export const mumbaiAddresses: IfcAddresses = { 20 | myAddress: "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266", 21 | bct: "0xf2438A14f668b1bbA53408346288f3d7C71c10a1", 22 | nct: "0x7beCBA11618Ca63Ead5605DE235f6dD3b25c530E", 23 | usdc: "0xe6b8a5CF854791412c1f6EFC7CAf629f5Df1c747", 24 | weth: "0xA6FA4fB5f76172d178d61B04b0ecd319C5d1C0aa", 25 | wmatic: "0x9c3C9283D3e44854697Cd22D3Faa240Cfb032889", 26 | }; 27 | 28 | export default addresses; 29 | -------------------------------------------------------------------------------- /utils/getTotalTCO2sHeld.ts: -------------------------------------------------------------------------------- 1 | import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; 2 | import { BigNumber } from "ethers"; 3 | import { ethers } from "hardhat"; 4 | import { parseEther } from "ethers/lib/utils"; 5 | import { OffsetHelper } from "../typechain"; 6 | import { IToucanPoolToken, IToucanCarbonOffsets } from "../typechain"; 7 | import * as tcoContract from "../artifacts/contracts/interfaces/IToucanCarbonOffsets.sol/IToucanCarbonOffsets.json"; 8 | 9 | const getTotalTCO2sHeld = async ( 10 | pooltoken: IToucanPoolToken, 11 | offsetHelper: OffsetHelper, 12 | owner: SignerWithAddress 13 | ): Promise => { 14 | const scoredTCO2s = await pooltoken.getScoredTCO2s(); 15 | 16 | let tokenContract: IToucanCarbonOffsets; 17 | let totalTCO2sHeld = parseEther("0.0"); 18 | 19 | await Promise.all( 20 | scoredTCO2s.map(async (token: string) => { 21 | // @ts-ignore 22 | tokenContract = new ethers.Contract(token, tcoContract.abi, owner); 23 | const balance = await tokenContract.balanceOf(offsetHelper.address); 24 | totalTCO2sHeld = totalTCO2sHeld.add(balance); 25 | }) 26 | ); 27 | 28 | return totalTCO2sHeld; 29 | }; 30 | 31 | export default getTotalTCO2sHeld; 32 | -------------------------------------------------------------------------------- /utils/impersonateAccount.ts: -------------------------------------------------------------------------------- 1 | import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; 2 | import { parseEther } from "ethers/lib/utils"; 3 | import { ethers, network } from "hardhat"; 4 | import addresses from "./addresses"; 5 | 6 | const impersonateAccount = async ( 7 | oldAddress: string, 8 | addressToImpersonate: string 9 | ): Promise => { 10 | await network.provider.request({ 11 | method: "hardhat_stopImpersonatingAccount", 12 | params: [oldAddress], 13 | }); 14 | await network.provider.request({ 15 | method: "hardhat_impersonateAccount", 16 | params: [addressToImpersonate], 17 | }); 18 | const signer: SignerWithAddress = await ethers.getSigner( 19 | addressToImpersonate 20 | ); 21 | return signer; 22 | }; 23 | 24 | export default impersonateAccount; 25 | -------------------------------------------------------------------------------- /utils/tokens.ts: -------------------------------------------------------------------------------- 1 | export const tokens = ["BCT", "NCT", "USDC", "WETH", "WMATIC"]; 2 | --------------------------------------------------------------------------------