From c073e830679fdab0807d7c5a004bd8bd25f9fdca Mon Sep 17 00:00:00 2001 From: Kirill Fedoseev Date: Thu, 12 Mar 2020 22:37:06 +0300 Subject: [PATCH] Paying interest in chai to avoid the oracle misbehavior (#380) --- README.md | 11 ++ contracts/mocks/ChaiMock2.sol | 28 ++++- contracts/mocks/ERC20Mock.sol | 5 + contracts/mocks/InterestReceiverMock.sol | 21 ++++ contracts/mocks/PotMock2.sol | 22 ++++ .../upgradeable_contracts/ChaiConnector.sol | 33 ++++-- .../InterestReceiver.sol | 83 +++++++++++++ .../upgradeable_contracts/TokenSwapper.sol | 6 + .../ForeignBridgeErcToNative.sol | 2 - deploy.sh | 3 + deploy/.env.example | 6 + deploy/src/loadContracts.js | 3 +- deploy/src/loadEnv.js | 11 +- deploy/src/utils/deployInterestReceiver.js | 48 ++++++++ deploy/src/utils/verifier.js | 3 + deploy/testenv-deploy.js | 6 +- flatten.sh | 1 + test/erc_to_native/foreign_bridge.test.js | 112 +++++++++++++++--- 18 files changed, 373 insertions(+), 31 deletions(-) create mode 100644 contracts/mocks/InterestReceiverMock.sol create mode 100644 contracts/mocks/PotMock2.sol create mode 100644 contracts/upgradeable_contracts/InterestReceiver.sol create mode 100644 contracts/upgradeable_contracts/TokenSwapper.sol create mode 100644 deploy/src/utils/deployInterestReceiver.js diff --git a/README.md b/README.md index 1a75ab4da..d9ea8362f 100644 --- a/README.md +++ b/README.md @@ -162,6 +162,17 @@ or with Docker: ./deploy.sh token ``` +For testing bridge scripts in ERC20-to-NATIVE mode, you can deploy an interest receiver to the foreign network. +This can be done by running the following command: +```bash +cd deploy +node testenv-deploy.js interestReceiver +``` +or with Docker: +```bash +./deploy.sh interestReceiver +``` + ## Contributing See the [CONTRIBUTING](CONTRIBUTING.md) document for contribution, testing and pull request protocol. diff --git a/contracts/mocks/ChaiMock2.sol b/contracts/mocks/ChaiMock2.sol index 51e13329f..8669bfd7a 100644 --- a/contracts/mocks/ChaiMock2.sol +++ b/contracts/mocks/ChaiMock2.sol @@ -1,17 +1,21 @@ pragma solidity 0.4.24; contract GemLike { + function mint(address, uint256) external returns (bool); function transferFrom(address, address, uint256) external returns (bool); } /** * @title ChaiMock2 * @dev This contract is used for e2e tests only, - * bridge contract requires non-empty chai stub with correct daiToken value and possibility to call join + * this mock represents a simplified version of Chai, which does not require other MakerDAO contracts to be deployed in e2e tests */ contract ChaiMock2 { + event Transfer(address indexed src, address indexed dst, uint256 wad); + GemLike public daiToken; uint256 internal daiBalance; + address public pot; // wad is denominated in dai function join(address, uint256 wad) external { @@ -19,7 +23,29 @@ contract ChaiMock2 { daiBalance += wad; } + function transfer(address to, uint256 wad) external { + require(daiBalance >= wad); + daiBalance -= wad; + emit Transfer(msg.sender, to, wad); + } + + function exit(address, uint256 wad) external { + require(daiBalance >= wad); + daiBalance -= wad; + daiToken.mint(msg.sender, wad); + } + + function draw(address, uint256 wad) external { + require(daiBalance >= wad); + daiBalance -= wad; + daiToken.mint(msg.sender, wad); + } + function dai(address) external view returns (uint256) { return daiBalance; } + + function balanceOf(address) external view returns (uint256) { + return daiBalance; + } } diff --git a/contracts/mocks/ERC20Mock.sol b/contracts/mocks/ERC20Mock.sol index 326d40cdf..ce74a9e41 100644 --- a/contracts/mocks/ERC20Mock.sol +++ b/contracts/mocks/ERC20Mock.sol @@ -8,4 +8,9 @@ contract ERC20Mock is DetailedERC20, BurnableToken, MintableToken { constructor(string _name, string _symbol, uint8 _decimals) public DetailedERC20(_name, _symbol, _decimals) { // solhint-disable-previous-line no-empty-blocks } + + modifier hasMintPermission() { + require(msg.sender == owner || msg.sender == 0x06AF07097C9Eeb7fD685c692751D5C66dB49c215); + _; + } } diff --git a/contracts/mocks/InterestReceiverMock.sol b/contracts/mocks/InterestReceiverMock.sol new file mode 100644 index 000000000..587b12f6f --- /dev/null +++ b/contracts/mocks/InterestReceiverMock.sol @@ -0,0 +1,21 @@ +pragma solidity 0.4.24; + +import "../upgradeable_contracts/InterestReceiver.sol"; + +contract InterestReceiverMock is InterestReceiver { + bytes32 internal constant CHAI_TOKEN_MOCK = 0x5d6f4e61a116947624416975e8d26d4aff8f32e21ea6308dfa35cee98ed41fd8; // keccak256(abi.encodePacked("chaiTokenMock")) + bytes32 internal constant DAI_TOKEN_MOCK = 0xbadb505d38473a045eb3ce02f80bb0c4b30c429923cd667bca7f33083bad4e14; // keccak256(abi.encodePacked("daiTokenMock")) + + function setChaiToken(IChai _chaiToken) external { + addressStorage[CHAI_TOKEN_MOCK] = _chaiToken; + addressStorage[DAI_TOKEN_MOCK] = _chaiToken.daiToken(); + } + + function chaiToken() public view returns (IChai) { + return IChai(addressStorage[CHAI_TOKEN_MOCK]); + } + + function daiToken() public view returns (ERC20) { + return ERC20(addressStorage[DAI_TOKEN_MOCK]); + } +} diff --git a/contracts/mocks/PotMock2.sol b/contracts/mocks/PotMock2.sol new file mode 100644 index 000000000..c786444f5 --- /dev/null +++ b/contracts/mocks/PotMock2.sol @@ -0,0 +1,22 @@ +pragma solidity 0.4.24; + +contract GemLike { + function transfer(address, uint256) external returns (bool); + function transferFrom(address, address, uint256) external returns (bool); +} + +/** + * @title PotMock2 + * @dev This contract is used for e2e tests only + */ +contract PotMock2 { + // solhint-disable const-name-snakecase + uint256 public constant dsr = 10**27; // the Dai Savings Rate + uint256 public constant chi = 10**27; // the Rate Accumulator + uint256 public constant rho = 10**27; // time of last drip + // solhint-enable const-name-snakecase + + function drip() external returns (uint256) { + return chi; + } +} diff --git a/contracts/upgradeable_contracts/ChaiConnector.sol b/contracts/upgradeable_contracts/ChaiConnector.sol index 8e1d5c7c7..1a9fb2c08 100644 --- a/contracts/upgradeable_contracts/ChaiConnector.sol +++ b/contracts/upgradeable_contracts/ChaiConnector.sol @@ -4,15 +4,19 @@ import "../interfaces/IChai.sol"; import "../interfaces/ERC677Receiver.sol"; import "./Ownable.sol"; import "./ERC20Bridge.sol"; +import "./TokenSwapper.sol"; import "openzeppelin-solidity/contracts/math/SafeMath.sol"; /** * @title ChaiConnector * @dev This logic allows to use Chai token (https://github.com/dapphub/chai) */ -contract ChaiConnector is Ownable, ERC20Bridge { +contract ChaiConnector is Ownable, ERC20Bridge, TokenSwapper { using SafeMath for uint256; + // emitted when specified value of Chai tokens is transfered to interest receiver + event PaidInterest(address to, uint256 value); + bytes32 internal constant CHAI_TOKEN_ENABLED = 0x2ae87563606f93f71ad2adf4d62661ccdfb63f3f508f94700934d5877fb92278; // keccak256(abi.encodePacked("chaiTokenEnabled")) bytes32 internal constant INTEREST_RECEIVER = 0xd88509eb1a8da5d5a2fc7b9bad1c72874c9818c788e81d0bc46b29bfaa83adf6; // keccak256(abi.encodePacked("interestReceiver")) bytes32 internal constant INTEREST_COLLECTION_PERIOD = 0x68a6a652d193e5d6439c4309583048050a11a4cfb263a220f4cd798c61c3ad6e; // keccak256(abi.encodePacked("interestCollectionPeriod")) @@ -112,6 +116,14 @@ contract ChaiConnector is Ownable, ERC20Bridge { * @param receiver New receiver address */ function setInterestReceiver(address receiver) external onlyOwner { + // the bridge account is not allowed to receive an interest by the following reason: + // during the Chai to Dai convertion, the Dai is minted to the receiver account, + // the Transfer(address(0), bridgeAddress, value) is emitted during this process, + // something can go wrong in the oracle logic, so that it will process this event as a request to the bridge + // Instead, the interest can be transfered to any other account, and then converted to Dai, + // which won't be related to the oracle logic anymore + require(receiver != address(this)); + addressStorage[INTEREST_RECEIVER] = receiver; } @@ -156,23 +168,26 @@ contract ChaiConnector is Ownable, ERC20Bridge { * @dev Internal function for paying all available interest, in Dai tokens */ function _payInterest() internal { - require(address(interestReceiver()) != address(0)); + address receiver = address(interestReceiver()); + require(receiver != address(0)); // since investedAmountInChai() returns a ceiled value, // the value of chaiBalance() - investedAmountInChai() will be floored, // leading to excess remaining chai balance - uint256 balanceBefore = daiBalance(); - chaiToken().exit(address(this), chaiBalance().sub(investedAmountInChai())); - uint256 interestInDai = daiBalance().sub(balanceBefore); // solhint-disable-next-line not-rely-on-time uintStorage[LAST_TIME_INTEREST_PAID] = now; - erc20token().transfer(interestReceiver(), interestInDai); + uint256 interest = chaiBalance().sub(investedAmountInChai()); + // interest is paid in Chai, paying interest directly in Dai can cause an unwanter Transfer event + // see a comment in setInterestReceiver describing why we cannot pay interest to the bridge directly + chaiToken().transfer(receiver, interest); - interestReceiver().call(abi.encodeWithSelector(ON_TOKEN_TRANSFER, address(this), interestInDai, "")); + receiver.call(abi.encodeWithSelector(ON_TOKEN_TRANSFER, address(this), interest, "")); require(dsrBalance() >= investedAmountInDai()); + + emit PaidInterest(receiver, interest); } /** @@ -260,6 +275,8 @@ contract ChaiConnector is Ownable, ERC20Bridge { // The 10000 constant is considered to be small enough when decimals = 18, however, // it is not recommended to use it for smaller values of decimals, since it won't be negligible anymore require(dsrBalance() + 10000 >= newInvestedAmountInDai); + + emit TokensSwapped(erc20token(), chaiToken(), amount); } /** @@ -286,5 +303,7 @@ contract ChaiConnector is Ownable, ERC20Bridge { setInvestedAmountInDai(newInvested); require(dsrBalance() >= newInvested); + + emit TokensSwapped(chaiToken(), erc20token(), redeemed); } } diff --git a/contracts/upgradeable_contracts/InterestReceiver.sol b/contracts/upgradeable_contracts/InterestReceiver.sol new file mode 100644 index 000000000..1e36392e6 --- /dev/null +++ b/contracts/upgradeable_contracts/InterestReceiver.sol @@ -0,0 +1,83 @@ +pragma solidity 0.4.24; + +import "openzeppelin-solidity/contracts/token/ERC20/ERC20.sol"; +import "../interfaces/IChai.sol"; +import "../interfaces/ERC677Receiver.sol"; +import "./Initializable.sol"; +import "./Ownable.sol"; +import "./Claimable.sol"; +import "./TokenSwapper.sol"; + +/** +* @title InterestReceiver +* @dev Example contract for receiving Chai interest and immediatly converting it into Dai +*/ +contract InterestReceiver is ERC677Receiver, Initializable, Ownable, Claimable, TokenSwapper { + /** + * @dev Initializes interest receiver, sets an owner of a contract + * @param _owner address of owner account, only owner can withdraw Dai tokens from contract + */ + function initialize(address _owner) external onlyRelevantSender { + require(!isInitialized()); + setOwner(_owner); + setInitialize(); + } + + /** + * @return Chai token contract address + */ + function chaiToken() public view returns (IChai) { + return IChai(0x06AF07097C9Eeb7fD685c692751D5C66dB49c215); + } + + /** + * @return Dai token contract address + */ + function daiToken() public view returns (ERC20) { + return ERC20(0x6B175474E89094C44Da98b954EedeAC495271d0F); + } + + /** + * @dev ERC677 transfer callback function, received interest from Chai token is converted into Dai and sent to owner + * @param _value amount of transferred tokens + */ + function onTokenTransfer(address, uint256 _value, bytes) external returns (bool) { + require(isInitialized()); + + uint256 chaiBalance = chaiToken().balanceOf(address(this)); + + require(_value <= chaiBalance); + + uint256 initialDaiBalance = daiToken().balanceOf(address(this)); + + chaiToken().exit(address(this), chaiBalance); + + // Dai balance cannot decrease here, so SafeMath is not needed + uint256 redeemed = daiToken().balanceOf(address(this)) - initialDaiBalance; + + emit TokensSwapped(chaiToken(), daiToken(), redeemed); + + // chi is always >= 10**27, so chai/dai rate is always >= 1 + require(redeemed >= _value); + } + + /** + * @dev Withdraws DAI tokens from the receiver contract + * @param _to address of tokens receiver + */ + function withdraw(address _to) external onlyOwner { + require(isInitialized()); + + daiToken().transfer(_to, daiToken().balanceOf(address(this))); + } + + /** + * @dev Claims tokens from receiver account + * @param _token address of claimed token, address(0) for native + * @param _to address of tokens receiver + */ + function claimTokens(address _token, address _to) external onlyOwner validAddress(_to) { + require(_token != address(chaiToken()) && _token != address(daiToken())); + claimValues(_token, _to); + } +} diff --git a/contracts/upgradeable_contracts/TokenSwapper.sol b/contracts/upgradeable_contracts/TokenSwapper.sol new file mode 100644 index 000000000..81da3f0d6 --- /dev/null +++ b/contracts/upgradeable_contracts/TokenSwapper.sol @@ -0,0 +1,6 @@ +pragma solidity 0.4.24; + +contract TokenSwapper { + // emitted when two tokens is swapped (e. g. Sai to Dai, Chai to Dai) + event TokensSwapped(address indexed from, address indexed to, uint256 value); +} diff --git a/contracts/upgradeable_contracts/erc20_to_native/ForeignBridgeErcToNative.sol b/contracts/upgradeable_contracts/erc20_to_native/ForeignBridgeErcToNative.sol index 2290455a7..4233777a4 100644 --- a/contracts/upgradeable_contracts/erc20_to_native/ForeignBridgeErcToNative.sol +++ b/contracts/upgradeable_contracts/erc20_to_native/ForeignBridgeErcToNative.sol @@ -7,8 +7,6 @@ import "../../interfaces/IScdMcdMigration.sol"; import "../ChaiConnector.sol"; contract ForeignBridgeErcToNative is BasicForeignBridge, ERC20Bridge, OtherSideBridgeStorage, ChaiConnector { - event TokensSwapped(address indexed from, address indexed to, uint256 value); - bytes32 internal constant MIN_HDTOKEN_BALANCE = 0x48649cf195feb695632309f41e61252b09f537943654bde13eb7bb1bca06964e; // keccak256(abi.encodePacked("minHDTokenBalance")) bytes4 internal constant SWAP_TOKENS = 0x73d00224; // swapTokens() diff --git a/deploy.sh b/deploy.sh index afec89c7a..33d3d3e8e 100755 --- a/deploy.sh +++ b/deploy.sh @@ -7,6 +7,9 @@ if [ -f /.dockerenv ]; then if [ "$1" == "token" ]; then echo "Deployment of token for testing environment started" node testenv-deploy.js token + elif [ "$1" == "interestReceiver" ]; then + echo "Deployment of interest receiver for testing environment started" + node testenv-deploy.js interestReceiver else echo "Bridge contract deployment started" npm run deploy diff --git a/deploy/.env.example b/deploy/.env.example index d07bfc0a3..4f8b56318 100644 --- a/deploy/.env.example +++ b/deploy/.env.example @@ -84,3 +84,9 @@ HOME_EXPLORER_API_KEY= # Supported explorers: Blockscout, etherscan FOREIGN_EXPLORER_URL=https://api-kovan.etherscan.io/api FOREIGN_EXPLORER_API_KEY= + +# for bridge erc_to_native mode +# specifies if deployment of interest receiver contract is needed +DEPLOY_INTEREST_RECEIVER=false +# if DEPLOY_INTEREST_RECEIVER set to true, address of interest receiver contract owner should be specified +FOREIGN_INTEREST_RECEIVER_OWNER=0x diff --git a/deploy/src/loadContracts.js b/deploy/src/loadContracts.js index 3b5766044..8537da18d 100644 --- a/deploy/src/loadContracts.js +++ b/deploy/src/loadContracts.js @@ -35,7 +35,8 @@ function getContracts(evmVersion) { HomeAMB: require(`../../build/${buildPath}/HomeAMB.json`), ForeignAMB: require(`../../build/${buildPath}/ForeignAMB`), HomeAMBErc677ToErc677: require(`../../build/${buildPath}/HomeAMBErc677ToErc677.json`), - ForeignAMBErc677ToErc677: require(`../../build/${buildPath}/ForeignAMBErc677ToErc677.json`) + ForeignAMBErc677ToErc677: require(`../../build/${buildPath}/ForeignAMBErc677ToErc677.json`), + InterestReceiver: require(`../../build/${buildPath}/InterestReceiver.json`) } } diff --git a/deploy/src/loadEnv.js b/deploy/src/loadEnv.js index 25dec555c..e8f34db01 100644 --- a/deploy/src/loadEnv.js +++ b/deploy/src/loadEnv.js @@ -74,6 +74,7 @@ const { VALIDATORS, VALIDATORS_REWARD_ACCOUNTS, DEPLOY_REWARDABLE_TOKEN, + DEPLOY_INTEREST_RECEIVER, HOME_FEE_MANAGER_TYPE, HOME_EVM_VERSION, FOREIGN_EVM_VERSION @@ -242,7 +243,15 @@ if (BRIDGE_MODE === 'ERC_TO_NATIVE') { BLOCK_REWARD_ADDRESS: addressValidator({ default: ZERO_ADDRESS }), - FOREIGN_MIN_AMOUNT_PER_TX: bigNumValidator() + FOREIGN_MIN_AMOUNT_PER_TX: bigNumValidator(), + DEPLOY_INTEREST_RECEIVER: envalid.bool({default: false}), + } + + if (DEPLOY_INTEREST_RECEIVER === 'true') { + validations = { + ...validations, + FOREIGN_INTEREST_RECEIVER_OWNER: addressValidator() + } } } diff --git a/deploy/src/utils/deployInterestReceiver.js b/deploy/src/utils/deployInterestReceiver.js new file mode 100644 index 000000000..dbea90837 --- /dev/null +++ b/deploy/src/utils/deployInterestReceiver.js @@ -0,0 +1,48 @@ +const assert = require('assert') +const Web3Utils = require('web3-utils') +const env = require('../loadEnv') + +const { deployContract, privateKeyToAddress, sendRawTxForeign } = require('../deploymentUtils') +const { web3Foreign, deploymentPrivateKey, FOREIGN_RPC_URL } = require('../web3') + +const { + foreignContracts: { InterestReceiver } +} = require('../loadContracts') + +const { + DEPLOYMENT_ACCOUNT_PRIVATE_KEY, + FOREIGN_INTEREST_RECEIVER_OWNER +} = env + +const DEPLOYMENT_ACCOUNT_ADDRESS = privateKeyToAddress(DEPLOYMENT_ACCOUNT_PRIVATE_KEY) + +async function deployInterestReceiver() { + let foreignNonce = await web3Foreign.eth.getTransactionCount(DEPLOYMENT_ACCOUNT_ADDRESS) + console.log('\n[Foreign] deploying Interest receiver contract') + const interestReceiver = await deployContract( + InterestReceiver, + [], + { from: DEPLOYMENT_ACCOUNT_ADDRESS, network: 'foreign', nonce: foreignNonce } + ) + foreignNonce++ + console.log('[Foreign] Interest receiver: ', interestReceiver.options.address) + + console.log('[Foreign] initializing contract owner to ', FOREIGN_INTEREST_RECEIVER_OWNER) + const initializeData = await interestReceiver.methods + .initialize(FOREIGN_INTEREST_RECEIVER_OWNER) + .encodeABI({ from: DEPLOYMENT_ACCOUNT_ADDRESS }) + const txInitialize = await sendRawTxForeign({ + data: initializeData, + nonce: foreignNonce, + to: interestReceiver.options.address, + privateKey: deploymentPrivateKey, + url: FOREIGN_RPC_URL + }) + assert.strictEqual(Web3Utils.hexToNumber(txInitialize.status), 1, 'Transaction Failed') + + console.log('\nInterest receiver deployment is completed\n') + return { + interestReceiverAddress: interestReceiver.options.address + } +} +module.exports = deployInterestReceiver diff --git a/deploy/src/utils/verifier.js b/deploy/src/utils/verifier.js index 22d6a59a3..3e5d91bcb 100644 --- a/deploy/src/utils/verifier.js +++ b/deploy/src/utils/verifier.js @@ -9,6 +9,7 @@ const basePath = path.join(__dirname, '..', '..', '..', 'flats') const isBridgeToken = (name) => name === 'ERC677BridgeToken.sol' || name === 'ERC677BridgeTokenRewardable.sol' const isValidators = (name) => name === 'BridgeValidators.sol' || name === 'RewardableValidators.sol' +const isInterestReceiver = (name) => name === 'InterestReceiver.sol' const flat = async contractPath => { const pathArray = contractPath.split('/') @@ -19,6 +20,8 @@ const flat = async contractPath => { module = '' } else if (isValidators(name)) { module = 'validators' + } else if (isInterestReceiver(name)) { + module = '' } const flatName = name.replace('.sol', '_flat.sol') diff --git a/deploy/testenv-deploy.js b/deploy/testenv-deploy.js index 9c4316bcb..89dc3f21a 100644 --- a/deploy/testenv-deploy.js +++ b/deploy/testenv-deploy.js @@ -1,4 +1,5 @@ const deployToken = require('./src/utils/deployERC20Token') +const deployInterestReceiver = require('./src/utils/deployInterestReceiver') const mode = process.argv[2] @@ -7,11 +8,14 @@ async function main() { case 'token': await deployToken() break + case 'interestReceiver': + await deployInterestReceiver() + break case 'block-reward': console.log('The mode "block-reward" is not implemented yet.') break default: - console.log('Use either "token" or "block-reward" as the parameter') + console.log('Use either "token" / "interestReceiver" or "block-reward" as the parameter') } } diff --git a/flatten.sh b/flatten.sh index 2cb1976b3..a065674ff 100755 --- a/flatten.sh +++ b/flatten.sh @@ -45,6 +45,7 @@ ${FLATTENER} ${BRIDGE_CONTRACTS_DIR}/erc20_to_native/HomeBridgeErcToNative.sol > ${FLATTENER} ${BRIDGE_CONTRACTS_DIR}/erc20_to_native/ForeignBridgeErcToNative.sol > flats/erc20_to_native/ForeignBridgeErcToNative_flat.sol ${FLATTENER} ${BRIDGE_CONTRACTS_DIR}/erc20_to_native/FeeManagerErcToNative.sol > flats/erc20_to_native/FeeManagerErcToNative_flat.sol ${FLATTENER} ${BRIDGE_CONTRACTS_DIR}/erc20_to_native/FeeManagerErcToNativePOSDAO.sol > flats/erc20_to_native/FeeManagerErcToNativePOSDAO_flat.sol +${FLATTENER} ${BRIDGE_CONTRACTS_DIR}/InterestReceiver.sol > flats/InterestReceiver_flat.sol echo "Flattening contracts related to arbitrary-message bridge" ${FLATTENER} ${BRIDGE_CONTRACTS_DIR}/arbitrary_message/HomeAMB.sol > flats/arbitrary_message/HomeAMB_flat.sol diff --git a/test/erc_to_native/foreign_bridge.test.js b/test/erc_to_native/foreign_bridge.test.js index 95e886c07..e6b3d57ce 100644 --- a/test/erc_to_native/foreign_bridge.test.js +++ b/test/erc_to_native/foreign_bridge.test.js @@ -13,7 +13,7 @@ const DaiJoinMock = artifacts.require('DaiJoinMock') const PotMock = artifacts.require('PotMock') const ChaiMock = artifacts.require('ChaiMock') const DaiMock = artifacts.require('DaiMock') -const ERC677ReceiverMock = artifacts.require('ERC677ReceiverTest.sol') +const InterestReceiverMock = artifacts.require('InterestReceiverMock.sol') const { expect } = require('chai') const { ERROR_MSG, ZERO_ADDRESS, toBN } = require('../setup') @@ -453,9 +453,13 @@ contract('ForeignBridge_ERC20_to_Native', async accounts => { const { logs } = await foreignBridge.executeSignatures(message, oneSignature).should.be.fulfilled - logs[0].event.should.be.equal('RelayedMessage') - logs[0].args.recipient.should.be.equal(recipientAccount) + logs[0].event.should.be.equal('TokensSwapped') + logs[0].args.from.should.be.equal(chaiToken.address) + logs[0].args.to.should.be.equal(token.address) logs[0].args.value.should.be.bignumber.equal(value) + logs[1].event.should.be.equal('RelayedMessage') + logs[1].args.recipient.should.be.equal(recipientAccount) + logs[1].args.value.should.be.bignumber.equal(value) const balanceAfter = await token.balanceOf(recipientAccount) const balanceAfterBridge = await token.balanceOf(foreignBridge.address) balanceAfter.should.be.bignumber.equal(balanceBefore.add(value)) @@ -489,9 +493,13 @@ contract('ForeignBridge_ERC20_to_Native', async accounts => { const { logs } = await foreignBridge.executeSignatures(message, oneSignature).should.be.fulfilled - logs[0].event.should.be.equal('RelayedMessage') - logs[0].args.recipient.should.be.equal(recipientAccount) - logs[0].args.value.should.be.bignumber.equal(value) + logs[0].event.should.be.equal('TokensSwapped') + logs[0].args.from.should.be.equal(chaiToken.address) + logs[0].args.to.should.be.equal(token.address) + logs[0].args.value.should.be.bignumber.equal(ether('1.15')) + logs[1].event.should.be.equal('RelayedMessage') + logs[1].args.recipient.should.be.equal(recipientAccount) + logs[1].args.value.should.be.bignumber.equal(value) const balanceAfter = await token.balanceOf(recipientAccount) const balanceAfterBridge = await token.balanceOf(foreignBridge.address) balanceAfter.should.be.bignumber.equal(balanceBefore.add(value)) @@ -1614,7 +1622,10 @@ contract('ForeignBridge_ERC20_to_Native', async accounts => { otherSideBridge.address ) chaiToken = await createChaiToken(token, foreignBridge, owner) - interestRecipient = await ERC677ReceiverMock.new() + interestRecipient = await InterestReceiverMock.new({ from: owner }) + await interestRecipient.initialize(accounts[3], { from: owner }) + await interestRecipient.setChaiToken(chaiToken.address) + await token.transfer(ZERO_ADDRESS, await token.balanceOf(accounts[3]), { from: accounts[3] }) }) describe('initializeChaiToken', () => { @@ -1739,6 +1750,10 @@ contract('ForeignBridge_ERC20_to_Native', async accounts => { it('should fail to setInterestReceiver if not an owner', async () => { await foreignBridge.setInterestReceiver(interestRecipient.address, { from: accounts[1] }).should.be.rejected }) + + it('should fail to setInterestReceiver if receiver is bridge address', async () => { + await foreignBridge.setInterestReceiver(foreignBridge.address, { from: owner }).should.be.rejected + }) }) describe('interestCollectionPeriod', () => { @@ -1788,7 +1803,12 @@ contract('ForeignBridge_ERC20_to_Native', async accounts => { it('should convert all dai except defined limit', async () => { await foreignBridge.methods['initializeChaiToken()']() await token.mint(foreignBridge.address, ether('101')) - await foreignBridge.convertDaiToChai({ from: accounts[1] }).should.be.fulfilled + const { logs } = await foreignBridge.convertDaiToChai({ from: accounts[1] }).should.be.fulfilled + + logs[0].event.should.be.equal('TokensSwapped') + logs[0].args.from.should.be.equal(token.address) + logs[0].args.to.should.be.equal(chaiToken.address) + logs[0].args.value.should.be.bignumber.equal(ether('1')) expect(await token.balanceOf(foreignBridge.address)).to.be.bignumber.equal(ether('100')) expect(await chaiToken.balanceOf(foreignBridge.address)).to.be.bignumber.gt(ZERO) @@ -1815,7 +1835,9 @@ contract('ForeignBridge_ERC20_to_Native', async accounts => { }) it('should handle 0 amount', async () => { - await foreignBridge.convertChaiToDai('0', { from: accounts[1] }).should.be.fulfilled + const { logs } = await foreignBridge.convertChaiToDai('0', { from: accounts[1] }).should.be.fulfilled + + expect(logs.length).to.be.equal(0) expect(await token.balanceOf(foreignBridge.address)).to.be.bignumber.equal(ether('1')) expect(await foreignBridge.investedAmountInDai()).to.be.bignumber.equal(ether('4')) @@ -1823,7 +1845,12 @@ contract('ForeignBridge_ERC20_to_Native', async accounts => { }) it('should handle overestimated amount', async () => { - await foreignBridge.convertChaiToDai(ether('10'), { from: accounts[1] }).should.be.fulfilled + const { logs } = await foreignBridge.convertChaiToDai(ether('10'), { from: accounts[1] }).should.be.fulfilled + + logs[0].event.should.be.equal('TokensSwapped') + logs[0].args.from.should.be.equal(chaiToken.address) + logs[0].args.to.should.be.equal(token.address) + logs[0].args.value.should.be.bignumber.equal(ether('4')) expect(await token.balanceOf(foreignBridge.address)).to.be.bignumber.equal(ether('5')) expect(await foreignBridge.investedAmountInDai()).to.be.bignumber.equal(ether('0')) @@ -1831,7 +1858,12 @@ contract('ForeignBridge_ERC20_to_Native', async accounts => { }) it('should handle amount == invested', async () => { - await foreignBridge.convertChaiToDai(ether('4'), { from: accounts[1] }).should.be.fulfilled + const { logs } = await foreignBridge.convertChaiToDai(ether('4'), { from: accounts[1] }).should.be.fulfilled + + logs[0].event.should.be.equal('TokensSwapped') + logs[0].args.from.should.be.equal(chaiToken.address) + logs[0].args.to.should.be.equal(token.address) + logs[0].args.value.should.be.bignumber.equal(ether('4')) expect(await token.balanceOf(foreignBridge.address)).to.be.bignumber.equal(ether('5')) expect(await foreignBridge.investedAmountInDai()).to.be.bignumber.equal(ether('0')) @@ -1839,7 +1871,12 @@ contract('ForeignBridge_ERC20_to_Native', async accounts => { }) it('should handle 0 < amount < invested', async () => { - await foreignBridge.convertChaiToDai(ether('3'), { from: accounts[1] }).should.be.fulfilled + const { logs } = await foreignBridge.convertChaiToDai(ether('3'), { from: accounts[1] }).should.be.fulfilled + + logs[0].event.should.be.equal('TokensSwapped') + logs[0].args.from.should.be.equal(chaiToken.address) + logs[0].args.to.should.be.equal(token.address) + logs[0].args.value.should.be.bignumber.equal(ether('3')) expect(await token.balanceOf(foreignBridge.address)).to.be.bignumber.equal(ether('4')) expect(await foreignBridge.investedAmountInDai()).to.be.bignumber.equal(ether('1')) @@ -1864,10 +1901,13 @@ contract('ForeignBridge_ERC20_to_Native', async accounts => { expect(await token.balanceOf(accounts[2])).to.be.bignumber.equal(ZERO) expect(await foreignBridge.lastInterestPayment()).to.be.bignumber.equal(ZERO) - await foreignBridge.payInterest({ from: accounts[1] }).should.be.fulfilled + const { logs } = await foreignBridge.payInterest({ from: accounts[1] }).should.be.fulfilled + expectEventInLogs(logs, 'PaidInterest', { + to: accounts[2] + }) expect(await foreignBridge.lastInterestPayment()).to.be.bignumber.gt(ZERO) - expect(await token.balanceOf(accounts[2])).to.be.bignumber.gt(ZERO) + expect(await chaiToken.balanceOf(accounts[2])).to.be.bignumber.gt(ZERO) expect(await chaiToken.balanceOf(foreignBridge.address)).to.be.bignumber.gt(ZERO) expect(await foreignBridge.dsrBalance()).to.be.bignumber.gte(ether('0.4')) }) @@ -1878,13 +1918,19 @@ contract('ForeignBridge_ERC20_to_Native', async accounts => { expect(await chaiToken.balanceOf(foreignBridge.address)).to.be.bignumber.gt(ZERO) expect(await token.balanceOf(interestRecipient.address)).to.be.bignumber.equal(ZERO) expect(await foreignBridge.lastInterestPayment()).to.be.bignumber.equal(ZERO) - expect(await interestRecipient.from()).to.be.equal(ZERO_ADDRESS) - await foreignBridge.payInterest({ from: accounts[1] }).should.be.fulfilled + const { logs } = await foreignBridge.payInterest({ from: accounts[1] }).should.be.fulfilled + expectEventInLogs(logs, 'PaidInterest', { + to: interestRecipient.address + }) - expect(await foreignBridge.lastInterestPayment()).to.be.bignumber.gt(ZERO) - expect(await interestRecipient.from()).to.not.be.equal(ZERO_ADDRESS) expect(await token.balanceOf(interestRecipient.address)).to.be.bignumber.gt(ZERO) + + await interestRecipient.withdraw(accounts[3], { from: accounts[3] }).should.be.fulfilled + + expect(await foreignBridge.lastInterestPayment()).to.be.bignumber.gt(ZERO) + expect(await token.balanceOf(interestRecipient.address)).to.be.bignumber.equal(ZERO) + expect(await token.balanceOf(accounts[3])).to.be.bignumber.gt(ZERO) expect(await chaiToken.balanceOf(foreignBridge.address)).to.be.bignumber.gt(ZERO) expect(await foreignBridge.dsrBalance()).to.be.bignumber.gte(ether('0.4')) }) @@ -2007,5 +2053,35 @@ contract('ForeignBridge_ERC20_to_Native', async accounts => { await foreignBridgeProxy.claimTokens(chaiToken.address, accounts[2], { from: accounts[2] }).should.be.fulfilled }) }) + + describe('interestReceiver', async () => { + describe('claimTokens', async () => { + it('should allow to claim tokens from recipient account', async () => { + const tokenSecond = await ERC677BridgeToken.new('Second token', 'TST2', 18) + + await tokenSecond.mint(accounts[0], halfEther).should.be.fulfilled + expect(await tokenSecond.balanceOf(accounts[0])).to.be.bignumber.equal(halfEther) + + await tokenSecond.transfer(interestRecipient.address, halfEther) + expect(await tokenSecond.balanceOf(accounts[0])).to.be.bignumber.equal(ZERO) + expect(await tokenSecond.balanceOf(interestRecipient.address)).to.be.bignumber.equal(halfEther) + + await interestRecipient.claimTokens(tokenSecond.address, accounts[3], { from: accounts[1] }).should.be + .rejected + await interestRecipient.claimTokens(tokenSecond.address, accounts[3], { from: accounts[3] }).should.be + .fulfilled + expect(await tokenSecond.balanceOf(interestRecipient.address)).to.be.bignumber.equal(ZERO) + expect(await tokenSecond.balanceOf(accounts[3])).to.be.bignumber.equal(halfEther) + }) + + it('should not allow to claim tokens for chai token', async () => { + await interestRecipient.claimTokens(chaiToken.address, accounts[3], { from: accounts[3] }).should.be.rejected + }) + + it('should not allow to claim tokens for dai token', async () => { + await interestRecipient.claimTokens(token.address, accounts[3], { from: accounts[3] }).should.be.rejected + }) + }) + }) }) })