diff --git a/contracts/interfaces/IScdMcdMigration.sol b/contracts/interfaces/IScdMcdMigration.sol index 215bed94b..e52a217fe 100644 --- a/contracts/interfaces/IScdMcdMigration.sol +++ b/contracts/interfaces/IScdMcdMigration.sol @@ -8,3 +8,7 @@ interface IScdMcdMigration { interface IDaiAdapter { function dai() public returns (address); } + +interface ISaiTop { + function caged() public returns (uint256); +} diff --git a/contracts/mocks/ForeignBridgeErcToNativeMock.sol b/contracts/mocks/ForeignBridgeErcToNativeMock.sol new file mode 100644 index 000000000..4e6572808 --- /dev/null +++ b/contracts/mocks/ForeignBridgeErcToNativeMock.sol @@ -0,0 +1,20 @@ +pragma solidity 0.4.24; + +import "../upgradeable_contracts/erc20_to_native/ForeignBridgeErcToNative.sol"; + +contract ForeignBridgeErcToNativeMock is ForeignBridgeErcToNative { + function migrationContract() internal pure returns (IScdMcdMigration) { + // Address generated in unit test + return IScdMcdMigration(0x44Bf5539DAAc4259f7F11A24280255ED2Fa3F7BF); + } + + function halfDuplexErc20token() public pure returns (ERC20) { + // Address generated in unit test + return ERC20(0x872D709De609c391741c7595F0053F6060e59e0D); + } + + function saiTopContract() internal pure returns (ISaiTop) { + // Address generated in unit test + return ISaiTop(0x96bc48adACdB60E6536E55a6727919a397F14540); + } +} diff --git a/contracts/mocks/SaiTopMock.sol b/contracts/mocks/SaiTopMock.sol new file mode 100644 index 000000000..5e3d5008d --- /dev/null +++ b/contracts/mocks/SaiTopMock.sol @@ -0,0 +1,9 @@ +pragma solidity 0.4.24; + +contract SaiTopMock { + uint256 public caged; + + function setCaged(uint256 _value) public { + caged = _value; + } +} diff --git a/contracts/upgradeable_contracts/BaseBridgeValidators.sol b/contracts/upgradeable_contracts/BaseBridgeValidators.sol index a55710d62..508a75eca 100644 --- a/contracts/upgradeable_contracts/BaseBridgeValidators.sol +++ b/contracts/upgradeable_contracts/BaseBridgeValidators.sol @@ -24,7 +24,7 @@ contract BaseBridgeValidators is InitializableBridge, Ownable { } function getBridgeValidatorsInterfacesVersion() external pure returns (uint64 major, uint64 minor, uint64 patch) { - return (2, 2, 0); + return (2, 3, 0); } function validatorList() external view returns (address[]) { @@ -102,4 +102,23 @@ contract BaseBridgeValidators is InitializableBridge, Ownable { function setNextValidator(address _prevValidator, address _validator) internal { addressStorage[keccak256(abi.encodePacked("validatorsList", _prevValidator))] = _validator; } + + function isValidatorDuty(address _validator) external view returns (bool) { + uint256 counter = 0; + address next = getNextValidator(F_ADDR); + require(next != address(0)); + + while (next != F_ADDR) { + if (next == _validator) { + return (block.number % validatorCount() == counter); + } + + next = getNextValidator(next); + counter++; + + require(next != address(0)); + } + + return false; + } } diff --git a/contracts/upgradeable_contracts/VersionableBridge.sol b/contracts/upgradeable_contracts/VersionableBridge.sol index 615ac71e9..765b28209 100644 --- a/contracts/upgradeable_contracts/VersionableBridge.sol +++ b/contracts/upgradeable_contracts/VersionableBridge.sol @@ -2,7 +2,7 @@ pragma solidity 0.4.24; contract VersionableBridge { function getBridgeInterfacesVersion() external pure returns (uint64 major, uint64 minor, uint64 patch) { - return (2, 4, 0); + return (2, 5, 0); } /* solcov ignore next */ diff --git a/contracts/upgradeable_contracts/erc20_to_native/ForeignBridgeErcToNative.sol b/contracts/upgradeable_contracts/erc20_to_native/ForeignBridgeErcToNative.sol index 122cc45ed..d01019eca 100644 --- a/contracts/upgradeable_contracts/erc20_to_native/ForeignBridgeErcToNative.sol +++ b/contracts/upgradeable_contracts/erc20_to_native/ForeignBridgeErcToNative.sol @@ -8,6 +8,9 @@ import "../../interfaces/IScdMcdMigration.sol"; contract ForeignBridgeErcToNative is BasicForeignBridge, ERC20Bridge, OtherSideBridgeStorage { 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() + function initialize( address _validatorContract, address _erc20token, @@ -61,6 +64,11 @@ contract ForeignBridgeErcToNative is BasicForeignBridge, ERC20Bridge, OtherSideB function claimTokens(address _token, address _to) public { require(_token != address(erc20token())); + if (_token == address(halfDuplexErc20token())) { + // SCD is not claimable if the bridge accepts deposits of this token + // solhint-disable-next-line not-rely-on-time + require(!isTokenSwapAllowed(now)); + } super.claimTokens(_token, _to); } @@ -71,7 +79,13 @@ contract ForeignBridgeErcToNative is BasicForeignBridge, ERC20Bridge, OtherSideB ) internal returns (bool) { setTotalExecutedPerDay(getCurrentDay(), totalExecutedPerDay(getCurrentDay()).add(_amount)); uint256 amount = _amount.div(10**decimalShift()); - return erc20token().transfer(_recipient, amount); + bool res = erc20token().transfer(_recipient, amount); + + if (tokenBalance(halfDuplexErc20token()) > 0) { + address(this).call(abi.encodeWithSelector(SWAP_TOKENS)); + } + + return res; } function onFailedMessage(address, uint256, bytes32) internal { @@ -83,23 +97,113 @@ contract ForeignBridgeErcToNative is BasicForeignBridge, ERC20Bridge, OtherSideB super._relayTokens(_sender, _receiver, _amount); } - function migrateToMCD(address _migrationContract) external onlyOwner { + function migrateToMCD() external { bytes32 storageAddress = 0x3378953eb16363e06fd9ea9701d36ed7285d206d9de7df55b778462d74596a89; // keccak256(abi.encodePacked("migrationToMcdCompleted")) - require(!boolStorage[storageAddress]); - require(AddressUtils.isContract(_migrationContract)); - - uint256 curBalance = erc20token().balanceOf(address(this)); - require(erc20token().approve(_migrationContract, curBalance)); - //It is important to note that this action will cause appearing of `Transfer` - //event as part of the tokens minting - IScdMcdMigration(_migrationContract).swapSaiToDai(curBalance); - address saiContract = erc20token(); - address mcdContract = IDaiAdapter(IScdMcdMigration(_migrationContract).daiJoin()).dai(); + + address mcdContract = IDaiAdapter(migrationContract().daiJoin()).dai(); setErc20token(mcdContract); - require(erc20token().balanceOf(address(this)) == curBalance); - emit TokensSwapped(saiContract, erc20token(), curBalance); + uintStorage[MIN_HDTOKEN_BALANCE] = 10 ether; + + swapTokens(); + boolStorage[storageAddress] = true; } + + function saiTopContract() internal pure returns (ISaiTop) { + return ISaiTop(0x9b0ccf7C8994E19F39b2B4CF708e0A7DF65fA8a3); + } + + function isTokenSwapAllowed(uint256 _ts) public view returns (bool) { + uint256 esTs = saiTopContract().caged(); + if (esTs > 0 && _ts > esTs) { + return false; + } + return true; + } + + function halfDuplexErc20token() public pure returns (ERC20) { + return ERC20(0x89d24A6b4CcB1B6fAA2625fE562bDD9a23260359); + } + + function setMinHDTokenBalance(uint256 _minBalance) external onlyOwner { + uintStorage[MIN_HDTOKEN_BALANCE] = _minBalance; + } + + function minHDTokenBalance() public view returns (uint256) { + return uintStorage[MIN_HDTOKEN_BALANCE]; + } + + function isHDTokenBalanceAboveMinBalance() public view returns (bool) { + if (tokenBalance(halfDuplexErc20token()) > minHDTokenBalance()) { + return true; + } + return false; + } + + function tokenBalance(ERC20 _token) internal view returns (uint256) { + return _token.balanceOf(address(this)); + } + + function migrationContract() internal pure returns (IScdMcdMigration) { + return IScdMcdMigration(0xc73e0383F3Aff3215E6f04B0331D58CeCf0Ab849); + } + + function swapTokens() public { + // solhint-disable-next-line not-rely-on-time + require(isTokenSwapAllowed(now)); + + IScdMcdMigration mcdMigrationContract = migrationContract(); + ERC20 hdToken = halfDuplexErc20token(); + ERC20 fdToken = erc20token(); + + uint256 curHDTokenBalance = tokenBalance(hdToken); + require(curHDTokenBalance > 0); + + uint256 curFDTokenBalance = tokenBalance(fdToken); + + require(hdToken.approve(mcdMigrationContract, curHDTokenBalance)); + mcdMigrationContract.swapSaiToDai(curHDTokenBalance); + + require(tokenBalance(fdToken).sub(curFDTokenBalance) == curHDTokenBalance); + + emit TokensSwapped(hdToken, fdToken, curHDTokenBalance); + } + + function relayTokens(address _from, address _receiver, uint256 _amount, address _token) external { + require(_from == msg.sender || _from == _receiver); + _relayTokens(_from, _receiver, _amount, _token); + } + + function relayTokens(address _receiver, uint256 _amount, address _token) external { + _relayTokens(msg.sender, _receiver, _amount, _token); + } + + function _relayTokens(address _sender, address _receiver, uint256 _amount, address _token) internal { + require(_receiver != bridgeContractOnOtherSide()); + require(_receiver != address(0)); + require(_receiver != address(this)); + require(_amount > 0); + require(withinLimit(_amount)); + + ERC20 tokenToOperate = ERC20(_token); + ERC20 hdToken = halfDuplexErc20token(); + ERC20 fdToken = erc20token(); + + if (tokenToOperate == ERC20(0x0)) { + tokenToOperate = fdToken; + } + + require(tokenToOperate == fdToken || tokenToOperate == hdToken); + + setTotalSpentPerDay(getCurrentDay(), totalSpentPerDay(getCurrentDay()).add(_amount)); + + tokenToOperate.transferFrom(_sender, address(this), _amount); + emit UserRequestForAffirmation(_receiver, _amount); + + if (tokenToOperate == hdToken) { + swapTokens(); + } + } } diff --git a/scripts/test.sh b/scripts/test.sh index 758f3f1b6..287eb19f8 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -36,6 +36,7 @@ start_ganache() { --account="0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501207,1000000000000000000000000" --account="0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501208,1000000000000000000000000" --account="0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501209,1000000000000000000000000" + --account="0x19fba401d77e4113b15095e9aa7117bcd25adcfac7f6111f8298894eef443600,1000000000000000000000000" ) if [ "$SOLIDITY_COVERAGE" != true ]; then diff --git a/test/erc_to_native/foreign_bridge.test.js b/test/erc_to_native/foreign_bridge.test.js index 4c5e41535..b744e214e 100644 --- a/test/erc_to_native/foreign_bridge.test.js +++ b/test/erc_to_native/foreign_bridge.test.js @@ -6,6 +6,8 @@ const ERC677BridgeToken = artifacts.require('ERC677BridgeToken.sol') const ERC20Mock = artifacts.require('ERC20Mock.sol') const ScdMcdMigrationMock = artifacts.require('ScdMcdMigrationMock.sol') const DaiAdapterMock = artifacts.require('DaiAdapterMock.sol') +const SaiTopMock = artifacts.require('SaiTopMock.sol') +const ForeignBridgeErcToNativeMock = artifacts.require('ForeignBridgeErcToNativeMock.sol') const { expect } = require('chai') const { ERROR_MSG, ZERO_ADDRESS, toBN } = require('../setup') @@ -15,6 +17,7 @@ const halfEther = ether('0.5') const requireBlockConfirmations = 8 const gasPrice = web3.utils.toWei('1', 'gwei') const oneEther = ether('1') +const twoEthers = ether('2') const homeDailyLimit = oneEther const homeMaxPerTx = halfEther const dailyLimit = oneEther @@ -29,12 +32,31 @@ contract('ForeignBridge_ERC20_to_Native', async accounts => { let owner let token let otherSideBridge + let sai + let dai + let migrationContract + let saiTop + const user = accounts[7] before(async () => { validatorContract = await BridgeValidators.new() authorities = [accounts[1], accounts[2]] owner = accounts[0] await validatorContract.initialize(1, authorities, owner) otherSideBridge = await ForeignBridge.new() + + // Used account 11 to deploy contracts. The contract addresses will be hardcoded in ForeignBridgeErcToNativeMock + const deployAccount = accounts[10] + sai = await ERC20Mock.new('sai', 'SAI', 18, { from: deployAccount }) + saiTop = await SaiTopMock.new({ from: deployAccount }) + dai = await ERC20Mock.new('dai', 'DAI', 18) + const daiAdapterMock = await DaiAdapterMock.new(dai.address) + migrationContract = await ScdMcdMigrationMock.new(sai.address, daiAdapterMock.address, { from: deployAccount }) + await sai.transferOwnership(accounts[0], { from: deployAccount }) + + await dai.mint(user, ether('100000')) + + // migration contract can mint dai + await dai.transferOwnership(migrationContract.address) }) describe('#initialize', async () => { it('should initialize', async () => { @@ -183,7 +205,7 @@ contract('ForeignBridge_ERC20_to_Native', async accounts => { const value = ether('0.25') let foreignBridge beforeEach(async () => { - foreignBridge = await ForeignBridge.new() + foreignBridge = await ForeignBridgeErcToNativeMock.new() token = await ERC677BridgeToken.new('Some ERC20', 'RSZT', 18) await foreignBridge.initialize( validatorContract.address, @@ -329,7 +351,7 @@ contract('ForeignBridge_ERC20_to_Native', async accounts => { await multisigValidatorContract.initialize(2, twoAuthorities, ownerOfValidatorContract, { from: ownerOfValidatorContract }) - foreignBridgeWithMultiSignatures = await ForeignBridge.new() + foreignBridgeWithMultiSignatures = await ForeignBridgeErcToNativeMock.new() await foreignBridgeWithMultiSignatures.initialize( multisigValidatorContract.address, token.address, @@ -396,7 +418,7 @@ contract('ForeignBridge_ERC20_to_Native', async accounts => { await validatorContractWith3Signatures.initialize(3, authoritiesFiveAccs, ownerOfValidators) const erc20Token = await ERC677BridgeToken.new('Some ERC20', 'RSZT', 18) const value = halfEther - const foreignBridgeWithThreeSigs = await ForeignBridge.new() + const foreignBridgeWithThreeSigs = await ForeignBridgeErcToNativeMock.new() await foreignBridgeWithThreeSigs.initialize( validatorContractWith3Signatures.address, @@ -553,7 +575,7 @@ contract('ForeignBridge_ERC20_to_Native', async accounts => { const valueOnHome = toBN(valueOnForeign * 10 ** decimalShiftTwo) const owner = accounts[0] - const foreignBridgeImpl = await ForeignBridge.new() + const foreignBridgeImpl = await ForeignBridgeErcToNativeMock.new() const storageProxy = await EternalStorageProxy.new().should.be.fulfilled await storageProxy.upgradeTo('1', foreignBridgeImpl.address).should.be.fulfilled const foreignBridge = await ForeignBridge.at(storageProxy.address) @@ -604,7 +626,7 @@ contract('ForeignBridge_ERC20_to_Native', async accounts => { await multisigValidatorContract.initialize(2, twoAuthorities, ownerOfValidatorContract, { from: ownerOfValidatorContract }) - const foreignBridgeWithMultiSignatures = await ForeignBridge.new() + const foreignBridgeWithMultiSignatures = await ForeignBridgeErcToNativeMock.new() await foreignBridgeWithMultiSignatures.initialize( multisigValidatorContract.address, token.address, @@ -839,15 +861,8 @@ contract('ForeignBridge_ERC20_to_Native', async accounts => { }) describe('migrateToMCD', () => { let foreignBridge - let sai - let dai - let migrationContract beforeEach(async () => { - foreignBridge = await ForeignBridge.new() - sai = await ERC20Mock.new('sai', 'SAI', 18) - dai = await ERC20Mock.new('dai', 'DAI', 18) - const daiAdapterMock = await DaiAdapterMock.new(dai.address) - migrationContract = await ScdMcdMigrationMock.new(sai.address, daiAdapterMock.address) + foreignBridge = await ForeignBridgeErcToNativeMock.new() await foreignBridge.initialize( validatorContract.address, @@ -863,9 +878,6 @@ contract('ForeignBridge_ERC20_to_Native', async accounts => { // Mint the bridge some sai tokens await sai.mint(foreignBridge.address, oneEther) - - // migration contract can mint dai - await dai.transferOwnership(migrationContract.address) }) it('should be able to swap tokens', async () => { // Given @@ -875,33 +887,377 @@ contract('ForeignBridge_ERC20_to_Native', async accounts => { // When - // migration address should be a contract - await foreignBridge.migrateToMCD(accounts[3], { from: owner }).should.be.rejectedWith(ERROR_MSG) - - // should be called by owner - await foreignBridge - .migrateToMCD(migrationContract.address, { from: accounts[5] }) - .should.be.rejectedWith(ERROR_MSG) - - const { logs } = await foreignBridge.migrateToMCD(migrationContract.address, { from: owner }).should.be.fulfilled + const { logs } = await foreignBridge.migrateToMCD({ from: owner }).should.be.fulfilled // can't migrate token again - await foreignBridge.migrateToMCD(migrationContract.address, { from: owner }).should.be.rejectedWith(ERROR_MSG) + await foreignBridge.migrateToMCD({ from: owner }).should.be.rejectedWith(ERROR_MSG) // Then expect(await sai.balanceOf(foreignBridge.address)).to.be.bignumber.equal(ZERO) expect(await dai.balanceOf(foreignBridge.address)).to.be.bignumber.equal(oneEther) expect(await foreignBridge.erc20token()).to.be.equal(dai.address) + expect(await foreignBridge.minHDTokenBalance()).to.be.bignumber.equal(ether('10')) expectEventInLogs(logs, 'TokensSwapped', { from: sai.address, to: dai.address, value: oneEther }) const transferEvent = await getEvents(dai, { event: 'Transfer' }) - expect(transferEvent.length).to.be.equal(1) - expect(transferEvent[0].returnValues.from).to.be.equal(ZERO_ADDRESS) - expect(transferEvent[0].returnValues.to).to.be.equal(foreignBridge.address) - expect(transferEvent[0].returnValues.value).to.be.equal(oneEther.toString()) + + // first transfer event was for minting to user in the top Before section + expect(transferEvent.length).to.be.equal(2) + expect(transferEvent[1].returnValues.from).to.be.equal(ZERO_ADDRESS) + expect(transferEvent[1].returnValues.to).to.be.equal(foreignBridge.address) + expect(transferEvent[1].returnValues.value).to.be.equal(oneEther.toString()) + }) + }) + describe('support two tokens', () => { + let foreignBridge + const recipient = accounts[8] + beforeEach(async () => { + foreignBridge = await ForeignBridgeErcToNativeMock.new() + + await foreignBridge.initialize( + validatorContract.address, + dai.address, + requireBlockConfirmations, + gasPrice, + [dailyLimit, maxPerTx, minPerTx], + [homeDailyLimit, homeMaxPerTx], + owner, + decimalShiftZero, + otherSideBridge.address + ) + + // Mint sai tokens to a user + await sai.mint(user, twoEthers) + }) + describe('isTokenSwapAllowed', () => { + it('isTokenSwapAllowed should return true if SCD ES was executed', async () => { + // Given + expect(await foreignBridge.isTokenSwapAllowed(100)).to.be.equal(true) + + // When + await saiTop.setCaged(150) + + // Then + expect(await foreignBridge.isTokenSwapAllowed(100)).to.be.equal(true) + expect(await foreignBridge.isTokenSwapAllowed(150)).to.be.equal(true) + expect(await foreignBridge.isTokenSwapAllowed(200)).to.be.equal(false) + + // reset the caged value + await saiTop.setCaged(0) + }) + }) + describe('isHDTokenBalanceAboveMinBalance', () => { + it('isHDTokenBalanceAboveMinBalance should return true if balance above the threshold ', async () => { + const threshold = halfEther + // Given + expect(await sai.balanceOf(foreignBridge.address)).to.be.bignumber.equal(ZERO) + + await foreignBridge.setMinHDTokenBalance(threshold) + + expect(await foreignBridge.minHDTokenBalance()).to.be.bignumber.equal(threshold) + + expect(await foreignBridge.isHDTokenBalanceAboveMinBalance()).to.be.equal(false) + + // When + await sai.mint(foreignBridge.address, oneEther) + + // Then + expect(await foreignBridge.isHDTokenBalanceAboveMinBalance()).to.be.equal(true) + }) + }) + describe('halfDuplexErc20token', () => { + it('should be able to get half duplex erc20 token', async () => { + expect(await foreignBridge.halfDuplexErc20token()).to.be.equal(sai.address) + }) + }) + describe('swapTokens', () => { + it('should be able to swap tokens calling swapTokens', async () => { + expect(await saiTop.caged()).to.be.bignumber.equal(ZERO) + expect(await sai.balanceOf(foreignBridge.address)).to.be.bignumber.equal(ZERO) + expect(await dai.balanceOf(foreignBridge.address)).to.be.bignumber.equal(ZERO) + + // should have sai balance + await foreignBridge.swapTokens().should.be.rejectedWith(ERROR_MSG) + + // mint sai tokens to bridge + await sai.mint(foreignBridge.address, oneEther) + + expect(await sai.balanceOf(foreignBridge.address)).to.be.bignumber.equal(oneEther) + expect(await dai.balanceOf(foreignBridge.address)).to.be.bignumber.equal(ZERO) + + const { logs } = await foreignBridge.swapTokens() + + expect(await sai.balanceOf(foreignBridge.address)).to.be.bignumber.equal(ZERO) + expect(await dai.balanceOf(foreignBridge.address)).to.be.bignumber.equal(oneEther) + expect(await foreignBridge.erc20token()).to.be.equal(dai.address) + expectEventInLogs(logs, 'TokensSwapped', { + from: sai.address, + to: dai.address, + value: oneEther + }) + + // mint more sai tokens to bridge + await sai.mint(foreignBridge.address, oneEther) + + expect(await sai.balanceOf(foreignBridge.address)).to.be.bignumber.equal(oneEther) + expect(await dai.balanceOf(foreignBridge.address)).to.be.bignumber.equal(oneEther) + + await foreignBridge.swapTokens().should.be.fulfilled + + expect(await sai.balanceOf(foreignBridge.address)).to.be.bignumber.equal(ZERO) + expect(await dai.balanceOf(foreignBridge.address)).to.be.bignumber.equal(twoEthers) + + const block = await web3.eth.getBlock('latest') + // Trigger Emergency Shutdown + await saiTop.setCaged(block.number) + + // mint sai tokens to bridge + await sai.mint(foreignBridge.address, oneEther) + + expect(await sai.balanceOf(foreignBridge.address)).to.be.bignumber.equal(oneEther) + expect(await dai.balanceOf(foreignBridge.address)).to.be.bignumber.equal(twoEthers) + + // should not be able to swap tokens after Emergency Shutdown + await foreignBridge.swapTokens().should.be.rejectedWith(ERROR_MSG) + + // reset the caged value + await saiTop.setCaged(0) + }) + }) + describe('relayTokens', () => { + const value = ether('0.25') + it('should allow to bridge tokens specifying the token address', async () => { + // Given + const balance = await dai.balanceOf(user) + const relayTokens = foreignBridge.methods['relayTokens(address,uint256,address)'] + + const currentDay = await foreignBridge.getCurrentDay() + expect(await foreignBridge.totalSpentPerDay(currentDay)).to.be.bignumber.equal(ZERO) + + await relayTokens(recipient, value, dai.address, { from: user }).should.be.rejectedWith(ERROR_MSG) + + await dai.approve(foreignBridge.address, value, { from: user }).should.be.fulfilled + + // When + await relayTokens(ZERO_ADDRESS, value, dai.address, { from: user }).should.be.rejectedWith(ERROR_MSG) + await relayTokens(foreignBridge.address, value, dai.address, { from: user }).should.be.rejectedWith(ERROR_MSG) + await relayTokens(otherSideBridge.address, value, dai.address, { from: user }).should.be.rejectedWith(ERROR_MSG) + await relayTokens(recipient, 0, dai.address, { from: user }).should.be.rejectedWith(ERROR_MSG) + const { logs } = await relayTokens(recipient, value, dai.address, { from: user }).should.be.fulfilled + + // Then + expect(await foreignBridge.totalSpentPerDay(currentDay)).to.be.bignumber.equal(value) + expectEventInLogs(logs, 'UserRequestForAffirmation', { + recipient, + value + }) + expect(await dai.balanceOf(user)).to.be.bignumber.equal(balance.sub(value)) + }) + it('should use erc20Token if token address is zero', async () => { + // Given + const balance = await dai.balanceOf(user) + const relayTokens = foreignBridge.methods['relayTokens(address,uint256,address)'] + + const currentDay = await foreignBridge.getCurrentDay() + expect(await foreignBridge.totalSpentPerDay(currentDay)).to.be.bignumber.equal(ZERO) + + await dai.approve(foreignBridge.address, value, { from: user }).should.be.fulfilled + + // When + const { logs } = await relayTokens(recipient, value, ZERO_ADDRESS, { from: user }).should.be.fulfilled + + // Then + expect(await foreignBridge.totalSpentPerDay(currentDay)).to.be.bignumber.equal(value) + expectEventInLogs(logs, 'UserRequestForAffirmation', { + recipient, + value + }) + expect(await dai.balanceOf(user)).to.be.bignumber.equal(balance.sub(value)) + }) + it('should swap token if half duplex token is used', async () => { + // Given + const relayTokens = foreignBridge.methods['relayTokens(address,uint256,address)'] + + const userBalance = await sai.balanceOf(user) + expect(await sai.balanceOf(foreignBridge.address)).to.be.bignumber.equal(ZERO) + expect(await dai.balanceOf(foreignBridge.address)).to.be.bignumber.equal(ZERO) + const currentDay = await foreignBridge.getCurrentDay() + expect(await foreignBridge.totalSpentPerDay(currentDay)).to.be.bignumber.equal(ZERO) + + await sai.approve(foreignBridge.address, value, { from: user }).should.be.fulfilled + + // When + const { logs } = await relayTokens(recipient, value, sai.address, { from: user }).should.be.fulfilled + + // Then + expect(await foreignBridge.totalSpentPerDay(currentDay)).to.be.bignumber.equal(value) + expectEventInLogs(logs, 'UserRequestForAffirmation', { + recipient, + value + }) + expectEventInLogs(logs, 'TokensSwapped', { + from: sai.address, + to: dai.address, + value + }) + expect(await sai.balanceOf(user)).to.be.bignumber.equal(userBalance.sub(value)) + expect(await sai.balanceOf(foreignBridge.address)).to.be.bignumber.equal(ZERO) + expect(await dai.balanceOf(foreignBridge.address)).to.be.bignumber.equal(value) + }) + it('should fail if token address is unknown', async () => { + const otherToken = await ERC20Mock.new('token', 'TOK', 18) + await otherToken.mint(user, twoEthers) + + const relayTokens = foreignBridge.methods['relayTokens(address,uint256,address)'] + + await otherToken.approve(foreignBridge.address, value, { from: user }).should.be.fulfilled + + await relayTokens(recipient, value, otherToken.address, { from: user }).should.be.rejectedWith(ERROR_MSG) + }) + it('should allow specify the sender and a different receiver', async () => { + // Given + const currentDay = await foreignBridge.getCurrentDay() + expect(await foreignBridge.totalSpentPerDay(currentDay)).to.be.bignumber.equal(ZERO) + const userBalance = await dai.balanceOf(user) + + const relayTokens = foreignBridge.methods['relayTokens(address,address,uint256,address)'] + + await relayTokens(user, recipient, value, dai.address, { from: user }).should.be.rejectedWith(ERROR_MSG) + + await dai.approve(foreignBridge.address, halfEther, { from: user }).should.be.fulfilled + + // When + await relayTokens(user, ZERO_ADDRESS, value, dai.address, { from: user }).should.be.rejectedWith(ERROR_MSG) + await relayTokens(user, foreignBridge.address, value, dai.address, { from: user }).should.be.rejectedWith( + ERROR_MSG + ) + await relayTokens(user, recipient, 0, dai.address, { from: user }).should.be.rejectedWith(ERROR_MSG) + await relayTokens(user, recipient, value, dai.address, { from: recipient }).should.be.rejectedWith(ERROR_MSG) + const { logs } = await relayTokens(user, recipient, value, dai.address, { from: user }).should.be.fulfilled + const { logs: logsSecondTx } = await relayTokens(user, user, value, dai.address, { from: recipient }).should.be + .fulfilled + + // Then + expectEventInLogs(logs, 'UserRequestForAffirmation', { + recipient, + value + }) + expectEventInLogs(logsSecondTx, 'UserRequestForAffirmation', { + recipient: user, + value + }) + expect(await foreignBridge.totalSpentPerDay(currentDay)).to.be.bignumber.equal(halfEther) + expect(await dai.balanceOf(user)).to.be.bignumber.equal(userBalance.sub(halfEther)) + expect(await dai.balanceOf(foreignBridge.address)).to.be.bignumber.equal(halfEther) + }) + it('should not be able to transfer more than limit', async () => { + // Given + const userSupply = ether('2') + const bigValue = oneEther + const smallValue = ether('0.001') + const currentDay = await foreignBridge.getCurrentDay() + expect(await foreignBridge.totalSpentPerDay(currentDay)).to.be.bignumber.equal(ZERO) + + const relayTokens = foreignBridge.methods['relayTokens(address,uint256,address)'] + + await dai.approve(foreignBridge.address, userSupply, { from: user }).should.be.fulfilled + + // When + // value < minPerTx + await relayTokens(recipient, smallValue, dai.address, { from: user }).should.be.rejectedWith(ERROR_MSG) + // value > maxPerTx + await relayTokens(recipient, bigValue, dai.address, { from: user }).should.be.rejectedWith(ERROR_MSG) + + await relayTokens(recipient, halfEther, dai.address, { from: user }).should.be.fulfilled + await relayTokens(recipient, halfEther, dai.address, { from: user }).should.be.fulfilled + // totalSpentPerDay > dailyLimit + await relayTokens(recipient, halfEther, dai.address, { from: user }).should.be.rejectedWith(ERROR_MSG) + + // Then + expect(await foreignBridge.totalSpentPerDay(currentDay)).to.be.bignumber.equal(oneEther) + }) + }) + describe('onExecuteMessage', () => { + it('should swapTokens in executeSignatures', async () => { + const value = ether('0.25') + const recipientAccount = accounts[3] + const balanceBefore = await dai.balanceOf(recipientAccount) + + // fund dai tokens + await dai.transfer(foreignBridge.address, value, { from: user }) + + // mint sai tokens to bridge + await sai.mint(foreignBridge.address, halfEther) + + const transactionHash = '0x1045bfe274b88120a6b1e5d01b5ec00ab5d01098346e90e7c7a3c9b8f0181c80' + const message = createMessage(recipientAccount, value, transactionHash, foreignBridge.address) + const signature = await sign(authorities[0], message) + const vrs = signatureToVRS(signature) + expect(await foreignBridge.relayedMessages(transactionHash)).to.be.equal(false) + + const { logs } = await foreignBridge.executeSignatures([vrs.v], [vrs.r], [vrs.s], message).should.be.fulfilled + + expectEventInLogs(logs, 'RelayedMessage', { + recipient: recipientAccount, + value + }) + expectEventInLogs(logs, 'TokensSwapped', { + from: sai.address, + to: dai.address, + value: halfEther + }) + expect(await dai.balanceOf(recipientAccount)).to.be.bignumber.equal(balanceBefore.add(value)) + expect(await sai.balanceOf(foreignBridge.address)).to.be.bignumber.equal(ZERO) + expect(await dai.balanceOf(foreignBridge.address)).to.be.bignumber.equal(halfEther) + expect(await foreignBridge.relayedMessages(transactionHash)).to.be.equal(true) + }) + }) + describe('claimTokens', async () => { + it('can send erc20', async () => { + const foreignBridgeImpl = await ForeignBridgeErcToNativeMock.new() + const storageProxy = await EternalStorageProxy.new().should.be.fulfilled + await storageProxy.upgradeTo('1', foreignBridgeImpl.address).should.be.fulfilled + const foreignBridge = await ForeignBridgeErcToNativeMock.at(storageProxy.address) + + await foreignBridge.initialize( + validatorContract.address, + dai.address, + requireBlockConfirmations, + gasPrice, + [dailyLimit, maxPerTx, minPerTx], + [homeDailyLimit, homeMaxPerTx], + owner, + decimalShiftZero, + otherSideBridge.address + ) + + expect(await saiTop.caged()).to.be.bignumber.equal(ZERO) + + // Mint sai tokens to the bridge + await sai.mint(foreignBridge.address, halfEther) + + await foreignBridge.claimTokens(sai.address, accounts[3], { from: owner }).should.be.rejectedWith(ERROR_MSG) + + const block = await web3.eth.getBlock('latest') + // Trigger Emergency Shutdown + await saiTop.setCaged(block.number) + + expect(await sai.balanceOf(foreignBridge.address)).to.be.bignumber.equal(halfEther) + expect(await sai.balanceOf(accounts[3])).to.be.bignumber.equal(ZERO) + + await foreignBridge + .claimTokens(sai.address, accounts[3], { from: accounts[3] }) + .should.be.rejectedWith(ERROR_MSG) + await foreignBridge.claimTokens(sai.address, accounts[3], { from: owner }) + expect(await sai.balanceOf(foreignBridge.address)).to.be.bignumber.equal(ZERO) + expect(await sai.balanceOf(accounts[3])).to.be.bignumber.equal(halfEther) + + // reset the caged value + await saiTop.setCaged(0) + }) }) }) }) diff --git a/test/rewardable_validators_test.js b/test/rewardable_validators_test.js index d36b850cb..92d5e975a 100644 --- a/test/rewardable_validators_test.js +++ b/test/rewardable_validators_test.js @@ -267,7 +267,7 @@ contract('RewardableValidators', async accounts => { await proxy.upgradeTo('1', bridgeValidatorsImpl.address) bridgeValidators = await BridgeValidators.at(proxy.address) const { initialize, isInitialized, removeValidator } = bridgeValidators - await initialize(1, accounts.slice(0, 5), accounts.slice(5), owner, { from: owner }).should.be.fulfilled + await initialize(1, accounts.slice(0, 5), accounts.slice(5, 10), owner, { from: owner }).should.be.fulfilled true.should.be.equal(await isInitialized()) // When @@ -283,7 +283,7 @@ contract('RewardableValidators', async accounts => { it(`reward address is properly assigned`, async () => { // Given const { initialize, isInitialized, getValidatorRewardAddress } = bridgeValidators - await initialize(1, accounts.slice(0, 5), accounts.slice(5), owner, { from: owner }).should.be.fulfilled + await initialize(1, accounts.slice(0, 5), accounts.slice(5, 10), owner, { from: owner }).should.be.fulfilled // When expect(await isInitialized()).to.be.equal(true) @@ -302,7 +302,7 @@ contract('RewardableValidators', async accounts => { const { initialize, validatorList } = bridgeValidators // When - await initialize(1, validators, accounts.slice(5), owner, { from: owner }).should.be.fulfilled + await initialize(1, validators, accounts.slice(5, 10), owner, { from: owner }).should.be.fulfilled // Then expect(await validatorList()).to.be.eql(validators) diff --git a/test/validators_test.js b/test/validators_test.js index a5969f7d3..ca710bb7e 100644 --- a/test/validators_test.js +++ b/test/validators_test.js @@ -248,4 +248,18 @@ contract('BridgeValidators', async accounts => { returnedList.should.be.eql(validators) }) }) + describe('#isValidatorDuty', () => { + it('should return if provided valdidator is on duty', async () => { + // Given + const validators = accounts.slice(0, 5) + const { initialize, isValidatorDuty } = bridgeValidators + await initialize(1, validators, owner, { from: owner }).should.be.fulfilled + + // When + const results = await Promise.all(validators.map(v => isValidatorDuty(v))) + + // Then + expect(results.filter(r => r === true).length).to.be.equal(1) + }) + }) }) diff --git a/truffle-config.js b/truffle-config.js index dec355d8c..a80a21688 100644 --- a/truffle-config.js +++ b/truffle-config.js @@ -29,8 +29,52 @@ if (process.env.SOLIDITY_COVERAGE === 'true') { }) provider.addProvider(global.coverageSubprovider) const ganacheSubprovider = new GanacheSubprovider({ - default_balance_ether: '1000000000000000000000000', - total_accounts: 10, + accounts: [ + { + secretKey: '0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501200', + balance: '1000000000000000000000000' + }, + { + secretKey: '0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501201', + balance: '1000000000000000000000000' + }, + { + secretKey: '0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501202', + balance: '1000000000000000000000000' + }, + { + secretKey: '0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501203', + balance: '1000000000000000000000000' + }, + { + secretKey: '0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501204', + balance: '1000000000000000000000000' + }, + { + secretKey: '0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501205', + balance: '1000000000000000000000000' + }, + { + secretKey: '0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501206', + balance: '1000000000000000000000000' + }, + { + secretKey: '0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501207', + balance: '1000000000000000000000000' + }, + { + secretKey: '0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501208', + balance: '1000000000000000000000000' + }, + { + secretKey: '0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501209', + balance: '1000000000000000000000000' + }, + { + secretKey: '0x19fba401d77e4113b15095e9aa7117bcd25adcfac7f6111f8298894eef443600', + balance: '1000000000000000000000000' + } + ], port: 8545 }) provider.addProvider(ganacheSubprovider)