diff --git a/contracts/interfaces/IChai.sol b/contracts/interfaces/IChai.sol new file mode 100644 index 000000000..e982a4a9b --- /dev/null +++ b/contracts/interfaces/IChai.sol @@ -0,0 +1,15 @@ +pragma solidity 0.4.24; + +import "openzeppelin-solidity/contracts/token/ERC20/ERC20.sol"; +import "../interfaces/IPot.sol"; + +interface IChai { + function pot() external view returns (IPot); + function daiToken() external view returns (ERC20); + function balanceOf(address) external view returns (uint256); + function dai(address) external view returns (uint256); + function join(address, uint256) external; + function draw(address, uint256) external; + function exit(address, uint256) external; + function transfer(address, uint256) external; +} diff --git a/contracts/interfaces/IPot.sol b/contracts/interfaces/IPot.sol new file mode 100644 index 000000000..cf44bfbe1 --- /dev/null +++ b/contracts/interfaces/IPot.sol @@ -0,0 +1,7 @@ +pragma solidity 0.4.24; + +interface IPot { + function chi() external view returns (uint256); + function rho() external view returns (uint256); + function drip() external returns (uint256); +} diff --git a/contracts/mocks/BridgeValidatorsDeterministic.sol b/contracts/mocks/BridgeValidatorsDeterministic.sol new file mode 100644 index 000000000..a016582e5 --- /dev/null +++ b/contracts/mocks/BridgeValidatorsDeterministic.sol @@ -0,0 +1,13 @@ +pragma solidity 0.4.24; + +import "../upgradeable_contracts/BridgeValidators.sol"; + +contract BridgeValidatorsDeterministic is BridgeValidators { + function isValidatorDuty(address _validator) external view returns (bool) { + address next = getNextValidator(F_ADDR); + require(next != address(0)); + + // first validator is always on duty, others are always not + return _validator == next; + } +} diff --git a/contracts/mocks/ChaiMock.sol b/contracts/mocks/ChaiMock.sol new file mode 100644 index 000000000..ddd77339c --- /dev/null +++ b/contracts/mocks/ChaiMock.sol @@ -0,0 +1,198 @@ +// chai.sol -- a dai savings token +// Copyright (C) 2017, 2018, 2019 dbrock, rain, mrchico, lucasvo, livnev + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* solhint-disable */ +pragma solidity 0.4.24; + +contract VatLike { + function hope(address) external; +} + +contract PotLike { + function chi() external returns (uint256); + function rho() external returns (uint256); + function drip() external returns (uint256); + function join(uint256) external; + function exit(uint256) external; +} + +contract JoinLike { + function join(address, uint256) external; + function exit(address, uint256) external; +} + +contract GemLike { + function transferFrom(address, address, uint256) external returns (bool); + function approve(address, uint256) external returns (bool); +} + +contract ChaiMock { + // --- Data --- + VatLike public vat = VatLike(0x35D1b3F3D7966A1DFe207aa4514C12a259A0492B); + PotLike public pot = PotLike(0x197E90f9FAD81970bA7976f33CbD77088E5D7cf7); + JoinLike public daiJoin = JoinLike(0x9759A6Ac90977b93B58547b4A71c78317f391A28); + GemLike public daiToken = GemLike(0x6B175474E89094C44Da98b954EedeAC495271d0F); + + // --- ERC20 Data --- + string public constant name = "Chai"; + string public constant symbol = "CHAI"; + string public constant version = "1"; + uint8 public constant decimals = 18; + uint256 public totalSupply; + + mapping(address => uint256) public balanceOf; + mapping(address => mapping(address => uint256)) public allowance; + mapping(address => uint256) public nonces; + + event Approval(address indexed src, address indexed guy, uint256 wad); + event Transfer(address indexed src, address indexed dst, uint256 wad); + + // --- Math --- + uint256 constant RAY = 10**27; + function add(uint256 x, uint256 y) internal pure returns (uint256 z) { + require((z = x + y) >= x); + } + function sub(uint256 x, uint256 y) internal pure returns (uint256 z) { + require((z = x - y) <= x); + } + function mul(uint256 x, uint256 y) internal pure returns (uint256 z) { + require(y == 0 || (z = x * y) / y == x); + } + function rmul(uint256 x, uint256 y) internal pure returns (uint256 z) { + // always rounds down + z = mul(x, y) / RAY; + } + function rdiv(uint256 x, uint256 y) internal pure returns (uint256 z) { + // always rounds down + z = mul(x, RAY) / y; + } + function rdivup(uint256 x, uint256 y) internal pure returns (uint256 z) { + // always rounds up + z = add(mul(x, RAY), sub(y, 1)) / y; + } + + // --- EIP712 niceties --- + bytes32 public constant DOMAIN_SEPARATOR = 0x0b50407de9fa158c2cba01a99633329490dfd22989a150c20e8c7b4c1fb0fcc3; + // keccak256("Permit(address holder,address spender,uint256 nonce,uint256 expiry,bool allowed)")); + bytes32 public constant PERMIT_TYPEHASH = 0xea2aa0a1be11a07ed86d755c93467f4f82362b452371d1ba94d1715123511acb; + + constructor(address _vat, address _pot, address _daiJoin, address _dai) public { + vat = VatLike(_vat); + pot = PotLike(_pot); + daiJoin = JoinLike(_daiJoin); + daiToken = GemLike(_dai); + + vat.hope(address(daiJoin)); + vat.hope(address(pot)); + + daiToken.approve(address(daiJoin), uint256(-1)); + } + + // --- Token --- + function transfer(address dst, uint256 wad) external returns (bool) { + return transferFrom(msg.sender, dst, wad); + } + // like transferFrom but dai-denominated + function move(address src, address dst, uint256 wad) external returns (bool) { + uint256 chi = (now > pot.rho()) ? pot.drip() : pot.chi(); + // rounding up ensures dst gets at least wad dai + return transferFrom(src, dst, rdivup(wad, chi)); + } + function transferFrom(address src, address dst, uint256 wad) public returns (bool) { + require(balanceOf[src] >= wad, "chai/insufficient-balance"); + if (src != msg.sender && allowance[src][msg.sender] != uint256(-1)) { + require(allowance[src][msg.sender] >= wad, "chai/insufficient-allowance"); + allowance[src][msg.sender] = sub(allowance[src][msg.sender], wad); + } + balanceOf[src] = sub(balanceOf[src], wad); + balanceOf[dst] = add(balanceOf[dst], wad); + emit Transfer(src, dst, wad); + return true; + } + function approve(address usr, uint256 wad) external returns (bool) { + allowance[msg.sender][usr] = wad; + emit Approval(msg.sender, usr, wad); + return true; + } + + // --- Approve by signature --- + function permit( + address holder, + address spender, + uint256 nonce, + uint256 expiry, + bool allowed, + uint8 v, + bytes32 r, + bytes32 s + ) external { + bytes32 digest = keccak256( + abi.encodePacked( + "\x19\x01", + DOMAIN_SEPARATOR, + keccak256(abi.encode(PERMIT_TYPEHASH, holder, spender, nonce, expiry, allowed)) + ) + ); + require(holder != address(0), "chai/invalid holder"); + require(holder == ecrecover(digest, v, r, s), "chai/invalid-permit"); + require(expiry == 0 || now <= expiry, "chai/permit-expired"); + require(nonce == nonces[holder]++, "chai/invalid-nonce"); + + uint256 can = allowed ? uint256(-1) : 0; + allowance[holder][spender] = can; + emit Approval(holder, spender, can); + } + + function dai(address usr) external returns (uint256 wad) { + uint256 chi = (now > pot.rho()) ? pot.drip() : pot.chi(); + wad = rmul(chi, balanceOf[usr]); + } + // wad is denominated in dai + function join(address dst, uint256 wad) external { + uint256 chi = (now > pot.rho()) ? pot.drip() : pot.chi(); + uint256 pie = rdiv(wad, chi); + balanceOf[dst] = add(balanceOf[dst], pie); + totalSupply = add(totalSupply, pie); + + daiToken.transferFrom(msg.sender, address(this), wad); + daiJoin.join(address(this), wad); + pot.join(pie); + emit Transfer(address(0), dst, pie); + } + + // wad is denominated in (1/chi) * dai + function exit(address src, uint256 wad) public { + require(balanceOf[src] >= wad, "chai/insufficient-balance"); + if (src != msg.sender && allowance[src][msg.sender] != uint256(-1)) { + require(allowance[src][msg.sender] >= wad, "chai/insufficient-allowance"); + allowance[src][msg.sender] = sub(allowance[src][msg.sender], wad); + } + balanceOf[src] = sub(balanceOf[src], wad); + totalSupply = sub(totalSupply, wad); + + uint256 chi = (now > pot.rho()) ? pot.drip() : pot.chi(); + pot.exit(wad); + daiJoin.exit(msg.sender, rmul(chi, wad)); + emit Transfer(src, address(0), wad); + } + + // wad is denominated in dai + function draw(address src, uint256 wad) external { + uint256 chi = (now > pot.rho()) ? pot.drip() : pot.chi(); + // rounding up ensures usr gets at least wad dai + exit(src, rdivup(wad, chi)); + } +} diff --git a/contracts/mocks/ChaiMock2.sol b/contracts/mocks/ChaiMock2.sol new file mode 100644 index 000000000..51e13329f --- /dev/null +++ b/contracts/mocks/ChaiMock2.sol @@ -0,0 +1,25 @@ +pragma solidity 0.4.24; + +contract GemLike { + 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 + */ +contract ChaiMock2 { + GemLike public daiToken; + uint256 internal daiBalance; + + // wad is denominated in dai + function join(address, uint256 wad) external { + daiToken.transferFrom(msg.sender, address(this), wad); + daiBalance += wad; + } + + function dai(address) external view returns (uint256) { + return daiBalance; + } +} diff --git a/contracts/mocks/DaiJoinMock.sol b/contracts/mocks/DaiJoinMock.sol new file mode 100644 index 000000000..5b297ac4b --- /dev/null +++ b/contracts/mocks/DaiJoinMock.sol @@ -0,0 +1,74 @@ +/// join.sol -- Basic token adapters + +// Copyright (C) 2018 Rain +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* solhint-disable */ +pragma solidity 0.4.24; + +contract DSTokenLike { + function mint(address, uint256) external; + function burn(address, uint256) external; + function allowance(address, address) external returns (uint256); +} + +contract VatLike { + function slip(bytes32, address, int256) external; + function move(address, address, uint256) external; + function setDai(address, uint256) external; +} + +contract DaiJoinMock { + // --- Auth --- + mapping(address => uint256) public wards; + function rely(address usr) external auth { + wards[usr] = 1; + } + function deny(address usr) external auth { + wards[usr] = 0; + } + modifier auth { + require(wards[msg.sender] == 1, "DaiJoin/not-authorized"); + _; + } + + VatLike public vat; + DSTokenLike public dai; + uint256 public live; // Access Flag + + constructor(address vat_, address dai_) public { + wards[msg.sender] = 1; + live = 1; + vat = VatLike(vat_); + dai = DSTokenLike(dai_); + vat.setDai(address(this), mul(ONE, ONE)); + } + function cage() external auth { + live = 0; + } + uint256 constant ONE = 10**27; + function mul(uint256 x, uint256 y) internal pure returns (uint256 z) { + require(y == 0 || (z = x * y) / y == x); + } + function join(address usr, uint256 wad) external { + vat.move(address(this), usr, mul(ONE, wad)); + dai.burn(msg.sender, wad); + } + function exit(address usr, uint256 wad) external { + require(live == 1, "DaiJoin/not-live"); + vat.move(msg.sender, address(this), mul(ONE, wad)); + dai.mint(usr, wad); + } +} diff --git a/contracts/mocks/DaiMock.sol b/contracts/mocks/DaiMock.sol new file mode 100644 index 000000000..bbef4e90e --- /dev/null +++ b/contracts/mocks/DaiMock.sol @@ -0,0 +1,138 @@ +// Copyright (C) 2017, 2018, 2019 dbrock, rain, mrchico + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* solhint-disable */ +pragma solidity 0.4.24; + +contract DaiMock { + // --- Auth --- + mapping(address => uint256) public wards; + function rely(address guy) external auth { + wards[guy] = 1; + } + function deny(address guy) external auth { + wards[guy] = 0; + } + modifier auth { + require(wards[msg.sender] == 1, "Dai/not-authorized"); + _; + } + + // --- ERC20 Data --- + string public constant name = "Dai Stablecoin"; + string public constant symbol = "DAI"; + string public constant version = "1"; + uint8 public constant decimals = 18; + uint256 public totalSupply; + + mapping(address => uint256) public balanceOf; + mapping(address => mapping(address => uint256)) public allowance; + mapping(address => uint256) public nonces; + + event Approval(address indexed src, address indexed guy, uint256 wad); + event Transfer(address indexed src, address indexed dst, uint256 wad); + + // --- Math --- + function add(uint256 x, uint256 y) internal pure returns (uint256 z) { + require((z = x + y) >= x); + } + function sub(uint256 x, uint256 y) internal pure returns (uint256 z) { + require((z = x - y) <= x); + } + + // --- EIP712 niceties --- + bytes32 public DOMAIN_SEPARATOR; + // bytes32 public constant PERMIT_TYPEHASH = keccak256("Permit(address holder,address spender,uint256 nonce,uint256 expiry,bool allowed)"); + bytes32 public constant PERMIT_TYPEHASH = 0xea2aa0a1be11a07ed86d755c93467f4f82362b452371d1ba94d1715123511acb; + + constructor() public { + wards[msg.sender] = 1; + } + + // --- Token --- + function transfer(address dst, uint256 wad) external returns (bool) { + return transferFrom(msg.sender, dst, wad); + } + function transferFrom(address src, address dst, uint256 wad) public returns (bool) { + require(balanceOf[src] >= wad, "Dai/insufficient-balance"); + if (src != msg.sender && allowance[src][msg.sender] != uint256(-1)) { + require(allowance[src][msg.sender] >= wad, "Dai/insufficient-allowance"); + allowance[src][msg.sender] = sub(allowance[src][msg.sender], wad); + } + balanceOf[src] = sub(balanceOf[src], wad); + balanceOf[dst] = add(balanceOf[dst], wad); + emit Transfer(src, dst, wad); + return true; + } + function mint(address usr, uint256 wad) external auth { + balanceOf[usr] = add(balanceOf[usr], wad); + totalSupply = add(totalSupply, wad); + emit Transfer(address(0), usr, wad); + } + function burn(address usr, uint256 wad) external { + require(balanceOf[usr] >= wad, "Dai/insufficient-balance"); + if (usr != msg.sender && allowance[usr][msg.sender] != uint256(-1)) { + require(allowance[usr][msg.sender] >= wad, "Dai/insufficient-allowance"); + allowance[usr][msg.sender] = sub(allowance[usr][msg.sender], wad); + } + balanceOf[usr] = sub(balanceOf[usr], wad); + totalSupply = sub(totalSupply, wad); + emit Transfer(usr, address(0), wad); + } + function approve(address usr, uint256 wad) external returns (bool) { + allowance[msg.sender][usr] = wad; + emit Approval(msg.sender, usr, wad); + return true; + } + + // --- Alias --- + function push(address usr, uint256 wad) external { + transferFrom(msg.sender, usr, wad); + } + function pull(address usr, uint256 wad) external { + transferFrom(usr, msg.sender, wad); + } + function move(address src, address dst, uint256 wad) external { + transferFrom(src, dst, wad); + } + + // --- Approve by signature --- + function permit( + address holder, + address spender, + uint256 nonce, + uint256 expiry, + bool allowed, + uint8 v, + bytes32 r, + bytes32 s + ) external { + bytes32 digest = keccak256( + abi.encodePacked( + "\x19\x01", + DOMAIN_SEPARATOR, + keccak256(abi.encode(PERMIT_TYPEHASH, holder, spender, nonce, expiry, allowed)) + ) + ); + + require(holder != address(0), "Dai/invalid-address-0"); + require(holder == ecrecover(digest, v, r, s), "Dai/invalid-permit"); + require(expiry == 0 || now <= expiry, "Dai/permit-expired"); + require(nonce == nonces[holder]++, "Dai/invalid-nonce"); + uint256 wad = allowed ? uint256(-1) : 0; + allowance[holder][spender] = wad; + emit Approval(holder, spender, wad); + } +} diff --git a/contracts/mocks/ForeignBridgeErcToNativeMock.sol b/contracts/mocks/ForeignBridgeErcToNativeMock.sol index 4e6572808..d33388f1e 100644 --- a/contracts/mocks/ForeignBridgeErcToNativeMock.sol +++ b/contracts/mocks/ForeignBridgeErcToNativeMock.sol @@ -17,4 +17,14 @@ contract ForeignBridgeErcToNativeMock is ForeignBridgeErcToNative { // Address generated in unit test return ISaiTop(0x96bc48adACdB60E6536E55a6727919a397F14540); } + + bytes32 internal constant CHAI_TOKEN_MOCK = 0x5d6f4e61a116947624416975e8d26d4aff8f32e21ea6308dfa35cee98ed41fd8; // keccak256(abi.encodePacked("chaiTokenMock")) + + function setChaiToken(IChai _chaiToken) external { + addressStorage[CHAI_TOKEN_MOCK] = _chaiToken; + } + + function chaiToken() public view returns (IChai) { + return IChai(addressStorage[CHAI_TOKEN_MOCK]); + } } diff --git a/contracts/mocks/PotMock.sol b/contracts/mocks/PotMock.sol new file mode 100644 index 000000000..d83ada5a8 --- /dev/null +++ b/contracts/mocks/PotMock.sol @@ -0,0 +1,187 @@ +/// pot.sol -- Dai Savings Rate + +// Copyright (C) 2018 Rain +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* solhint-disable */ +pragma solidity 0.4.24; + +/* + "Savings Dai" is obtained when Dai is deposited into + this contract. Each "Savings Dai" accrues Dai interest + at the "Dai Savings Rate". + This contract does not implement a user tradeable token + and is intended to be used with adapters. + --- `save` your `dai` in the `pot` --- + - `dsr`: the Dai Savings Rate + - `pie`: user balance of Savings Dai + - `join`: start saving some dai + - `exit`: remove some dai + - `drip`: perform rate collection +*/ + +contract VatLike { + function move(address, address, uint256) external; + function suck(address, address, uint256) external; +} + +contract PotMock { + // --- Auth --- + mapping(address => uint256) public wards; + function rely(address guy) external auth { + wards[guy] = 1; + } + function deny(address guy) external auth { + wards[guy] = 0; + } + modifier auth { + require(wards[msg.sender] == 1, "Pot/not-authorized"); + _; + } + + // --- Data --- + mapping(address => uint256) public pie; // user Savings Dai + + uint256 public Pie; // total Savings Dai + uint256 public dsr; // the Dai Savings Rate + uint256 public chi; // the Rate Accumulator + + VatLike public vat; // CDP engine + address public vow; // debt engine + uint256 public rho; // time of last drip + + uint256 public live; // Access Flag + + // --- Init --- + constructor(address vat_) public { + wards[msg.sender] = 1; + vat = VatLike(vat_); + dsr = 1000000021979553151239153028; // 100% anually + chi = ONE; + rho = now; + live = 1; + } + + // --- Math --- + uint256 constant ONE = 10**27; + function rpow(uint256 x, uint256 n, uint256 base) internal pure returns (uint256 z) { + assembly { + switch x + case 0 { + switch n + case 0 { + z := base + } + default { + z := 0 + } + } + default { + switch mod(n, 2) + case 0 { + z := base + } + default { + z := x + } + let half := div(base, 2) // for rounding. + for { + n := div(n, 2) + } n { + n := div(n, 2) + } { + let xx := mul(x, x) + if iszero(eq(div(xx, x), x)) { + revert(0, 0) + } + let xxRound := add(xx, half) + if lt(xxRound, xx) { + revert(0, 0) + } + x := div(xxRound, base) + if mod(n, 2) { + let zx := mul(z, x) + if and(iszero(iszero(x)), iszero(eq(div(zx, x), z))) { + revert(0, 0) + } + let zxRound := add(zx, half) + if lt(zxRound, zx) { + revert(0, 0) + } + z := div(zxRound, base) + } + } + } + } + } + + function rmul(uint256 x, uint256 y) internal pure returns (uint256 z) { + z = mul(x, y) / ONE; + } + + function add(uint256 x, uint256 y) internal pure returns (uint256 z) { + require((z = x + y) >= x); + } + + function sub(uint256 x, uint256 y) internal pure returns (uint256 z) { + require((z = x - y) <= x); + } + + function mul(uint256 x, uint256 y) internal pure returns (uint256 z) { + require(y == 0 || (z = x * y) / y == x); + } + + // --- Administration --- + function file(bytes32 what, uint256 data) external auth { + require(live == 1, "Pot/not-live"); + require(now == rho, "Pot/rho-not-updated"); + if (what == "dsr") dsr = data; + else revert("Pot/file-unrecognized-param"); + } + + function file(bytes32 what, address addr) external auth { + if (what == "vow") vow = addr; + else revert("Pot/file-unrecognized-param"); + } + + function cage() external auth { + live = 0; + dsr = ONE; + } + + // --- Savings Rate Accumulation --- + function drip() external returns (uint256 tmp) { + require(now >= rho, "Pot/invalid-now"); + tmp = rmul(rpow(dsr, now - rho, ONE), chi); + uint256 chi_ = sub(tmp, chi); + chi = tmp; + rho = now; + vat.suck(address(vow), address(this), mul(Pie, chi_)); + } + + // --- Savings Dai Management --- + function join(uint256 wad) external { + require(now == rho, "Pot/rho-not-updated"); + pie[msg.sender] = add(pie[msg.sender], wad); + Pie = add(Pie, wad); + vat.move(msg.sender, address(this), mul(chi, wad)); + } + + function exit(uint256 wad) external { + pie[msg.sender] = sub(pie[msg.sender], wad); + Pie = sub(Pie, wad); + vat.move(address(this), msg.sender, mul(chi, wad)); + } +} diff --git a/contracts/mocks/VatMock.sol b/contracts/mocks/VatMock.sol new file mode 100644 index 000000000..0af15fbd7 --- /dev/null +++ b/contracts/mocks/VatMock.sol @@ -0,0 +1,283 @@ +/// vat.sol -- Dai CDP database + +// Copyright (C) 2018 Rain +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* solhint-disable */ +pragma solidity 0.4.24; + +contract VatMock { + // --- Auth --- + mapping(address => uint256) public wards; + function rely(address usr) external note auth { + require(live == 1, "Vat/not-live"); + wards[usr] = 1; + } + function deny(address usr) external note auth { + require(live == 1, "Vat/not-live"); + wards[usr] = 0; + } + modifier auth { + require(wards[msg.sender] == 1, "Vat/not-authorized"); + _; + } + + mapping(address => mapping(address => uint256)) public can; + function hope(address usr) external note { + can[msg.sender][usr] = 1; + } + function nope(address usr) external note { + can[msg.sender][usr] = 0; + } + function wish(address bit, address usr) internal view returns (bool) { + return either(bit == usr, can[bit][usr] == 1); + } + + // --- Data --- + struct Ilk { + uint256 Art; // Total Normalised Debt [wad] + uint256 rate; // Accumulated Rates [ray] + uint256 spot; // Price with Safety Margin [ray] + uint256 line; // Debt Ceiling [rad] + uint256 dust; // Urn Debt Floor [rad] + } + struct Urn { + uint256 ink; // Locked Collateral [wad] + uint256 art; // Normalised Debt [wad] + } + + mapping(bytes32 => Ilk) public ilks; + mapping(bytes32 => mapping(address => Urn)) public urns; + mapping(bytes32 => mapping(address => uint256)) public gem; // [wad] + mapping(address => uint256) public dai; // [rad] + mapping(address => uint256) public sin; // [rad] + + uint256 public debt; // Total Dai Issued [rad] + uint256 public vice; // Total Unbacked Dai [rad] + uint256 public Line; // Total Debt Ceiling [rad] + uint256 public live; // Access Flag + + // --- Logs --- + event LogNote(bytes4 indexed sig, bytes32 indexed arg1, bytes32 indexed arg2, bytes32 indexed arg3, bytes data) anonymous; + + modifier note { + _; + assembly { + // log an 'anonymous' event with a constant 6 words of calldata + // and four indexed topics: the selector and the first three args + let mark := msize // end of memory ensures zero + mstore(0x40, add(mark, 288)) // update free memory pointer + mstore(mark, 0x20) // bytes type data offset + mstore(add(mark, 0x20), 224) // bytes size (padded) + calldatacopy(add(mark, 0x40), 0, 224) // bytes payload + log4( + mark, + 288, // calldata + shl(224, shr(224, calldataload(0))), // msg.sig + calldataload(4), // arg1 + calldataload(36), // arg2 + calldataload(68) // arg3 + ) + } + } + + // --- Init --- + constructor() public { + wards[msg.sender] = 1; + live = 1; + } + + function setDai(address a, uint256 v) external { + dai[a] = v; + } + + // --- Math --- + function add(uint256 x, int256 y) internal pure returns (uint256 z) { + z = x + uint256(y); + require(y >= 0 || z <= x); + require(y <= 0 || z >= x); + } + function sub(uint256 x, int256 y) internal pure returns (uint256 z) { + z = x - uint256(y); + require(y <= 0 || z <= x); + require(y >= 0 || z >= x); + } + function mul(uint256 x, int256 y) internal pure returns (int256 z) { + z = int256(x) * y; + require(int256(x) >= 0); + require(y == 0 || z / y == int256(x)); + } + function add(uint256 x, uint256 y) internal pure returns (uint256 z) { + require((z = x + y) >= x); + } + function sub(uint256 x, uint256 y) internal pure returns (uint256 z) { + require((z = x - y) <= x); + } + function mul(uint256 x, uint256 y) internal pure returns (uint256 z) { + require(y == 0 || (z = x * y) / y == x); + } + + // --- Administration --- + function init(bytes32 ilk) external note auth { + require(ilks[ilk].rate == 0, "Vat/ilk-already-init"); + ilks[ilk].rate = 10**27; + } + function file(bytes32 what, uint256 data) external note auth { + require(live == 1, "Vat/not-live"); + if (what == "Line") Line = data; + else revert("Vat/file-unrecognized-param"); + } + function file(bytes32 ilk, bytes32 what, uint256 data) external note auth { + require(live == 1, "Vat/not-live"); + if (what == "spot") ilks[ilk].spot = data; + else if (what == "line") ilks[ilk].line = data; + else if (what == "dust") ilks[ilk].dust = data; + else revert("Vat/file-unrecognized-param"); + } + function cage() external note auth { + live = 0; + } + + // --- Fungibility --- + function slip(bytes32 ilk, address usr, int256 wad) external note auth { + gem[ilk][usr] = add(gem[ilk][usr], wad); + } + function flux(bytes32 ilk, address src, address dst, uint256 wad) external note { + require(wish(src, msg.sender), "Vat/not-allowed"); + gem[ilk][src] = sub(gem[ilk][src], wad); + gem[ilk][dst] = add(gem[ilk][dst], wad); + } + function move(address src, address dst, uint256 rad) external note { + require(wish(src, msg.sender), "Vat/not-allowed"); + dai[src] = sub(dai[src], rad); + dai[dst] = add(dai[dst], rad); + } + + function either(bool x, bool y) internal pure returns (bool z) { + assembly { + z := or(x, y) + } + } + function both(bool x, bool y) internal pure returns (bool z) { + assembly { + z := and(x, y) + } + } + + // --- CDP Manipulation --- + function frob(bytes32 i, address u, address v, address w, int256 dink, int256 dart) external note { + // system is live + require(live == 1, "Vat/not-live"); + + Urn memory urn = urns[i][u]; + Ilk memory ilk = ilks[i]; + // ilk has been initialised + require(ilk.rate != 0, "Vat/ilk-not-init"); + + urn.ink = add(urn.ink, dink); + urn.art = add(urn.art, dart); + ilk.Art = add(ilk.Art, dart); + + int256 dtab = mul(ilk.rate, dart); + uint256 tab = mul(ilk.rate, urn.art); + debt = add(debt, dtab); + + // either debt has decreased, or debt ceilings are not exceeded + require(either(dart <= 0, both(mul(ilk.Art, ilk.rate) <= ilk.line, debt <= Line)), "Vat/ceiling-exceeded"); + // urn is either less risky than before, or it is safe + require(either(both(dart <= 0, dink >= 0), tab <= mul(urn.ink, ilk.spot)), "Vat/not-safe"); + + // urn is either more safe, or the owner consents + require(either(both(dart <= 0, dink >= 0), wish(u, msg.sender)), "Vat/not-allowed-u"); + // collateral src consents + require(either(dink <= 0, wish(v, msg.sender)), "Vat/not-allowed-v"); + // debt dst consents + require(either(dart >= 0, wish(w, msg.sender)), "Vat/not-allowed-w"); + + // urn has no debt, or a non-dusty amount + require(either(urn.art == 0, tab >= ilk.dust), "Vat/dust"); + + gem[i][v] = sub(gem[i][v], dink); + dai[w] = add(dai[w], dtab); + + urns[i][u] = urn; + ilks[i] = ilk; + } + // --- CDP Fungibility --- + function fork(bytes32 ilk, address src, address dst, int256 dink, int256 dart) external note { + Urn storage u = urns[ilk][src]; + Urn storage v = urns[ilk][dst]; + Ilk storage i = ilks[ilk]; + + u.ink = sub(u.ink, dink); + u.art = sub(u.art, dart); + v.ink = add(v.ink, dink); + v.art = add(v.art, dart); + + uint256 utab = mul(u.art, i.rate); + uint256 vtab = mul(v.art, i.rate); + + // both sides consent + require(both(wish(src, msg.sender), wish(dst, msg.sender)), "Vat/not-allowed"); + + // both sides safe + require(utab <= mul(u.ink, i.spot), "Vat/not-safe-src"); + require(vtab <= mul(v.ink, i.spot), "Vat/not-safe-dst"); + + // both sides non-dusty + require(either(utab >= i.dust, u.art == 0), "Vat/dust-src"); + require(either(vtab >= i.dust, v.art == 0), "Vat/dust-dst"); + } + // --- CDP Confiscation --- + function grab(bytes32 i, address u, address v, address w, int256 dink, int256 dart) external note auth { + Urn storage urn = urns[i][u]; + Ilk storage ilk = ilks[i]; + + urn.ink = add(urn.ink, dink); + urn.art = add(urn.art, dart); + ilk.Art = add(ilk.Art, dart); + + int256 dtab = mul(ilk.rate, dart); + + gem[i][v] = sub(gem[i][v], dink); + sin[w] = sub(sin[w], dtab); + vice = sub(vice, dtab); + } + + // --- Settlement --- + function heal(uint256 rad) external note { + address u = msg.sender; + sin[u] = sub(sin[u], rad); + dai[u] = sub(dai[u], rad); + vice = sub(vice, rad); + debt = sub(debt, rad); + } + function suck(address u, address v, uint256 rad) external note auth { + sin[u] = add(sin[u], rad); + dai[v] = add(dai[v], rad); + vice = add(vice, rad); + debt = add(debt, rad); + } + + // --- Rates --- + function fold(bytes32 i, address u, int256 rate) external note auth { + require(live == 1, "Vat/not-live"); + Ilk storage ilk = ilks[i]; + ilk.rate = add(ilk.rate, rate); + int256 rad = mul(ilk.Art, rate); + dai[u] = add(dai[u], rad); + debt = add(debt, rad); + } +} diff --git a/contracts/upgradeable_contracts/ChaiConnector.sol b/contracts/upgradeable_contracts/ChaiConnector.sol new file mode 100644 index 000000000..f58db1458 --- /dev/null +++ b/contracts/upgradeable_contracts/ChaiConnector.sol @@ -0,0 +1,276 @@ +pragma solidity 0.4.24; + +import "../interfaces/IChai.sol"; +import "../interfaces/ERC677Receiver.sol"; +import "./Ownable.sol"; +import "./ERC20Bridge.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 { + using SafeMath for uint256; + + 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")) + bytes32 internal constant LAST_TIME_INTEREST_PAYED = 0x120db89f168bb39d737b6a1d240da847e2ead5ecca5b2e4c5e94edbe39d614d9; // keccak256(abi.encodePacked("lastTimeInterestPayed")) + bytes32 internal constant INVESTED_AMOUNT = 0xb6afb3323c9d7dc0e9dab5d34c3a1d1ae7739d2224c048d4ee7675d3c759dd1b; // keccak256(abi.encodePacked("investedAmount")) + bytes32 internal constant MIN_DAI_TOKEN_BALANCE = 0xce70e1dac97909c26a87aa4ada3d490673a153b3a75b22ea3364c4c7df7c551f; // keccak256(abi.encodePacked("minDaiTokenBalance")) + bytes4 internal constant ON_TOKEN_TRANSFER = 0xa4c0ed36; // onTokenTransfer(address,uint256,bytes) + + uint256 internal constant ONE = 10**27; + + /** + * @dev Throws if chai token is not enabled + */ + modifier chaiTokenEnabled { + require(isChaiTokenEnabled()); + /* solcov ignore next */ + _; + } + + /** + * @dev Fixed point division + * @return Ceiled value of x / y + */ + function rdivup(uint256 x, uint256 y) internal pure returns (uint256) { + return x.mul(ONE).add(y.sub(1)) / y; + } + + /** + * @return true, if chai token is enabled + */ + function isChaiTokenEnabled() public view returns (bool) { + return boolStorage[CHAI_TOKEN_ENABLED]; + } + + /** + * @return Chai token contract address + */ + function chaiToken() public view returns (IChai) { + return IChai(0x06AF07097C9Eeb7fD685c692751D5C66dB49c215); + } + + /** + * @dev Initializes chai token + */ + function initializeChaiToken() external onlyOwner { + require(!isChaiTokenEnabled()); + require(address(chaiToken().daiToken()) == address(erc20token())); + boolStorage[CHAI_TOKEN_ENABLED] = true; + uintStorage[MIN_DAI_TOKEN_BALANCE] = 100 ether; + uintStorage[INTEREST_COLLECTION_PERIOD] = 1 weeks; + } + + /** + * @dev Sets minimum DAI limit, needed for converting DAI into CHAI + */ + function setMinDaiTokenBalance(uint256 _minBalance) external onlyOwner { + uintStorage[MIN_DAI_TOKEN_BALANCE] = _minBalance; + } + + /** + * @dev Evaluates edge DAI token balance, which has an impact on the invest amounts + * @return Value in DAI + */ + function minDaiTokenBalance() public view returns (uint256) { + return uintStorage[MIN_DAI_TOKEN_BALANCE]; + } + + /** + * @dev Withdraws all invested tokens, pays remaining interest, removes chai token from contract storage + */ + function removeChaiToken() external onlyOwner chaiTokenEnabled { + _convertChaiToDai(investedAmountInDai()); + _payInterest(); + delete boolStorage[CHAI_TOKEN_ENABLED]; + } + + /** + * @return Configured address of a receiver + */ + function interestReceiver() public view returns (ERC677Receiver) { + return ERC677Receiver(addressStorage[INTEREST_RECEIVER]); + } + + /** + * Updates interest receiver contract address + * @param receiver New receiver contract address + */ + function setInterestReceiver(address receiver) external onlyOwner { + addressStorage[INTEREST_RECEIVER] = receiver; + } + + /** + * @return Timestamp of last interest payment + */ + function lastInterestPayment() public view returns (uint256) { + return uintStorage[LAST_TIME_INTEREST_PAYED]; + } + + /** + * @return Configured minimum interest collection period + */ + function interestCollectionPeriod() public view returns (uint256) { + return uintStorage[INTEREST_COLLECTION_PERIOD]; + } + + /** + * @dev Configures minimum interest collection period + * @param period collection period + */ + function setInterestCollectionPeriod(uint256 period) external onlyOwner { + uintStorage[INTEREST_COLLECTION_PERIOD] = period; + } + + /** + * @dev Pays all available interest, in Dai tokens. + * Upgradeability owner can call this method without time restrictions, + * for others, the method can be called only once a specified period. + */ + function payInterest() external chaiTokenEnabled { + // solhint-disable not-rely-on-time + if ( + lastInterestPayment() + interestCollectionPeriod() < now || + IUpgradeabilityOwnerStorage(this).upgradeabilityOwner() == msg.sender + ) { + uintStorage[LAST_TIME_INTEREST_PAYED] = now; + _payInterest(); + } + // solhint-enable not-rely-on-time + } + + /** + * @dev Internal function for paying all available interest, in Dai tokens + */ + function _payInterest() internal { + require(address(interestReceiver()) != 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); + + erc20token().transfer(interestReceiver(), interestInDai); + + interestReceiver().call(abi.encodeWithSelector(ON_TOKEN_TRANSFER, address(this), interestInDai, "")); + + require(dsrBalance() >= investedAmountInDai()); + } + + /** + * @dev Evaluates bridge balance for tokens, holded in DSR + * @return Balance in dai, truncated + */ + function dsrBalance() public view returns (uint256) { + return chaiToken().dai(address(this)); + } + + /** + * @dev Evaluates bridge balance in Chai tokens + * @return Balance in chai, exact + */ + function chaiBalance() public view returns (uint256) { + return chaiToken().balanceOf(address(this)); + } + + /** + * @dev Evaluates bridge balance in Dai tokens + * @return Balance in Dai + */ + function daiBalance() internal view returns (uint256) { + return erc20token().balanceOf(address(this)); + } + + /** + * @dev Evaluates exact current invested amount, id DAI + * @return Value in DAI + */ + function investedAmountInDai() public view returns (uint256) { + return uintStorage[INVESTED_AMOUNT]; + } + + /** + * @dev Updates current invested amount, id DAI + * @return Value in DAI + */ + function setInvestedAmointInDai(uint256 amount) internal { + uintStorage[INVESTED_AMOUNT] = amount; + } + + /** + * @dev Evaluates amount of chai tokens that is sufficent to cover 100% of the invested DAI + * @return Amount in chai, ceiled + */ + function investedAmountInChai() internal returns (uint256) { + IPot pot = chaiToken().pot(); + // solhint-disable-next-line not-rely-on-time + uint256 chi = (now > pot.rho()) ? pot.drip() : pot.chi(); + return rdivup(investedAmountInDai(), chi); + } + + /** + * @dev Checks if DAI balance is high enough to be partially converted to Chai + * Twice limit is used in order to decrease frequency of convertDaiToChai calls, + * In case of high bridge utilization in DAI => xDAI direction, + * convertDaiToChai() will be called as soon as DAI balance reaches 2 * limit, + * limit DAI will be left as a buffer for future operations. + * @return true if convertDaiToChai() call is needed to be performed by the oracle + */ + function isDaiNeedsToBeInvested() public view returns (bool) { + // chai token needs to be initialized, DAI balance should be at least twice greater than minDaiTokenBalance + return isChaiTokenEnabled() && daiBalance() > 2 * minDaiTokenBalance(); + } + + /** + * @dev Converts all DAI into Chai tokens, keeping minDaiTokenBalance() DAI as a buffer + */ + function convertDaiToChai() public chaiTokenEnabled { + // there is not need to consider overflow when performing a + operation, + // since both values are controlled by the bridge and can't take extremely high values + uint256 amount = daiBalance().sub(minDaiTokenBalance()); + setInvestedAmointInDai(investedAmountInDai() + amount); + erc20token().approve(chaiToken(), amount); + chaiToken().join(address(this), amount); + + // When evaluating the amount of DAI kept in Chai using dsrBalance(), there are some fixed point truncations. + // The dependency between invested amount of DAI - value and returned value of dsrBalance() - res is the following: + // res = floor(floor(value / chi) * chi)), where chi is the coefficient from MakerDAO Pot contract + // This can lead up to losses of ceil(chi) DAI in this balance evaluation. + // The constant is needed here for making sure that everything works fine, and this error is small enough + require(dsrBalance() + 10000 >= investedAmountInDai()); + } + + /** + * @dev Redeems DAI from Chai, the total redeemed amount will be at least equal to specified amount + * @param amount Amount of DAI to redeem + */ + function _convertChaiToDai(uint256 amount) internal { + uint256 invested = investedAmountInDai(); + uint256 initialDaiBalance = daiBalance(); + if (amount >= invested) { + // onExecuteMessage can call a convert operation with argument greater than the current invested amount, + // in this case bridge should withdraw all invested funds + chaiToken().draw(address(this), invested); + setInvestedAmointInDai(0); + + // Make sure all invested tokens were withdrawn + require(daiBalance() - initialDaiBalance >= invested); + } else if (amount > 0) { + chaiToken().draw(address(this), amount); + uint256 redeemed = daiBalance() - initialDaiBalance; + + // Make sure that at least requested amount was withdrawn + require(redeemed >= amount); + + setInvestedAmointInDai(redeemed < invested ? invested - redeemed : 0); + } + + require(dsrBalance() >= investedAmountInDai()); + } +} diff --git a/contracts/upgradeable_contracts/erc20_to_native/ForeignBridgeErcToNative.sol b/contracts/upgradeable_contracts/erc20_to_native/ForeignBridgeErcToNative.sol index 0341d2889..4d5c457a4 100644 --- a/contracts/upgradeable_contracts/erc20_to_native/ForeignBridgeErcToNative.sol +++ b/contracts/upgradeable_contracts/erc20_to_native/ForeignBridgeErcToNative.sol @@ -4,8 +4,9 @@ import "../BasicForeignBridge.sol"; import "../ERC20Bridge.sol"; import "../OtherSideBridgeStorage.sol"; import "../../interfaces/IScdMcdMigration.sol"; +import "../ChaiConnector.sol"; -contract ForeignBridgeErcToNative is BasicForeignBridge, ERC20Bridge, OtherSideBridgeStorage { +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")) @@ -64,6 +65,8 @@ contract ForeignBridgeErcToNative is BasicForeignBridge, ERC20Bridge, OtherSideB function claimTokens(address _token, address _to) public { require(_token != address(erc20token())); + // Chai token is not claimable if investing into Chai is enabled + require(_token != address(chaiToken()) || !isChaiTokenEnabled()); if (_token == address(halfDuplexErc20token())) { // SCD is not claimable if the bridge accepts deposits of this token // solhint-disable-next-line not-rely-on-time @@ -79,6 +82,17 @@ contract ForeignBridgeErcToNative is BasicForeignBridge, ERC20Bridge, OtherSideB ) internal returns (bool) { setTotalExecutedPerDay(getCurrentDay(), totalExecutedPerDay(getCurrentDay()).add(_amount)); uint256 amount = _amount.div(10**decimalShift()); + + uint256 currentBalance = tokenBalance(erc20token()); + + // Convert part of Chai tokens back to DAI, is DAI balance is insufficient. + // If Chai token is disabled, bridge will keep all funds directly in DAI token, + // so it will have enough funds to cover any xDai => Dai transfer, + // and currentBalance >= amount will always hold. + if (currentBalance < amount) { + _convertChaiToDai(amount.sub(currentBalance).add(minDaiTokenBalance())); + } + bool res = erc20token().transfer(_recipient, amount); if (tokenBalance(halfDuplexErc20token()) > 0) { @@ -92,11 +106,6 @@ contract ForeignBridgeErcToNative is BasicForeignBridge, ERC20Bridge, OtherSideB revert(); } - function _relayTokens(address _sender, address _receiver, uint256 _amount) internal { - require(_receiver != bridgeContractOnOtherSide()); - super._relayTokens(_sender, _receiver, _amount); - } - function migrateToMCD() external { bytes32 storageAddress = 0x3378953eb16363e06fd9ea9701d36ed7285d206d9de7df55b778462d74596a89; // keccak256(abi.encodePacked("migrationToMcdCompleted")) require(!boolStorage[storageAddress]); @@ -171,7 +180,15 @@ contract ForeignBridgeErcToNative is BasicForeignBridge, ERC20Bridge, OtherSideB emit TokensSwapped(hdToken, fdToken, curHDTokenBalance); } - function relayTokens(address _from, address _receiver, uint256 _amount, address _token) external { + function relayTokens(address _receiver, uint256 _amount) external { + _relayTokens(msg.sender, _receiver, _amount, erc20token()); + } + + function relayTokens(address _sender, address _receiver, uint256 _amount) external { + relayTokens(_sender, _receiver, _amount, erc20token()); + } + + function relayTokens(address _from, address _receiver, uint256 _amount, address _token) public { require(_from == msg.sender || _from == _receiver); _relayTokens(_from, _receiver, _amount, _token); } @@ -205,5 +222,8 @@ contract ForeignBridgeErcToNative is BasicForeignBridge, ERC20Bridge, OtherSideB if (tokenToOperate == hdToken) { swapTokens(); } + if (isDaiNeedsToBeInvested()) { + convertDaiToChai(); + } } } diff --git a/package-lock.json b/package-lock.json index 5f8290613..20d113c01 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4836,7 +4836,7 @@ } }, "ethereumjs-abi": { - "version": "git+https://github.com/ethereumjs/ethereumjs-abi.git#8431eab7b3384e65e8126a4602520b78031666fb", + "version": "git+https://github.com/ethereumjs/ethereumjs-abi.git#1cfbb13862f90f0b391d8a699544d5fe4dfb8c7b", "from": "git+https://github.com/ethereumjs/ethereumjs-abi.git", "requires": { "bn.js": "^4.11.8", @@ -17696,8 +17696,7 @@ "commander": { "version": "2.20.0", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.0.tgz", - "integrity": "sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==", - "optional": true + "integrity": "sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==" } } }, diff --git a/test/erc_to_native/foreign_bridge.test.js b/test/erc_to_native/foreign_bridge.test.js index ebc772b5c..429bf96c9 100644 --- a/test/erc_to_native/foreign_bridge.test.js +++ b/test/erc_to_native/foreign_bridge.test.js @@ -8,6 +8,12 @@ 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 VatMock = artifacts.require('VatMock') +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 { expect } = require('chai') const { ERROR_MSG, ZERO_ADDRESS, toBN } = require('../setup') @@ -19,10 +25,12 @@ const { expectEventInLogs, getEvents, createFullAccounts, + delay, packSignatures } = require('../helpers/helpers') const halfEther = ether('0.5') +const minDaiLimit = ether('100') const requireBlockConfirmations = 8 const gasPrice = web3.utils.toWei('1', 'gwei') const oneEther = ether('1') @@ -38,6 +46,18 @@ const MAX_SIGNATURES = MAX_VALIDATORS const MAX_GAS = 8000000 const decimalShiftZero = 0 +async function createChaiToken(token, bridge, owner) { + const vat = await VatMock.new({ from: owner }) + const daiJoin = await DaiJoinMock.new(vat.address, token.address, { from: owner }) + const pot = await PotMock.new(vat.address, { from: owner }) + await vat.rely(pot.address) + await vat.rely(daiJoin.address) + await token.rely(daiJoin.address) + const chaiToken = await ChaiMock.new(vat.address, pot.address, daiJoin.address, token.address, { from: owner }) + await bridge.setChaiToken(chaiToken.address) + return chaiToken +} + contract('ForeignBridge_ERC20_to_Native', async accounts => { let validatorContract let authorities @@ -358,6 +378,129 @@ contract('ForeignBridge_ERC20_to_Native', async accounts => { await foreignBridge.executeSignatures(message3, oneSignature3).should.be.rejectedWith(ERROR_MSG) }) }) + describe('#executeSignatures with chai', async () => { + const value = ether('0.25') + let foreignBridge + let chaiToken + beforeEach(async () => { + foreignBridge = await ForeignBridgeErcToNativeMock.new() + token = await DaiMock.new({ from: owner }) + chaiToken = await createChaiToken(token, foreignBridge, owner) + await foreignBridge.initialize( + validatorContract.address, + token.address, + requireBlockConfirmations, + gasPrice, + [dailyLimit, maxPerTx, minPerTx], + [homeDailyLimit, homeMaxPerTx], + owner, + decimalShiftZero, + otherSideBridge.address + ) + await token.mint(foreignBridge.address, value) + }) + + it('should executeSignatures with enabled chai token, enough dai', async () => { + await foreignBridge.initializeChaiToken() + expect(await chaiToken.balanceOf(foreignBridge.address)).to.be.bignumber.equal(ZERO) + expect(await token.balanceOf(foreignBridge.address)).to.be.bignumber.equal(value) + + const recipientAccount = accounts[3] + const balanceBefore = await token.balanceOf(recipientAccount) + + const transactionHash = '0x1045bfe274b88120a6b1e5d01b5ec00ab5d01098346e90e7c7a3c9b8f0181c80' + const message = createMessage(recipientAccount, value, transactionHash, foreignBridge.address) + const signature = await sign(authorities[0], message) + const vrs = signatureToVRS(signature) + const oneSignature = packSignatures([vrs]) + false.should.be.equal(await foreignBridge.relayedMessages(transactionHash)) + + 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) + const balanceAfter = await token.balanceOf(recipientAccount) + const balanceAfterBridge = await token.balanceOf(foreignBridge.address) + balanceAfter.should.be.bignumber.equal(balanceBefore.add(value)) + balanceAfterBridge.should.be.bignumber.equal(ZERO) + true.should.be.equal(await foreignBridge.relayedMessages(transactionHash)) + + expect(await chaiToken.balanceOf(foreignBridge.address)).to.be.bignumber.equal(ZERO) + }) + + it('should executeSignatures with enabled chai token, not enough dai, low dai limit', async () => { + await foreignBridge.initializeChaiToken({ from: owner }) + await token.mint(foreignBridge.address, ether('1'), { from: owner }) + // in case of low limit, bridge should withdraw tokens up to specified DAI limit + await foreignBridge.setMinDaiTokenBalance(ether('0.1'), { from: owner }) + await foreignBridge.convertDaiToChai() + expect(await chaiToken.balanceOf(foreignBridge.address)).to.be.bignumber.gt(ZERO) + expect(await token.balanceOf(foreignBridge.address)).to.be.bignumber.equal(ether('0.1')) + + const recipientAccount = accounts[3] + const balanceBefore = await token.balanceOf(recipientAccount) + + const transactionHash = '0x1045bfe274b88120a6b1e5d01b5ec00ab5d01098346e90e7c7a3c9b8f0181c80' + const message = createMessage(recipientAccount, value, transactionHash, foreignBridge.address) + const signature = await sign(authorities[0], message) + const vrs = signatureToVRS(signature) + const oneSignature = packSignatures([vrs]) + false.should.be.equal(await foreignBridge.relayedMessages(transactionHash)) + + // wait for a small interest in DSR + await delay(1500) + + 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) + const balanceAfter = await token.balanceOf(recipientAccount) + const balanceAfterBridge = await token.balanceOf(foreignBridge.address) + balanceAfter.should.be.bignumber.equal(balanceBefore.add(value)) + balanceAfterBridge.should.be.bignumber.equal(ether('0.1')) + true.should.be.equal(await foreignBridge.relayedMessages(transactionHash)) + }) + + it('should executeSignatures with enabled chai token, not enough dai, high dai limit', async () => { + await foreignBridge.initializeChaiToken({ from: owner }) + await token.mint(foreignBridge.address, ether('1'), { from: owner }) + await foreignBridge.setMinDaiTokenBalance(ether('0.1'), { from: owner }) + await foreignBridge.convertDaiToChai() + // in case of high limit, bridge should withdraw all invested tokens + await foreignBridge.setMinDaiTokenBalance(ether('5'), { from: owner }) + expect(await chaiToken.balanceOf(foreignBridge.address)).to.be.bignumber.gt(ZERO) + expect(await token.balanceOf(foreignBridge.address)).to.be.bignumber.gte(ether('0.1')) + expect(await token.balanceOf(foreignBridge.address)).to.be.bignumber.lte(ether('0.11')) + + const recipientAccount = accounts[3] + const balanceBefore = await token.balanceOf(recipientAccount) + + const transactionHash = '0x1045bfe274b88120a6b1e5d01b5ec00ab5d01098346e90e7c7a3c9b8f0181c80' + const message = createMessage(recipientAccount, value, transactionHash, foreignBridge.address) + const signature = await sign(authorities[0], message) + const vrs = signatureToVRS(signature) + const oneSignature = packSignatures([vrs]) + false.should.be.equal(await foreignBridge.relayedMessages(transactionHash)) + + // wait for a small interest in DSR + await delay(1500) + + 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) + const balanceAfter = await token.balanceOf(recipientAccount) + const balanceAfterBridge = await token.balanceOf(foreignBridge.address) + balanceAfter.should.be.bignumber.equal(balanceBefore.add(value)) + balanceAfterBridge.should.be.bignumber.gte(ether('1')) + true.should.be.equal(await foreignBridge.relayedMessages(transactionHash)) + // small remaining interest, collected between calls to convertDaiToChai() and executeSignatures() + expect(await chaiToken.balanceOf(foreignBridge.address)).to.be.bignumber.lt(ether('0.0001')) + }) + }) describe('#withdraw with 2 minimum signatures', async () => { let multisigValidatorContract let twoAuthorities @@ -910,6 +1053,86 @@ contract('ForeignBridge_ERC20_to_Native', async accounts => { }) }) }) + describe('#relayTokens with chai', async () => { + const user = accounts[7] + const recipient = accounts[8] + const value = ether('0.25') + let foreignBridge + let chaiToken + beforeEach(async () => { + foreignBridge = await ForeignBridgeErcToNativeMock.new() + token = await DaiMock.new({ from: owner }) + chaiToken = await createChaiToken(token, foreignBridge, owner) + await foreignBridge.initialize( + validatorContract.address, + token.address, + requireBlockConfirmations, + gasPrice, + [dailyLimit, maxPerTx, minPerTx], + [homeDailyLimit, homeMaxPerTx], + owner, + decimalShiftZero, + otherSideBridge.address + ) + await token.mint(user, ether('2')) + }) + + it('should allow to bridge tokens with chai token enabled', async () => { + await foreignBridge.initializeChaiToken() + expect(await chaiToken.balanceOf(foreignBridge.address)).to.be.bignumber.equal(ZERO) + + const currentDay = await foreignBridge.getCurrentDay() + expect(await foreignBridge.totalSpentPerDay(currentDay)).to.be.bignumber.equal(ZERO) + + await foreignBridge.methods['relayTokens(address,address,uint256)'](user, recipient, value, { + from: user + }).should.be.rejectedWith(ERROR_MSG) + + await token.approve(foreignBridge.address, value, { from: user }).should.be.fulfilled + + await foreignBridge.methods['relayTokens(address,address,uint256)'](user, ZERO_ADDRESS, value, { + from: user + }).should.be.rejectedWith(ERROR_MSG) + await foreignBridge.methods['relayTokens(address,address,uint256)'](user, foreignBridge.address, value, { + from: user + }).should.be.rejectedWith(ERROR_MSG) + await foreignBridge.methods['relayTokens(address,address,uint256)'](user, user, 0, { + from: user + }).should.be.rejectedWith(ERROR_MSG) + const { logs } = await foreignBridge.methods['relayTokens(address,address,uint256)'](user, user, value, { + from: user + }).should.be.fulfilled + + expect(await foreignBridge.totalSpentPerDay(currentDay)).to.be.bignumber.equal(value) + expectEventInLogs(logs, 'UserRequestForAffirmation', { + recipient: user, + value + }) + + expect(await chaiToken.balanceOf(foreignBridge.address)).to.be.bignumber.equal(ZERO) + expect(await token.balanceOf(foreignBridge.address)).to.be.bignumber.equal(value) + }) + + it('should allow to bridge tokens with chai token enabled, excess tokens', async () => { + await foreignBridge.initializeChaiToken() + await token.mint(foreignBridge.address, ether('201')) + expect(await chaiToken.balanceOf(foreignBridge.address)).to.be.bignumber.equal(ZERO) + + await token.approve(foreignBridge.address, value, { from: user }).should.be.fulfilled + + const { logs } = await foreignBridge.methods['relayTokens(address,address,uint256)'](user, user, value, { + from: user + }).should.be.fulfilled + + expectEventInLogs(logs, 'UserRequestForAffirmation', { + recipient: user, + value + }) + + expect(await chaiToken.balanceOf(foreignBridge.address)).to.be.bignumber.gt(ZERO) + expect(await token.balanceOf(foreignBridge.address)).to.be.bignumber.equal(minDaiLimit) + }) + }) describe('migrateToMCD', () => { let foreignBridge beforeEach(async () => { @@ -1231,6 +1454,63 @@ contract('ForeignBridge_ERC20_to_Native', async accounts => { expect(await foreignBridge.totalSpentPerDay(currentDay)).to.be.bignumber.equal(oneEther) }) }) + describe('relayTokens with chai token', async () => { + let foreignBridge + let dai + const value = ether('0.25') + const recipient = accounts[8] + beforeEach(async () => { + dai = await DaiMock.new({ from: owner }) + foreignBridge = await ForeignBridgeErcToNativeMock.new() + + await foreignBridge.initialize( + validatorContract.address, + dai.address, + requireBlockConfirmations, + gasPrice, + [dailyLimit, maxPerTx, minPerTx], + [homeDailyLimit, homeMaxPerTx], + owner, + decimalShiftZero, + otherSideBridge.address + ) + + await dai.mint(user, twoEthers) + }) + + it('should allow to bridge tokens specifying the token address with chai token enabled', async () => { + // Given + const chaiToken = await createChaiToken(dai, foreignBridge, owner) + await foreignBridge.initializeChaiToken() + expect(await chaiToken.balanceOf(foreignBridge.address)).to.be.bignumber.equal(ZERO) + + 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)) + expect(await chaiToken.balanceOf(foreignBridge.address)).to.be.bignumber.equal(ZERO) + }) + }) describe('onExecuteMessage', () => { it('should swapTokens in executeSignatures', async () => { const value = ether('0.25') @@ -1312,4 +1592,352 @@ contract('ForeignBridge_ERC20_to_Native', async accounts => { }) }) }) + + describe('chai token', async () => { + let token + let foreignBridge + let chaiToken + let interestRecipient + + beforeEach(async () => { + token = await DaiMock.new({ from: owner }) + foreignBridge = await ForeignBridgeErcToNativeMock.new() + await foreignBridge.initialize( + validatorContract.address, + token.address, + requireBlockConfirmations, + gasPrice, + [dailyLimit, maxPerTx, minPerTx], + [homeDailyLimit, homeMaxPerTx], + owner, + decimalShiftZero, + otherSideBridge.address + ) + chaiToken = await createChaiToken(token, foreignBridge, owner) + interestRecipient = await ERC677ReceiverMock.new() + }) + + describe('initializeChaiToken', () => { + it('should be initialized', async () => { + await foreignBridge.initializeChaiToken().should.be.fulfilled + }) + + it('should fail to initialize twice', async () => { + await foreignBridge.initializeChaiToken().should.be.fulfilled + await foreignBridge.initializeChaiToken().should.be.rejected + }) + + it('should fail if not an owner', async () => { + await foreignBridge.initializeChaiToken({ from: accounts[1] }).should.be.rejected + }) + }) + + describe('chaiTokenEnabled', () => { + it('should return false', async () => { + expect(await foreignBridge.isChaiTokenEnabled()).to.be.equal(false) + }) + + it('should return true', async () => { + await foreignBridge.initializeChaiToken().should.be.fulfilled + expect(await foreignBridge.isChaiTokenEnabled()).to.be.equal(true) + }) + }) + + describe('removeChaiToken', () => { + beforeEach(async () => { + await foreignBridge.initializeChaiToken().should.be.fulfilled + await foreignBridge.setInterestReceiver(interestRecipient.address).should.be.fulfilled + }) + + it('should be removed', async () => { + expect(await foreignBridge.isChaiTokenEnabled()).to.be.equal(true) + await foreignBridge.removeChaiToken().should.be.fulfilled + expect(await foreignBridge.isChaiTokenEnabled()).to.be.equal(false) + expect(await chaiToken.balanceOf(foreignBridge.address)).to.be.bignumber.equal(ZERO) + }) + + it('should be removed with tokens withdraw', async () => { + await token.mint(foreignBridge.address, oneEther, { from: owner }) + await foreignBridge.setMinDaiTokenBalance(ether('0.1'), { from: owner }) + await foreignBridge.convertDaiToChai() + expect(await foreignBridge.isChaiTokenEnabled()).to.be.equal(true) + + await delay(1500) + + await foreignBridge.removeChaiToken().should.be.fulfilled + expect(await foreignBridge.isChaiTokenEnabled()).to.be.equal(false) + expect(await chaiToken.balanceOf(foreignBridge.address)).to.be.bignumber.equal(ZERO) + expect(await token.balanceOf(interestRecipient.address)).to.be.bignumber.gt(ZERO) + expect(await token.balanceOf(foreignBridge.address)).to.be.bignumber.gte(halfEther) + }) + + it('should fail if not an owner', async () => { + await foreignBridge.removeChaiToken({ from: accounts[1] }).should.be.rejected + await foreignBridge.removeChaiToken({ from: owner }).should.be.fulfilled + }) + + it('should fail if chai token is not enabled', async () => { + await foreignBridge.removeChaiToken({ from: owner }).should.be.fulfilled + await foreignBridge.removeChaiToken({ from: owner }).should.be.rejected + }) + }) + + describe('min dai limit', () => { + beforeEach(async () => { + await foreignBridge.initializeChaiToken({ from: owner }) + }) + + it('should return minDaiTokenBalance', async () => { + expect(await foreignBridge.minDaiTokenBalance()).to.be.bignumber.equal(minDaiLimit) + }) + + it('should update minDaiTokenBalance', async () => { + await foreignBridge.setMinDaiTokenBalance(ether('101'), { from: owner }).should.be.fulfilled + expect(await foreignBridge.minDaiTokenBalance()).to.be.bignumber.equal(ether('101')) + }) + + it('should fail to update if not an owner', async () => { + await foreignBridge.setMinDaiTokenBalance(ether('101'), { from: accounts[1] }).should.be.rejected + }) + }) + + describe('interestReceiver', () => { + beforeEach(async () => { + await foreignBridge.initializeChaiToken({ from: owner }) + }) + + it('should return interestReceiver', async () => { + expect(await foreignBridge.interestReceiver()).to.be.equal(ZERO_ADDRESS) + }) + + it('should update interestReceiver', async () => { + await foreignBridge.setInterestReceiver(interestRecipient.address, { from: owner }).should.be.fulfilled + expect(await foreignBridge.interestReceiver()).to.be.equal(interestRecipient.address) + }) + + it('should fail to setInterestReceiver if not an owner', async () => { + await foreignBridge.setInterestReceiver(interestRecipient.address, { from: accounts[1] }).should.be.rejected + }) + }) + + describe('interestCollectionPeriod', () => { + beforeEach(async () => { + await foreignBridge.initializeChaiToken({ from: owner }) + }) + + it('should return interestCollectionPeriod', async () => { + expect(await foreignBridge.interestCollectionPeriod()).to.be.bignumber.gt(ZERO) + }) + + it('should update interestCollectionPeriod', async () => { + await foreignBridge.setInterestCollectionPeriod('100', { from: owner }).should.be.fulfilled + expect(await foreignBridge.interestCollectionPeriod()).to.be.bignumber.equal('100') + }) + + it('should fail to setInterestCollectionPeriod if not an owner', async () => { + await foreignBridge.setInterestCollectionPeriod('100', { from: accounts[1] }).should.be.rejected + }) + }) + + describe('isDaiNeedsToBeInvested', () => { + it('should return false on empty balance', async () => { + await foreignBridge.initializeChaiToken() + expect(await foreignBridge.isDaiNeedsToBeInvested()).to.be.equal(false) + }) + + it('should return false on insufficient balance', async () => { + await foreignBridge.initializeChaiToken() + await token.mint(foreignBridge.address, ether('101'), { from: owner }) + expect(await foreignBridge.isDaiNeedsToBeInvested()).to.be.equal(false) + }) + + it('should return true on sufficient balance', async () => { + await foreignBridge.initializeChaiToken() + await token.mint(foreignBridge.address, ether('201'), { from: owner }) + expect(await foreignBridge.isDaiNeedsToBeInvested()).to.be.equal(true) + }) + + it('should return false if chai token is not defined', async () => { + await token.mint(foreignBridge.address, ether('201'), { from: owner }) + expect(await foreignBridge.isDaiNeedsToBeInvested()).to.be.equal(false) + }) + }) + + describe('convertDaiToChai', () => { + it('should convert all dai except defined limit', async () => { + await foreignBridge.initializeChaiToken() + await token.mint(foreignBridge.address, ether('101')) + await foreignBridge.convertDaiToChai({ from: accounts[1] }).should.be.fulfilled + + expect(await token.balanceOf(foreignBridge.address)).to.be.bignumber.equal(ether('100')) + expect(await chaiToken.balanceOf(foreignBridge.address)).to.be.bignumber.gt(ZERO) + }) + + it('should not allow to convert if chai token is disabled', async () => { + await token.mint(foreignBridge.address, ether('101')) + await foreignBridge.convertDaiToChai({ from: owner }).should.be.rejected + }) + }) + + describe('payInterest', () => { + beforeEach(async () => { + await foreignBridge.initializeChaiToken() + await token.mint(foreignBridge.address, halfEther) + await foreignBridge.setMinDaiTokenBalance(ether('0.1'), { from: owner }) + await foreignBridge.convertDaiToChai() + + await delay(1500) + }) + + it('should pay full interest to regular account', async () => { + await foreignBridge.setInterestReceiver(accounts[2], { from: owner }) + expect(await token.balanceOf(foreignBridge.address)).to.be.bignumber.equal(ether('0.1')) + expect(await chaiToken.balanceOf(foreignBridge.address)).to.be.bignumber.gt(ZERO) + 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 + + expect(await foreignBridge.lastInterestPayment()).to.be.bignumber.gt(ZERO) + expect(await token.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')) + }) + + it('should pay full interest to contract', async () => { + await foreignBridge.setInterestReceiver(interestRecipient.address, { from: owner }) + expect(await token.balanceOf(foreignBridge.address)).to.be.bignumber.equal(ether('0.1')) + 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 + + 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) + expect(await chaiToken.balanceOf(foreignBridge.address)).to.be.bignumber.gt(ZERO) + expect(await foreignBridge.dsrBalance()).to.be.bignumber.gte(ether('0.4')) + }) + + it('should not allow not pay interest twice within short time period', async () => { + await foreignBridge.setInterestReceiver(interestRecipient.address, { from: owner }) + + await foreignBridge.payInterest({ from: accounts[1] }).should.be.fulfilled + + await delay(1500) + + await foreignBridge.payInterest({ from: accounts[1] }).should.be.rejected + }) + + it('should allow to pay interest after some time', async () => { + await foreignBridge.setInterestCollectionPeriod('5', { from: owner }) // 5 seconds + await foreignBridge.setInterestReceiver(interestRecipient.address, { from: owner }) + + await foreignBridge.payInterest({ from: accounts[1] }).should.be.fulfilled + + await delay(1500) + + await foreignBridge.payInterest({ from: accounts[1] }).should.be.rejected + + await delay(5000) + + await foreignBridge.payInterest({ from: accounts[1] }).should.be.fulfilled + }) + + it('should not allow to pay interest if chaiToken is disabled', async () => { + await foreignBridge.setInterestReceiver(interestRecipient.address, { from: owner }) + await foreignBridge.removeChaiToken({ from: owner }) + + await foreignBridge.payInterest().should.be.rejected + }) + + it('should not pay interest on empty address', async () => { + await foreignBridge.payInterest().should.be.rejected + }) + }) + + describe('payInterest for upgradeabilityOwner', () => { + it('should allow to pay interest without time restrictions', async () => { + let foreignBridgeProxy = await EternalStorageProxy.new({ from: accounts[2] }).should.be.fulfilled + await foreignBridgeProxy.upgradeTo('1', foreignBridge.address, { from: accounts[2] }).should.be.fulfilled + foreignBridgeProxy = await ForeignBridgeErcToNativeMock.at(foreignBridgeProxy.address) + foreignBridgeProxy.setChaiToken(chaiToken.address) + await foreignBridgeProxy.initialize( + validatorContract.address, + token.address, + requireBlockConfirmations, + gasPrice, + [dailyLimit, maxPerTx, minPerTx], + [homeDailyLimit, homeMaxPerTx], + owner, + decimalShiftZero, + otherSideBridge.address, + { from: accounts[2] } + ) + + await foreignBridgeProxy.initializeChaiToken() + await token.mint(foreignBridgeProxy.address, halfEther) + await foreignBridgeProxy.setMinDaiTokenBalance(ether('0.1'), { from: owner }) + await foreignBridgeProxy.convertDaiToChai() + await foreignBridgeProxy.setInterestReceiver(interestRecipient.address, { from: owner }) + + await delay(1500) + + await foreignBridgeProxy.payInterest({ from: accounts[2] }).should.be.fulfilled + + await delay(1500) + + await foreignBridgeProxy.payInterest({ from: accounts[2] }).should.be.fulfilled + + expect(await foreignBridgeProxy.lastInterestPayment()).to.be.bignumber.gt(ZERO) + expect(await token.balanceOf(interestRecipient.address)).to.be.bignumber.gt(ZERO) + expect(await chaiToken.balanceOf(foreignBridgeProxy.address)).to.be.bignumber.gt(ZERO) + expect(await foreignBridgeProxy.dsrBalance()).to.be.bignumber.gte(ether('0.4')) + }) + }) + + describe('claimTokens', async () => { + let foreignBridgeProxy + + beforeEach(async () => { + foreignBridgeProxy = await EternalStorageProxy.new({ from: accounts[2] }).should.be.fulfilled + await foreignBridgeProxy.upgradeTo('1', foreignBridge.address, { from: accounts[2] }).should.be.fulfilled + foreignBridgeProxy = await ForeignBridgeErcToNativeMock.at(foreignBridgeProxy.address) + foreignBridgeProxy.setChaiToken(chaiToken.address) + await foreignBridgeProxy.initialize( + validatorContract.address, + token.address, + requireBlockConfirmations, + gasPrice, + [dailyLimit, maxPerTx, minPerTx], + [homeDailyLimit, homeMaxPerTx], + owner, + decimalShiftZero, + otherSideBridge.address, + { from: accounts[2] } + ) + }) + + it('should not allow to claim Chai, if it is enabled', async () => { + await foreignBridgeProxy.initializeChaiToken({ from: owner }) + await token.mint(foreignBridgeProxy.address, halfEther) + await foreignBridgeProxy.setMinDaiTokenBalance(ether('0.1'), { from: owner }) + await foreignBridgeProxy.convertDaiToChai() + expect(await foreignBridgeProxy.isChaiTokenEnabled()).to.be.equal(true) + + await foreignBridgeProxy.claimTokens(chaiToken.address, accounts[2], { from: accounts[2] }).should.be.rejected + }) + + it('should allow to claim chai after it is disabled', async () => { + expect(await foreignBridgeProxy.isChaiTokenEnabled()).to.be.equal(false) + await token.mint(accounts[3], halfEther) + await token.approve(chaiToken.address, halfEther, { from: accounts[3] }) + await chaiToken.join(accounts[3], halfEther, { from: accounts[3] }).should.be.fulfilled + + await foreignBridgeProxy.claimTokens(chaiToken.address, accounts[2], { from: accounts[2] }).should.be.fulfilled + }) + }) + }) }) diff --git a/test/helpers/helpers.js b/test/helpers/helpers.js index 693e2d011..7e9e9aa2f 100644 --- a/test/helpers/helpers.js +++ b/test/helpers/helpers.js @@ -202,3 +202,9 @@ function addTxHashToAMBData(encodedData, transactionHash) { } module.exports.addTxHashToAMBData = addTxHashToAMBData + +async function delay(ms) { + return new Promise(res => setTimeout(res, ms)) +} + +module.exports.delay = delay