Skip to content

Commit

Permalink
Paying interest in chai to avoid the oracle misbehavior (#380)
Browse files Browse the repository at this point in the history
  • Loading branch information
k1rill-fedoseev committed Mar 12, 2020
1 parent 4ffda3a commit c073e83
Show file tree
Hide file tree
Showing 18 changed files with 373 additions and 31 deletions.
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,17 @@ or with Docker:
./deploy.sh token
```

For testing bridge scripts in ERC20-to-NATIVE mode, you can deploy an interest receiver to the foreign network.
This can be done by running the following command:
```bash
cd deploy
node testenv-deploy.js interestReceiver
```
or with Docker:
```bash
./deploy.sh interestReceiver
```

## Contributing

See the [CONTRIBUTING](CONTRIBUTING.md) document for contribution, testing and pull request protocol.
Expand Down
28 changes: 27 additions & 1 deletion contracts/mocks/ChaiMock2.sol
Original file line number Diff line number Diff line change
@@ -1,25 +1,51 @@
pragma solidity 0.4.24;

contract GemLike {
function mint(address, uint256) external returns (bool);
function transferFrom(address, address, uint256) external returns (bool);
}

/**
* @title ChaiMock2
* @dev This contract is used for e2e tests only,
* bridge contract requires non-empty chai stub with correct daiToken value and possibility to call join
* this mock represents a simplified version of Chai, which does not require other MakerDAO contracts to be deployed in e2e tests
*/
contract ChaiMock2 {
event Transfer(address indexed src, address indexed dst, uint256 wad);

GemLike public daiToken;
uint256 internal daiBalance;
address public pot;

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

function transfer(address to, uint256 wad) external {
require(daiBalance >= wad);
daiBalance -= wad;
emit Transfer(msg.sender, to, wad);
}

function exit(address, uint256 wad) external {
require(daiBalance >= wad);
daiBalance -= wad;
daiToken.mint(msg.sender, wad);
}

function draw(address, uint256 wad) external {
require(daiBalance >= wad);
daiBalance -= wad;
daiToken.mint(msg.sender, wad);
}

function dai(address) external view returns (uint256) {
return daiBalance;
}

function balanceOf(address) external view returns (uint256) {
return daiBalance;
}
}
5 changes: 5 additions & 0 deletions contracts/mocks/ERC20Mock.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,9 @@ contract ERC20Mock is DetailedERC20, BurnableToken, MintableToken {
constructor(string _name, string _symbol, uint8 _decimals) public DetailedERC20(_name, _symbol, _decimals) {
// solhint-disable-previous-line no-empty-blocks
}

modifier hasMintPermission() {
require(msg.sender == owner || msg.sender == 0x06AF07097C9Eeb7fD685c692751D5C66dB49c215);
_;
}
}
21 changes: 21 additions & 0 deletions contracts/mocks/InterestReceiverMock.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
pragma solidity 0.4.24;

import "../upgradeable_contracts/InterestReceiver.sol";

contract InterestReceiverMock is InterestReceiver {
bytes32 internal constant CHAI_TOKEN_MOCK = 0x5d6f4e61a116947624416975e8d26d4aff8f32e21ea6308dfa35cee98ed41fd8; // keccak256(abi.encodePacked("chaiTokenMock"))
bytes32 internal constant DAI_TOKEN_MOCK = 0xbadb505d38473a045eb3ce02f80bb0c4b30c429923cd667bca7f33083bad4e14; // keccak256(abi.encodePacked("daiTokenMock"))

function setChaiToken(IChai _chaiToken) external {
addressStorage[CHAI_TOKEN_MOCK] = _chaiToken;
addressStorage[DAI_TOKEN_MOCK] = _chaiToken.daiToken();
}

function chaiToken() public view returns (IChai) {
return IChai(addressStorage[CHAI_TOKEN_MOCK]);
}

function daiToken() public view returns (ERC20) {
return ERC20(addressStorage[DAI_TOKEN_MOCK]);
}
}
22 changes: 22 additions & 0 deletions contracts/mocks/PotMock2.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
pragma solidity 0.4.24;

contract GemLike {
function transfer(address, uint256) external returns (bool);
function transferFrom(address, address, uint256) external returns (bool);
}

/**
* @title PotMock2
* @dev This contract is used for e2e tests only
*/
contract PotMock2 {
// solhint-disable const-name-snakecase
uint256 public constant dsr = 10**27; // the Dai Savings Rate
uint256 public constant chi = 10**27; // the Rate Accumulator
uint256 public constant rho = 10**27; // time of last drip
// solhint-enable const-name-snakecase

function drip() external returns (uint256) {
return chi;
}
}
33 changes: 26 additions & 7 deletions contracts/upgradeable_contracts/ChaiConnector.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,19 @@ import "../interfaces/IChai.sol";
import "../interfaces/ERC677Receiver.sol";
import "./Ownable.sol";
import "./ERC20Bridge.sol";
import "./TokenSwapper.sol";
import "openzeppelin-solidity/contracts/math/SafeMath.sol";

/**
* @title ChaiConnector
* @dev This logic allows to use Chai token (https://github.com/dapphub/chai)
*/
contract ChaiConnector is Ownable, ERC20Bridge {
contract ChaiConnector is Ownable, ERC20Bridge, TokenSwapper {
using SafeMath for uint256;

// emitted when specified value of Chai tokens is transfered to interest receiver
event PaidInterest(address to, uint256 value);

bytes32 internal constant CHAI_TOKEN_ENABLED = 0x2ae87563606f93f71ad2adf4d62661ccdfb63f3f508f94700934d5877fb92278; // keccak256(abi.encodePacked("chaiTokenEnabled"))
bytes32 internal constant INTEREST_RECEIVER = 0xd88509eb1a8da5d5a2fc7b9bad1c72874c9818c788e81d0bc46b29bfaa83adf6; // keccak256(abi.encodePacked("interestReceiver"))
bytes32 internal constant INTEREST_COLLECTION_PERIOD = 0x68a6a652d193e5d6439c4309583048050a11a4cfb263a220f4cd798c61c3ad6e; // keccak256(abi.encodePacked("interestCollectionPeriod"))
Expand Down Expand Up @@ -112,6 +116,14 @@ contract ChaiConnector is Ownable, ERC20Bridge {
* @param receiver New receiver address
*/
function setInterestReceiver(address receiver) external onlyOwner {
// the bridge account is not allowed to receive an interest by the following reason:
// during the Chai to Dai convertion, the Dai is minted to the receiver account,
// the Transfer(address(0), bridgeAddress, value) is emitted during this process,
// something can go wrong in the oracle logic, so that it will process this event as a request to the bridge
// Instead, the interest can be transfered to any other account, and then converted to Dai,
// which won't be related to the oracle logic anymore
require(receiver != address(this));

addressStorage[INTEREST_RECEIVER] = receiver;
}

Expand Down Expand Up @@ -156,23 +168,26 @@ contract ChaiConnector is Ownable, ERC20Bridge {
* @dev Internal function for paying all available interest, in Dai tokens
*/
function _payInterest() internal {
require(address(interestReceiver()) != address(0));
address receiver = address(interestReceiver());
require(receiver != address(0));

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

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

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

interestReceiver().call(abi.encodeWithSelector(ON_TOKEN_TRANSFER, address(this), interestInDai, ""));
receiver.call(abi.encodeWithSelector(ON_TOKEN_TRANSFER, address(this), interest, ""));

require(dsrBalance() >= investedAmountInDai());

emit PaidInterest(receiver, interest);
}

/**
Expand Down Expand Up @@ -260,6 +275,8 @@ contract ChaiConnector is Ownable, ERC20Bridge {
// The 10000 constant is considered to be small enough when decimals = 18, however,
// it is not recommended to use it for smaller values of decimals, since it won't be negligible anymore
require(dsrBalance() + 10000 >= newInvestedAmountInDai);

emit TokensSwapped(erc20token(), chaiToken(), amount);
}

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

require(dsrBalance() >= newInvested);

emit TokensSwapped(chaiToken(), erc20token(), redeemed);
}
}
83 changes: 83 additions & 0 deletions contracts/upgradeable_contracts/InterestReceiver.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
pragma solidity 0.4.24;

import "openzeppelin-solidity/contracts/token/ERC20/ERC20.sol";
import "../interfaces/IChai.sol";
import "../interfaces/ERC677Receiver.sol";
import "./Initializable.sol";
import "./Ownable.sol";
import "./Claimable.sol";
import "./TokenSwapper.sol";

/**
* @title InterestReceiver
* @dev Example contract for receiving Chai interest and immediatly converting it into Dai
*/
contract InterestReceiver is ERC677Receiver, Initializable, Ownable, Claimable, TokenSwapper {
/**
* @dev Initializes interest receiver, sets an owner of a contract
* @param _owner address of owner account, only owner can withdraw Dai tokens from contract
*/
function initialize(address _owner) external onlyRelevantSender {
require(!isInitialized());
setOwner(_owner);
setInitialize();
}

/**
* @return Chai token contract address
*/
function chaiToken() public view returns (IChai) {
return IChai(0x06AF07097C9Eeb7fD685c692751D5C66dB49c215);
}

/**
* @return Dai token contract address
*/
function daiToken() public view returns (ERC20) {
return ERC20(0x6B175474E89094C44Da98b954EedeAC495271d0F);
}

/**
* @dev ERC677 transfer callback function, received interest from Chai token is converted into Dai and sent to owner
* @param _value amount of transferred tokens
*/
function onTokenTransfer(address, uint256 _value, bytes) external returns (bool) {
require(isInitialized());

uint256 chaiBalance = chaiToken().balanceOf(address(this));

require(_value <= chaiBalance);

uint256 initialDaiBalance = daiToken().balanceOf(address(this));

chaiToken().exit(address(this), chaiBalance);

// Dai balance cannot decrease here, so SafeMath is not needed
uint256 redeemed = daiToken().balanceOf(address(this)) - initialDaiBalance;

emit TokensSwapped(chaiToken(), daiToken(), redeemed);

// chi is always >= 10**27, so chai/dai rate is always >= 1
require(redeemed >= _value);
}

/**
* @dev Withdraws DAI tokens from the receiver contract
* @param _to address of tokens receiver
*/
function withdraw(address _to) external onlyOwner {
require(isInitialized());

daiToken().transfer(_to, daiToken().balanceOf(address(this)));
}

/**
* @dev Claims tokens from receiver account
* @param _token address of claimed token, address(0) for native
* @param _to address of tokens receiver
*/
function claimTokens(address _token, address _to) external onlyOwner validAddress(_to) {
require(_token != address(chaiToken()) && _token != address(daiToken()));
claimValues(_token, _to);
}
}
6 changes: 6 additions & 0 deletions contracts/upgradeable_contracts/TokenSwapper.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
pragma solidity 0.4.24;

contract TokenSwapper {
// emitted when two tokens is swapped (e. g. Sai to Dai, Chai to Dai)
event TokensSwapped(address indexed from, address indexed to, uint256 value);
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@ import "../../interfaces/IScdMcdMigration.sol";
import "../ChaiConnector.sol";

contract ForeignBridgeErcToNative is BasicForeignBridge, ERC20Bridge, OtherSideBridgeStorage, ChaiConnector {
event TokensSwapped(address indexed from, address indexed to, uint256 value);

bytes32 internal constant MIN_HDTOKEN_BALANCE = 0x48649cf195feb695632309f41e61252b09f537943654bde13eb7bb1bca06964e; // keccak256(abi.encodePacked("minHDTokenBalance"))
bytes4 internal constant SWAP_TOKENS = 0x73d00224; // swapTokens()

Expand Down
3 changes: 3 additions & 0 deletions deploy.sh
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ if [ -f /.dockerenv ]; then
if [ "$1" == "token" ]; then
echo "Deployment of token for testing environment started"
node testenv-deploy.js token
elif [ "$1" == "interestReceiver" ]; then
echo "Deployment of interest receiver for testing environment started"
node testenv-deploy.js interestReceiver
else
echo "Bridge contract deployment started"
npm run deploy
Expand Down
6 changes: 6 additions & 0 deletions deploy/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -84,3 +84,9 @@ HOME_EXPLORER_API_KEY=
# Supported explorers: Blockscout, etherscan
FOREIGN_EXPLORER_URL=https://api-kovan.etherscan.io/api
FOREIGN_EXPLORER_API_KEY=

# for bridge erc_to_native mode
# specifies if deployment of interest receiver contract is needed
DEPLOY_INTEREST_RECEIVER=false
# if DEPLOY_INTEREST_RECEIVER set to true, address of interest receiver contract owner should be specified
FOREIGN_INTEREST_RECEIVER_OWNER=0x
3 changes: 2 additions & 1 deletion deploy/src/loadContracts.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ function getContracts(evmVersion) {
HomeAMB: require(`../../build/${buildPath}/HomeAMB.json`),
ForeignAMB: require(`../../build/${buildPath}/ForeignAMB`),
HomeAMBErc677ToErc677: require(`../../build/${buildPath}/HomeAMBErc677ToErc677.json`),
ForeignAMBErc677ToErc677: require(`../../build/${buildPath}/ForeignAMBErc677ToErc677.json`)
ForeignAMBErc677ToErc677: require(`../../build/${buildPath}/ForeignAMBErc677ToErc677.json`),
InterestReceiver: require(`../../build/${buildPath}/InterestReceiver.json`)
}
}

Expand Down
11 changes: 10 additions & 1 deletion deploy/src/loadEnv.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ const {
VALIDATORS,
VALIDATORS_REWARD_ACCOUNTS,
DEPLOY_REWARDABLE_TOKEN,
DEPLOY_INTEREST_RECEIVER,
HOME_FEE_MANAGER_TYPE,
HOME_EVM_VERSION,
FOREIGN_EVM_VERSION
Expand Down Expand Up @@ -242,7 +243,15 @@ if (BRIDGE_MODE === 'ERC_TO_NATIVE') {
BLOCK_REWARD_ADDRESS: addressValidator({
default: ZERO_ADDRESS
}),
FOREIGN_MIN_AMOUNT_PER_TX: bigNumValidator()
FOREIGN_MIN_AMOUNT_PER_TX: bigNumValidator(),
DEPLOY_INTEREST_RECEIVER: envalid.bool({default: false}),
}

if (DEPLOY_INTEREST_RECEIVER === 'true') {
validations = {
...validations,
FOREIGN_INTEREST_RECEIVER_OWNER: addressValidator()
}
}
}

Expand Down

0 comments on commit c073e83

Please sign in to comment.