├── test ├── .gitkeep ├── utils.spec.js ├── 00_WhiteAggregator.test.js └── 01_WhiteDebridge.test.js ├── .gitignore ├── contracts ├── interfaces │ ├── IDefiController.sol │ ├── IFeeProxy.sol │ ├── IUniswapV2Callee.sol │ ├── IWrappedAsset.sol │ ├── IERC677Receiver.sol │ ├── ILinkToken.sol │ ├── ILightDarkDebridge.sol │ ├── ILightWhiteDebridge.sol │ ├── IWhiteAggregator.sol │ ├── IWETH.sol │ ├── IUniswapV2Factory.sol │ ├── IDarkDebridge.sol │ ├── IWhiteDebridge.sol │ ├── IUniswapV2ERC20.sol │ └── IUniswapV2Pair.sol ├── periphery │ ├── DefiController.sol │ ├── FeeProxy.sol │ └── WrappedAsset.sol ├── mock │ ├── MockToken.sol │ └── MockLinkToken.sol ├── chainlink │ ├── CommitmentAggregator.sol │ ├── WithdrawalAggregator.sol │ ├── WhiteAggregator.sol │ └── Aggregator.sol └── transfers │ └── WhiteDebridge.sol ├── migrations ├── 4_defi_controller_migration.js ├── 3_fee_proxy_migration.js ├── 2_white_aggregator_payment_migration.js ├── 1_white_aggregator_migration.js ├── 5_white_debridge_migration.js ├── 6_white_debridge_asset_migration.js └── utils.js ├── LICENSE ├── package.json ├── assets ├── supportedChains.json └── debridgeInitParams.json ├── README.md ├── truffle-config.js └── Test.md /test/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .env -------------------------------------------------------------------------------- /test/utils.spec.js: -------------------------------------------------------------------------------- 1 | const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"; 2 | 3 | module.exports.ZERO_ADDRESS = ZERO_ADDRESS; 4 | -------------------------------------------------------------------------------- /contracts/interfaces/IDefiController.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.2; 3 | 4 | interface IDefiController { 5 | function claimReserve(address _tokenAddress, uint256 _amount) external; 6 | } 7 | -------------------------------------------------------------------------------- /migrations/4_defi_controller_migration.js: -------------------------------------------------------------------------------- 1 | const DefiController = artifacts.require("DefiController"); 2 | 3 | module.exports = async function (deployer, network) { 4 | if (network == "test") return; 5 | 6 | await deployer.deploy(DefiController); 7 | }; 8 | -------------------------------------------------------------------------------- /contracts/interfaces/IFeeProxy.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.2; 3 | 4 | interface IFeeProxy { 5 | function swapToLink( 6 | address _erc20Token, 7 | uint256 _amount, 8 | address _receiver 9 | ) external; 10 | } 11 | -------------------------------------------------------------------------------- /contracts/interfaces/IUniswapV2Callee.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.2; 3 | 4 | interface IUniswapV2Callee { 5 | function uniswapV2Call( 6 | address sender, 7 | uint256 amount0, 8 | uint256 amount1, 9 | bytes calldata data 10 | ) external; 11 | } 12 | -------------------------------------------------------------------------------- /contracts/interfaces/IWrappedAsset.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.2; 3 | 4 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 5 | 6 | interface IWrappedAsset is IERC20 { 7 | function mint(address _receiver, uint256 _amount) external; 8 | 9 | function burn(uint256 _amount) external; 10 | } 11 | -------------------------------------------------------------------------------- /contracts/interfaces/IERC677Receiver.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.2; 3 | 4 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 5 | 6 | interface IERC677Receiver is IERC20 { 7 | function onTokenTransfer( 8 | address _sender, 9 | uint256 _value, 10 | bytes memory _data 11 | ) external; 12 | } 13 | -------------------------------------------------------------------------------- /contracts/interfaces/ILinkToken.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.2; 3 | 4 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 5 | 6 | interface ILinkToken is IERC20 { 7 | function transferAndCall( 8 | address to, 9 | uint256 value, 10 | bytes memory data 11 | ) external returns (bool success); 12 | } 13 | -------------------------------------------------------------------------------- /contracts/interfaces/ILightDarkDebridge.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.2; 3 | 4 | interface ILightDarkDebridge { 5 | function deposit(bytes32 _commitment) external payable; 6 | 7 | function withdraw( 8 | address payable _recipient, 9 | address payable _relayer, 10 | uint256 _fee, 11 | uint256 _refund 12 | ) external payable; 13 | } 14 | -------------------------------------------------------------------------------- /contracts/periphery/DefiController.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.2; 3 | 4 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 5 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 6 | import "@openzeppelin/contracts/access/AccessControl.sol"; 7 | import "../interfaces/IUniswapV2Pair.sol"; 8 | import "../interfaces/IUniswapV2Factory.sol"; 9 | 10 | contract DefiController is AccessControl { 11 | constructor() {} 12 | } 13 | -------------------------------------------------------------------------------- /migrations/3_fee_proxy_migration.js: -------------------------------------------------------------------------------- 1 | const FeeProxy = artifacts.require("FeeProxy"); 2 | const { getLinkAddress, getUniswapFactory } = require("./utils"); 3 | 4 | module.exports = async function (deployer, network, accounts) { 5 | if (network == "test") return; 6 | 7 | const link = await getLinkAddress(deployer, network, accounts); 8 | const uniswapFactory = await getUniswapFactory(deployer, network); 9 | 10 | await deployer.deploy(FeeProxy, link, uniswapFactory); 11 | }; 12 | -------------------------------------------------------------------------------- /contracts/interfaces/ILightWhiteDebridge.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.2; 3 | 4 | interface ILightWhiteDebridge { 5 | function deposit( 6 | bytes32 _debridgeId, 7 | uint256 _amount, 8 | address _receiver, 9 | uint256 _networkId 10 | ) external payable; 11 | 12 | function withdraw( 13 | bytes32 _debridgeId, 14 | uint256 _amount, 15 | address _receiver, 16 | uint256 _networkId 17 | ) external payable; 18 | } 19 | -------------------------------------------------------------------------------- /contracts/interfaces/IWhiteAggregator.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.2; 3 | 4 | import "@openzeppelin/contracts/access/AccessControl.sol"; 5 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 6 | import "../interfaces/IWhiteDebridge.sol"; 7 | 8 | interface IWhiteAggregator { 9 | function submitMint(bytes32 _mintId) external; 10 | 11 | function submitBurn(bytes32 _burntId) external; 12 | 13 | function isMintConfirmed(bytes32 _mintId) external view returns (bool); 14 | 15 | function isBurntConfirmed(bytes32 _burntId) external view returns (bool); 16 | } 17 | -------------------------------------------------------------------------------- /migrations/2_white_aggregator_payment_migration.js: -------------------------------------------------------------------------------- 1 | const WhiteAggregator = artifacts.require("WhiteAggregator"); 2 | const ILinkToken = artifacts.require("ILinkToken"); 3 | const { getLinkAddress } = require("./utils"); 4 | 5 | module.exports = async function (deployer, network) { 6 | if (network == "test") return; 7 | 8 | let amount = web3.utils.toWei("1"); 9 | const link = await getLinkAddress(deployer, network); 10 | 11 | const linkTokenInstance = await ILinkToken.at(link); 12 | await linkTokenInstance.transferAndCall( 13 | WhiteAggregator.address.toString(), 14 | amount, 15 | "0x" 16 | ); 17 | }; 18 | -------------------------------------------------------------------------------- /migrations/1_white_aggregator_migration.js: -------------------------------------------------------------------------------- 1 | const WhiteAggregator = artifacts.require("WhiteAggregator"); 2 | const { getLinkAddress } = require("./utils"); 3 | 4 | module.exports = async function (deployer, network, accounts) { 5 | if (network == "test") return; 6 | const link = getLinkAddress(deployer, network, accounts); 7 | const debridgeInitParams = require("../assets/debridgeInitParams")[network]; 8 | await deployer.deploy( 9 | WhiteAggregator, 10 | debridgeInitParams.oracleCount, 11 | debridgeInitParams.oraclePayment, 12 | link 13 | ); 14 | 15 | const whiteAggregatorInstance = await WhiteAggregator.deployed(); 16 | for (let oracle of debridgeInitParams.oracles) { 17 | await whiteAggregatorInstance.addOracle(oracle.address, oracle.admin); 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /contracts/interfaces/IWETH.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.2; 3 | 4 | interface IWETH { 5 | function deposit() external payable; 6 | 7 | function withdraw(uint256 wad) external; 8 | 9 | function totalSupply() external view returns (uint256); 10 | 11 | function balanceOf(address account) external view returns (uint256); 12 | 13 | function transfer(address recipient, uint256 amount) 14 | external 15 | returns (bool); 16 | 17 | function allowance(address owner, address spender) 18 | external 19 | view 20 | returns (uint256); 21 | 22 | function approve(address spender, uint256 amount) external returns (bool); 23 | 24 | function transferFrom( 25 | address sender, 26 | address recipient, 27 | uint256 amount 28 | ) external returns (bool); 29 | } 30 | -------------------------------------------------------------------------------- /contracts/mock/MockToken.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.2; 3 | 4 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 5 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 6 | import "@openzeppelin/contracts/access/AccessControl.sol"; 7 | 8 | contract MockToken is ERC20 { 9 | uint8 private _decimals; 10 | 11 | constructor( 12 | string memory _name, 13 | string memory _symbol, 14 | uint8 _decimal 15 | ) ERC20(_name, _symbol) { 16 | _decimals = _decimal; 17 | } 18 | 19 | function mint(address _receiver, uint256 _amount) external { 20 | _mint(_receiver, _amount); 21 | } 22 | 23 | function burn(uint256 _amount) external { 24 | _burn(msg.sender, _amount); 25 | } 26 | 27 | function decimals() public view override returns (uint8) { 28 | return _decimals; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /migrations/5_white_debridge_migration.js: -------------------------------------------------------------------------------- 1 | const WhiteDebridge = artifacts.require("WhiteDebridge"); 2 | const WhiteAggregator = artifacts.require("WhiteAggregator"); 3 | const FeeProxy = artifacts.require("FeeProxy"); 4 | const DefiController = artifacts.require("DefiController"); 5 | const { getWeth } = require("./utils"); 6 | 7 | module.exports = async function (deployer, network) { 8 | if (network == "test") return; 9 | 10 | const debridgeInitParams = require("../assets/debridgeInitParams")[network]; 11 | let weth = await getWeth(deployer, network); 12 | 13 | await deployer.deploy( 14 | WhiteDebridge, 15 | debridgeInitParams.minTransferAmount, 16 | debridgeInitParams.transferFee, 17 | debridgeInitParams.minReserves, 18 | WhiteAggregator.address.toString(), 19 | debridgeInitParams.supportedChains, 20 | weth, 21 | FeeProxy.address.toString(), 22 | DefiController.address.toString() 23 | ); 24 | }; 25 | -------------------------------------------------------------------------------- /contracts/interfaces/IUniswapV2Factory.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.2; 3 | 4 | interface IUniswapV2Factory { 5 | event PairCreated( 6 | address indexed token0, 7 | address indexed token1, 8 | address pair, 9 | uint256 10 | ); 11 | 12 | function feeTo() external view returns (address); 13 | 14 | function feeToSetter() external view returns (address); 15 | 16 | function getPair(address tokenA, address tokenB) 17 | external 18 | view 19 | returns (address pair); 20 | 21 | function allPairs(uint256) external view returns (address pair); 22 | 23 | function allPairsLength() external view returns (uint256); 24 | 25 | function createPair(address tokenA, address tokenB) 26 | external 27 | returns (address pair); 28 | 29 | function setFeeTo(address) external; 30 | 31 | function setFeeToSetter(address) external; 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 debridge-finance 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /contracts/interfaces/IDarkDebridge.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.2; 3 | 4 | interface IDarkDebridge { 5 | function deposit(bytes32 _commitment) external payable; 6 | 7 | function externalDeposit(bytes32 _commitment) external payable; 8 | 9 | function withdraw( 10 | bytes calldata _proof, 11 | bytes32 _root, 12 | bytes32 _nullifierHash, 13 | address payable _recipient, 14 | address payable _relayer, 15 | uint256 _fee, 16 | uint256 _refund 17 | ) external payable; 18 | 19 | function withdrawExternal( 20 | bytes calldata _proof, 21 | bytes32 _root, 22 | bytes32 _nullifierHash, 23 | address payable _recipient, 24 | address payable _relayer, 25 | uint256 _fee, 26 | uint256 _refund, 27 | uint256 _networkId 28 | ) external payable; 29 | 30 | function isSpent(bytes32 _nullifierHash) external view returns (bool); 31 | 32 | function isSpentArray(bytes32[] calldata _nullifierHashes) 33 | external 34 | view 35 | returns (bool[] memory spent); 36 | 37 | function updateVerifier(address _newVerifier) external; 38 | } 39 | -------------------------------------------------------------------------------- /migrations/6_white_debridge_asset_migration.js: -------------------------------------------------------------------------------- 1 | const WhiteDebridge = artifacts.require("WhiteDebridge"); 2 | const WhiteAggregator = artifacts.require("WhiteAggregator"); 3 | const FeeProxy = artifacts.require("FeeProxy"); 4 | const DefiController = artifacts.require("DefiController"); 5 | 6 | module.exports = async function (_deployer, network) { 7 | if (network == "test") return; 8 | 9 | const whiteDebridgeInstance = await WhiteDebridge.deployed(); 10 | const otherAssetInfos = require("../assets/supportedChains")[network]; 11 | for (let otherAssetInfo of otherAssetInfos) { 12 | await whiteDebridgeInstance.addExternalAsset( 13 | otherAssetInfo.tokenAddress, 14 | otherAssetInfo.chainId, 15 | otherAssetInfo.minAmount, 16 | otherAssetInfo.transferFee, 17 | otherAssetInfo.minReserves, 18 | [otherAssetInfo.chainId], 19 | otherAssetInfo.name, 20 | otherAssetInfo.symbol 21 | ); 22 | } 23 | console.log("Network: " + network); 24 | console.log("WhiteAggregator: " + WhiteAggregator.address); 25 | console.log("WhiteDebridge: " + WhiteDebridge.address); 26 | console.log("FeeProxy: " + FeeProxy.address); 27 | console.log("DefiController: " + DefiController.address); 28 | }; 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "debridge-contracts", 3 | "version": "1.0.0", 4 | "description": "", 5 | "scripts": { 6 | "precompile": "mkdir -p build/contracts/ && cp precompiled/* build/contracts/", 7 | "compile": "truffle compile", 8 | "migrate": "truffle migrate", 9 | "verify": "truffle run verify WhiteAggregator WhiteDebridge ", 10 | "vefrify": "truffle run verify WhiteAggregator WhiteDebridge ", 11 | "start-ganache": "ganache-cli", 12 | "test": "truffle test --network test" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/KStasi/debridge-contracts.git" 17 | }, 18 | "author": "", 19 | "license": "ISC", 20 | "bugs": { 21 | "url": "https://github.com/KStasi/debridge-contracts/issues" 22 | }, 23 | "homepage": "https://github.com/KStasi/debridge-contracts#readme", 24 | "dependencies": { 25 | "@chainlink/contracts": "^0.1.6", 26 | "@openzeppelin/contracts": "4.0.0-rc.0", 27 | "@openzeppelin/test-helpers": "^0.5.10", 28 | "@truffle/hdwallet-provider": "^1.2.3", 29 | "dotenv-flow": "^3.2.0", 30 | "eth-gas-reporter": "^0.2.22", 31 | "truffle": "^5.2.4", 32 | "truffle-plugin-verify": "^0.5.7", 33 | "web3": "^1.3.4" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /assets/supportedChains.json: -------------------------------------------------------------------------------- 1 | { 2 | "kovan": [ 3 | { 4 | "tokenAddress": "0x0000000000000000000000000000000000000000", 5 | "chainId": "56", 6 | "minAmount": "100000", 7 | "minReserves": "100000000000000000", 8 | "transferFee": "100000000", 9 | "name": "Wrapped BNB", 10 | "symbol": "WBNB" 11 | } 12 | ], 13 | "bsctest": [ 14 | { 15 | "tokenAddress": "0x0000000000000000000000000000000000000000", 16 | "chainId": "42", 17 | "minAmount": "100000", 18 | "minReserves": "100000000000000000", 19 | "transferFee": "100000000", 20 | "name": "Wrapped KETH", 21 | "symbol": "WKETH" 22 | } 23 | ], 24 | "bsc": [ 25 | { 26 | "tokenAddress": "0x0000000000000000000000000000000000000000", 27 | "chainId": "42", 28 | "minAmount": "100000", 29 | "minReserves": "100000000000000000", 30 | "transferFee": "100000000", 31 | "name": "Wrapped KETH", 32 | "symbol": "WKETH" 33 | } 34 | ], 35 | "development": [ 36 | { 37 | "tokenAddress": "0x0000000000000000000000000000000000000000", 38 | "chainId": "42", 39 | "minAmount": "100000", 40 | "minReserves": "100000000000000000", 41 | "transferFee": "100000000", 42 | "name": "Wrapped KETH", 43 | "symbol": "WKETH" 44 | } 45 | ] 46 | } 47 | -------------------------------------------------------------------------------- /contracts/interfaces/IWhiteDebridge.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.2; 3 | 4 | interface IWhiteDebridge { 5 | function send( 6 | bytes32 _debridgeId, 7 | address _receiver, 8 | uint256 _amount, 9 | uint256 _chainIdTo 10 | ) external payable; 11 | 12 | function mint( 13 | bytes32 _debridgeId, 14 | address _receiver, 15 | uint256 _amount, 16 | uint256 _nonce 17 | ) external; 18 | 19 | function burn( 20 | bytes32 _debridgeId, 21 | address _receiver, 22 | uint256 _amount 23 | ) external; 24 | 25 | function claim( 26 | bytes32 _debridgeId, 27 | address _receiver, 28 | uint256 _amount, 29 | uint256 _nonce 30 | ) external; 31 | 32 | function addNativeAsset( 33 | address _tokenAddress, 34 | uint256 _minAmount, 35 | uint256 _transferFee, 36 | uint256 _minReserves, 37 | uint256[] memory _supportedChainIds 38 | ) external; 39 | 40 | function setChainIdSupport( 41 | bytes32 _debridgeId, 42 | uint256 _chainId, 43 | bool _isSupported 44 | ) external; 45 | 46 | function addExternalAsset( 47 | address _tokenAddress, 48 | uint256 _chainId, 49 | uint256 _minAmount, 50 | uint256 _transferFee, 51 | uint256 _minReserves, 52 | uint256[] memory _supportedChainIds, 53 | string memory _name, 54 | string memory _symbol 55 | ) external; 56 | } 57 | -------------------------------------------------------------------------------- /contracts/interfaces/IUniswapV2ERC20.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.2; 3 | 4 | interface IUniswapV2ERC20 { 5 | event Approval( 6 | address indexed owner, 7 | address indexed spender, 8 | uint256 value 9 | ); 10 | event Transfer(address indexed from, address indexed to, uint256 value); 11 | 12 | function name() external pure returns (string memory); 13 | 14 | function symbol() external pure returns (string memory); 15 | 16 | function decimals() external pure returns (uint8); 17 | 18 | function totalSupply() external view returns (uint256); 19 | 20 | function balanceOf(address owner) external view returns (uint256); 21 | 22 | function allowance(address owner, address spender) 23 | external 24 | view 25 | returns (uint256); 26 | 27 | function approve(address spender, uint256 value) external returns (bool); 28 | 29 | function transfer(address to, uint256 value) external returns (bool); 30 | 31 | function transferFrom( 32 | address from, 33 | address to, 34 | uint256 value 35 | ) external returns (bool); 36 | 37 | function DOMAIN_SEPARATOR() external view returns (bytes32); 38 | 39 | function PERMIT_TYPEHASH() external pure returns (bytes32); 40 | 41 | function nonces(address owner) external view returns (uint256); 42 | 43 | function permit( 44 | address owner, 45 | address spender, 46 | uint256 value, 47 | uint256 deadline, 48 | uint8 v, 49 | bytes32 r, 50 | bytes32 s 51 | ) external; 52 | } 53 | -------------------------------------------------------------------------------- /contracts/mock/MockLinkToken.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.2; 3 | 4 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 5 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 6 | import "@openzeppelin/contracts/access/AccessControl.sol"; 7 | import "../interfaces/IERC677Receiver.sol"; 8 | 9 | contract MockLinkToken is ERC20 { 10 | uint8 private _decimals; 11 | 12 | constructor( 13 | string memory _name, 14 | string memory _symbol, 15 | uint8 _decimal 16 | ) ERC20(_name, _symbol) { 17 | _decimals = _decimal; 18 | } 19 | 20 | function mint(address _receiver, uint256 _amount) external { 21 | _mint(_receiver, _amount); 22 | } 23 | 24 | function burn(uint256 _amount) external { 25 | _burn(msg.sender, _amount); 26 | } 27 | 28 | function decimals() public view override returns (uint8) { 29 | return _decimals; 30 | } 31 | 32 | function transferAndCall( 33 | address _to, 34 | uint256 _value, 35 | bytes memory _data 36 | ) public returns (bool success) { 37 | super.transfer(_to, _value); 38 | emit Transfer(msg.sender, _to, _value); 39 | if (isContract(_to)) { 40 | contractFallback(_to, _value, _data); 41 | } 42 | return true; 43 | } 44 | 45 | function contractFallback( 46 | address _to, 47 | uint256 _value, 48 | bytes memory _data 49 | ) private { 50 | IERC677Receiver receiver = IERC677Receiver(_to); 51 | receiver.onTokenTransfer(msg.sender, _value, _data); 52 | } 53 | 54 | function isContract(address _addr) private view returns (bool hasCode) { 55 | uint256 length; 56 | assembly { 57 | length := extcodesize(_addr) 58 | } 59 | return length > 0; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /contracts/periphery/FeeProxy.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.2; 3 | 4 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 5 | import "../interfaces/IUniswapV2Pair.sol"; 6 | import "../interfaces/IUniswapV2Factory.sol"; 7 | 8 | contract FeeProxy { 9 | address public linkToken; 10 | IUniswapV2Factory public uniswapFactory; 11 | 12 | constructor(address _linkToken, IUniswapV2Factory _uniswapFactory) { 13 | linkToken = _linkToken; 14 | uniswapFactory = _uniswapFactory; 15 | } 16 | 17 | function getAmountOut( 18 | uint256 amountIn, 19 | uint256 reserveIn, 20 | uint256 reserveOut 21 | ) internal pure returns (uint256 amountOut) { 22 | require(amountIn > 0, "getAmountOut: insuffient amount"); 23 | require( 24 | reserveIn > 0 && reserveOut > 0, 25 | "getAmountOut: insuffient liquidity" 26 | ); 27 | uint256 amountInWithFee = amountIn * 997; 28 | uint256 numerator = amountInWithFee * reserveOut; 29 | uint256 denominator = reserveIn * 1000 + amountInWithFee; 30 | amountOut = numerator / denominator; 31 | } 32 | 33 | function swapToLink( 34 | address _erc20Token, 35 | uint256 _amount, 36 | address _receiver 37 | ) external { 38 | IERC20 erc20 = IERC20(_erc20Token); 39 | _amount = erc20.balanceOf(address(this)); 40 | IUniswapV2Pair uniswapPair = 41 | IUniswapV2Pair(uniswapFactory.getPair(linkToken, _erc20Token)); 42 | erc20.transfer(address(uniswapPair), _amount); 43 | bool linkFirst = linkToken < _erc20Token; 44 | (uint256 reserve0, uint256 reserve1, ) = uniswapPair.getReserves(); 45 | if (linkFirst) { 46 | uint256 amountOut = getAmountOut(_amount, reserve1, reserve0); 47 | uniswapPair.swap(amountOut, 0, _receiver, ""); 48 | } else { 49 | uint256 amountOut = getAmountOut(_amount, reserve0, reserve1); 50 | uniswapPair.swap(0, amountOut, _receiver, ""); 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /contracts/chainlink/CommitmentAggregator.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.2; 3 | 4 | import "@openzeppelin/contracts/access/AccessControl.sol"; 5 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 6 | import "../interfaces/IDarkDebridge.sol"; 7 | import "./Aggregator.sol"; 8 | 9 | contract CommitmentAggregator is Aggregator { 10 | struct CommitmentInfo { 11 | bool broadcasted; 12 | uint256 confirmations; 13 | bytes32 debridgeId; // hash(chainId, tokenAddress, amount) 14 | bytes32 commitment; 15 | mapping(address => bool) hasVerified; 16 | } 17 | 18 | mapping(bytes32 => CommitmentInfo) public getCommitmentInfo; 19 | mapping(bytes32 => IDarkDebridge) public getDebridge; 20 | 21 | event Confirmed(bytes32 commitment, bytes32 debridgeId, address operator); 22 | event Broadcasted(bytes32 debridgeId, bytes32 commitment); 23 | 24 | constructor( 25 | uint256 _minConfirmations, 26 | uint128 _payment, 27 | IERC20 _link 28 | ) Aggregator(_minConfirmations, _payment, _link) {} 29 | 30 | function submit(bytes32 _commitment, bytes32 _debridgeId) 31 | external 32 | onlyOracle 33 | { 34 | bytes32 depositId = 35 | keccak256(abi.encodePacked(_commitment, _debridgeId)); 36 | CommitmentInfo storage commitmentInfo = getCommitmentInfo[depositId]; 37 | require( 38 | !commitmentInfo.hasVerified[msg.sender], 39 | "submit: submitted already" 40 | ); 41 | if (commitmentInfo.confirmations == 0) { 42 | commitmentInfo.commitment = _commitment; 43 | commitmentInfo.debridgeId = _debridgeId; 44 | } 45 | commitmentInfo.confirmations += 1; 46 | commitmentInfo.hasVerified[msg.sender] = true; 47 | if (commitmentInfo.confirmations == minConfirmations) { 48 | getDebridge[_debridgeId].externalDeposit(_commitment); 49 | commitmentInfo.broadcasted = true; 50 | } 51 | _payOracle(msg.sender); 52 | } 53 | 54 | function setDebridge(bytes32 _debridgeId, IDarkDebridge _debridge) 55 | external 56 | onlyAdmin 57 | { 58 | getDebridge[_debridgeId] = _debridge; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /assets/debridgeInitParams.json: -------------------------------------------------------------------------------- 1 | { 2 | "kovan": { 3 | "oracleCount": 1, 4 | "oraclePayment": "100000000", 5 | "minTransferAmount": "1000000000", 6 | "transferFee": "10000000000000000", 7 | "minReserves": "100000000000000000", 8 | "oracles": [ 9 | { 10 | "address": "0x366fe4f76d5699e032c4a432ae955fa23fa4987a", 11 | "admin": "0x6CDf260c39A390fE3249d7FA3ba4986b17d109E0" 12 | }, 13 | { 14 | "address": "0x0b341A3fD55d4cc8aDb856859Bd426231a21a0d3", 15 | "admin": "0x6CDf260c39A390fE3249d7FA3ba4986b17d109E0" 16 | } 17 | ], 18 | "supportedChains": [56] 19 | }, 20 | "bsctest": { 21 | "oracleCount": 1, 22 | "oraclePayment": "100000000", 23 | "minTransferAmount": "1000000000", 24 | "transferFee": "10000000000000000", 25 | "minReserves": "100000000000000000", 26 | "oracles": [ 27 | { 28 | "address": "0x366fe4f76d5699e032c4a432ae955fa23fa4987a", 29 | "admin": "0x6CDf260c39A390fE3249d7FA3ba4986b17d109E0" 30 | }, 31 | { 32 | "address": "0x0b341A3fD55d4cc8aDb856859Bd426231a21a0d3", 33 | "admin": "0x6CDf260c39A390fE3249d7FA3ba4986b17d109E0" 34 | } 35 | ], 36 | "supportedChains": [42] 37 | }, 38 | "bsc": { 39 | "oracleCount": 1, 40 | "oraclePayment": "100000000", 41 | "minTransferAmount": "1000000000", 42 | "transferFee": "10000000000000000", 43 | "minReserves": "100000000000000000", 44 | "oracles": [ 45 | { 46 | "address": "0x366fe4f76d5699e032c4a432ae955fa23fa4987a", 47 | "admin": "0x6CDf260c39A390fE3249d7FA3ba4986b17d109E0" 48 | }, 49 | { 50 | "address": "0x0b341A3fD55d4cc8aDb856859Bd426231a21a0d3", 51 | "admin": "0x6CDf260c39A390fE3249d7FA3ba4986b17d109E0" 52 | } 53 | ], 54 | "supportedChains": [42] 55 | }, 56 | "development": { 57 | "oracleCount": 1, 58 | "oraclePayment": "100000000", 59 | "minTransferAmount": "1000000000", 60 | "transferFee": "10000000000000000", 61 | "minReserves": "100000000000000000", 62 | "oracles": [ 63 | { 64 | "address": "0x366fe4f76d5699e032c4a432ae955fa23fa4987a", 65 | "admin": "0x6CDf260c39A390fE3249d7FA3ba4986b17d109E0" 66 | }, 67 | { 68 | "address": "0x0b341A3fD55d4cc8aDb856859Bd426231a21a0d3", 69 | "admin": "0x6CDf260c39A390fE3249d7FA3ba4986b17d109E0" 70 | } 71 | ], 72 | "supportedChains": [42] 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /migrations/utils.js: -------------------------------------------------------------------------------- 1 | const zeroAddress = "0x0000000000000000000000000000000000000000"; 2 | 3 | const MockLinkToken = artifacts.require("MockLinkToken"); 4 | const WETH9 = artifacts.require("WETH9"); 5 | const UniswapV2Factory = artifacts.require("UniswapV2Factory"); 6 | 7 | module.exports.getLinkAddress = async (deployer, network, accounts) => { 8 | let link; 9 | switch (network) { 10 | case "development": 11 | try { 12 | await MockLinkToken.deployed(); 13 | } catch (e) { 14 | await deployer.deploy(MockLinkToken, "Link Token", "dLINK", 18); 15 | const linkToken = await MockLinkToken.deployed(); 16 | await linkToken.mint(accounts[0], "100000000000000000000"); 17 | } 18 | link = (await MockLinkToken.deployed()).address; 19 | break; 20 | case "kovan": 21 | link = "0xa36085F69e2889c224210F603D836748e7dC0088"; 22 | break; 23 | case "bsctest": 24 | link = "0x84b9b910527ad5c03a9ca831909e21e236ea7b06"; 25 | break; 26 | case "bsc": 27 | link = "0x89F3A11E8d3B7a9F29bDB3CdC1f04c7e6095B357"; 28 | break; 29 | default: 30 | link = "0x514910771af9ca656af840dff83e8264ecf986ca"; 31 | break; 32 | } 33 | return link; 34 | }; 35 | 36 | module.exports.getUniswapFactory = async (deployer, network) => { 37 | let uniswapFactory; 38 | switch (network) { 39 | case "development": 40 | try { 41 | await UniswapV2Factory.deployed(); 42 | } catch (e) { 43 | await deployer.deploy(UniswapV2Factory, zeroAddress); 44 | } 45 | uniswapFactory = (await UniswapV2Factory.deployed()).address; 46 | break; 47 | case "kovan": 48 | case "ethereum": 49 | uniswapFactory = "0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f"; 50 | break; 51 | case "bsc": 52 | uniswapFactory = "0xBCfCcbde45cE874adCB698cC183deBcF17952812"; 53 | break; 54 | default: 55 | break; 56 | } 57 | return uniswapFactory; 58 | }; 59 | 60 | module.exports.getWeth = async (deployer, network) => { 61 | let weth; 62 | switch (network) { 63 | case "development": 64 | case "kovan": 65 | case "bsctest": 66 | try { 67 | await WETH9.deployed(); 68 | } catch (e) { 69 | await deployer.deploy(WETH9); 70 | } 71 | weth = (await WETH9.deployed()).address; 72 | break; 73 | case "ethereum": 74 | weth = "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"; 75 | break; 76 | case "bsc": 77 | weth = "0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c"; 78 | break; 79 | default: 80 | break; 81 | } 82 | return weth; 83 | }; 84 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## How White Transfers Works 2 | 3 | The transfer of the asset from chain A to chain B goes through the followed steps: 4 | 5 | 1. If the transfered asset isn't the native blockchain token (i.e ETH, BNB) the `approve` is done otherwise the transfered amount is attached to the next method. 6 | 2. The `send` method of `WhiteDebridge` contract is called. The `amount` of asset is locked on the contract, the fee is charged from the `amount` and the `Sent` event is emited. 7 | 3. The ChainLink nodes listen to the event on the `WhiteDebridge` contract and after 3 blocks confirmations submit the sent request identifier(`submissionId`) which is hash of concatination of `debridgeId`, `amount`, `receiver`, `nonce`. `DebridgeId` is hash of network id of the chain where the original token exists and token address on the original chain. The oracles are rewarded with LINKs immediatly after the submission. 8 | 4. After enough confirmations from Chainlink oracles (lets say 3 out of 5) the send request status becomes `confirmed`. 9 | 5. The user or any other party can call `mint` method of `WhiteDebridge` contract with the correct `debridgeId`, `amount`, `receiver`, `nonce` parameters that results into `submissionId`. If the submission is confirmed the wrapped asset is minted to the `receiver` address. 10 | 11 | The transfer of the wrapped asset on chain B back to the original chain A to chain B goes through the followed steps: 12 | 13 | 1. The `approve` to spent the wrapped asset by `WhiteDebridge` is done. 14 | 2. The `burn` method of `WhiteDebridge` contract is called. The `amount` of the asset is burnt and the `Burnt` event is emited. 15 | 3. The ChainLink nodes listen to the event on the `WhiteDebridge` contract and after 3 blocks confirmations submit the burnt request identifier(`submissionId`) which is hash of concatination of `debridgeId`, `amount`, `receiver`, `nonce`. `DebridgeId` is hash of network id of the chain where the original token exists and token address on the original chain. The oracles are rewarded with LINKs immediatly after the submission. 16 | 4. After enough confirmations from Chainlink oracles (lets say 3 out of 5) the burnt request status becomes `confirmed`. 17 | 5. The user or any other party can call `claim` method of `WhiteDebridge` contract with the correct `debridgeId`, `amount`, `receiver`, `nonce` parameters that results into `submissionId`. If the submission is confirmed the fee is transfer fee is charged and original asset is sent to the `receiver` address. 18 | 19 | **Note**: the chainlink node can only submit up to 32 bytes per one transaction to the chain that is why `debridgeId`, `amount`, `receiver`, `nonce` can't be submitted by the node in one transaction. To solve it the hash of the parameters is used. 20 | 21 | ## Aggregator 22 | 23 | ## Test 24 | 25 | ``` 26 | yarn start-ganache & 27 | yarn test 28 | ``` 29 | 30 | # Ideas Backlog 31 | 32 | - [ ] use assets in other protocols 33 | 34 | - [ ] support NFT to make transfer fee lower 35 | -------------------------------------------------------------------------------- /contracts/chainlink/WithdrawalAggregator.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.2; 3 | 4 | import "@openzeppelin/contracts/access/AccessControl.sol"; 5 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 6 | import "../interfaces/ILightDarkDebridge.sol"; 7 | import "./Aggregator.sol"; 8 | 9 | contract WithdrawalAggregator is Aggregator { 10 | struct WithdrawalInfo { 11 | bool broadcasted; 12 | uint256 confirmations; 13 | bytes32 debridgeId; // hash(chainId, tokenAddress, amount) 14 | bytes32 nullifierHash; 15 | address recipient; 16 | address relayer; 17 | uint256 fee; 18 | uint256 refund; 19 | mapping(address => bool) hasVerified; 20 | } 21 | 22 | mapping(bytes32 => WithdrawalInfo) public getWithdrawalInfo; 23 | mapping(bytes32 => ILightDarkDebridge) public getDebridge; 24 | 25 | event Confirmed(bytes32 commitment, bytes32 debridgeId, address operator); 26 | event Broadcasted(bytes32 debridgeId, bytes32 commitment); 27 | 28 | constructor( 29 | uint256 _minConfirmations, 30 | uint128 _payment, 31 | IERC20 _link 32 | ) Aggregator(_minConfirmations, _payment, _link) {} 33 | 34 | function submit( 35 | bytes32 _debridgeId, 36 | bytes32 _nullifierHash, 37 | uint256 _fee, 38 | uint256 _refund, 39 | address payable _recipient, 40 | address payable _relayer 41 | ) external onlyOracle { 42 | bytes32 withdrawalId = 43 | keccak256( 44 | abi.encodePacked( 45 | _debridgeId, 46 | _nullifierHash, 47 | _fee, 48 | _refund, 49 | _recipient, 50 | _relayer 51 | ) 52 | ); 53 | WithdrawalInfo storage withdrawalInfo = getWithdrawalInfo[withdrawalId]; 54 | require( 55 | !withdrawalInfo.hasVerified[msg.sender], 56 | "submit: submitted already" 57 | ); 58 | if (withdrawalInfo.confirmations == 0) { 59 | withdrawalInfo.debridgeId = _debridgeId; 60 | withdrawalInfo.nullifierHash = _nullifierHash; 61 | withdrawalInfo.fee = _fee; 62 | withdrawalInfo.refund = _refund; 63 | withdrawalInfo.recipient = _recipient; 64 | withdrawalInfo.relayer = _relayer; 65 | } 66 | withdrawalInfo.confirmations += 1; 67 | withdrawalInfo.hasVerified[msg.sender] = true; 68 | if (withdrawalInfo.confirmations == minConfirmations) { 69 | getDebridge[_debridgeId].withdraw( 70 | _recipient, 71 | _relayer, 72 | _fee, 73 | _refund 74 | ); 75 | withdrawalInfo.broadcasted = true; 76 | } 77 | _payOracle(msg.sender); 78 | } 79 | 80 | function setDebridge(bytes32 _debridgeId, ILightDarkDebridge _debridge) 81 | external 82 | onlyAdmin 83 | { 84 | getDebridge[_debridgeId] = _debridge; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /contracts/periphery/WrappedAsset.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.2; 3 | 4 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 5 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 6 | import "@openzeppelin/contracts/access/AccessControl.sol"; 7 | import "../interfaces/IWrappedAsset.sol"; 8 | 9 | contract WrappedAsset is AccessControl, IWrappedAsset, ERC20 { 10 | bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE"); 11 | bytes32 public DOMAIN_SEPARATOR; 12 | // keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"); 13 | bytes32 public constant PERMIT_TYPEHASH = 14 | 0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9; 15 | mapping(address => uint256) public nonces; 16 | 17 | modifier onlyMinter { 18 | require(hasRole(MINTER_ROLE, msg.sender), "onlyAggregator: bad role"); 19 | _; 20 | } 21 | 22 | constructor(string memory _name, string memory _symbol) 23 | ERC20(_name, _symbol) 24 | { 25 | _setupRole(DEFAULT_ADMIN_ROLE, msg.sender); 26 | _setupRole(MINTER_ROLE, msg.sender); 27 | uint256 chainId; 28 | assembly { 29 | chainId := chainid() 30 | } 31 | DOMAIN_SEPARATOR = keccak256( 32 | abi.encode( 33 | keccak256( 34 | "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" 35 | ), 36 | keccak256(bytes(_name)), 37 | keccak256(bytes("1")), 38 | chainId, 39 | address(this) 40 | ) 41 | ); 42 | } 43 | 44 | function mint(address _receiver, uint256 _amount) 45 | external 46 | override 47 | onlyMinter() 48 | { 49 | _mint(_receiver, _amount); 50 | } 51 | 52 | function burn(uint256 _amount) external override onlyMinter() { 53 | _burn(msg.sender, _amount); 54 | } 55 | 56 | function permit( 57 | address owner, 58 | address spender, 59 | uint256 value, 60 | uint256 deadline, 61 | uint8 v, 62 | bytes32 r, 63 | bytes32 s 64 | ) external { 65 | require(deadline >= block.timestamp, "UniswapV2: EXPIRED"); 66 | bytes32 digest = 67 | keccak256( 68 | abi.encodePacked( 69 | "\x19\x01", 70 | DOMAIN_SEPARATOR, 71 | keccak256( 72 | abi.encode( 73 | PERMIT_TYPEHASH, 74 | owner, 75 | spender, 76 | value, 77 | nonces[owner]++, 78 | deadline 79 | ) 80 | ) 81 | ) 82 | ); 83 | address recoveredAddress = ecrecover(digest, v, r, s); 84 | require( 85 | recoveredAddress != address(0) && recoveredAddress == owner, 86 | "permit: invalid signature" 87 | ); 88 | _approve(owner, spender, value); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /contracts/interfaces/IUniswapV2Pair.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.2; 3 | 4 | interface IUniswapV2Pair { 5 | event Approval( 6 | address indexed owner, 7 | address indexed spender, 8 | uint256 value 9 | ); 10 | event Transfer(address indexed from, address indexed to, uint256 value); 11 | 12 | function name() external pure returns (string memory); 13 | 14 | function symbol() external pure returns (string memory); 15 | 16 | function decimals() external pure returns (uint8); 17 | 18 | function totalSupply() external view returns (uint256); 19 | 20 | function balanceOf(address owner) external view returns (uint256); 21 | 22 | function allowance(address owner, address spender) 23 | external 24 | view 25 | returns (uint256); 26 | 27 | function approve(address spender, uint256 value) external returns (bool); 28 | 29 | function transfer(address to, uint256 value) external returns (bool); 30 | 31 | function transferFrom( 32 | address from, 33 | address to, 34 | uint256 value 35 | ) external returns (bool); 36 | 37 | function DOMAIN_SEPARATOR() external view returns (bytes32); 38 | 39 | function PERMIT_TYPEHASH() external pure returns (bytes32); 40 | 41 | function nonces(address owner) external view returns (uint256); 42 | 43 | function permit( 44 | address owner, 45 | address spender, 46 | uint256 value, 47 | uint256 deadline, 48 | uint8 v, 49 | bytes32 r, 50 | bytes32 s 51 | ) external; 52 | 53 | event Mint(address indexed sender, uint256 amount0, uint256 amount1); 54 | event Burn( 55 | address indexed sender, 56 | uint256 amount0, 57 | uint256 amount1, 58 | address indexed to 59 | ); 60 | event Swap( 61 | address indexed sender, 62 | uint256 amount0In, 63 | uint256 amount1In, 64 | uint256 amount0Out, 65 | uint256 amount1Out, 66 | address indexed to 67 | ); 68 | event Sync(uint112 reserve0, uint112 reserve1); 69 | 70 | function MINIMUM_LIQUIDITY() external pure returns (uint256); 71 | 72 | function factory() external view returns (address); 73 | 74 | function token0() external view returns (address); 75 | 76 | function token1() external view returns (address); 77 | 78 | function getReserves() 79 | external 80 | view 81 | returns ( 82 | uint112 reserve0, 83 | uint112 reserve1, 84 | uint32 blockTimestampLast 85 | ); 86 | 87 | function price0CumulativeLast() external view returns (uint256); 88 | 89 | function price1CumulativeLast() external view returns (uint256); 90 | 91 | function kLast() external view returns (uint256); 92 | 93 | function mint(address to) external returns (uint256 liquidity); 94 | 95 | function burn(address to) 96 | external 97 | returns (uint256 amount0, uint256 amount1); 98 | 99 | function swap( 100 | uint256 amount0Out, 101 | uint256 amount1Out, 102 | address to, 103 | bytes calldata data 104 | ) external; 105 | 106 | function skim(address to) external; 107 | 108 | function sync() external; 109 | 110 | function initialize(address, address) external; 111 | } 112 | -------------------------------------------------------------------------------- /contracts/chainlink/WhiteAggregator.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.2; 3 | 4 | import "@openzeppelin/contracts/access/AccessControl.sol"; 5 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 6 | import "./Aggregator.sol"; 7 | import "../interfaces/IWhiteAggregator.sol"; 8 | 9 | contract WhiteAggregator is Aggregator, IWhiteAggregator { 10 | struct SubmissionInfo { 11 | bool confirmed; // whether is confirmed 12 | uint256 confirmations; // received confirmations count 13 | mapping(address => bool) hasVerified; // verifier => has already voted 14 | } 15 | 16 | mapping(bytes32 => SubmissionInfo) public getMintInfo; // mint id => submission info 17 | mapping(bytes32 => SubmissionInfo) public getBurntInfo; // burnt id => submission info 18 | 19 | event Confirmed(bytes32 submissionId, address operator); // emitted once the submission is confirmed 20 | event SubmissionApproved(bytes32 submissionId); // emitted once the submission is confirmed 21 | 22 | /// @dev Constructor that initializes the most important configurations. 23 | /// @param _minConfirmations Minimal required confirmations. 24 | /// @param _payment Oracle reward. 25 | /// @param _link Link token to pay to oracles. 26 | constructor( 27 | uint256 _minConfirmations, 28 | uint128 _payment, 29 | IERC20 _link 30 | ) Aggregator(_minConfirmations, _payment, _link) {} 31 | 32 | /// @dev Confirms the mint request. 33 | /// @param _mintId Submission identifier. 34 | function submitMint(bytes32 _mintId) external override onlyOracle { 35 | SubmissionInfo storage mintInfo = getMintInfo[_mintId]; 36 | require(!mintInfo.hasVerified[msg.sender], "submit: submitted already"); 37 | mintInfo.confirmations += 1; 38 | mintInfo.hasVerified[msg.sender] = true; 39 | if (mintInfo.confirmations >= minConfirmations) { 40 | mintInfo.confirmed = true; 41 | emit SubmissionApproved(_mintId); 42 | } 43 | _payOracle(msg.sender); 44 | emit Confirmed(_mintId, msg.sender); 45 | } 46 | 47 | /// @dev Confirms the burnnt request. 48 | /// @param _burntId Submission identifier. 49 | function submitBurn(bytes32 _burntId) external override onlyOracle { 50 | SubmissionInfo storage burnInfo = getBurntInfo[_burntId]; 51 | require(!burnInfo.hasVerified[msg.sender], "submit: submitted already"); 52 | burnInfo.confirmations += 1; 53 | burnInfo.hasVerified[msg.sender] = true; 54 | if (burnInfo.confirmations >= minConfirmations) { 55 | burnInfo.confirmed = true; 56 | emit SubmissionApproved(_burntId); 57 | } 58 | emit Confirmed(_burntId, msg.sender); 59 | _payOracle(msg.sender); 60 | } 61 | 62 | /// @dev Returns whether mint request is confirmed. 63 | /// @param _mintId Submission identifier. 64 | /// @return Whether mint request is confirmed. 65 | function isMintConfirmed(bytes32 _mintId) 66 | external 67 | view 68 | override 69 | returns (bool) 70 | { 71 | return getMintInfo[_mintId].confirmed; 72 | } 73 | 74 | /// @dev Returns whether burnnt request is confirmed. 75 | /// @param _burntId Submission identifier. 76 | /// @return Whether burnnt request is confirmed. 77 | function isBurntConfirmed(bytes32 _burntId) 78 | external 79 | view 80 | override 81 | returns (bool) 82 | { 83 | return getBurntInfo[_burntId].confirmed; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /contracts/chainlink/Aggregator.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.2; 3 | 4 | import "@openzeppelin/contracts/access/AccessControl.sol"; 5 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 6 | 7 | contract Aggregator is AccessControl { 8 | struct OracleInfo { 9 | uint256 withdrawable; // amount of withdrawable LINKs 10 | address admin; // current oracle admin 11 | } 12 | 13 | bytes32 public constant ORACLE_ROLE = keccak256("ORACLE_ROLE"); // role allowed to submit the data 14 | uint256 public minConfirmations; // minimal required confimations 15 | uint256 public allocatedFunds; // LINK's amount payed to oracles 16 | uint256 public availableFunds; // LINK's amount available to be payed to oracles 17 | uint256 public payment; // payment for one submission 18 | IERC20 public link; // LINK's token address 19 | mapping(address => OracleInfo) public getOracleInfo; // oracle address => oracle details 20 | 21 | modifier onlyAdmin { 22 | require(hasRole(DEFAULT_ADMIN_ROLE, msg.sender), "onlyAdmin: bad role"); 23 | _; 24 | } 25 | modifier onlyOracle { 26 | require(hasRole(ORACLE_ROLE, msg.sender), "onlyOracle: bad role"); 27 | _; 28 | } 29 | 30 | /* PUBLIC */ 31 | 32 | /// @dev Constructor that initializes the most important configurations. 33 | /// @param _minConfirmations Minimal required confirmations. 34 | /// @param _payment Oracle reward. 35 | /// @param _link Link token to pay to oracles. 36 | constructor( 37 | uint256 _minConfirmations, 38 | uint128 _payment, 39 | IERC20 _link 40 | ) { 41 | minConfirmations = _minConfirmations; 42 | payment = _payment; 43 | link = _link; 44 | _setupRole(DEFAULT_ADMIN_ROLE, msg.sender); 45 | _setupRole(ORACLE_ROLE, msg.sender); 46 | } 47 | 48 | /// @dev Withdraws oracle reward. 49 | /// @param _oracle Oracle address. 50 | /// @param _recipient Recepient reward. 51 | /// @param _amount Amount to withdraw. 52 | function withdrawPayment( 53 | address _oracle, 54 | address _recipient, 55 | uint256 _amount 56 | ) external { 57 | require( 58 | getOracleInfo[_oracle].admin == msg.sender, 59 | "withdrawPayment: only callable by admin" 60 | ); 61 | uint256 available = getOracleInfo[_oracle].withdrawable; 62 | require( 63 | available >= _amount, 64 | "withdrawPayment: insufficient withdrawable funds" 65 | ); 66 | getOracleInfo[_oracle].withdrawable = available - _amount; 67 | allocatedFunds -= _amount; 68 | assert(link.transfer(_recipient, _amount)); 69 | } 70 | 71 | /// @dev Updates available rewards to be distributed. 72 | function updateAvailableFunds() public { 73 | availableFunds = link.balanceOf(address(this)) - allocatedFunds; 74 | } 75 | 76 | function onTokenTransfer( 77 | address, 78 | uint256, 79 | bytes calldata _data 80 | ) external { 81 | require(msg.sender == address(link), "onTokenTransfer: not the Link"); 82 | require(_data.length == 0, "transfer doesn't accept calldata"); 83 | updateAvailableFunds(); 84 | } 85 | 86 | /* ADMIN */ 87 | 88 | /// @dev Withdraws available LINK's. 89 | /// @param _recipient Recepient reward. 90 | /// @param _amount Amount to withdraw. 91 | function withdrawFunds(address _recipient, uint256 _amount) 92 | external 93 | onlyAdmin() 94 | { 95 | require( 96 | uint256(availableFunds) >= _amount, 97 | "insufficient reserve funds" 98 | ); 99 | require( 100 | link.transfer(_recipient, _amount), 101 | "withdrawFunds: transfer failed" 102 | ); 103 | updateAvailableFunds(); 104 | } 105 | 106 | /// @dev Sets minimal required confirmations. 107 | /// @param _minConfirmations Minimal required confirmations. 108 | function setMinConfirmations(uint256 _minConfirmations) external onlyAdmin { 109 | minConfirmations = _minConfirmations; 110 | } 111 | 112 | /// @dev Sets new oracle reward. 113 | /// @param _payment Oracle reward. 114 | function setPayment(uint128 _payment) external onlyAdmin { 115 | payment = _payment; 116 | } 117 | 118 | /// @dev Add new oracle. 119 | /// @param _oracle Oracle address. 120 | /// @param _admin Admin address. 121 | function addOracle(address _oracle, address _admin) external onlyAdmin { 122 | grantRole(ORACLE_ROLE, _oracle); 123 | getOracleInfo[_oracle].admin = _admin; 124 | } 125 | 126 | /// @dev Remove oracle. 127 | /// @param _oracle Oracle address. 128 | function removeOracle(address _oracle) external onlyAdmin { 129 | revokeRole(ORACLE_ROLE, _oracle); 130 | } 131 | 132 | /* INTERNAL */ 133 | 134 | /// @dev Assess teh oracle rewards. 135 | /// @param _oracle Oracle address. 136 | function _payOracle(address _oracle) internal { 137 | availableFunds -= payment; 138 | allocatedFunds += payment; 139 | getOracleInfo[_oracle].withdrawable += payment; 140 | } 141 | 142 | /* VIEW */ 143 | 144 | /// @dev Withdraws oracle reward. 145 | /// @param _oracle Oracle address. 146 | /// @return Oracle rewards. 147 | function withdrawablePayment(address _oracle) 148 | external 149 | view 150 | returns (uint256) 151 | { 152 | return getOracleInfo[_oracle].withdrawable; 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /truffle-config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Use this file to configure your truffle project. It's seeded with some 3 | * common settings for different networks and features like migrations, 4 | * compilation and testing. Uncomment the ones you need or modify 5 | * them to suit your project as necessary. 6 | * 7 | * More information about configuration can be found at: 8 | * 9 | * trufflesuite.com/docs/advanced/configuration 10 | * 11 | * To deploy via Infura you'll need a wallet provider (like @truffle/hdwallet-provider) 12 | * to sign your transactions before they're sent to a remote public node. Infura accounts 13 | * are available for free at: infura.io/register. 14 | * 15 | * You'll also need a mnemonic - the twelve word phrase the wallet uses to generate 16 | * public/private key pairs. If you're publishing your code to GitHub make sure you load this 17 | * phrase from a file you've .gitignored so it doesn't accidentally become public. 18 | * 19 | */ 20 | require("dotenv-flow").config(); 21 | const HDWalletProvider = require("@truffle/hdwallet-provider"); 22 | // const infuraKey = "fj4jll3k....."; 23 | // 24 | // const fs = require('fs'); 25 | // const mnemonic = fs.readFileSync(".secret").toString().trim(); 26 | 27 | module.exports = { 28 | /** 29 | * Networks define how you connect to your ethereum client and let you set the 30 | * defaults web3 uses to send transactions. If you don't specify one truffle 31 | * will spin up a development blockchain for you on port 9545 when you 32 | * run `develop` or `test`. You can ask a truffle command to use a specific 33 | * network from the command line, e.g 34 | * 35 | * $ truffle test --network 36 | */ 37 | 38 | networks: { 39 | // Useful for testing. The `development` name is special - truffle uses it by default 40 | // if it's defined here and no other network is specified at the command line. 41 | // You should run a client (like ganache-cli, geth or parity) in a separate terminal 42 | // tab if you use this network and you must also set the `host`, `port` and `network_id` 43 | // options below to some value. 44 | // 45 | development: { 46 | network_id: "*", // Any network (default: none) 47 | provider: () => 48 | new HDWalletProvider( 49 | [process.env.DEPLOYER_PRIVATE_KEY], 50 | "http://127.0.0.1:8545", 51 | 0, 52 | 1 53 | ), 54 | gas: 10000000, 55 | skipDryRun: false, 56 | }, 57 | test: { 58 | host: "127.0.0.1", // Localhost (default: none) 59 | port: 8545, // Standard Ethereum port (default: none) 60 | network_id: "*", // Any network (default: none), 61 | from: process.env.DEPLOYER_ACCOUNT, 62 | }, 63 | kovan: { 64 | network_id: "42", 65 | provider: () => 66 | new HDWalletProvider( 67 | [process.env.DEPLOYER_PRIVATE_KEY], 68 | "https://kovan.infura.io/v3/" + process.env.INFURA_ID, 69 | 0, 70 | 1 71 | ), 72 | gasPrice: 5000000000, // 80 gwei 73 | gas: 6900000, 74 | from: process.env.DEPLOYER_ACCOUNT, 75 | timeoutBlocks: 5000, 76 | skipDryRun: true, 77 | }, 78 | bsctest: { 79 | network_id: "97", 80 | provider: () => 81 | new HDWalletProvider( 82 | [process.env.DEPLOYER_PRIVATE_KEY], 83 | "https://data-seed-prebsc-2-s3.binance.org:8545/", 84 | 0, 85 | 1 86 | ), 87 | from: process.env.DEPLOYER_ACCOUNT, 88 | timeoutBlocks: 5000, 89 | skipDryRun: true, 90 | }, 91 | bsc: { 92 | network_id: "56", 93 | provider: () => 94 | new HDWalletProvider( 95 | [process.env.DEPLOYER_PRIVATE_KEY], 96 | "https://bsc-dataseed.binance.org/", 97 | 0, 98 | 1 99 | ), 100 | from: process.env.DEPLOYER_ACCOUNT, 101 | timeoutBlocks: 5000, 102 | gasPrice: 10000000000, // 5 gwei 103 | gas: 6000000, 104 | skipDryRun: true, 105 | }, 106 | mainnet: { 107 | network_id: "1", 108 | provider: () => 109 | new HDWalletProvider( 110 | [process.env.DEPLOYER_PRIVATE_KEY], 111 | "https://mainnet.infura.io/v3/" + process.env.INFURA_ID, 112 | 0, 113 | 1 114 | ), 115 | gasPrice: 51000000000, // 80 gwei 116 | gas: 6900000, 117 | from: process.env.DEPLOYER_ACCOUNT, 118 | timeoutBlocks: 5000, 119 | skipDryRun: true, 120 | }, 121 | // Another network with more advanced options... 122 | // advanced: { 123 | // port: 8777, // Custom port 124 | // network_id: 1342, // Custom network 125 | // gas: 8500000, // Gas sent with each transaction (default: ~6700000) 126 | // gasPrice: 20000000000, // 20 gwei (in wei) (default: 100 gwei) 127 | // from:
, // Account to send txs from (default: accounts[0]) 128 | // websockets: true // Enable EventEmitter interface for web3 (default: false) 129 | // }, 130 | // Useful for deploying to a public network. 131 | // NB: It's important to wrap the provider as a function. 132 | // ropsten: { 133 | // provider: () => new HDWalletProvider(mnemonic, `https://ropsten.infura.io/v3/YOUR-PROJECT-ID`), 134 | // network_id: 3, // Ropsten's id 135 | // gas: 5500000, // Ropsten has a lower block limit than mainnet 136 | // confirmations: 2, // # of confs to wait between deployments. (default: 0) 137 | // timeoutBlocks: 200, // # of blocks before a deployment times out (minimum/default: 50) 138 | // skipDryRun: true // Skip dry run before migrations? (default: false for public nets ) 139 | // }, 140 | // Useful for private networks 141 | // private: { 142 | // provider: () => new HDWalletProvider(mnemonic, `https://network.io`), 143 | // network_id: 2111, // This network is yours, in the cloud. 144 | // production: true // Treats this network as if it was a public net. (default: false) 145 | // } 146 | }, 147 | 148 | // Set default mocha options here, use special reporters etc. 149 | mocha: { 150 | reporter: "eth-gas-reporter", 151 | reporterOptions: { 152 | url: "http://localhost:8545", 153 | currency: "USD", 154 | gasPrice: 100, 155 | }, 156 | }, 157 | 158 | // Configure your compilers 159 | compilers: { 160 | solc: { 161 | version: "0.8.2", // Fetch exact version from solc-bin (default: truffle's version) 162 | // docker: true, // Use "0.5.1" you've installed locally with docker (default: false) 163 | settings: { 164 | // See the solidity docs for advice about optimization and evmVersion 165 | optimizer: { 166 | enabled: true, 167 | runs: 200, 168 | }, 169 | // evmVersion: "byzantium" 170 | }, 171 | }, 172 | }, 173 | plugins: ["truffle-plugin-verify"], 174 | api_keys: { 175 | etherscan: process.env.ETHERSCAN_API_KEY, 176 | bscscan: process.env.BSCSCAN_API_KEY, 177 | }, 178 | }; 179 | -------------------------------------------------------------------------------- /Test.md: -------------------------------------------------------------------------------- 1 | ## Test cases 2 | 3 | ## Test Item: WhiteAggregator 4 | 5 | ### General requirements 6 | 7 | 1. Oracles allowed to submit the data are set by the admin. 8 | 2. The number of the required confirmatios is set by the admin. 9 | 3. The contract holds the Links. 10 | 4. The oracles receives the reward to their virtual balance after submission. 11 | 5. The reward can be withdrawn any time. 12 | 6. The admin can withdraw unalocated Links any time. 13 | 7. The oracle's payment can be configured by the admin. 14 | 8. The mint and burnt requests are confirmed only after requered amount of confirmations are received. 15 | 16 | ### Test Item: admin-only functions 17 | 18 | **Scope**: Test the configurations of the contract. 19 | 20 | **Action**: Invoke the `setMinConfirmations`, `setPayment`, `addOracle`, `removeOracle` methods. 21 | 22 | **Verification Steps**: Verify the operation only from authorized actors are permitted and the changes take effect. 23 | 24 | **Scenario 1**: Update configs by: 25 | 26 | - [x] admin 27 | - [x] one without admin permissions 28 | 29 | **Scenario 2**: Withdraw unalocated Links: 30 | 31 | - [x] admin 32 | - [x] one without admin permissions 33 | 34 | ### Test Item: oracle-only functions 35 | 36 | **Scope**: Test the withdrawing rewards of the contract. 37 | 38 | **Action**: Invoke the `withdrawPayment`. 39 | 40 | **Verification Steps**: Verify the operation only from authorized actors are permitted and the changes take effect. 41 | 42 | **Scenario 1**: Withdraw by: 43 | 44 | - [x] admin 45 | - [x] one without admin permissions 46 | 47 | **Scenario 2**: Withdraw amount: 48 | 49 | - [x] less then total reward 50 | - [x] higher then total reward 51 | 52 | **Scope**: Test the oracles-related functions of the contract. 53 | 54 | **Action**: Invoke the `submitMint`, `submitBurn` methods. 55 | 56 | **Verification Steps**: Verify the operation only from authorized actors are permitted and the changes take effect. 57 | 58 | **Scenario 1**: Test call by: 59 | 60 | - [x] oracle 61 | - [x] one without oracle permissions 62 | 63 | **Scenario 2**: Test confirmation: 64 | 65 | - [x] once 66 | - [x] twice 67 | 68 | **Scenario 3**: Test number of confirmation by different oracles: 69 | 70 | - [x] 1 confirmations out of 3 (2 is required) 71 | - [x] 2 confirmations out of 3 (2 is required) 72 | - [x] 2 confirmations out of 3 (2 is required) 73 | 74 | ## Test Item: WrappedAsset 75 | 76 | ### General requirements 77 | 78 | 1. Implements ERC20 and ERC2612. 79 | 2. Only admin can set minters. 80 | 3. Only minters can create new tokens. 81 | 4. Only minters can burn tokens. 82 | 83 | ### Test Item: admint-only functions 84 | 85 | **Scope**: Test the minter functions of the contract. 86 | 87 | **Action**: Invoke the `grantRole` methods. 88 | 89 | **Verification Steps**: Verify the operation only from authorized actors are permitted and the changes take effect. 90 | 91 | **Scenario 1**: Call `grantRole` by: 92 | 93 | - [ ] admin 94 | - [ ] one without admin permissions 95 | 96 | ### Test Item: minter-only functions 97 | 98 | **Scope**: Test the minter functions of the contract. 99 | 100 | **Action**: Invoke the `mint`, `burn` methods. 101 | 102 | **Verification Steps**: Verify the operation only from authorized actors are permitted and the changes take effect. 103 | 104 | **Scenario 1**: Call `mint` by: 105 | 106 | - [ ] minter 107 | - [ ] one without minter permissions 108 | 109 | **Scenario 2**: Call `burn` by: 110 | 111 | - [ ] minter 112 | - [ ] one without minter permissions 113 | 114 | ### Test Item: off-chain permit 115 | 116 | **Scope**: Test the off-chain permit of the contract. 117 | 118 | **Action**: Invoke the `permit` methods. 119 | 120 | **Verification Steps**: Verify the operation only with the correct signature are permitted and the changes take effect. 121 | 122 | **Scenario 1**: Call `permit`: 123 | 124 | - [ ] with correct signature 125 | - [ ] without correct signature 126 | 127 | ## Test Item: WhiteDebridge 128 | 129 | ### General requirements 130 | 131 | 1. Admin can add the support for the assets. 132 | 2. Both native chain's token and ERC20 tokens can be added. 133 | 3. If the asset isn't from the current chain the new wrapped asset (ERC20) is created. 134 | 4. To succeed the token transfer should be supported on both chains. 135 | 5. The transfer fee is charged when the transfer from original chain is started and/or returned back to the original chain. 136 | 6. The collected fees can be swapped to Link token and used to fund the CL aggregator. 137 | 7. Part of the fee can be withdrawn. 138 | 8. The aggregator can be replaced. 139 | 9. The part of locked tokens can be used in DEFI protocol. 140 | 10. The transfers must be confirmed by the oracles to be compleated. 141 | 142 | ### Test Item: admin-only actions 143 | 144 | **Scope**: Test configurations. 145 | 146 | **Action**: Invoke the `setAggregator`, `setFeeProxy`, `setDefiController`, `setWeth` methods. 147 | 148 | **Verification Steps**: Verify the operation works fine. 149 | 150 | **Scenario 1**: Call each of the methods by: 151 | 152 | - [x] admin 153 | - [x] not admin 154 | 155 | **Scope**: Test adding/removing assets. 156 | 157 | **Action**: Invoke the `setChainIdSupport`, `addNativeAsset`, `addExternalAsset` methods. 158 | 159 | **Verification Steps**: Verify the operation works fine. 160 | 161 | **Scenario 1**: Add asset by: 162 | 163 | - [x] admin 164 | - [x] not admin 165 | 166 | **Scenario 2**: Add asset: 167 | 168 | - [x] new 169 | - [x] added before 170 | 171 | **Scope**: Test fee managemnet. 172 | 173 | **Action**: Invoke the `fundAggregator`, `withdrawFee` methods. 174 | 175 | **Verification Steps**: Verify the operation works fine. 176 | 177 | **Scenario 1**: Calle methods by: 178 | 179 | - [ ] admin 180 | - [ ] not admin 181 | 182 | **Scenario 2**: Try to withdraw fee: 183 | 184 | - [ ] more than collected fee 185 | - [ ] less than collected fee 186 | 187 | **Scenario 3**: Try to fund the aggregator: 188 | 189 | - [ ] more than collected fee 190 | - [ ] less than collected fee 191 | 192 | **Scenario 4**: Try to use fees: 193 | 194 | - [ ] collected from the asset on the current chain 195 | - [ ] collected from the asset on the other chain 196 | 197 | ### Test Item: users actions 198 | 199 | **Scope**: Test send. 200 | 201 | **Action**: Invoke the `send` methods. 202 | 203 | **Verification Steps**: Verify the operation works fine. 204 | 205 | **Scenario 1**: Call send with different chains when: 206 | 207 | - [x] the current chain's asset 208 | - [x] the outside asset 209 | 210 | **Scenario 2**: Call send with different target chains when: 211 | 212 | - [x] the target chain is supported 213 | - [x] the target chain isn't supported 214 | 215 | **Scenario 3**: Call send with different amounts: 216 | 217 | - [x] the amount is enough 218 | - [x] to few tokens 219 | 220 | **Scenario 4**: Call send with different assets: 221 | 222 | - [x] the ERC20 223 | - [x] native token 224 | 225 | **Scope**: Test mint. 226 | 227 | **Action**: Invoke the `mint` methods. 228 | 229 | **Verification Steps**: Verify the operation works fine. 230 | 231 | **Scenario 1**: Call mint with different approvals when: 232 | 233 | - [x] the mint is approved 234 | - [x] the mint isn't approved 235 | 236 | **Scenario 2**: Call mint few times: 237 | 238 | - [x] first time 239 | - [x] second time 240 | 241 | **Scenario 3**: Call mint with different chains: 242 | 243 | - [x] supported chain 244 | - [x] prohibited chain 245 | 246 | **Scope**: Test burn. 247 | 248 | **Action**: Invoke the `burn` methods. 249 | 250 | **Verification Steps**: Verify the operation works fine. 251 | 252 | **Scenario 1**: Call burn with different chains when: 253 | 254 | - [x] with the current chain 255 | - [x] with the different chain 256 | 257 | **Scenario 2**: Call burn with diffrent amounts when: 258 | 259 | - [x] enough tokens are transfered 260 | - [x] too few tokens are sent 261 | 262 | **Scope**: Test claim. 263 | 264 | **Action**: Invoke the `claim` methods. 265 | 266 | **Verification Steps**: Verify the operation works fine. 267 | 268 | **Scenario 1**: Call claim with different chains when: 269 | 270 | - [x] the current chain's asset 271 | - [x] the outside asset 272 | 273 | **Scenario 2**: Call claim with different confirmations when: 274 | 275 | - [x] the burnt is confiremd 276 | - [x] the burnt isn't confirmed 277 | 278 | **Scenario 3**: Call claim few times: 279 | 280 | - [x] in the first time 281 | - [x] in the second time 282 | 283 | **Scenario 4**: Call claim with different assets: 284 | 285 | - [x] the ERC20 286 | - [x] native token 287 | 288 | ### Test Item: fee management 289 | 290 | **Scope**: Test fee withdrawal. 291 | 292 | **Action**: Invoke the `withdrawFee` methods. 293 | 294 | **Verification Steps**: Verify the operation works fine. 295 | 296 | **Scenario 1**: Call `withdrawFee` by : 297 | 298 | - [x] admin 299 | - [x] non-admin 300 | 301 | **Scenario 2**: Call `withdrawFees` with different chains when: 302 | 303 | - [x] the current chain's asset 304 | - [x] the outside asset 305 | 306 | **Scenario 3**: Call `withdrawFee` with different assets: 307 | 308 | - [x] the ERC20 309 | - [x] native token 310 | 311 | **Scope**: Test fund aggregator. 312 | 313 | **Action**: Invoke the `fundAggregator` methods. 314 | 315 | **Verification Steps**: Verify the operation works fine. 316 | 317 | **Scenario 1**: Call `fundAggregator` by : 318 | 319 | - [x] admin 320 | - [x] non-admin 321 | 322 | **Scenario 2**: Call `fundAggregator` with different chains when: 323 | 324 | - [x] the current chain's asset 325 | - [x] the outside asset 326 | 327 | **Scenario 3**: Call `fundAggregator` with different assets: 328 | 329 | - [x] the ERC20 330 | - [x] native token 331 | 332 | ## Test Item: FeeProxy 333 | 334 | ### General requirements 335 | 336 | 1. Should swap any tokens on the balance to Link. 337 | 338 | ### Test Item: off-chain permit 339 | 340 | **Scope**: Test swap. 341 | 342 | **Action**: Invoke the `swapToLink` methods. 343 | 344 | **Verification Steps**: Verify the operation works fine. 345 | 346 | **Scenario 1**: Call `permit`: 347 | 348 | - [ ] swap native asset 349 | - [ ] swap token 350 | -------------------------------------------------------------------------------- /test/00_WhiteAggregator.test.js: -------------------------------------------------------------------------------- 1 | const { expectRevert } = require("@openzeppelin/test-helpers"); 2 | const WhiteAggregator = artifacts.require("WhiteAggregator"); 3 | const MockLinkToken = artifacts.require("MockLinkToken"); 4 | const { toWei, fromWei, toBN } = web3.utils; 5 | 6 | contract("WhiteAggregator", function ([alice, bob, carol, eve, devid]) { 7 | before(async function () { 8 | this.linkToken = await MockLinkToken.new("Link Token", "dLINK", 18, { 9 | from: alice, 10 | }); 11 | this.oraclePayment = toWei("0.001"); 12 | this.minConfirmations = 2; 13 | this.whiteAggregator = await WhiteAggregator.new( 14 | this.minConfirmations, 15 | this.oraclePayment, 16 | this.linkToken.address, 17 | { 18 | from: alice, 19 | } 20 | ); 21 | this.initialOracles = [ 22 | { 23 | address: alice, 24 | admin: alice, 25 | }, 26 | { 27 | address: bob, 28 | admin: carol, 29 | }, 30 | { 31 | address: eve, 32 | admin: carol, 33 | }, 34 | ]; 35 | for (let oracle of this.initialOracles) { 36 | await this.whiteAggregator.addOracle(oracle.address, oracle.admin, { 37 | from: alice, 38 | }); 39 | } 40 | }); 41 | 42 | it("should have correct initial values", async function () { 43 | const minConfirmations = await this.whiteAggregator.minConfirmations(); 44 | const allocatedFunds = await this.whiteAggregator.allocatedFunds(); 45 | const availableFunds = await this.whiteAggregator.availableFunds(); 46 | const payment = await this.whiteAggregator.payment(); 47 | const link = await this.whiteAggregator.link(); 48 | assert.equal(minConfirmations, this.minConfirmations); 49 | assert.equal(allocatedFunds, 0); 50 | assert.equal(availableFunds, 0); 51 | assert.equal(payment, this.oraclePayment); 52 | assert.equal(link, this.linkToken.address); 53 | }); 54 | 55 | context("Test setting configurations by different users", () => { 56 | it("should set min confirmations if called by the admin", async function () { 57 | const newConfirmations = 2; 58 | await this.whiteAggregator.setMinConfirmations(newConfirmations, { 59 | from: alice, 60 | }); 61 | const minConfirmations = await this.whiteAggregator.minConfirmations(); 62 | assert.equal(minConfirmations, newConfirmations); 63 | }); 64 | 65 | it("should set oracle payment if called by the admin", async function () { 66 | const newPayment = toWei("0.1"); 67 | await this.whiteAggregator.setPayment(newPayment, { 68 | from: alice, 69 | }); 70 | const payment = await this.whiteAggregator.payment(); 71 | assert.equal(newPayment, payment); 72 | }); 73 | 74 | it("should add new oracle if called by the admin", async function () { 75 | await this.whiteAggregator.addOracle(devid, eve, { 76 | from: alice, 77 | }); 78 | const oracleInfo = await this.whiteAggregator.getOracleInfo(devid); 79 | assert.ok(oracleInfo.withdrawable, 0); 80 | assert.ok(oracleInfo.admin, eve); 81 | }); 82 | 83 | it("should remove existed oracle if called by the admin", async function () { 84 | await this.whiteAggregator.removeOracle(devid, { 85 | from: alice, 86 | }); 87 | const oracleInfo = await this.whiteAggregator.getOracleInfo(devid); 88 | assert.ok(oracleInfo.withdrawable, 0); 89 | assert.ok(oracleInfo.admin, eve); 90 | }); 91 | 92 | it("should reject setting min confirmations if called by the non-admin", async function () { 93 | const newConfirmations = 2; 94 | await expectRevert( 95 | this.whiteAggregator.setMinConfirmations(newConfirmations, { 96 | from: bob, 97 | }), 98 | "onlyAdmin: bad role" 99 | ); 100 | }); 101 | 102 | it("should reject setting oracle payment if called by the non-admin", async function () { 103 | const newPayment = toWei("0.0001"); 104 | await expectRevert( 105 | this.whiteAggregator.setPayment(newPayment, { 106 | from: bob, 107 | }), 108 | "onlyAdmin: bad role" 109 | ); 110 | }); 111 | 112 | it("should reject adding the new oracle if called by the non-admin", async function () { 113 | await expectRevert( 114 | this.whiteAggregator.addOracle(devid, eve, { 115 | from: bob, 116 | }), 117 | "onlyAdmin: bad role" 118 | ); 119 | }); 120 | 121 | it("should reject removing the new oracle if called by the non-admin", async function () { 122 | await expectRevert( 123 | this.whiteAggregator.removeOracle(devid, { 124 | from: bob, 125 | }), 126 | "onlyAdmin: bad role" 127 | ); 128 | }); 129 | }); 130 | 131 | context("Test funding the contract", () => { 132 | before(async function () { 133 | const amount = toWei("100"); 134 | await this.linkToken.mint(alice, amount, { 135 | from: alice, 136 | }); 137 | }); 138 | 139 | it("should update virtual balances once the tokens are transfered with callback", async function () { 140 | const amount = toWei("10"); 141 | const prevAvailableFunds = await this.whiteAggregator.availableFunds(); 142 | await this.linkToken.transferAndCall( 143 | this.whiteAggregator.address.toString(), 144 | amount, 145 | "0x", 146 | { 147 | from: alice, 148 | } 149 | ); 150 | const newAvailableFunds = await this.whiteAggregator.availableFunds(); 151 | assert.equal( 152 | prevAvailableFunds.add(toBN(amount)).toString(), 153 | newAvailableFunds.toString() 154 | ); 155 | }); 156 | 157 | it("should update virtual balances once the tokens are transfered and the update method is called", async function () { 158 | const amount = toWei("10"); 159 | const prevAvailableFunds = await this.whiteAggregator.availableFunds(); 160 | await this.linkToken.transfer( 161 | this.whiteAggregator.address.toString(), 162 | amount, 163 | { 164 | from: alice, 165 | } 166 | ); 167 | await this.whiteAggregator.updateAvailableFunds({ 168 | from: alice, 169 | }); 170 | const newAvailableFunds = await this.whiteAggregator.availableFunds(); 171 | assert.equal( 172 | prevAvailableFunds.add(toBN(amount)).toString(), 173 | newAvailableFunds.toString() 174 | ); 175 | }); 176 | }); 177 | 178 | context("Test withdrawing unallocated funds from the contract", () => { 179 | it("should withdraw unallocated funds by the admin", async function () { 180 | const amount = toWei("5"); 181 | const prevAvailableFunds = await this.whiteAggregator.availableFunds(); 182 | await this.whiteAggregator.withdrawFunds(bob, amount, { 183 | from: alice, 184 | }); 185 | const newAvailableFunds = await this.whiteAggregator.availableFunds(); 186 | assert.equal( 187 | prevAvailableFunds.sub(toBN(amount)).toString(), 188 | newAvailableFunds.toString() 189 | ); 190 | }); 191 | 192 | it("should reject withdrawing unallocated funds if called by the non-admin", async function () { 193 | const amount = toWei("5"); 194 | await expectRevert( 195 | this.whiteAggregator.withdrawFunds(devid, amount, { 196 | from: bob, 197 | }), 198 | "onlyAdmin: bad role" 199 | ); 200 | }); 201 | 202 | it("should reject withdrawing more than available", async function () { 203 | const amount = toWei("50"); 204 | await expectRevert( 205 | this.whiteAggregator.withdrawFunds(devid, amount, { 206 | from: alice, 207 | }), 208 | "insufficient reserve funds" 209 | ); 210 | }); 211 | }); 212 | 213 | context("Test data submission", () => { 214 | it("should submit mint identifier by the oracle", async function () { 215 | const submission = 216 | "0x89584038ebea621ff70560fbaf39157324a6628536a6ba30650b3bf4fcb73aed"; 217 | const prevAvailableFunds = await this.whiteAggregator.availableFunds(); 218 | const prevAllocatedFunds = await this.whiteAggregator.allocatedFunds(); 219 | const payment = await this.whiteAggregator.payment(); 220 | await this.whiteAggregator.submitMint(submission, { 221 | from: bob, 222 | }); 223 | const mintInfo = await this.whiteAggregator.getMintInfo(submission); 224 | const newAvailableFunds = await this.whiteAggregator.availableFunds(); 225 | const newAllocatedFunds = await this.whiteAggregator.allocatedFunds(); 226 | assert.equal(mintInfo.confirmations, 1); 227 | assert.ok(!mintInfo.confirmed); 228 | assert.equal( 229 | prevAvailableFunds.sub(toBN(payment)).toString(), 230 | newAvailableFunds.toString() 231 | ); 232 | assert.equal( 233 | prevAllocatedFunds.add(toBN(payment)).toString(), 234 | newAllocatedFunds.toString() 235 | ); 236 | }); 237 | 238 | it("should submit burnt identifier by the oracle", async function () { 239 | const submission = 240 | "0x89584038ebea621ff70560fbaf39157324a6628536a6ba30650b3bf4fcb73aed"; 241 | const prevAvailableFunds = await this.whiteAggregator.availableFunds(); 242 | const prevAllocatedFunds = await this.whiteAggregator.allocatedFunds(); 243 | const payment = await this.whiteAggregator.payment(); 244 | await this.whiteAggregator.submitBurn(submission, { 245 | from: bob, 246 | }); 247 | const burntInfo = await this.whiteAggregator.getBurntInfo(submission); 248 | const newAvailableFunds = await this.whiteAggregator.availableFunds(); 249 | const newAllocatedFunds = await this.whiteAggregator.allocatedFunds(); 250 | assert.equal(burntInfo.confirmations, 1); 251 | assert.ok(!burntInfo.confirmed); 252 | assert.equal( 253 | prevAvailableFunds.sub(toBN(payment)).toString(), 254 | newAvailableFunds.toString() 255 | ); 256 | assert.equal( 257 | prevAllocatedFunds.add(toBN(payment)).toString(), 258 | newAllocatedFunds.toString() 259 | ); 260 | }); 261 | 262 | it("should submit mint identifier by the second oracle", async function () { 263 | const submission = 264 | "0x89584038ebea621ff70560fbaf39157324a6628536a6ba30650b3bf4fcb73aed"; 265 | const prevAvailableFunds = await this.whiteAggregator.availableFunds(); 266 | const prevAllocatedFunds = await this.whiteAggregator.allocatedFunds(); 267 | const payment = await this.whiteAggregator.payment(); 268 | await this.whiteAggregator.submitMint(submission, { 269 | from: alice, 270 | }); 271 | const mintInfo = await this.whiteAggregator.getMintInfo(submission); 272 | const newAvailableFunds = await this.whiteAggregator.availableFunds(); 273 | const newAllocatedFunds = await this.whiteAggregator.allocatedFunds(); 274 | assert.equal(mintInfo.confirmations, 2); 275 | assert.ok(mintInfo.confirmed); 276 | assert.equal( 277 | prevAvailableFunds.sub(toBN(payment)).toString(), 278 | newAvailableFunds.toString() 279 | ); 280 | assert.equal( 281 | prevAllocatedFunds.add(toBN(payment)).toString(), 282 | newAllocatedFunds.toString() 283 | ); 284 | }); 285 | 286 | it("should submit burnt identifier by the second oracle", async function () { 287 | const submission = 288 | "0x89584038ebea621ff70560fbaf39157324a6628536a6ba30650b3bf4fcb73aed"; 289 | const prevAvailableFunds = await this.whiteAggregator.availableFunds(); 290 | const prevAllocatedFunds = await this.whiteAggregator.allocatedFunds(); 291 | const payment = await this.whiteAggregator.payment(); 292 | await this.whiteAggregator.submitBurn(submission, { 293 | from: alice, 294 | }); 295 | const burntInfo = await this.whiteAggregator.getBurntInfo(submission); 296 | const newAvailableFunds = await this.whiteAggregator.availableFunds(); 297 | const newAllocatedFunds = await this.whiteAggregator.allocatedFunds(); 298 | assert.equal(burntInfo.confirmations, 2); 299 | assert.ok(burntInfo.confirmed); 300 | assert.equal( 301 | prevAvailableFunds.sub(toBN(payment)).toString(), 302 | newAvailableFunds.toString() 303 | ); 304 | assert.equal( 305 | prevAllocatedFunds.add(toBN(payment)).toString(), 306 | newAllocatedFunds.toString() 307 | ); 308 | }); 309 | 310 | it("should submit mint identifier by the extra oracle", async function () { 311 | const submission = 312 | "0x89584038ebea621ff70560fbaf39157324a6628536a6ba30650b3bf4fcb73aed"; 313 | const prevAvailableFunds = await this.whiteAggregator.availableFunds(); 314 | const prevAllocatedFunds = await this.whiteAggregator.allocatedFunds(); 315 | const payment = await this.whiteAggregator.payment(); 316 | await this.whiteAggregator.submitMint(submission, { 317 | from: eve, 318 | }); 319 | const mintInfo = await this.whiteAggregator.getMintInfo(submission); 320 | const newAvailableFunds = await this.whiteAggregator.availableFunds(); 321 | const newAllocatedFunds = await this.whiteAggregator.allocatedFunds(); 322 | assert.equal(mintInfo.confirmations, 3); 323 | assert.ok(mintInfo.confirmed); 324 | assert.equal( 325 | prevAvailableFunds.sub(toBN(payment)).toString(), 326 | newAvailableFunds.toString() 327 | ); 328 | assert.equal( 329 | prevAllocatedFunds.add(toBN(payment)).toString(), 330 | newAllocatedFunds.toString() 331 | ); 332 | }); 333 | 334 | it("should submit burnt identifier by the extra oracle", async function () { 335 | const submission = 336 | "0x89584038ebea621ff70560fbaf39157324a6628536a6ba30650b3bf4fcb73aed"; 337 | const prevAvailableFunds = await this.whiteAggregator.availableFunds(); 338 | const prevAllocatedFunds = await this.whiteAggregator.allocatedFunds(); 339 | const payment = await this.whiteAggregator.payment(); 340 | await this.whiteAggregator.submitBurn(submission, { 341 | from: eve, 342 | }); 343 | const burntInfo = await this.whiteAggregator.getBurntInfo(submission); 344 | const newAvailableFunds = await this.whiteAggregator.availableFunds(); 345 | const newAllocatedFunds = await this.whiteAggregator.allocatedFunds(); 346 | assert.equal(burntInfo.confirmations, 3); 347 | assert.ok(burntInfo.confirmed); 348 | assert.equal( 349 | prevAvailableFunds.sub(toBN(payment)).toString(), 350 | newAvailableFunds.toString() 351 | ); 352 | assert.equal( 353 | prevAllocatedFunds.add(toBN(payment)).toString(), 354 | newAllocatedFunds.toString() 355 | ); 356 | }); 357 | 358 | it("should reject submition of mint identifier if called by the non-admin", async function () { 359 | const submission = 360 | "0x2a16bc164de069184383a55bbddb893f418fd72781f5b2db1b68de1dc697ea44"; 361 | await expectRevert( 362 | this.whiteAggregator.submitMint(submission, { 363 | from: devid, 364 | }), 365 | "onlyOracle: bad role" 366 | ); 367 | }); 368 | 369 | it("should reject submition of burnt identifier if called by the non-admin", async function () { 370 | const submission = 371 | "0x2a16bc164de069184383a55bbddb893f418fd72781f5b2db1b68de1dc697ea44"; 372 | await expectRevert( 373 | this.whiteAggregator.submitBurn(submission, { 374 | from: devid, 375 | }), 376 | "onlyOracle: bad role" 377 | ); 378 | }); 379 | 380 | it("should reject submition of dublicated mint identifiers with the same id by the same oracle", async function () { 381 | const submission = 382 | "0x89584038ebea621ff70560fbaf39157324a6628536a6ba30650b3bf4fcb73aed"; 383 | await expectRevert( 384 | this.whiteAggregator.submitMint(submission, { 385 | from: bob, 386 | }), 387 | "submit: submitted already" 388 | ); 389 | }); 390 | 391 | it("should reject submition of dublicated burnt identifiers with the same id by the same oracle", async function () { 392 | const submission = 393 | "0x89584038ebea621ff70560fbaf39157324a6628536a6ba30650b3bf4fcb73aed"; 394 | await expectRevert( 395 | this.whiteAggregator.submitBurn(submission, { 396 | from: bob, 397 | }), 398 | "submit: submitted already" 399 | ); 400 | }); 401 | }); 402 | 403 | context("Test withdrawal oracle reward", () => { 404 | it("should withdraw the reward by the oracle admin", async function () { 405 | const prevAvailableFunds = await this.whiteAggregator.availableFunds(); 406 | const prevAllocatedFunds = await this.whiteAggregator.allocatedFunds(); 407 | const amount = toWei("0.1"); 408 | const prevOracleInfo = await this.whiteAggregator.getOracleInfo(bob); 409 | await this.whiteAggregator.withdrawPayment(bob, carol, amount, { 410 | from: carol, 411 | }); 412 | const newOracleInfo = await this.whiteAggregator.getOracleInfo(bob); 413 | const newAvailableFunds = await this.whiteAggregator.availableFunds(); 414 | const newAllocatedFunds = await this.whiteAggregator.allocatedFunds(); 415 | assert.equal( 416 | prevOracleInfo.withdrawable.sub(toBN(amount)).toString(), 417 | newOracleInfo.withdrawable.toString() 418 | ); 419 | assert.equal(prevAvailableFunds.toString(), newAvailableFunds.toString()); 420 | assert.equal( 421 | prevAllocatedFunds.sub(toBN(amount)).toString(), 422 | newAllocatedFunds.toString() 423 | ); 424 | }); 425 | 426 | it("should reject withdrawing by non-admint", async function () { 427 | const amount = toWei("50"); 428 | await expectRevert( 429 | this.whiteAggregator.withdrawPayment(bob, carol, amount, { 430 | from: bob, 431 | }), 432 | "withdrawPayment: only callable by admin" 433 | ); 434 | }); 435 | 436 | it("should reject withdrawing more than available", async function () { 437 | const amount = toWei("50"); 438 | await expectRevert( 439 | this.whiteAggregator.withdrawPayment(bob, carol, amount, { 440 | from: carol, 441 | }), 442 | "withdrawPayment: insufficient withdrawable funds" 443 | ); 444 | }); 445 | }); 446 | }); 447 | -------------------------------------------------------------------------------- /contracts/transfers/WhiteDebridge.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.2; 3 | 4 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 5 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 6 | import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; 7 | import "@openzeppelin/contracts/access/AccessControl.sol"; 8 | import "../interfaces/IWhiteDebridge.sol"; 9 | import "../interfaces/IFeeProxy.sol"; 10 | import "../interfaces/IWETH.sol"; 11 | import "../interfaces/IDefiController.sol"; 12 | import "../interfaces/IWhiteAggregator.sol"; 13 | import "../periphery/WrappedAsset.sol"; 14 | 15 | contract WhiteDebridge is AccessControl, IWhiteDebridge { 16 | using SafeERC20 for IERC20; 17 | 18 | struct DebridgeInfo { 19 | address tokenAddress; // asset address on the current chain 20 | uint256 chainId; // native chain id 21 | uint256 minAmount; // minimal amount to transfer 22 | uint256 transferFee; // transfer fee rate 23 | uint256 collectedFees; // total collected fees that can be used to buy LINK 24 | uint256 balance; // total locked assets 25 | uint256 minReserves; // minimal hot reserves 26 | uint256[] chainIds; // list of all supported chain ids 27 | mapping(uint256 => bool) isSupported; // wheter the chain for the asset is supported 28 | } 29 | 30 | uint256 public constant DENOMINATOR = 1e18; // accuacy multiplyer 31 | uint256 public chainId; // current chain id 32 | IWhiteAggregator public aggregator; // chainlink aggregator address 33 | IFeeProxy public feeProxy; // proxy to convert the collected fees into Link's 34 | IDefiController public defiController; // proxy to use the locked assets in Defi protocols 35 | IWETH public weth; // wrapped native token contract 36 | mapping(bytes32 => DebridgeInfo) public getDebridge; // debridgeId (i.e. hash(native chainId, native tokenAddress)) => token 37 | mapping(bytes32 => bool) public isSubmissionUsed; // submissionId (i.e. hash( debridgeId, amount, receiver, nonce)) => whether is claimed 38 | mapping(address => uint256) public getUserNonce; // submissionId (i.e. hash( debridgeId, amount, receiver, nonce)) => whether is claimed 39 | 40 | event Sent( 41 | bytes32 submissionId, 42 | bytes32 debridgeId, 43 | uint256 amount, 44 | address receiver, 45 | uint256 nonce, 46 | uint256 chainIdTo 47 | ); // emited once the native tokens are locked to be sent to the other chain 48 | event Minted( 49 | bytes32 submissionId, 50 | uint256 amount, 51 | address receiver, 52 | bytes32 debridgeId 53 | ); // emited once the wrapped tokens are minted on the current chain 54 | event Burnt( 55 | bytes32 submissionId, 56 | bytes32 debridgeId, 57 | uint256 amount, 58 | address receiver, 59 | uint256 nonce, 60 | uint256 chainIdTo 61 | ); // emited once the wrapped tokens are sent to the contract 62 | event Claimed( 63 | bytes32 submissionId, 64 | uint256 amount, 65 | address receiver, 66 | bytes32 debridgeId 67 | ); // emited once the tokens are withdrawn on native chain 68 | event PairAdded( 69 | bytes32 indexed debridgeId, 70 | address indexed tokenAddress, 71 | uint256 indexed chainId, 72 | uint256 minAmount, 73 | uint256 transferFee, 74 | uint256 minReserves 75 | ); // emited when new asset is supported 76 | event ChainSupportAdded( 77 | bytes32 indexed debridgeId, 78 | uint256 indexed chainId 79 | ); // emited when the asset is allowed to be spent on other chains 80 | event ChainSupportRemoved( 81 | bytes32 indexed debridgeId, 82 | uint256 indexed chainId 83 | ); // emited when the asset is disallowed to be spent on other chains 84 | 85 | modifier onlyAggregator { 86 | require(address(aggregator) == msg.sender, "onlyAggregator: bad role"); 87 | _; 88 | } 89 | modifier onlyDefiController { 90 | require( 91 | address(defiController) == msg.sender, 92 | "defiController: bad role" 93 | ); 94 | _; 95 | } 96 | modifier onlyAdmin { 97 | require(hasRole(DEFAULT_ADMIN_ROLE, msg.sender), "onlyAdmin: bad role"); 98 | _; 99 | } 100 | 101 | /* EXTERNAL */ 102 | 103 | /// @dev Constructor that initializes the most important configurations. 104 | /// @param _minAmount Minimal amount of current chain token to be wrapped. 105 | /// @param _transferFee Transfer fee rate. 106 | /// @param _minReserves Minimal reserve ratio. 107 | /// @param _aggregator Submission aggregator address. 108 | /// @param _supportedChainIds Chain ids where native token of the current chain can be wrapped. 109 | constructor( 110 | uint256 _minAmount, 111 | uint256 _transferFee, 112 | uint256 _minReserves, 113 | IWhiteAggregator _aggregator, 114 | uint256[] memory _supportedChainIds, 115 | IWETH _weth, 116 | IFeeProxy _feeProxy, 117 | IDefiController _defiController 118 | ) { 119 | uint256 cid; 120 | assembly { 121 | cid := chainid() 122 | } 123 | chainId = cid; 124 | bytes32 debridgeId = getDebridgeId(chainId, address(0)); 125 | _addAsset( 126 | debridgeId, 127 | address(0), 128 | chainId, 129 | _minAmount, 130 | _transferFee, 131 | _minReserves, 132 | _supportedChainIds 133 | ); 134 | aggregator = _aggregator; 135 | weth = _weth; 136 | feeProxy = _feeProxy; 137 | _defiController = defiController; 138 | _setupRole(DEFAULT_ADMIN_ROLE, msg.sender); 139 | } 140 | 141 | /// @dev Locks asset on the chain and enables minting on the other chain. 142 | /// @param _debridgeId Asset identifier. 143 | /// @param _receiver Receiver address. 144 | /// @param _amount Amount to be transfered (note: the fee can be applyed). 145 | /// @param _chainIdTo Chain id of the target chain. 146 | function send( 147 | bytes32 _debridgeId, 148 | address _receiver, 149 | uint256 _amount, 150 | uint256 _chainIdTo 151 | ) external payable override { 152 | DebridgeInfo storage debridge = getDebridge[_debridgeId]; 153 | require(debridge.chainId == chainId, "send: not native chain"); 154 | require(debridge.isSupported[_chainIdTo], "send: wrong targed chain"); 155 | require(_amount >= debridge.minAmount, "send: amount too low"); 156 | if (debridge.tokenAddress == address(0)) { 157 | require(_amount == msg.value, "send: amount mismatch"); 158 | } else { 159 | IERC20(debridge.tokenAddress).safeTransferFrom( 160 | msg.sender, 161 | address(this), 162 | _amount 163 | ); 164 | } 165 | uint256 transferFee = (_amount * debridge.transferFee) / DENOMINATOR; 166 | if (transferFee > 0) { 167 | debridge.collectedFees += transferFee; 168 | _amount -= transferFee; 169 | } 170 | debridge.balance += _amount; 171 | uint256 nonce = getUserNonce[_receiver]; 172 | bytes32 sentId = getSubmisionId(_debridgeId, _amount, _receiver, nonce); 173 | emit Sent(sentId, _debridgeId, _amount, _receiver, nonce, _chainIdTo); 174 | getUserNonce[_receiver]++; 175 | } 176 | 177 | /// @dev Mints wrapped asset on the current chain. 178 | /// @param _debridgeId Asset identifier. 179 | /// @param _receiver Receiver address. 180 | /// @param _amount Amount of the transfered asset (note: without applyed fee). 181 | /// @param _nonce Submission id. 182 | function mint( 183 | bytes32 _debridgeId, 184 | address _receiver, 185 | uint256 _amount, 186 | uint256 _nonce 187 | ) external override { 188 | bytes32 mintId = 189 | getSubmisionId(_debridgeId, _amount, _receiver, _nonce); 190 | require(aggregator.isMintConfirmed(mintId), "mint: not confirmed"); 191 | require(!isSubmissionUsed[mintId], "mint: already used"); 192 | DebridgeInfo storage debridge = getDebridge[_debridgeId]; 193 | isSubmissionUsed[mintId] = true; 194 | IWrappedAsset(debridge.tokenAddress).mint(_receiver, _amount); 195 | emit Minted(mintId, _amount, _receiver, _debridgeId); 196 | } 197 | 198 | /// @dev Burns wrapped asset and allowss to claim it on the other chain. 199 | /// @param _debridgeId Asset identifier. 200 | /// @param _receiver Receiver address. 201 | /// @param _amount Amount of the transfered asset (note: the fee can be applyed). 202 | function burn( 203 | bytes32 _debridgeId, 204 | address _receiver, 205 | uint256 _amount 206 | ) external override { 207 | DebridgeInfo storage debridge = getDebridge[_debridgeId]; 208 | require(debridge.chainId != chainId, "burn: native asset"); 209 | require(_amount >= debridge.minAmount, "burn: amount too low"); 210 | IWrappedAsset wrappedAsset = IWrappedAsset(debridge.tokenAddress); 211 | wrappedAsset.transferFrom(msg.sender, address(this), _amount); 212 | wrappedAsset.burn(_amount); 213 | uint256 nonce = getUserNonce[_receiver]; 214 | bytes32 burntId = 215 | getSubmisionId(_debridgeId, _amount, _receiver, nonce); 216 | emit Burnt( 217 | burntId, 218 | _debridgeId, 219 | _amount, 220 | _receiver, 221 | nonce, 222 | debridge.chainId 223 | ); 224 | getUserNonce[_receiver]++; 225 | } 226 | 227 | /// @dev Unlock the asset on the current chain and transfer to receiver. 228 | /// @param _debridgeId Asset identifier. 229 | /// @param _receiver Receiver address. 230 | /// @param _amount Amount of the transfered asset (note: the fee can be applyed). 231 | /// @param _nonce Submission id. 232 | function claim( 233 | bytes32 _debridgeId, 234 | address _receiver, 235 | uint256 _amount, 236 | uint256 _nonce 237 | ) external override { 238 | bytes32 burntId = 239 | getSubmisionId(_debridgeId, _amount, _receiver, _nonce); 240 | require(aggregator.isBurntConfirmed(burntId), "claim: not confirmed"); 241 | DebridgeInfo storage debridge = getDebridge[_debridgeId]; 242 | require(debridge.chainId == chainId, "claim: wrong target chain"); 243 | require(!isSubmissionUsed[burntId], "claim: already used"); 244 | isSubmissionUsed[burntId] = true; 245 | uint256 transferFee = (_amount * debridge.transferFee) / DENOMINATOR; 246 | debridge.balance -= _amount; 247 | if (transferFee > 0) { 248 | debridge.collectedFees += transferFee; 249 | _amount -= transferFee; 250 | } 251 | _ensureReserves(debridge, _amount); 252 | if (debridge.tokenAddress == address(0)) { 253 | payable(_receiver).transfer(_amount); 254 | } else { 255 | IERC20(debridge.tokenAddress).safeTransfer(_receiver, _amount); 256 | } 257 | emit Claimed(burntId, _amount, _receiver, _debridgeId); 258 | } 259 | 260 | /* ADMIN */ 261 | 262 | /// @dev Add support for the asset on the current chain. 263 | /// @param _tokenAddress Address of the asset on the current chain. 264 | /// @param _minAmount Minimal amount of current chain token to be wrapped. 265 | /// @param _transferFee Transfer fee rate. 266 | /// @param _minReserves Minimal reserve ration. 267 | /// @param _supportedChainIds Chain ids where native token of the current chain can be wrapped. 268 | function addNativeAsset( 269 | address _tokenAddress, 270 | uint256 _minAmount, 271 | uint256 _transferFee, 272 | uint256 _minReserves, 273 | uint256[] memory _supportedChainIds 274 | ) external override onlyAdmin() { 275 | bytes32 debridgeId = getDebridgeId(chainId, _tokenAddress); 276 | _addAsset( 277 | debridgeId, 278 | _tokenAddress, 279 | chainId, 280 | _minAmount, 281 | _transferFee, 282 | _minReserves, 283 | _supportedChainIds 284 | ); 285 | } 286 | 287 | /// @dev Add support for the asset from the other chain, deploy new wrapped asset. 288 | /// @param _tokenAddress Address of the asset on the other chain. 289 | /// @param _chainId Current chain id. 290 | /// @param _minAmount Minimal amount of the asset to be wrapped. 291 | /// @param _transferFee Transfer fee rate. 292 | /// @param _minReserves Minimal reserve ration. 293 | /// @param _supportedChainIds Chain ids where the token of the current chain can be transfered. 294 | /// @param _name Wrapped asset name. 295 | /// @param _symbol Wrapped asset symbol. 296 | function addExternalAsset( 297 | address _tokenAddress, 298 | uint256 _chainId, 299 | uint256 _minAmount, 300 | uint256 _transferFee, 301 | uint256 _minReserves, 302 | uint256[] memory _supportedChainIds, 303 | string memory _name, 304 | string memory _symbol 305 | ) external override onlyAdmin() { 306 | bytes32 debridgeId = getDebridgeId(_chainId, _tokenAddress); 307 | address tokenAddress = address(new WrappedAsset(_name, _symbol)); 308 | _addAsset( 309 | debridgeId, 310 | tokenAddress, 311 | _chainId, 312 | _minAmount, 313 | _transferFee, 314 | _minReserves, 315 | _supportedChainIds 316 | ); 317 | } 318 | 319 | /// @dev Set support for the chains where the token can be transfered. 320 | /// @param _debridgeId Asset identifier. 321 | /// @param _chainId Current chain id. 322 | /// @param _isSupported Whether the token is transferable to the other chain. 323 | function setChainIdSupport( 324 | bytes32 _debridgeId, 325 | uint256 _chainId, 326 | bool _isSupported 327 | ) external override onlyAdmin() { 328 | DebridgeInfo storage debridge = getDebridge[_debridgeId]; 329 | debridge.isSupported[_chainId] = _isSupported; 330 | if (_isSupported) { 331 | emit ChainSupportAdded(_debridgeId, _chainId); 332 | } else { 333 | emit ChainSupportRemoved(_debridgeId, _chainId); 334 | } 335 | } 336 | 337 | /// @dev Set aggregator address. 338 | /// @param _aggregator Submission aggregator address. 339 | function setAggregator(IWhiteAggregator _aggregator) external onlyAdmin() { 340 | aggregator = _aggregator; 341 | } 342 | 343 | /// @dev Set fee converter proxy. 344 | /// @param _feeProxy Submission aggregator address. 345 | function setFeeProxy(IFeeProxy _feeProxy) external onlyAdmin() { 346 | feeProxy = _feeProxy; 347 | } 348 | 349 | /// @dev Set defi controoler. 350 | /// @param _defiController Submission aggregator address. 351 | function setDefiController(IDefiController _defiController) 352 | external 353 | onlyAdmin() 354 | { 355 | // TODO: claim all the reserves before 356 | defiController = _defiController; 357 | } 358 | 359 | /// @dev Set wrapped native asset address. 360 | /// @param _weth Submission aggregator address. 361 | function setWeth(IWETH _weth) external onlyAdmin() { 362 | weth = _weth; 363 | } 364 | 365 | /// @dev Withdraw fees. 366 | /// @param _debridgeId Asset identifier. 367 | /// @param _receiver Receiver address. 368 | /// @param _amount Submission aggregator address. 369 | function withdrawFee( 370 | bytes32 _debridgeId, 371 | address _receiver, 372 | uint256 _amount 373 | ) external onlyAdmin() { 374 | DebridgeInfo storage debridge = getDebridge[_debridgeId]; 375 | require(debridge.chainId == chainId, "withdrawFee: wrong target chain"); 376 | require( 377 | debridge.collectedFees >= _amount, 378 | "withdrawFee: not enough fee" 379 | ); 380 | debridge.collectedFees -= _amount; 381 | if (debridge.tokenAddress == address(0)) { 382 | payable(_receiver).transfer(_amount); 383 | } else { 384 | IERC20(debridge.tokenAddress).safeTransfer(_receiver, _amount); 385 | } 386 | } 387 | 388 | /// @dev Request the assets to be used in defi protocol. 389 | /// @param _tokenAddress Asset address. 390 | /// @param _amount Submission aggregator address. 391 | function requestReserves(address _tokenAddress, uint256 _amount) 392 | external 393 | onlyDefiController() 394 | { 395 | bytes32 debridgeId = getDebridgeId(chainId, _tokenAddress); 396 | DebridgeInfo storage debridge = getDebridge[debridgeId]; 397 | uint256 minReserves = 398 | (debridge.balance * debridge.minReserves) / DENOMINATOR; 399 | uint256 balance = getBalance(debridge.tokenAddress); 400 | require( 401 | minReserves + _amount > balance, 402 | "requestReserves: not enough reserves" 403 | ); 404 | if (debridge.tokenAddress == address(0)) { 405 | payable(address(defiController)).transfer(_amount); 406 | } else { 407 | IERC20(debridge.tokenAddress).safeTransfer( 408 | address(defiController), 409 | _amount 410 | ); 411 | } 412 | } 413 | 414 | /// @dev Return the assets that were used in defi protocol. 415 | /// @param _tokenAddress Asset address. 416 | /// @param _amount Submission aggregator address. 417 | function returnReserves(address _tokenAddress, uint256 _amount) 418 | external 419 | payable 420 | onlyDefiController() 421 | { 422 | bytes32 debridgeId = getDebridgeId(chainId, _tokenAddress); 423 | DebridgeInfo storage debridge = getDebridge[debridgeId]; 424 | if (debridge.tokenAddress != address(0)) { 425 | IERC20(debridge.tokenAddress).safeTransferFrom( 426 | address(defiController), 427 | address(this), 428 | _amount 429 | ); 430 | } 431 | } 432 | 433 | /// @dev Fund aggregator. 434 | /// @param _debridgeId Asset identifier. 435 | /// @param _amount Submission aggregator address. 436 | function fundAggregator(bytes32 _debridgeId, uint256 _amount) 437 | external 438 | onlyAdmin() 439 | { 440 | DebridgeInfo storage debridge = getDebridge[_debridgeId]; 441 | require( 442 | debridge.chainId == chainId, 443 | "fundAggregator: wrong target chain" 444 | ); 445 | require( 446 | debridge.collectedFees >= _amount, 447 | "fundAggregator: not enough fee" 448 | ); 449 | debridge.collectedFees -= _amount; 450 | if (debridge.tokenAddress == address(0)) { 451 | weth.deposit{value: _amount}(); 452 | weth.transfer(address(feeProxy), _amount); 453 | feeProxy.swapToLink(address(weth), _amount, address(aggregator)); 454 | } else { 455 | IERC20(debridge.tokenAddress).safeTransfer( 456 | address(feeProxy), 457 | _amount 458 | ); 459 | feeProxy.swapToLink( 460 | debridge.tokenAddress, 461 | _amount, 462 | address(aggregator) 463 | ); 464 | } 465 | } 466 | 467 | /* INTERNAL */ 468 | 469 | /// @dev Add support for the asset. 470 | /// @param _debridgeId Asset identifier. 471 | /// @param _tokenAddress Address of the asset on the other chain. 472 | /// @param _chainId Current chain id. 473 | /// @param _minAmount Minimal amount of the asset to be wrapped. 474 | /// @param _transferFee Transfer fee rate. 475 | /// @param _minReserves Minimal reserve ration. 476 | /// @param _supportedChainIds Chain ids where the token of the current chain can be transfered. 477 | function _addAsset( 478 | bytes32 _debridgeId, 479 | address _tokenAddress, 480 | uint256 _chainId, 481 | uint256 _minAmount, 482 | uint256 _transferFee, 483 | uint256 _minReserves, 484 | uint256[] memory _supportedChainIds 485 | ) internal { 486 | DebridgeInfo storage debridge = getDebridge[_debridgeId]; 487 | debridge.tokenAddress = _tokenAddress; 488 | debridge.chainId = _chainId; 489 | debridge.minAmount = _minAmount; 490 | debridge.transferFee = _transferFee; 491 | debridge.minReserves = _minReserves; 492 | uint256 supportedChainId; 493 | for (uint256 i = 0; i < _supportedChainIds.length; i++) { 494 | supportedChainId = _supportedChainIds[i]; 495 | debridge.isSupported[supportedChainId] = true; 496 | debridge.chainIds.push(supportedChainId); 497 | emit ChainSupportAdded(_debridgeId, supportedChainId); 498 | } 499 | emit PairAdded( 500 | _debridgeId, 501 | _tokenAddress, 502 | _chainId, 503 | _minAmount, 504 | _transferFee, 505 | _minReserves 506 | ); 507 | } 508 | 509 | /// @dev Request the assets to be used in defi protocol. 510 | /// @param _debridge Asset info. 511 | /// @param _amount Submission aggregator address. 512 | function _ensureReserves(DebridgeInfo storage _debridge, uint256 _amount) 513 | internal 514 | { 515 | uint256 minReserves = 516 | (_debridge.balance * _debridge.minReserves) / DENOMINATOR; 517 | uint256 balance = getBalance(_debridge.tokenAddress); 518 | uint256 requestedReserves = 519 | minReserves > _amount ? minReserves : _amount; 520 | if (requestedReserves > balance) { 521 | requestedReserves = requestedReserves - balance; 522 | defiController.claimReserve( 523 | _debridge.tokenAddress, 524 | requestedReserves 525 | ); 526 | } 527 | } 528 | 529 | /* VIEW */ 530 | 531 | /// @dev Check the balance. 532 | /// @param _tokenAddress Address of the asset on the other chain. 533 | function getBalance(address _tokenAddress) public view returns (uint256) { 534 | if (_tokenAddress == address(0)) { 535 | return address(this).balance; 536 | } else { 537 | return IERC20(_tokenAddress).balanceOf(address(this)); 538 | } 539 | } 540 | 541 | /// @dev Calculates asset identifier. 542 | /// @param _tokenAddress Address of the asset on the other chain. 543 | /// @param _chainId Current chain id. 544 | function getDebridgeId(uint256 _chainId, address _tokenAddress) 545 | public 546 | pure 547 | returns (bytes32) 548 | { 549 | return keccak256(abi.encodePacked(_chainId, _tokenAddress)); 550 | } 551 | 552 | /// @dev Calculate submission id. 553 | /// @param _debridgeId Asset identifier. 554 | /// @param _receiver Receiver address. 555 | /// @param _amount Amount of the transfered asset (note: the fee can be applyed). 556 | /// @param _nonce Submission id. 557 | function getSubmisionId( 558 | bytes32 _debridgeId, 559 | uint256 _amount, 560 | address _receiver, 561 | uint256 _nonce 562 | ) public pure returns (bytes32) { 563 | return 564 | keccak256( 565 | abi.encodePacked(_debridgeId, _amount, _receiver, _nonce) 566 | ); 567 | } 568 | 569 | /// @dev Get all supported chain ids. 570 | /// @param _debridgeId Asset identifier. 571 | function getSupportedChainIds(bytes32 _debridgeId) 572 | public 573 | view 574 | returns (uint256[] memory) 575 | { 576 | return getDebridge[_debridgeId].chainIds; 577 | } 578 | } 579 | -------------------------------------------------------------------------------- /test/01_WhiteDebridge.test.js: -------------------------------------------------------------------------------- 1 | const { expectRevert } = require("@openzeppelin/test-helpers"); 2 | const { ZERO_ADDRESS } = require("./utils.spec"); 3 | const WhiteAggregator = artifacts.require("WhiteAggregator"); 4 | const MockLinkToken = artifacts.require("MockLinkToken"); 5 | const MockToken = artifacts.require("MockToken"); 6 | const WhiteDebridge = artifacts.require("WhiteDebridge"); 7 | const WrappedAsset = artifacts.require("WrappedAsset"); 8 | const FeeProxy = artifacts.require("FeeProxy"); 9 | const UniswapV2Factory = artifacts.require("UniswapV2Factory"); 10 | const IUniswapV2Pair = artifacts.require("IUniswapV2Pair"); 11 | const DefiController = artifacts.require("DefiController"); 12 | const WETH9 = artifacts.require("WETH9"); 13 | const { toWei, fromWei, toBN } = web3.utils; 14 | const MAX = web3.utils.toTwosComplement(-1); 15 | 16 | contract("WhiteDebridge", function ([alice, bob, carol, eve, devid]) { 17 | before(async function () { 18 | this.mockToken = await MockToken.new("Link Token", "dLINK", 18, { 19 | from: alice, 20 | }); 21 | this.linkToken = await MockLinkToken.new("Link Token", "dLINK", 18, { 22 | from: alice, 23 | }); 24 | this.oraclePayment = toWei("0.001"); 25 | this.minConfirmations = 1; 26 | this.whiteAggregator = await WhiteAggregator.new( 27 | this.minConfirmations, 28 | this.oraclePayment, 29 | this.linkToken.address, 30 | { 31 | from: alice, 32 | } 33 | ); 34 | this.initialOracles = [ 35 | { 36 | address: alice, 37 | admin: alice, 38 | }, 39 | { 40 | address: bob, 41 | admin: carol, 42 | }, 43 | { 44 | address: eve, 45 | admin: carol, 46 | }, 47 | ]; 48 | for (let oracle of this.initialOracles) { 49 | await this.whiteAggregator.addOracle(oracle.address, oracle.admin, { 50 | from: alice, 51 | }); 52 | } 53 | this.uniswapFactory = await UniswapV2Factory.new(carol, { 54 | from: alice, 55 | }); 56 | this.feeProxy = await FeeProxy.new( 57 | this.linkToken.address, 58 | this.uniswapFactory.address, 59 | { 60 | from: alice, 61 | } 62 | ); 63 | this.defiController = await DefiController.new({ 64 | from: alice, 65 | }); 66 | const minAmount = toWei("1"); 67 | const transferFee = toWei("0.001"); 68 | const minReserves = toWei("0.2"); 69 | const supportedChainIds = [42]; 70 | this.weth = await WETH9.new({ 71 | from: alice, 72 | }); 73 | this.whiteDebridge = await WhiteDebridge.new( 74 | minAmount, 75 | transferFee, 76 | minReserves, 77 | ZERO_ADDRESS, 78 | supportedChainIds, 79 | ZERO_ADDRESS, 80 | ZERO_ADDRESS, 81 | ZERO_ADDRESS, 82 | { 83 | from: alice, 84 | } 85 | ); 86 | }); 87 | 88 | context("Test setting configurations by different users", () => { 89 | it("should set aggregator if called by the admin", async function () { 90 | const aggregator = this.whiteAggregator.address; 91 | await this.whiteDebridge.setAggregator(aggregator, { 92 | from: alice, 93 | }); 94 | const newAggregator = await this.whiteDebridge.aggregator(); 95 | assert.equal(aggregator, newAggregator); 96 | }); 97 | 98 | it("should set fee proxy if called by the admin", async function () { 99 | const feeProxy = this.feeProxy.address; 100 | await this.whiteDebridge.setFeeProxy(feeProxy, { 101 | from: alice, 102 | }); 103 | const newFeeProxy = await this.whiteDebridge.feeProxy(); 104 | assert.equal(feeProxy, newFeeProxy); 105 | }); 106 | 107 | it("should set defi controller if called by the admin", async function () { 108 | const defiController = this.defiController.address; 109 | await this.whiteDebridge.setDefiController(defiController, { 110 | from: alice, 111 | }); 112 | const newDefiController = await this.whiteDebridge.defiController(); 113 | assert.equal(defiController, newDefiController); 114 | }); 115 | 116 | it("should set weth if called by the admin", async function () { 117 | const weth = this.weth.address; 118 | await this.whiteDebridge.setWeth(weth, { 119 | from: alice, 120 | }); 121 | const newWeth = await this.whiteDebridge.weth(); 122 | assert.equal(weth, newWeth); 123 | }); 124 | 125 | it("should reject setting aggregator if called by the non-admin", async function () { 126 | await expectRevert( 127 | this.whiteDebridge.setAggregator(ZERO_ADDRESS, { 128 | from: bob, 129 | }), 130 | "onlyAdmin: bad role" 131 | ); 132 | }); 133 | 134 | it("should reject setting fee proxy if called by the non-admin", async function () { 135 | await expectRevert( 136 | this.whiteDebridge.setFeeProxy(ZERO_ADDRESS, { 137 | from: bob, 138 | }), 139 | "onlyAdmin: bad role" 140 | ); 141 | }); 142 | 143 | it("should reject setting defi controller if called by the non-admin", async function () { 144 | await expectRevert( 145 | this.whiteDebridge.setDefiController(ZERO_ADDRESS, { 146 | from: bob, 147 | }), 148 | "onlyAdmin: bad role" 149 | ); 150 | }); 151 | 152 | it("should reject setting weth if called by the non-admin", async function () { 153 | await expectRevert( 154 | this.whiteDebridge.setWeth(ZERO_ADDRESS, { 155 | from: bob, 156 | }), 157 | "onlyAdmin: bad role" 158 | ); 159 | }); 160 | }); 161 | 162 | context("Test managing assets", () => { 163 | it("should add external asset if called by the admin", async function () { 164 | const tokenAddress = "0x1f9840a85d5af5bf1d1762f925bdaddc4201f984"; 165 | const chainId = 56; 166 | const minAmount = toWei("100"); 167 | const transferFee = toWei("0.01"); 168 | const minReserves = toWei("0.2"); 169 | const supportedChainIds = [42, 3]; 170 | const name = "MUSD"; 171 | const symbol = "Magic Dollar"; 172 | await this.whiteDebridge.addExternalAsset( 173 | tokenAddress, 174 | chainId, 175 | minAmount, 176 | transferFee, 177 | minReserves, 178 | supportedChainIds, 179 | name, 180 | symbol, 181 | { 182 | from: alice, 183 | } 184 | ); 185 | const debridgeId = await this.whiteDebridge.getDebridgeId( 186 | chainId, 187 | tokenAddress 188 | ); 189 | const debridge = await this.whiteDebridge.getDebridge(debridgeId); 190 | assert.equal(debridge.chainId.toString(), chainId); 191 | assert.equal(debridge.minAmount.toString(), minAmount); 192 | assert.equal(debridge.transferFee.toString(), transferFee); 193 | assert.equal(debridge.collectedFees.toString(), "0"); 194 | assert.equal(debridge.balance.toString(), "0"); 195 | assert.equal(debridge.minReserves.toString(), minReserves); 196 | }); 197 | 198 | it("should add native asset if called by the admin", async function () { 199 | const tokenAddress = this.mockToken.address; 200 | const chainId = await this.whiteDebridge.chainId(); 201 | const minAmount = toWei("100"); 202 | const transferFee = toWei("0.01"); 203 | const minReserves = toWei("0.2"); 204 | const supportedChainIds = [42, 3]; 205 | await this.whiteDebridge.addNativeAsset( 206 | tokenAddress, 207 | minAmount, 208 | transferFee, 209 | minReserves, 210 | supportedChainIds, 211 | { 212 | from: alice, 213 | } 214 | ); 215 | const debridgeId = await this.whiteDebridge.getDebridgeId( 216 | chainId, 217 | tokenAddress 218 | ); 219 | const debridge = await this.whiteDebridge.getDebridge(debridgeId); 220 | assert.equal(debridge.tokenAddress, tokenAddress); 221 | assert.equal(debridge.chainId.toString(), chainId); 222 | assert.equal(debridge.minAmount.toString(), minAmount); 223 | assert.equal(debridge.transferFee.toString(), transferFee); 224 | assert.equal(debridge.collectedFees.toString(), "0"); 225 | assert.equal(debridge.balance.toString(), "0"); 226 | assert.equal(debridge.minReserves.toString(), minReserves); 227 | }); 228 | 229 | it("should reject adding external asset if called by the non-admin", async function () { 230 | await expectRevert( 231 | this.whiteDebridge.addExternalAsset( 232 | ZERO_ADDRESS, 233 | 0, 234 | 0, 235 | 0, 236 | 0, 237 | [0], 238 | "name", 239 | "symbol", 240 | { 241 | from: bob, 242 | } 243 | ), 244 | "onlyAdmin: bad role" 245 | ); 246 | }); 247 | 248 | it("should reject setting native asset if called by the non-admin", async function () { 249 | await expectRevert( 250 | this.whiteDebridge.addNativeAsset(ZERO_ADDRESS, 0, 0, 0, [0], { 251 | from: bob, 252 | }), 253 | "onlyAdmin: bad role" 254 | ); 255 | }); 256 | }); 257 | 258 | context("Test send method", () => { 259 | it("should send native tokens from the current chain", async function () { 260 | const tokenAddress = ZERO_ADDRESS; 261 | const chainId = await this.whiteDebridge.chainId(); 262 | const receiver = bob; 263 | const amount = toBN(toWei("1")); 264 | const chainIdTo = 42; 265 | const debridgeId = await this.whiteDebridge.getDebridgeId( 266 | chainId, 267 | tokenAddress 268 | ); 269 | const balance = toBN( 270 | await web3.eth.getBalance(this.whiteDebridge.address) 271 | ); 272 | const debridge = await this.whiteDebridge.getDebridge(debridgeId); 273 | const fees = debridge.transferFee.mul(amount).div(toBN(toWei("1"))); 274 | await this.whiteDebridge.send(debridgeId, receiver, amount, chainIdTo, { 275 | value: amount, 276 | from: alice, 277 | }); 278 | const newBalance = toBN( 279 | await web3.eth.getBalance(this.whiteDebridge.address) 280 | ); 281 | const newDebridge = await this.whiteDebridge.getDebridge(debridgeId); 282 | assert.equal(balance.add(amount).toString(), newBalance.toString()); 283 | assert.equal( 284 | debridge.collectedFees.add(fees).toString(), 285 | newDebridge.collectedFees.toString() 286 | ); 287 | }); 288 | 289 | it("should send ERC20 tokens from the current chain", async function () { 290 | const tokenAddress = this.mockToken.address; 291 | const chainId = await this.whiteDebridge.chainId(); 292 | const receiver = bob; 293 | const amount = toBN(toWei("100")); 294 | const chainIdTo = 42; 295 | await this.mockToken.mint(alice, amount, { 296 | from: alice, 297 | }); 298 | await this.mockToken.approve(this.whiteDebridge.address, amount, { 299 | from: alice, 300 | }); 301 | const debridgeId = await this.whiteDebridge.getDebridgeId( 302 | chainId, 303 | tokenAddress 304 | ); 305 | const balance = toBN( 306 | await this.mockToken.balanceOf(this.whiteDebridge.address) 307 | ); 308 | const debridge = await this.whiteDebridge.getDebridge(debridgeId); 309 | const fees = debridge.transferFee.mul(amount).div(toBN(toWei("1"))); 310 | await this.whiteDebridge.send(debridgeId, receiver, amount, chainIdTo, { 311 | from: alice, 312 | }); 313 | const newBalance = toBN( 314 | await this.mockToken.balanceOf(this.whiteDebridge.address) 315 | ); 316 | const newDebridge = await this.whiteDebridge.getDebridge(debridgeId); 317 | assert.equal(balance.add(amount).toString(), newBalance.toString()); 318 | assert.equal( 319 | debridge.collectedFees.add(fees).toString(), 320 | newDebridge.collectedFees.toString() 321 | ); 322 | }); 323 | 324 | it("should reject sending too mismatched amount of native tokens", async function () { 325 | const tokenAddress = ZERO_ADDRESS; 326 | const receiver = bob; 327 | const chainId = await this.whiteDebridge.chainId(); 328 | const amount = toBN(toWei("1")); 329 | const chainIdTo = 42; 330 | const debridgeId = await this.whiteDebridge.getDebridgeId( 331 | chainId, 332 | tokenAddress 333 | ); 334 | await expectRevert( 335 | this.whiteDebridge.send(debridgeId, receiver, amount, chainIdTo, { 336 | value: toWei("0.1"), 337 | from: alice, 338 | }), 339 | "send: amount mismatch" 340 | ); 341 | }); 342 | 343 | it("should reject sending too few tokens", async function () { 344 | const tokenAddress = ZERO_ADDRESS; 345 | const receiver = bob; 346 | const chainId = await this.whiteDebridge.chainId(); 347 | const amount = toBN(toWei("0.1")); 348 | const chainIdTo = 42; 349 | const debridgeId = await this.whiteDebridge.getDebridgeId( 350 | chainId, 351 | tokenAddress 352 | ); 353 | await expectRevert( 354 | this.whiteDebridge.send(debridgeId, receiver, amount, chainIdTo, { 355 | value: amount, 356 | from: alice, 357 | }), 358 | "send: amount too low" 359 | ); 360 | }); 361 | 362 | it("should reject sending tokens to unsupported chain", async function () { 363 | const tokenAddress = ZERO_ADDRESS; 364 | const receiver = bob; 365 | const chainId = await this.whiteDebridge.chainId(); 366 | const amount = toBN(toWei("1")); 367 | const chainIdTo = chainId; 368 | const debridgeId = await this.whiteDebridge.getDebridgeId( 369 | chainId, 370 | tokenAddress 371 | ); 372 | await expectRevert( 373 | this.whiteDebridge.send(debridgeId, receiver, amount, chainIdTo, { 374 | value: amount, 375 | from: alice, 376 | }), 377 | "send: wrong targed chain" 378 | ); 379 | }); 380 | 381 | it("should reject sending tokens originated on the other chain", async function () { 382 | const tokenAddress = ZERO_ADDRESS; 383 | const receiver = bob; 384 | const amount = toBN(toWei("1")); 385 | const chainIdTo = 42; 386 | const debridgeId = await this.whiteDebridge.getDebridgeId( 387 | 42, 388 | tokenAddress 389 | ); 390 | await expectRevert( 391 | this.whiteDebridge.send(debridgeId, receiver, amount, chainIdTo, { 392 | value: amount, 393 | from: alice, 394 | }), 395 | "send: not native chain" 396 | ); 397 | }); 398 | }); 399 | 400 | context("Test mint method", () => { 401 | const tokenAddress = "0x1f9840a85d5af5bf1d1762f925bdaddc4201f984"; 402 | const chainId = 56; 403 | const receiver = bob; 404 | const amount = toBN(toWei("100")); 405 | const nonce = 2; 406 | before(async function () { 407 | const newSupply = toWei("100"); 408 | await this.linkToken.mint(alice, newSupply, { 409 | from: alice, 410 | }); 411 | await this.linkToken.transferAndCall( 412 | this.whiteAggregator.address.toString(), 413 | newSupply, 414 | "0x", 415 | { 416 | from: alice, 417 | } 418 | ); 419 | const debridgeId = await this.whiteDebridge.getDebridgeId( 420 | chainId, 421 | tokenAddress 422 | ); 423 | const submission = await this.whiteDebridge.getSubmisionId( 424 | debridgeId, 425 | amount, 426 | receiver, 427 | nonce 428 | ); 429 | await this.whiteAggregator.submitMint(submission, { 430 | from: bob, 431 | }); 432 | }); 433 | 434 | it("should mint when the submission is approved", async function () { 435 | const debridgeId = await this.whiteDebridge.getDebridgeId( 436 | chainId, 437 | tokenAddress 438 | ); 439 | const debridge = await this.whiteDebridge.getDebridge(debridgeId); 440 | const wrappedAsset = await WrappedAsset.at(debridge.tokenAddress); 441 | const balance = toBN(await wrappedAsset.balanceOf(receiver)); 442 | await this.whiteDebridge.mint(debridgeId, receiver, amount, nonce, { 443 | from: alice, 444 | }); 445 | const newBalance = toBN(await wrappedAsset.balanceOf(receiver)); 446 | const submissionId = await this.whiteDebridge.getSubmisionId( 447 | debridgeId, 448 | amount, 449 | receiver, 450 | nonce 451 | ); 452 | const isSubmissionUsed = await this.whiteDebridge.isSubmissionUsed( 453 | submissionId 454 | ); 455 | assert.equal(balance.add(amount).toString(), newBalance.toString()); 456 | assert.ok(isSubmissionUsed); 457 | }); 458 | 459 | it("should reject minting with unconfirmed submission", async function () { 460 | const nonce = 4; 461 | const debridgeId = await this.whiteDebridge.getDebridgeId( 462 | chainId, 463 | tokenAddress 464 | ); 465 | await expectRevert( 466 | this.whiteDebridge.mint(debridgeId, receiver, amount, nonce, { 467 | from: alice, 468 | }), 469 | "mint: not confirmed" 470 | ); 471 | }); 472 | 473 | it("should reject minting twice", async function () { 474 | const debridgeId = await this.whiteDebridge.getDebridgeId( 475 | chainId, 476 | tokenAddress 477 | ); 478 | await expectRevert( 479 | this.whiteDebridge.mint(debridgeId, receiver, amount, nonce, { 480 | from: alice, 481 | }), 482 | "mint: already used" 483 | ); 484 | }); 485 | }); 486 | 487 | context("Test burn method", () => { 488 | it("should burning when the amount is suficient", async function () { 489 | const tokenAddress = "0x1f9840a85d5af5bf1d1762f925bdaddc4201f984"; 490 | const chainId = 56; 491 | const receiver = alice; 492 | const amount = toBN(toWei("100")); 493 | const debridgeId = await this.whiteDebridge.getDebridgeId( 494 | chainId, 495 | tokenAddress 496 | ); 497 | const debridge = await this.whiteDebridge.getDebridge(debridgeId); 498 | const wrappedAsset = await WrappedAsset.at(debridge.tokenAddress); 499 | const balance = toBN(await wrappedAsset.balanceOf(bob)); 500 | await wrappedAsset.approve(this.whiteDebridge.address, amount, { 501 | from: bob, 502 | }); 503 | await this.whiteDebridge.burn(debridgeId, receiver, amount, { 504 | from: bob, 505 | }); 506 | const newBalance = toBN(await wrappedAsset.balanceOf(bob)); 507 | assert.equal(balance.sub(amount).toString(), newBalance.toString()); 508 | }); 509 | 510 | it("should reject burning from current chain", async function () { 511 | const tokenAddress = ZERO_ADDRESS; 512 | const chainId = await this.whiteDebridge.chainId(); 513 | const receiver = bob; 514 | const amount = toBN(toWei("1")); 515 | const debridgeId = await this.whiteDebridge.getDebridgeId( 516 | chainId, 517 | tokenAddress 518 | ); 519 | await expectRevert( 520 | this.whiteDebridge.burn(debridgeId, receiver, amount, { 521 | from: alice, 522 | }), 523 | "burn: native asset" 524 | ); 525 | }); 526 | 527 | it("should reject burning too few tokens", async function () { 528 | const tokenAddress = "0x1f9840a85d5af5bf1d1762f925bdaddc4201f984"; 529 | const chainId = 56; 530 | const receiver = bob; 531 | const amount = toBN(toWei("0.1")); 532 | const debridgeId = await this.whiteDebridge.getDebridgeId( 533 | chainId, 534 | tokenAddress 535 | ); 536 | await expectRevert( 537 | this.whiteDebridge.burn(debridgeId, receiver, amount, { 538 | from: alice, 539 | }), 540 | "burn: amount too low" 541 | ); 542 | }); 543 | }); 544 | 545 | context("Test claim method", () => { 546 | const tokenAddress = ZERO_ADDRESS; 547 | const receiver = bob; 548 | const amount = toBN(toWei("0.9")); 549 | const nonce = 4; 550 | let chainId; 551 | let debridgeId; 552 | let outsideDebridgeId; 553 | let erc20DebridgeId; 554 | before(async function () { 555 | chainId = await this.whiteDebridge.chainId(); 556 | debridgeId = await this.whiteDebridge.getDebridgeId( 557 | chainId, 558 | tokenAddress 559 | ); 560 | outsideDebridgeId = await this.whiteDebridge.getDebridgeId( 561 | 42, 562 | tokenAddress 563 | ); 564 | erc20DebridgeId = await this.whiteDebridge.getDebridgeId( 565 | chainId, 566 | this.mockToken.address 567 | ); 568 | const cuurentChainSubmission = await this.whiteDebridge.getSubmisionId( 569 | debridgeId, 570 | amount, 571 | receiver, 572 | nonce 573 | ); 574 | await this.whiteAggregator.submitBurn(cuurentChainSubmission, { 575 | from: bob, 576 | }); 577 | const outsideChainSubmission = await this.whiteDebridge.getSubmisionId( 578 | outsideDebridgeId, 579 | amount, 580 | receiver, 581 | nonce 582 | ); 583 | await this.whiteAggregator.submitBurn(outsideChainSubmission, { 584 | from: bob, 585 | }); 586 | const erc20Submission = await this.whiteDebridge.getSubmisionId( 587 | erc20DebridgeId, 588 | amount, 589 | receiver, 590 | nonce 591 | ); 592 | await this.whiteAggregator.submitBurn(erc20Submission, { 593 | from: bob, 594 | }); 595 | }); 596 | 597 | it("should claim native token when the submission is approved", async function () { 598 | const debridge = await this.whiteDebridge.getDebridge(debridgeId); 599 | const balance = toBN(await web3.eth.getBalance(receiver)); 600 | await this.whiteDebridge.claim(debridgeId, receiver, amount, nonce, { 601 | from: alice, 602 | }); 603 | const newBalance = toBN(await web3.eth.getBalance(receiver)); 604 | const submissionId = await this.whiteDebridge.getSubmisionId( 605 | debridgeId, 606 | amount, 607 | receiver, 608 | nonce 609 | ); 610 | const isSubmissionUsed = await this.whiteDebridge.isSubmissionUsed( 611 | submissionId 612 | ); 613 | const fees = debridge.transferFee.mul(amount).div(toBN(toWei("1"))); 614 | const newDebridge = await this.whiteDebridge.getDebridge(debridgeId); 615 | assert.equal( 616 | balance.add(amount).sub(fees).toString(), 617 | newBalance.toString() 618 | ); 619 | assert.equal( 620 | debridge.collectedFees.add(fees).toString(), 621 | newDebridge.collectedFees.toString() 622 | ); 623 | assert.ok(isSubmissionUsed); 624 | }); 625 | 626 | it("should claim ERC20 when the submission is approved", async function () { 627 | const debridge = await this.whiteDebridge.getDebridge(erc20DebridgeId); 628 | const balance = toBN(await this.mockToken.balanceOf(receiver)); 629 | await this.whiteDebridge.claim(erc20DebridgeId, receiver, amount, nonce, { 630 | from: alice, 631 | }); 632 | const newBalance = toBN(await this.mockToken.balanceOf(receiver)); 633 | const submissionId = await this.whiteDebridge.getSubmisionId( 634 | erc20DebridgeId, 635 | amount, 636 | receiver, 637 | nonce 638 | ); 639 | const isSubmissionUsed = await this.whiteDebridge.isSubmissionUsed( 640 | submissionId 641 | ); 642 | const fees = debridge.transferFee.mul(amount).div(toBN(toWei("1"))); 643 | const newDebridge = await this.whiteDebridge.getDebridge(erc20DebridgeId); 644 | assert.equal( 645 | balance.add(amount).sub(fees).toString(), 646 | newBalance.toString() 647 | ); 648 | assert.equal( 649 | debridge.collectedFees.add(fees).toString(), 650 | newDebridge.collectedFees.toString() 651 | ); 652 | assert.ok(isSubmissionUsed); 653 | }); 654 | 655 | it("should reject claiming with unconfirmed submission", async function () { 656 | const nonce = 1; 657 | await expectRevert( 658 | this.whiteDebridge.claim(debridgeId, receiver, amount, nonce, { 659 | from: alice, 660 | }), 661 | "claim: not confirmed" 662 | ); 663 | }); 664 | 665 | it("should reject claiming the token from outside chain", async function () { 666 | await expectRevert( 667 | this.whiteDebridge.claim(outsideDebridgeId, receiver, amount, nonce, { 668 | from: alice, 669 | }), 670 | "claim: wrong target chain" 671 | ); 672 | }); 673 | 674 | it("should reject claiming twice", async function () { 675 | await expectRevert( 676 | this.whiteDebridge.claim(debridgeId, receiver, amount, nonce, { 677 | from: alice, 678 | }), 679 | "claim: already used" 680 | ); 681 | }); 682 | }); 683 | 684 | context("Test fee maangement", () => { 685 | const tokenAddress = ZERO_ADDRESS; 686 | const receiver = bob; 687 | const amount = toBN(toWei("0.00001")); 688 | let chainId; 689 | let debridgeId; 690 | let outsideDebridgeId; 691 | let erc20DebridgeId; 692 | 693 | before(async function () { 694 | chainId = await this.whiteDebridge.chainId(); 695 | debridgeId = await this.whiteDebridge.getDebridgeId( 696 | chainId, 697 | tokenAddress 698 | ); 699 | outsideDebridgeId = await this.whiteDebridge.getDebridgeId( 700 | 42, 701 | tokenAddress 702 | ); 703 | erc20DebridgeId = await this.whiteDebridge.getDebridgeId( 704 | chainId, 705 | this.mockToken.address 706 | ); 707 | }); 708 | 709 | it("should withdraw fee of native token if it is called by the admin", async function () { 710 | const debridge = await this.whiteDebridge.getDebridge(debridgeId); 711 | const balance = toBN(await web3.eth.getBalance(receiver)); 712 | await this.whiteDebridge.withdrawFee(debridgeId, receiver, amount, { 713 | from: alice, 714 | }); 715 | const newBalance = toBN(await web3.eth.getBalance(receiver)); 716 | const newDebridge = await this.whiteDebridge.getDebridge(debridgeId); 717 | assert.equal( 718 | debridge.collectedFees.sub(amount).toString(), 719 | newDebridge.collectedFees.toString() 720 | ); 721 | assert.equal(balance.add(amount).toString(), newBalance.toString()); 722 | }); 723 | 724 | it("should withdraw fee of ERC20 token if it is called by the admin", async function () { 725 | const debridge = await this.whiteDebridge.getDebridge(erc20DebridgeId); 726 | const balance = toBN(await this.mockToken.balanceOf(receiver)); 727 | await this.whiteDebridge.withdrawFee(erc20DebridgeId, receiver, amount, { 728 | from: alice, 729 | }); 730 | const newBalance = toBN(await this.mockToken.balanceOf(receiver)); 731 | const newDebridge = await this.whiteDebridge.getDebridge(erc20DebridgeId); 732 | assert.equal( 733 | debridge.collectedFees.sub(amount).toString(), 734 | newDebridge.collectedFees.toString() 735 | ); 736 | assert.equal(balance.add(amount).toString(), newBalance.toString()); 737 | }); 738 | 739 | it("should reject withdrawing fee by non-admin", async function () { 740 | await expectRevert( 741 | this.whiteDebridge.withdrawFee(debridgeId, receiver, amount, { 742 | from: bob, 743 | }), 744 | "onlyAdmin: bad role" 745 | ); 746 | }); 747 | 748 | it("should reject withdrawing too many fees", async function () { 749 | const amount = toBN(toWei("100")); 750 | await expectRevert( 751 | this.whiteDebridge.withdrawFee(debridgeId, receiver, amount, { 752 | from: alice, 753 | }), 754 | "withdrawFee: not enough fee" 755 | ); 756 | }); 757 | 758 | it("should reject withdrawing fees if the token not from current chain", async function () { 759 | const amount = toBN(toWei("100")); 760 | await expectRevert( 761 | this.whiteDebridge.withdrawFee(outsideDebridgeId, receiver, amount, { 762 | from: alice, 763 | }), 764 | "withdrawFee: wrong target chain" 765 | ); 766 | }); 767 | }); 768 | 769 | context("Test fund aggregator", async function () { 770 | const tokenAddress = ZERO_ADDRESS; 771 | const amount = toBN(toWei("0.0001")); 772 | let receiver; 773 | let chainId; 774 | let debridgeId; 775 | let outsideDebridgeId; 776 | let erc20DebridgeId; 777 | let wethUniPool; 778 | let mockErc20UniPool; 779 | 780 | before(async function () { 781 | receiver = this.whiteAggregator.address.toString(); 782 | await this.uniswapFactory.createPair( 783 | this.linkToken.address, 784 | this.weth.address, 785 | { 786 | from: alice, 787 | } 788 | ); 789 | await this.uniswapFactory.createPair( 790 | this.linkToken.address, 791 | this.mockToken.address, 792 | { 793 | from: alice, 794 | } 795 | ); 796 | const wethUniPoolAddres = await this.uniswapFactory.getPair( 797 | this.linkToken.address, 798 | this.weth.address 799 | ); 800 | const mockErc20UniPoolAddress = await this.uniswapFactory.getPair( 801 | this.linkToken.address, 802 | this.mockToken.address 803 | ); 804 | const wethUniPool = await IUniswapV2Pair.at(wethUniPoolAddres); 805 | const mockErc20UniPool = await IUniswapV2Pair.at(mockErc20UniPoolAddress); 806 | await this.linkToken.approve(wethUniPool.address, MAX, { from: alice }); 807 | await this.weth.approve(wethUniPool.address, MAX, { from: alice }); 808 | await this.linkToken.approve(mockErc20UniPool.address, MAX, { 809 | from: alice, 810 | }); 811 | await this.mockToken.approve(mockErc20UniPool.address, MAX, { 812 | from: alice, 813 | }); 814 | 815 | await this.linkToken.mint(wethUniPool.address, toWei("100"), { 816 | from: alice, 817 | }); 818 | await this.weth.deposit({ 819 | from: carol, 820 | value: toWei("20"), 821 | }); 822 | await this.weth.transfer(wethUniPool.address, toWei("10"), { 823 | from: carol, 824 | }); 825 | await this.linkToken.mint(mockErc20UniPool.address, toWei("100"), { 826 | from: alice, 827 | }); 828 | await this.mockToken.mint(mockErc20UniPool.address, toWei("100"), { 829 | from: alice, 830 | }); 831 | 832 | await wethUniPool.mint(alice, { from: alice }); 833 | await mockErc20UniPool.mint(alice, { from: alice }); 834 | 835 | chainId = await this.whiteDebridge.chainId(); 836 | debridgeId = await this.whiteDebridge.getDebridgeId( 837 | chainId, 838 | tokenAddress 839 | ); 840 | outsideDebridgeId = await this.whiteDebridge.getDebridgeId( 841 | 42, 842 | tokenAddress 843 | ); 844 | erc20DebridgeId = await this.whiteDebridge.getDebridgeId( 845 | chainId, 846 | this.mockToken.address 847 | ); 848 | }); 849 | 850 | it("should fund aggregator of native token if it is called by the admin", async function () { 851 | const debridge = await this.whiteDebridge.getDebridge(debridgeId); 852 | const balance = toBN(await this.linkToken.balanceOf(receiver)); 853 | console.log(debridge.collectedFees.toString()); 854 | await this.whiteDebridge.fundAggregator(debridgeId, amount, { 855 | from: alice, 856 | }); 857 | const newBalance = toBN(await this.linkToken.balanceOf(receiver)); 858 | const newDebridge = await this.whiteDebridge.getDebridge(debridgeId); 859 | assert.equal( 860 | debridge.collectedFees.sub(amount).toString(), 861 | newDebridge.collectedFees.toString() 862 | ); 863 | assert.ok(newBalance.gt(balance)); 864 | }); 865 | 866 | it("should fund aggregator of ERC20 token if it is called by the admin", async function () { 867 | const debridge = await this.whiteDebridge.getDebridge(erc20DebridgeId); 868 | const balance = toBN(await this.linkToken.balanceOf(receiver)); 869 | console.log(debridge.collectedFees.toString()); 870 | await this.whiteDebridge.fundAggregator(erc20DebridgeId, amount, { 871 | from: alice, 872 | }); 873 | const newBalance = toBN(await this.linkToken.balanceOf(receiver)); 874 | const newDebridge = await this.whiteDebridge.getDebridge(erc20DebridgeId); 875 | assert.equal( 876 | debridge.collectedFees.sub(amount).toString(), 877 | newDebridge.collectedFees.toString() 878 | ); 879 | assert.ok(newBalance.gt(balance)); 880 | }); 881 | 882 | it("should reject funding aggregator by non-admin", async function () { 883 | await expectRevert( 884 | this.whiteDebridge.fundAggregator(debridgeId, amount, { 885 | from: bob, 886 | }), 887 | "onlyAdmin: bad role" 888 | ); 889 | }); 890 | 891 | it("should reject funding aggregator with too many fees", async function () { 892 | const amount = toBN(toWei("100")); 893 | await expectRevert( 894 | this.whiteDebridge.fundAggregator(debridgeId, amount, { 895 | from: alice, 896 | }), 897 | "fundAggregator: not enough fee" 898 | ); 899 | }); 900 | 901 | it("should reject funding aggregator if the token not from current chain", async function () { 902 | const amount = toBN(toWei("0.1")); 903 | await expectRevert( 904 | this.whiteDebridge.fundAggregator(outsideDebridgeId, amount, { 905 | from: alice, 906 | }), 907 | "fundAggregator: wrong target chain" 908 | ); 909 | }); 910 | }); 911 | }); 912 | --------------------------------------------------------------------------------