Skip to content

Commit c073e83

Browse files
Paying interest in chai to avoid the oracle misbehavior (#380)
1 parent 4ffda3a commit c073e83

File tree

18 files changed

+373
-31
lines changed

18 files changed

+373
-31
lines changed

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,17 @@ or with Docker:
162162
./deploy.sh token
163163
```
164164

165+
For testing bridge scripts in ERC20-to-NATIVE mode, you can deploy an interest receiver to the foreign network.
166+
This can be done by running the following command:
167+
```bash
168+
cd deploy
169+
node testenv-deploy.js interestReceiver
170+
```
171+
or with Docker:
172+
```bash
173+
./deploy.sh interestReceiver
174+
```
175+
165176
## Contributing
166177

167178
See the [CONTRIBUTING](CONTRIBUTING.md) document for contribution, testing and pull request protocol.

contracts/mocks/ChaiMock2.sol

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,51 @@
11
pragma solidity 0.4.24;
22

33
contract GemLike {
4+
function mint(address, uint256) external returns (bool);
45
function transferFrom(address, address, uint256) external returns (bool);
56
}
67

78
/**
89
* @title ChaiMock2
910
* @dev This contract is used for e2e tests only,
10-
* bridge contract requires non-empty chai stub with correct daiToken value and possibility to call join
11+
* this mock represents a simplified version of Chai, which does not require other MakerDAO contracts to be deployed in e2e tests
1112
*/
1213
contract ChaiMock2 {
14+
event Transfer(address indexed src, address indexed dst, uint256 wad);
15+
1316
GemLike public daiToken;
1417
uint256 internal daiBalance;
18+
address public pot;
1519

1620
// wad is denominated in dai
1721
function join(address, uint256 wad) external {
1822
daiToken.transferFrom(msg.sender, address(this), wad);
1923
daiBalance += wad;
2024
}
2125

26+
function transfer(address to, uint256 wad) external {
27+
require(daiBalance >= wad);
28+
daiBalance -= wad;
29+
emit Transfer(msg.sender, to, wad);
30+
}
31+
32+
function exit(address, uint256 wad) external {
33+
require(daiBalance >= wad);
34+
daiBalance -= wad;
35+
daiToken.mint(msg.sender, wad);
36+
}
37+
38+
function draw(address, uint256 wad) external {
39+
require(daiBalance >= wad);
40+
daiBalance -= wad;
41+
daiToken.mint(msg.sender, wad);
42+
}
43+
2244
function dai(address) external view returns (uint256) {
2345
return daiBalance;
2446
}
47+
48+
function balanceOf(address) external view returns (uint256) {
49+
return daiBalance;
50+
}
2551
}

contracts/mocks/ERC20Mock.sol

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,9 @@ contract ERC20Mock is DetailedERC20, BurnableToken, MintableToken {
88
constructor(string _name, string _symbol, uint8 _decimals) public DetailedERC20(_name, _symbol, _decimals) {
99
// solhint-disable-previous-line no-empty-blocks
1010
}
11+
12+
modifier hasMintPermission() {
13+
require(msg.sender == owner || msg.sender == 0x06AF07097C9Eeb7fD685c692751D5C66dB49c215);
14+
_;
15+
}
1116
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
pragma solidity 0.4.24;
2+
3+
import "../upgradeable_contracts/InterestReceiver.sol";
4+
5+
contract InterestReceiverMock is InterestReceiver {
6+
bytes32 internal constant CHAI_TOKEN_MOCK = 0x5d6f4e61a116947624416975e8d26d4aff8f32e21ea6308dfa35cee98ed41fd8; // keccak256(abi.encodePacked("chaiTokenMock"))
7+
bytes32 internal constant DAI_TOKEN_MOCK = 0xbadb505d38473a045eb3ce02f80bb0c4b30c429923cd667bca7f33083bad4e14; // keccak256(abi.encodePacked("daiTokenMock"))
8+
9+
function setChaiToken(IChai _chaiToken) external {
10+
addressStorage[CHAI_TOKEN_MOCK] = _chaiToken;
11+
addressStorage[DAI_TOKEN_MOCK] = _chaiToken.daiToken();
12+
}
13+
14+
function chaiToken() public view returns (IChai) {
15+
return IChai(addressStorage[CHAI_TOKEN_MOCK]);
16+
}
17+
18+
function daiToken() public view returns (ERC20) {
19+
return ERC20(addressStorage[DAI_TOKEN_MOCK]);
20+
}
21+
}

contracts/mocks/PotMock2.sol

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
pragma solidity 0.4.24;
2+
3+
contract GemLike {
4+
function transfer(address, uint256) external returns (bool);
5+
function transferFrom(address, address, uint256) external returns (bool);
6+
}
7+
8+
/**
9+
* @title PotMock2
10+
* @dev This contract is used for e2e tests only
11+
*/
12+
contract PotMock2 {
13+
// solhint-disable const-name-snakecase
14+
uint256 public constant dsr = 10**27; // the Dai Savings Rate
15+
uint256 public constant chi = 10**27; // the Rate Accumulator
16+
uint256 public constant rho = 10**27; // time of last drip
17+
// solhint-enable const-name-snakecase
18+
19+
function drip() external returns (uint256) {
20+
return chi;
21+
}
22+
}

contracts/upgradeable_contracts/ChaiConnector.sol

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,19 @@ import "../interfaces/IChai.sol";
44
import "../interfaces/ERC677Receiver.sol";
55
import "./Ownable.sol";
66
import "./ERC20Bridge.sol";
7+
import "./TokenSwapper.sol";
78
import "openzeppelin-solidity/contracts/math/SafeMath.sol";
89

910
/**
1011
* @title ChaiConnector
1112
* @dev This logic allows to use Chai token (https://github.com/dapphub/chai)
1213
*/
13-
contract ChaiConnector is Ownable, ERC20Bridge {
14+
contract ChaiConnector is Ownable, ERC20Bridge, TokenSwapper {
1415
using SafeMath for uint256;
1516

17+
// emitted when specified value of Chai tokens is transfered to interest receiver
18+
event PaidInterest(address to, uint256 value);
19+
1620
bytes32 internal constant CHAI_TOKEN_ENABLED = 0x2ae87563606f93f71ad2adf4d62661ccdfb63f3f508f94700934d5877fb92278; // keccak256(abi.encodePacked("chaiTokenEnabled"))
1721
bytes32 internal constant INTEREST_RECEIVER = 0xd88509eb1a8da5d5a2fc7b9bad1c72874c9818c788e81d0bc46b29bfaa83adf6; // keccak256(abi.encodePacked("interestReceiver"))
1822
bytes32 internal constant INTEREST_COLLECTION_PERIOD = 0x68a6a652d193e5d6439c4309583048050a11a4cfb263a220f4cd798c61c3ad6e; // keccak256(abi.encodePacked("interestCollectionPeriod"))
@@ -112,6 +116,14 @@ contract ChaiConnector is Ownable, ERC20Bridge {
112116
* @param receiver New receiver address
113117
*/
114118
function setInterestReceiver(address receiver) external onlyOwner {
119+
// the bridge account is not allowed to receive an interest by the following reason:
120+
// during the Chai to Dai convertion, the Dai is minted to the receiver account,
121+
// the Transfer(address(0), bridgeAddress, value) is emitted during this process,
122+
// something can go wrong in the oracle logic, so that it will process this event as a request to the bridge
123+
// Instead, the interest can be transfered to any other account, and then converted to Dai,
124+
// which won't be related to the oracle logic anymore
125+
require(receiver != address(this));
126+
115127
addressStorage[INTEREST_RECEIVER] = receiver;
116128
}
117129

@@ -156,23 +168,26 @@ contract ChaiConnector is Ownable, ERC20Bridge {
156168
* @dev Internal function for paying all available interest, in Dai tokens
157169
*/
158170
function _payInterest() internal {
159-
require(address(interestReceiver()) != address(0));
171+
address receiver = address(interestReceiver());
172+
require(receiver != address(0));
160173

161174
// since investedAmountInChai() returns a ceiled value,
162175
// the value of chaiBalance() - investedAmountInChai() will be floored,
163176
// leading to excess remaining chai balance
164-
uint256 balanceBefore = daiBalance();
165-
chaiToken().exit(address(this), chaiBalance().sub(investedAmountInChai()));
166-
uint256 interestInDai = daiBalance().sub(balanceBefore);
167177

168178
// solhint-disable-next-line not-rely-on-time
169179
uintStorage[LAST_TIME_INTEREST_PAID] = now;
170180

171-
erc20token().transfer(interestReceiver(), interestInDai);
181+
uint256 interest = chaiBalance().sub(investedAmountInChai());
182+
// interest is paid in Chai, paying interest directly in Dai can cause an unwanter Transfer event
183+
// see a comment in setInterestReceiver describing why we cannot pay interest to the bridge directly
184+
chaiToken().transfer(receiver, interest);
172185

173-
interestReceiver().call(abi.encodeWithSelector(ON_TOKEN_TRANSFER, address(this), interestInDai, ""));
186+
receiver.call(abi.encodeWithSelector(ON_TOKEN_TRANSFER, address(this), interest, ""));
174187

175188
require(dsrBalance() >= investedAmountInDai());
189+
190+
emit PaidInterest(receiver, interest);
176191
}
177192

178193
/**
@@ -260,6 +275,8 @@ contract ChaiConnector is Ownable, ERC20Bridge {
260275
// The 10000 constant is considered to be small enough when decimals = 18, however,
261276
// it is not recommended to use it for smaller values of decimals, since it won't be negligible anymore
262277
require(dsrBalance() + 10000 >= newInvestedAmountInDai);
278+
279+
emit TokensSwapped(erc20token(), chaiToken(), amount);
263280
}
264281

265282
/**
@@ -286,5 +303,7 @@ contract ChaiConnector is Ownable, ERC20Bridge {
286303
setInvestedAmountInDai(newInvested);
287304

288305
require(dsrBalance() >= newInvested);
306+
307+
emit TokensSwapped(chaiToken(), erc20token(), redeemed);
289308
}
290309
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
pragma solidity 0.4.24;
2+
3+
import "openzeppelin-solidity/contracts/token/ERC20/ERC20.sol";
4+
import "../interfaces/IChai.sol";
5+
import "../interfaces/ERC677Receiver.sol";
6+
import "./Initializable.sol";
7+
import "./Ownable.sol";
8+
import "./Claimable.sol";
9+
import "./TokenSwapper.sol";
10+
11+
/**
12+
* @title InterestReceiver
13+
* @dev Example contract for receiving Chai interest and immediatly converting it into Dai
14+
*/
15+
contract InterestReceiver is ERC677Receiver, Initializable, Ownable, Claimable, TokenSwapper {
16+
/**
17+
* @dev Initializes interest receiver, sets an owner of a contract
18+
* @param _owner address of owner account, only owner can withdraw Dai tokens from contract
19+
*/
20+
function initialize(address _owner) external onlyRelevantSender {
21+
require(!isInitialized());
22+
setOwner(_owner);
23+
setInitialize();
24+
}
25+
26+
/**
27+
* @return Chai token contract address
28+
*/
29+
function chaiToken() public view returns (IChai) {
30+
return IChai(0x06AF07097C9Eeb7fD685c692751D5C66dB49c215);
31+
}
32+
33+
/**
34+
* @return Dai token contract address
35+
*/
36+
function daiToken() public view returns (ERC20) {
37+
return ERC20(0x6B175474E89094C44Da98b954EedeAC495271d0F);
38+
}
39+
40+
/**
41+
* @dev ERC677 transfer callback function, received interest from Chai token is converted into Dai and sent to owner
42+
* @param _value amount of transferred tokens
43+
*/
44+
function onTokenTransfer(address, uint256 _value, bytes) external returns (bool) {
45+
require(isInitialized());
46+
47+
uint256 chaiBalance = chaiToken().balanceOf(address(this));
48+
49+
require(_value <= chaiBalance);
50+
51+
uint256 initialDaiBalance = daiToken().balanceOf(address(this));
52+
53+
chaiToken().exit(address(this), chaiBalance);
54+
55+
// Dai balance cannot decrease here, so SafeMath is not needed
56+
uint256 redeemed = daiToken().balanceOf(address(this)) - initialDaiBalance;
57+
58+
emit TokensSwapped(chaiToken(), daiToken(), redeemed);
59+
60+
// chi is always >= 10**27, so chai/dai rate is always >= 1
61+
require(redeemed >= _value);
62+
}
63+
64+
/**
65+
* @dev Withdraws DAI tokens from the receiver contract
66+
* @param _to address of tokens receiver
67+
*/
68+
function withdraw(address _to) external onlyOwner {
69+
require(isInitialized());
70+
71+
daiToken().transfer(_to, daiToken().balanceOf(address(this)));
72+
}
73+
74+
/**
75+
* @dev Claims tokens from receiver account
76+
* @param _token address of claimed token, address(0) for native
77+
* @param _to address of tokens receiver
78+
*/
79+
function claimTokens(address _token, address _to) external onlyOwner validAddress(_to) {
80+
require(_token != address(chaiToken()) && _token != address(daiToken()));
81+
claimValues(_token, _to);
82+
}
83+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
pragma solidity 0.4.24;
2+
3+
contract TokenSwapper {
4+
// emitted when two tokens is swapped (e. g. Sai to Dai, Chai to Dai)
5+
event TokensSwapped(address indexed from, address indexed to, uint256 value);
6+
}

contracts/upgradeable_contracts/erc20_to_native/ForeignBridgeErcToNative.sol

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,6 @@ import "../../interfaces/IScdMcdMigration.sol";
77
import "../ChaiConnector.sol";
88

99
contract ForeignBridgeErcToNative is BasicForeignBridge, ERC20Bridge, OtherSideBridgeStorage, ChaiConnector {
10-
event TokensSwapped(address indexed from, address indexed to, uint256 value);
11-
1210
bytes32 internal constant MIN_HDTOKEN_BALANCE = 0x48649cf195feb695632309f41e61252b09f537943654bde13eb7bb1bca06964e; // keccak256(abi.encodePacked("minHDTokenBalance"))
1311
bytes4 internal constant SWAP_TOKENS = 0x73d00224; // swapTokens()
1412

deploy.sh

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ if [ -f /.dockerenv ]; then
77
if [ "$1" == "token" ]; then
88
echo "Deployment of token for testing environment started"
99
node testenv-deploy.js token
10+
elif [ "$1" == "interestReceiver" ]; then
11+
echo "Deployment of interest receiver for testing environment started"
12+
node testenv-deploy.js interestReceiver
1013
else
1114
echo "Bridge contract deployment started"
1215
npm run deploy

0 commit comments

Comments
 (0)