From 72e3c324deeb0a0bafc9eb2cfbed1334b5f89c9b Mon Sep 17 00:00:00 2001 From: Kirill Fedoseev Date: Mon, 10 Jan 2022 21:22:51 +0400 Subject: [PATCH] Helper contract for token-subsidized claims --- .../helpers/OmnibridgeSubsidizeHelper.sol | 154 ++++++++++++++++++ contracts/interfaces/IOmnibridge.sol | 2 + .../upgradeable_contracts/HomeOmnibridge.sol | 2 +- .../fee_manager/OmnibridgeFeeManager.sol | 98 +++++++---- .../OmnibridgeFeeManagerConnector.sol | 4 +- deploy/src/omnibridge/home.js | 12 +- e2e-tests/run.js | 6 +- .../OmnibridgeSubsidizeHelper.test.js | 105 ++++++++++++ test/omnibridge/common.test.js | 119 +++++++++----- test/setup.js | 3 +- 10 files changed, 421 insertions(+), 84 deletions(-) create mode 100644 contracts/helpers/OmnibridgeSubsidizeHelper.sol create mode 100644 test/omnibridge/OmnibridgeSubsidizeHelper.test.js diff --git a/contracts/helpers/OmnibridgeSubsidizeHelper.sol b/contracts/helpers/OmnibridgeSubsidizeHelper.sol new file mode 100644 index 0000000..f4ef29f --- /dev/null +++ b/contracts/helpers/OmnibridgeSubsidizeHelper.sol @@ -0,0 +1,154 @@ +pragma solidity 0.7.5; + +import "@openzeppelin/contracts/math/SafeMath.sol"; +import "@openzeppelin/contracts/token/ERC20/SafeERC20.sol"; +import "../interfaces/IOmnibridge.sol"; +import "../interfaces/IERC677.sol"; +import "../libraries/AddressHelper.sol"; +import "../libraries/Bytes.sol"; +import "../upgradeable_contracts/modules/OwnableModule.sol"; +import "../upgradeable_contracts/modules/fee_manager/OmnibridgeFeeManager.sol"; +import "../upgradeable_contracts/Claimable.sol"; + +/** + * @title OmnibridgeSubsidizeHelper + * @dev Intermediary helper contract for taking extra fees for automatic claim. + */ +contract OmnibridgeSubsidizeHelper is IOmnibridge, OwnableModule, Claimable { + using SafeERC20 for IERC677; + using SafeERC20 for IERC20; + using SafeMath for uint256; + + IOmnibridge public immutable bridge; + OmnibridgeFeeManager public feeManager; + + mapping(address => bool) public enabledTokens; + + bytes32 public constant HOME_TO_FOREIGN_FEE = 0x741ede137d0537e88e0ea0ff25b1f22d837903dbbee8980b4a06e8523247ee26; // keccak256(abi.encodePacked("homeToForeignFee")) + + /** + * @dev Initializes this contract. + * @param _bridge address of the HomeOmnibridge/ForeignOmnibridge contract. + * @param _owner address of the contract owner. + */ + constructor(IOmnibridge _bridge, address _owner) OwnableModule(_owner) { + bridge = _bridge; + } + + /** + * @dev Sets the fee manager address. + * @param _feeManager address of the OmnibridgeFeeManager contract. + */ + function setFeeManager(OmnibridgeFeeManager _feeManager) external onlyOwner { + feeManager = _feeManager; + } + + /** + * @dev Enables/disables automatic claim for some particular token contract address. + * @param _token address of the token contract. + * @param _enabled true to enable automatic claim. + */ + function enableToken(address _token, bool _enabled) external onlyOwner { + enabledTokens[_token] = _enabled; + } + + /** + * @dev ERC677 transfer callback function. + * Subtracts a configured fee and bridges the remaining amount to the regular OB. + * @param _from address of tokens sender. + * @param _value amount of transferred tokens. + * @param _data additional transfer data, can be used for passing alternative receiver address. + */ + function onTokenTransfer( + address _from, + uint256 _value, + bytes memory _data + ) external returns (bool) { + uint256 valueToBridge = _distributeFee(msg.sender, _from, _value); + IERC677(msg.sender).transferAndCall(address(bridge), valueToBridge, _data); + + return true; + } + + /** + * @dev Initiate the bridge operation for some amount of tokens from msg.sender to msg.sender on the other side. + * The user should first call Approve method of the ERC677 token. + * Subtracts a configured fee and bridges the remaining amount to the regular OB. + * @param _token bridged token contract address. + * @param _value amount of tokens to be transferred to the other network. + */ + function relayTokens(address _token, uint256 _value) external override { + relayTokens(_token, msg.sender, _value); + } + + /** + * @dev Initiate the bridge operation for some amount of tokens from msg.sender. + * The user should first call Approve method of the ERC677 token. + * Subtracts a configured fee and bridges the remaining amount to the regular OB. + * @param _token bridged token contract address. + * @param _receiver address that will receive tokens on the other network. + * @param _value amount of tokens to be transferred to the other network. + */ + function relayTokens( + address _token, + address _receiver, + uint256 _value + ) public override { + IERC20(_token).safeTransferFrom(msg.sender, address(this), _value); + uint256 valueToBridge = _distributeFee(_token, msg.sender, _value); + IERC20(_token).approve(address(bridge), valueToBridge); + bridge.relayTokens(_token, _receiver, valueToBridge); + } + + /** + * @dev Initiate the bridge operation for some amount of tokens from msg.sender. + * The user should first call Approve method of the ERC677 token. + * Subtracts a configured fee and bridges the remaining amount to the regular OB. + * @param _token bridged token contract address. + * @param _receiver address that will receive the native tokens on the other network. + * @param _value amount of tokens to be transferred to the other network. + * @param _data additional transfer data to be used on the other side. + */ + function relayTokensAndCall( + address _token, + address _receiver, + uint256 _value, + bytes memory _data + ) external override { + IERC20(_token).safeTransferFrom(msg.sender, address(this), _value); + uint256 valueToBridge = _distributeFee(_token, msg.sender, _value); + IERC20(_token).approve(address(bridge), valueToBridge); + bridge.relayTokensAndCall(_token, _receiver, valueToBridge, _data); + } + + function _distributeFee( + address _token, + address _from, + uint256 _value + ) internal returns (uint256) { + require(enabledTokens[_token]); + + uint256 fee = feeManager.calculateFee(HOME_TO_FOREIGN_FEE, _token, _from, _value); + IERC677(_token).safeTransfer(address(feeManager), fee); + feeManager.distributeFee(_token); + return _value.sub(fee); + } + + /** + * @dev Claims stuck coins/tokens. + * Only contract owner can call this method. + * @param _token address of claimed token contract, address(0) for native coins. + * @param _to address of tokens receiver + */ + function claimTokens(address _token, address _to) external onlyOwner { + claimValues(_token, _to); + } + + function calculateFee( + address _token, + address _from, + uint256 _value + ) external view returns (uint256) { + return feeManager.calculateFee(HOME_TO_FOREIGN_FEE, _token, _from, _value); + } +} diff --git a/contracts/interfaces/IOmnibridge.sol b/contracts/interfaces/IOmnibridge.sol index fcf83eb..4ba1182 100644 --- a/contracts/interfaces/IOmnibridge.sol +++ b/contracts/interfaces/IOmnibridge.sol @@ -1,6 +1,8 @@ pragma solidity 0.7.5; interface IOmnibridge { + function relayTokens(address _token, uint256 _value) external; + function relayTokens( address _token, address _receiver, diff --git a/contracts/upgradeable_contracts/HomeOmnibridge.sol b/contracts/upgradeable_contracts/HomeOmnibridge.sol index 6055dc8..8742785 100644 --- a/contracts/upgradeable_contracts/HomeOmnibridge.sol +++ b/contracts/upgradeable_contracts/HomeOmnibridge.sol @@ -127,7 +127,7 @@ contract HomeOmnibridge is addTotalExecutedPerDay(_token, getCurrentDay(), _value); uint256 valueToBridge = _value; - uint256 fee = _distributeFee(FOREIGN_TO_HOME_FEE, _isNative, address(0), _token, valueToBridge); + uint256 fee = _distributeFee(FOREIGN_TO_HOME_FEE, _isNative, _recipient, _token, valueToBridge); bytes32 _messageId = messageId(); if (fee > 0) { emit FeeDistributed(fee, _token, _messageId); diff --git a/contracts/upgradeable_contracts/modules/fee_manager/OmnibridgeFeeManager.sol b/contracts/upgradeable_contracts/modules/fee_manager/OmnibridgeFeeManager.sol index 9ce89dd..81aa028 100644 --- a/contracts/upgradeable_contracts/modules/fee_manager/OmnibridgeFeeManager.sol +++ b/contracts/upgradeable_contracts/modules/fee_manager/OmnibridgeFeeManager.sol @@ -1,4 +1,6 @@ pragma solidity 0.7.5; +// solhint-disable-next-line compiler-version +pragma abicoder v2; import "@openzeppelin/contracts/math/SafeMath.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; @@ -19,32 +21,40 @@ contract OmnibridgeFeeManager is MediatorOwnableModule { uint256 internal constant MAX_FEE = 1 ether; uint256 internal constant MAX_REWARD_ACCOUNTS = 50; + address internal constant ANY_ADDRESS = 0xFFfFfFffFFfffFFfFFfFFFFFffFFFffffFfFFFfF; bytes32 public constant HOME_TO_FOREIGN_FEE = 0x741ede137d0537e88e0ea0ff25b1f22d837903dbbee8980b4a06e8523247ee26; // keccak256(abi.encodePacked("homeToForeignFee")) bytes32 public constant FOREIGN_TO_HOME_FEE = 0x03be2b2875cb41e0e77355e802a16769bb8dfcf825061cde185c73bf94f12625; // keccak256(abi.encodePacked("foreignToHomeFee")) - // mapping feeType => token address => fee percentage - mapping(bytes32 => mapping(address => uint256)) internal fees; + struct FeeParams { + uint256 percentage; + uint256 minFee; + uint256 maxFee; + } + + // mapping feeType => token address => token sender => fee params + mapping(bytes32 => mapping(address => mapping(address => FeeParams))) internal fees; address[] internal rewardAddresses; - event FeeUpdated(bytes32 feeType, address indexed token, uint256 fee); + event FeeUpdated(bytes32 indexed feeType, address indexed token, address sender, FeeParams fee); /** * @dev Stores the initial parameters of the fee manager. * @param _mediator address of the mediator contract used together with this fee manager. * @param _owner address of the contract owner. * @param _rewardAddresses list of unique initial reward addresses, between whom fees will be distributed - * @param _fees array with initial fees for both bridge directions. - * [ 0 = homeToForeignFee, 1 = foreignToHomeFee ] + * @param _homeToForeignFees initial fee parameters for HOME_TO_FOREIGN direction. + * @param _foreignToHomeFees initial fee parameters for FOREIGN_TO_HOME direction. */ constructor( address _mediator, address _owner, address[] memory _rewardAddresses, - uint256[2] memory _fees + FeeParams memory _homeToForeignFees, + FeeParams memory _foreignToHomeFees ) MediatorOwnableModule(_mediator, _owner) { require(_rewardAddresses.length <= MAX_REWARD_ACCOUNTS); - _setFee(HOME_TO_FOREIGN_FEE, address(0), _fees[0]); - _setFee(FOREIGN_TO_HOME_FEE, address(0), _fees[1]); + _setFee(HOME_TO_FOREIGN_FEE, ANY_ADDRESS, ANY_ADDRESS, _homeToForeignFees); + _setFee(FOREIGN_TO_HOME_FEE, ANY_ADDRESS, ANY_ADDRESS, _foreignToHomeFees); for (uint256 i = 0; i < _rewardAddresses.length; i++) { require(_isValidAddress(_rewardAddresses[i])); @@ -70,16 +80,7 @@ contract OmnibridgeFeeManager is MediatorOwnableModule { uint64 patch ) { - return (1, 0, 0); - } - - /** - * @dev Throws if given fee amount is invalid. - */ - modifier validFee(uint256 _fee) { - require(_fee < MAX_FEE); - /* solcov ignore next */ - _; + return (2, 0, 0); } /** @@ -96,49 +97,68 @@ contract OmnibridgeFeeManager is MediatorOwnableModule { * Only the owner can call this method. * @param _feeType type of the updated fee, can be one of [HOME_TO_FOREIGN_FEE, FOREIGN_TO_HOME_FEE]. * @param _token address of the token contract for which fee should apply, 0x00..00 describes the initial fee for newly created tokens. - * @param _fee new fee value, in percentage (1 ether == 10**18 == 100%). + * @param _sender address of the specific tokens sender to apply fees for. + * @param _params new fee parameters. */ function setFee( bytes32 _feeType, address _token, - uint256 _fee + address _sender, + FeeParams memory _params ) external validFeeType(_feeType) onlyOwner { - _setFee(_feeType, _token, _fee); + _setFee(_feeType, _token, _sender, _params); } /** * @dev Retrieves the value for the particular fee type. * @param _feeType type of the updated fee, can be one of [HOME_TO_FOREIGN_FEE, FOREIGN_TO_HOME_FEE]. * @param _token address of the token contract for which fee should apply, 0x00..00 describes the initial fee for newly created tokens. - * @return fee value associated with the requested fee type. + * @param _sender address of the specific tokens sender to get fees for. + * @return fee parameters value associated with the specific fee type, token and sender addresses. */ - function getFee(bytes32 _feeType, address _token) public view validFeeType(_feeType) returns (uint256) { - // use token-specific fee if one is registered - uint256 _tokenFee = fees[_feeType][_token]; - if (_tokenFee > 0) { - return _tokenFee - 1; + function getFee( + bytes32 _feeType, + address _token, + address _sender + ) public view validFeeType(_feeType) returns (FeeParams memory) { + FeeParams memory params = fees[_feeType][_token][_sender]; + if (params.maxFee > 0) { + return params; + } + params = fees[_feeType][ANY_ADDRESS][_sender]; + if (params.maxFee > 0) { + return params; } - // use default fee otherwise - return fees[_feeType][address(0)] - 1; + params = fees[_feeType][_token][ANY_ADDRESS]; + if (params.maxFee > 0) { + return params; + } + return fees[_feeType][ANY_ADDRESS][ANY_ADDRESS]; } /** * @dev Calculates the amount of fee to pay for the value of the particular fee type. * @param _feeType type of the updated fee, can be one of [HOME_TO_FOREIGN_FEE, FOREIGN_TO_HOME_FEE]. * @param _token address of the token contract for which fee should apply, 0x00..00 describes the initial fee for newly created tokens. + * @param _sender address of the specific tokens sender to get fees for. * @param _value bridged value, for which fee should be evaluated. * @return amount of fee to be subtracted from the transferred value. */ function calculateFee( bytes32 _feeType, address _token, + address _sender, uint256 _value ) public view returns (uint256) { if (rewardAddresses.length == 0) { return 0; } - uint256 _fee = getFee(_feeType, _token); - return _value.mul(_fee).div(MAX_FEE); + FeeParams memory params = getFee(_feeType, _token, _sender); + uint256 proportionalFee = _value.mul(params.percentage).div(MAX_FEE); + return + proportionalFee > params.minFee + ? (proportionalFee > params.maxFee ? params.maxFee : proportionalFee) + : params.minFee; } /** @@ -240,15 +260,21 @@ contract OmnibridgeFeeManager is MediatorOwnableModule { * @dev Internal function for updating the fee value for the given fee type. * @param _feeType type of the updated fee, can be one of [HOME_TO_FOREIGN_FEE, FOREIGN_TO_HOME_FEE]. * @param _token address of the token contract for which fee should apply, 0x00..00 describes the initial fee for newly created tokens. - * @param _fee new fee value, in percentage (1 ether == 10**18 == 100%). + * @param _sender address of the specific tokens sender to set fees for. + * @param _params new fee parameters. */ function _setFee( bytes32 _feeType, address _token, - uint256 _fee - ) internal validFee(_fee) { - fees[_feeType][_token] = 1 + _fee; - emit FeeUpdated(_feeType, _token, _fee); + address _sender, + FeeParams memory _params + ) internal { + require(_params.percentage < MAX_FEE); + require(_params.minFee <= _params.maxFee); + + fees[_feeType][_token][_sender] = _params; + + emit FeeUpdated(_feeType, _token, _sender, _params); } /** diff --git a/contracts/upgradeable_contracts/modules/fee_manager/OmnibridgeFeeManagerConnector.sol b/contracts/upgradeable_contracts/modules/fee_manager/OmnibridgeFeeManagerConnector.sol index 38ce764..6ebe7c8 100644 --- a/contracts/upgradeable_contracts/modules/fee_manager/OmnibridgeFeeManagerConnector.sol +++ b/contracts/upgradeable_contracts/modules/fee_manager/OmnibridgeFeeManagerConnector.sol @@ -50,7 +50,7 @@ abstract contract OmnibridgeFeeManagerConnector is Ownable { * @dev Internal function for calculating and distributing fee through the separate fee manager contract. * @param _feeType type of the fee, can be one of [HOME_TO_FOREIGN_FEE, FOREIGN_TO_HOME_FEE]. * @param _isNative true, if distributed token is native to this side of the bridge. - * @param _from address of the tokens sender, needed only if _feeType is HOME_TO_FOREIGN_FEE. + * @param _from address of the tokens sender. * @param _token address of the token contract, for which fee should be processed. * @param _value amount of tokens bridged. * @return total amount of fee distributed. @@ -71,7 +71,7 @@ abstract contract OmnibridgeFeeManagerConnector is Ownable { if (_feeType == HOME_TO_FOREIGN_FEE && manager.isRewardAddress(_from)) { return 0; } - uint256 fee = manager.calculateFee(_feeType, _token, _value); + uint256 fee = manager.calculateFee(_feeType, _token, _from, _value); if (fee > 0) { if (_feeType == HOME_TO_FOREIGN_FEE) { // for home -> foreign direction, fee is collected using transfer(address,uint256) method diff --git a/deploy/src/omnibridge/home.js b/deploy/src/omnibridge/home.js index 500801b..14b6e4c 100644 --- a/deploy/src/omnibridge/home.js +++ b/deploy/src/omnibridge/home.js @@ -69,9 +69,19 @@ async function deployHome() { HOME_TO_FOREIGN_FEE: ${homeFeeInWei} which is ${HOME_TRANSACTIONS_FEE * 100}% FOREIGN_TO_HOME_FEE: ${foreignFeeInWei} which is ${FOREIGN_TRANSACTIONS_FEE * 100}% `) + const homeFee = { + percentage: homeFeeInWei, + minFee: '0', + maxFee: toWei('100'), + } + const foreignFee = { + percentage: foreignFeeInWei, + minFee: '0', + maxFee: toWei('100'), + } const manager = await deployContract( OmnibridgeFeeManager, - [homeBridgeStorage.options.address, HOME_BRIDGE_OWNER, rewardList, [homeFeeInWei, foreignFeeInWei]], + [homeBridgeStorage.options.address, HOME_BRIDGE_OWNER, rewardList, homeFee, foreignFee], { nonce: nonce++ } ) feeManager = manager.options.address diff --git a/e2e-tests/run.js b/e2e-tests/run.js index cf99819..692f2dd 100644 --- a/e2e-tests/run.js +++ b/e2e-tests/run.js @@ -233,8 +233,10 @@ async function createEnv(web3Home, web3Foreign) { const feeEnabled = (await feeManager.methods.rewardAddressCount().call()) > 0 console.log('Fetching fee values') - const homeFee = toBN(feeEnabled ? await feeManager.methods.getFee(homeFeeType, ZERO_ADDRESS).call() : 0) - const foreignFee = toBN(feeEnabled ? await feeManager.methods.getFee(foreignFeeType, ZERO_ADDRESS).call() : 0) + const homeFeeParams = await feeManager.methods.getFee(homeFeeType, ZERO_ADDRESS, ZERO_ADDRESS).call() + const foreignFeeParams = await feeManager.methods.getFee(foreignFeeType, ZERO_ADDRESS, ZERO_ADDRESS).call() + const homeFee = toBN(feeEnabled ? homeFeeParams.percentage : 0) + const foreignFee = toBN(feeEnabled ? foreignFeeParams.percentage : 0) const oneEthBN = toBN('1000000000000000000') console.log(`Home fee: ${homeFee.div(toBN('10000000000000000')).toString(10)}%`) console.log(`Foreign fee: ${foreignFee.div(toBN('10000000000000000')).toString(10)}%`) diff --git a/test/omnibridge/OmnibridgeSubsidizeHelper.test.js b/test/omnibridge/OmnibridgeSubsidizeHelper.test.js new file mode 100644 index 0000000..bbe8859 --- /dev/null +++ b/test/omnibridge/OmnibridgeSubsidizeHelper.test.js @@ -0,0 +1,105 @@ +const HomeOmnibridge = artifacts.require('HomeOmnibridge') +const AMBMock = artifacts.require('AMBMock') +const TokenFactory = artifacts.require('TokenFactory') +const OmnibridgeSubsidizeHelper = artifacts.require('OmnibridgeSubsidizeHelper') +const OmnibridgeFeeManager = artifacts.require('OmnibridgeFeeManager') +const MultiTokenForwardingRulesManager = artifacts.require('MultiTokenForwardingRulesManager') +const WETH = artifacts.require('WETH') + +const { expect } = require('chai') +const { getEvents, ether } = require('../helpers/helpers') +const { requirePrecompiled, ZERO_ADDRESS, toWei } = require('../setup') + +const oneEther = ether('1') +const dailyLimit = ether('10') +const maxPerTx = oneEther +const minPerTx = ether('0.01') +const executionDailyLimit = dailyLimit +const executionMaxPerTx = maxPerTx + +contract.only('OmnibridgeSubsidizeHelper', (accounts) => { + let token + let mediator + let ambBridgeContract + let feeManager + let forwardingManager + let helper + const owner = accounts[0] + const user = accounts[1] + const ONE_PERCENT_FEE = { + percentage: toWei('0.01'), + minFee: toWei('0.05'), + maxFee: toWei('100'), + } + + beforeEach(async () => { + const PermittableToken = await requirePrecompiled('PermittableToken') + + token = await PermittableToken.new('TEST', 'TST', 18, 1337) + const tokenFactory = await TokenFactory.new(owner, token.address) + mediator = await HomeOmnibridge.new(' on Testnet') + ambBridgeContract = await AMBMock.new() + forwardingManager = await MultiTokenForwardingRulesManager.new(owner) + await mediator.initialize( + ambBridgeContract.address, + mediator.address, + [dailyLimit, maxPerTx, minPerTx], + [executionDailyLimit, executionMaxPerTx], + ZERO_ADDRESS, + owner, + tokenFactory.address, + ZERO_ADDRESS, + forwardingManager.address + ) + helper = await OmnibridgeSubsidizeHelper.new(mediator.address, owner) + feeManager = await OmnibridgeFeeManager.new(helper.address, owner, [owner], ONE_PERCENT_FEE, ONE_PERCENT_FEE) + await helper.setFeeManager(feeManager.address) + await forwardingManager.setSenderForwardingRule(helper.address, true) + + await token.mint(user, ether('100')) + await token.approve(helper.address, ether('100'), { from: user }) + await helper.enableToken(token.address, true) + }) + + it('relayTokens', async () => { + await helper.methods['relayTokens(address,address,uint256)'](token.address, accounts[2], oneEther, { from: user }) + await helper.methods['relayTokens(address,uint256)'](token.address, oneEther, { from: user }) + await helper.relayTokensAndCall(token.address, accounts[2], oneEther, '0x55667788', { from: user }) + await token.transferAndCall(helper.address, oneEther, accounts[2], { from: user }) + await token.transferAndCall(mediator.address, oneEther, accounts[2], { from: user }) + + const depositEvents = await getEvents(mediator, { event: 'TokensBridgingInitiated' }) + expect(depositEvents.length).to.be.equal(5) + for (let i = 0; i < 5; i++) { + expect(depositEvents[i].returnValues.token).to.be.equal(token.address) + expect(depositEvents[i].returnValues.sender).to.be.equal(i === 4 ? user : helper.address) + expect(depositEvents[i].returnValues.value).to.be.equal(ether(i === 4 ? '1' : '0.95').toString()) + expect(depositEvents[i].returnValues.messageId).to.include('0x11223344') + } + const ambEvents = await getEvents(ambBridgeContract, { event: 'MockedEvent' }) + expect(ambEvents.length).to.be.equal(5) + expect(ambEvents[0].returnValues.data).to.include(accounts[2].slice(2).toLowerCase()) + expect(ambEvents[1].returnValues.data).to.include(user.slice(2).toLowerCase()) + expect(ambEvents[2].returnValues.data).to.include(accounts[2].slice(2).toLowerCase()) + expect(ambEvents[3].returnValues.data).to.include(accounts[2].slice(2).toLowerCase()) + expect(ambEvents[4].returnValues.data).to.include(accounts[2].slice(2).toLowerCase()) + expect(ambEvents[2].returnValues.data).to.include('55667788') + expect(ambEvents[0].returnValues.dataType).to.be.bignumber.equal('0') + expect(ambEvents[1].returnValues.dataType).to.be.bignumber.equal('0') + expect(ambEvents[2].returnValues.dataType).to.be.bignumber.equal('0') + expect(ambEvents[3].returnValues.dataType).to.be.bignumber.equal('0') + expect(ambEvents[4].returnValues.dataType).to.be.bignumber.equal('128') + expect(await token.balanceOf(owner)).to.be.bignumber.equal(ether('0.2')) + }) + + it('claimTokens', async () => { + const wethToken = await WETH.new() + await wethToken.deposit({ value: oneEther }) + await wethToken.transfer(helper.address, oneEther) + + await helper.claimTokens(wethToken.address, user, { from: user }).should.be.rejected + await helper.claimTokens(wethToken.address, user, { from: owner }) + + expect(await wethToken.balanceOf(user)).to.be.bignumber.equal(oneEther) + }) +}) diff --git a/test/omnibridge/common.test.js b/test/omnibridge/common.test.js index 6b9f268..f814e82 100644 --- a/test/omnibridge/common.test.js +++ b/test/omnibridge/common.test.js @@ -13,7 +13,7 @@ const AAVEInterestERC20 = artifacts.require('AAVEInterestERC20Mock') const { expect } = require('chai') const { getEvents, ether, expectEventInLogs } = require('../helpers/helpers') -const { ZERO_ADDRESS, toBN, requirePrecompiled } = require('../setup') +const { ZERO_ADDRESS, toBN, toWei, requirePrecompiled } = require('../setup') const getCompoundContracts = require('../compound/contracts') const getAAVEContracts = require('../aave/contracts') @@ -1674,12 +1674,39 @@ function runTests(accounts, isHome) { if (isHome) { describe('fees management', () => { + const ANY_ADDRESS = '0xFFfFfFffFFfffFFfFFfFFFFFffFFFffffFfFFFfF' + const toFee = ({ percentage, minFee, maxFee }) => ({ percentage, minFee, maxFee }) + const ZERO_FEE = { + percentage: '0', + minFee: '0', + maxFee: toWei('100'), + } + const ONE_PERCENT_FEE = { + percentage: toWei('0.01'), + minFee: '0', + maxFee: toWei('100'), + } + const TWO_PERCENT_FEE = { + percentage: toWei('0.02'), + minFee: '0', + maxFee: toWei('100'), + } + const TEN_PERCENT_FEE = { + percentage: toWei('0.1'), + minFee: '0', + maxFee: toWei('100'), + } + const TWENTY_PERCENT_FEE = { + percentage: toWei('0.2'), + minFee: '0', + maxFee: toWei('100'), + } let homeToForeignFee let foreignToHomeFee let feeManager beforeEach(async () => { await initialize().should.be.fulfilled - feeManager = await OmnibridgeFeeManager.new(contract.address, owner, [owner], [ether('0.02'), ether('0.01')]) + feeManager = await OmnibridgeFeeManager.new(contract.address, owner, [owner], TWO_PERCENT_FEE, ONE_PERCENT_FEE) await contract.setFeeManager(feeManager.address, { from: owner }).should.be.fulfilled const initialEvents = await getEvents(ambBridgeContract, { event: 'MockedEvent' }) @@ -1728,8 +1755,8 @@ function runTests(accounts, isHome) { await token.mint(user, ether('10'), { from: owner }).should.be.fulfilled await token.transferAndCall(contract.address, value, '0x', { from: user }).should.be.fulfilled - expect(await feeManager.getFee(homeToForeignFee, token.address)).to.be.bignumber.equal(ether('0.02')) - expect(await feeManager.getFee(foreignToHomeFee, token.address)).to.be.bignumber.equal(ether('0.01')) + expect(toFee(await feeManager.getFee(homeToForeignFee, token.address, user))).to.be.eql(TWO_PERCENT_FEE) + expect(toFee(await feeManager.getFee(foreignToHomeFee, token.address, user))).to.be.eql(ONE_PERCENT_FEE) }) it('should initialize fees for bridged token', async () => { @@ -1738,32 +1765,42 @@ function runTests(accounts, isHome) { expect(await executeMessageCall(exampleMessageId, data)).to.be.equal(true) const bridgedToken = await contract.bridgedTokenAddress(otherSideToken1) - expect(await feeManager.getFee(homeToForeignFee, bridgedToken)).to.be.bignumber.equal(ether('0.02')) - expect(await feeManager.getFee(foreignToHomeFee, bridgedToken)).to.be.bignumber.equal(ether('0.01')) + expect(toFee(await feeManager.getFee(homeToForeignFee, bridgedToken, user))).to.be.eql(TWO_PERCENT_FEE) + expect(toFee(await feeManager.getFee(foreignToHomeFee, bridgedToken, user))).to.be.eql(ONE_PERCENT_FEE) }) }) describe('update fee parameters', () => { it('should update default fee value', async () => { - await feeManager.setFee(homeToForeignFee, ZERO_ADDRESS, ether('0.1'), { from: user }).should.be.rejected - await feeManager.setFee(homeToForeignFee, ZERO_ADDRESS, ether('1.1'), { from: owner }).should.be.rejected - const { logs } = await feeManager.setFee(homeToForeignFee, ZERO_ADDRESS, ether('0.1'), { from: owner }).should - .be.fulfilled + await feeManager.setFee(homeToForeignFee, ANY_ADDRESS, ANY_ADDRESS, TEN_PERCENT_FEE, { from: user }).should.be + .rejected + await feeManager.setFee( + homeToForeignFee, + ANY_ADDRESS, + ANY_ADDRESS, + { percentage: toWei('1.1'), minFee: '0', maxFee: '100' }, + { from: owner } + ).should.be.rejected + await feeManager.setFee( + homeToForeignFee, + ANY_ADDRESS, + ANY_ADDRESS, + { percentage: toWei('0.1'), minFee: '100', maxFee: '0' }, + { from: owner } + ).should.be.rejected + const { logs } = await feeManager.setFee(homeToForeignFee, ANY_ADDRESS, ANY_ADDRESS, TEN_PERCENT_FEE) expectEventInLogs(logs, 'FeeUpdated') - expect(await feeManager.getFee(homeToForeignFee, ZERO_ADDRESS)).to.be.bignumber.equal(ether('0.1')) - expect(await feeManager.getFee(foreignToHomeFee, ZERO_ADDRESS)).to.be.bignumber.equal(ether('0.01')) + expect(toFee(await feeManager.getFee(homeToForeignFee, token.address, user))).to.be.eql(TEN_PERCENT_FEE) + expect(toFee(await feeManager.getFee(foreignToHomeFee, token.address, user))).to.be.eql(ONE_PERCENT_FEE) }) it('should update default opposite direction fee value', async () => { - await feeManager.setFee(foreignToHomeFee, ZERO_ADDRESS, ether('0.1'), { from: user }).should.be.rejected - await feeManager.setFee(foreignToHomeFee, ZERO_ADDRESS, ether('1.1'), { from: owner }).should.be.rejected - const { logs } = await feeManager.setFee(foreignToHomeFee, ZERO_ADDRESS, ether('0.1'), { from: owner }).should - .be.fulfilled + const { logs } = await feeManager.setFee(foreignToHomeFee, ANY_ADDRESS, ANY_ADDRESS, TEN_PERCENT_FEE) expectEventInLogs(logs, 'FeeUpdated') - expect(await feeManager.getFee(foreignToHomeFee, ZERO_ADDRESS)).to.be.bignumber.equal(ether('0.1')) - expect(await feeManager.getFee(homeToForeignFee, ZERO_ADDRESS)).to.be.bignumber.equal(ether('0.02')) + expect(toFee(await feeManager.getFee(foreignToHomeFee, token.address, user))).to.be.eql(TEN_PERCENT_FEE) + expect(toFee(await feeManager.getFee(homeToForeignFee, token.address, user))).to.be.eql(TWO_PERCENT_FEE) }) it('should update fee value for native token', async () => { @@ -1771,19 +1808,18 @@ function runTests(accounts, isHome) { await token.transferAndCall(contract.address, value, '0x', { from: user }).should.be.fulfilled - await feeManager.setFee(homeToForeignFee, token.address, ether('0.1'), { from: user }).should.be.rejected - await feeManager.setFee(homeToForeignFee, token.address, ether('1.1'), { from: owner }).should.be.rejected - const { logs: logs1 } = await feeManager.setFee(homeToForeignFee, token.address, ether('0.1'), { - from: owner, - }).should.be.fulfilled - const { logs: logs2 } = await feeManager.setFee(foreignToHomeFee, token.address, ether('0.2'), { - from: owner, - }).should.be.fulfilled + const { logs: logs1 } = await feeManager.setFee(homeToForeignFee, token.address, ANY_ADDRESS, TEN_PERCENT_FEE) + const { logs: logs2 } = await feeManager.setFee( + foreignToHomeFee, + token.address, + ANY_ADDRESS, + TWENTY_PERCENT_FEE + ) expectEventInLogs(logs1, 'FeeUpdated') expectEventInLogs(logs2, 'FeeUpdated') - expect(await feeManager.getFee(homeToForeignFee, token.address)).to.be.bignumber.equal(ether('0.1')) - expect(await feeManager.getFee(foreignToHomeFee, token.address)).to.be.bignumber.equal(ether('0.2')) + expect(toFee(await feeManager.getFee(homeToForeignFee, token.address, user))).to.be.eql(TEN_PERCENT_FEE) + expect(toFee(await feeManager.getFee(foreignToHomeFee, token.address, user))).to.be.eql(TWENTY_PERCENT_FEE) }) it('should update fee value for bridged token', async () => { @@ -1793,23 +1829,24 @@ function runTests(accounts, isHome) { expect(await executeMessageCall(exampleMessageId, data)).to.be.equal(true) const bridgedToken = await contract.bridgedTokenAddress(otherSideToken1) - await feeManager.setFee(homeToForeignFee, bridgedToken, ether('0.1'), { from: user }).should.be.rejected - await feeManager.setFee(homeToForeignFee, bridgedToken, ether('1.1'), { from: owner }).should.be.rejected - const { logs: logs1 } = await feeManager.setFee(homeToForeignFee, bridgedToken, ether('0.1'), { from: owner }) - .should.be.fulfilled - const { logs: logs2 } = await feeManager.setFee(foreignToHomeFee, bridgedToken, ether('0.2'), { from: owner }) - .should.be.fulfilled + const { logs: logs1 } = await feeManager.setFee(homeToForeignFee, bridgedToken, ANY_ADDRESS, TEN_PERCENT_FEE) + const { logs: logs2 } = await feeManager.setFee( + foreignToHomeFee, + bridgedToken, + ANY_ADDRESS, + TWENTY_PERCENT_FEE + ) expectEventInLogs(logs1, 'FeeUpdated') expectEventInLogs(logs2, 'FeeUpdated') - expect(await feeManager.getFee(homeToForeignFee, bridgedToken)).to.be.bignumber.equal(ether('0.1')) - expect(await feeManager.getFee(foreignToHomeFee, bridgedToken)).to.be.bignumber.equal(ether('0.2')) + expect(toFee(await feeManager.getFee(homeToForeignFee, bridgedToken, user))).to.be.eql(TEN_PERCENT_FEE) + expect(toFee(await feeManager.getFee(foreignToHomeFee, bridgedToken, user))).to.be.eql(TWENTY_PERCENT_FEE) }) }) function testHomeToForeignFee(isNative) { it('should collect and distribute 0% fee', async () => { - await feeManager.setFee(homeToForeignFee, isNative ? ZERO_ADDRESS : token.address, ZERO).should.be.fulfilled + await feeManager.setFee(homeToForeignFee, isNative ? ANY_ADDRESS : token.address, ANY_ADDRESS, ZERO_FEE) expect(await contract.totalSpentPerDay(token.address, currentDay)).to.be.bignumber.equal(ZERO) await token.transferAndCall(contract.address, value, '0x', { from: user }) expect(await contract.totalSpentPerDay(token.address, currentDay)).to.be.bignumber.equal(value) @@ -1895,14 +1932,14 @@ function runTests(accounts, isHome) { describe('distribute fee for foreign => home direction', async () => { beforeEach(async () => { - await feeManager.setFee(homeToForeignFee, ZERO_ADDRESS, ZERO).should.be.fulfilled + await feeManager.setFee(homeToForeignFee, ANY_ADDRESS, ANY_ADDRESS, ZERO_FEE).should.be.fulfilled await token.mint(user, ether('10'), { from: owner }).should.be.fulfilled await token.transferAndCall(contract.address, value, '0x', { from: user }).should.be.fulfilled await token.transferAndCall(contract.address, value, '0x', { from: user }).should.be.fulfilled }) it('should collect and distribute 0% fee', async () => { - await feeManager.setFee(foreignToHomeFee, token.address, ZERO).should.be.fulfilled + await feeManager.setFee(foreignToHomeFee, token.address, ANY_ADDRESS, ZERO_FEE).should.be.fulfilled const data = contract.contract.methods.handleNativeTokens(token.address, user, value).encodeABI() expect(await executeMessageCall(exampleMessageId, data)).to.be.equal(true) @@ -2014,7 +2051,7 @@ function runTests(accounts, isHome) { describe('distribute fee for bridged tokens', () => { describe('distribute fee for foreign => home direction', async () => { it('should collect and distribute 0% fee', async () => { - await feeManager.setFee(foreignToHomeFee, ZERO_ADDRESS, ZERO).should.be.fulfilled + await feeManager.setFee(foreignToHomeFee, ANY_ADDRESS, ANY_ADDRESS, ZERO_FEE).should.be.fulfilled const args = [otherSideToken1, 'Test', 'TST', 18, user, value] const deployData = contract.contract.methods.deployAndHandleBridgedTokens(...args).encodeABI() @@ -2144,7 +2181,7 @@ function runTests(accounts, isHome) { describe('distribute fee for home => foreign direction', async () => { beforeEach(async () => { - await feeManager.setFee(foreignToHomeFee, ZERO_ADDRESS, ZERO).should.be.fulfilled + await feeManager.setFee(foreignToHomeFee, ANY_ADDRESS, ANY_ADDRESS, ZERO_FEE).should.be.fulfilled const args = [otherSideToken1, 'Test', 'TST', 18, user, value] const deployData = contract.contract.methods.deployAndHandleBridgedTokens(...args).encodeABI() diff --git a/test/setup.js b/test/setup.js index 7845073..1bdfe4d 100644 --- a/test/setup.js +++ b/test/setup.js @@ -1,4 +1,4 @@ -const { BN, toBN } = web3.utils +const { BN, toBN, toWei } = web3.utils require('chai').use(require('chai-as-promised')).use(require('chai-bn')(BN)) @@ -8,6 +8,7 @@ const truffleContract = require('@truffle/contract') exports.BN = BN exports.toBN = toBN +exports.toWei = toWei exports.ERROR_MSG = 'VM Exception while processing transaction: revert' exports.ERROR_MSG_OPCODE = 'VM Exception while processing transaction: invalid opcode' exports.ZERO_ADDRESS = '0x0000000000000000000000000000000000000000'