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