Skip to content

Commit

Permalink
Add native to erc20 mediator on top of AMB (#367)
Browse files Browse the repository at this point in the history
  • Loading branch information
patitonar committed May 16, 2020
1 parent cfcc717 commit 0a7b10e
Show file tree
Hide file tree
Showing 45 changed files with 5,508 additions and 256 deletions.
1 change: 1 addition & 0 deletions .solhint.json
Expand Up @@ -9,6 +9,7 @@
"multiple-sends": "off",
"bracket-align": "off",
"no-complex-fallback": "off",
"no-simple-event-func-name": "off",
"compiler-version": ["error", "0.4.24"]
}
}
10 changes: 6 additions & 4 deletions README.md
Expand Up @@ -3,7 +3,7 @@
[![Coverage Status](https://coveralls.io/repos/github/poanetwork/tokenbridge-contracts/badge.svg?branch=master)](https://coveralls.io/github/poanetwork/tokenbridge-contracts?branch=master)

# POA Bridge Smart Contracts
These contracts provide the core functionality for the POA bridge. They implement the logic to relay assests between two EVM-based blockchain networks. The contracts collect bridge validator's signatures to approve and facilitate relay operations.
These contracts provide the core functionality for the POA bridge. They implement the logic to relay assests between two EVM-based blockchain networks. The contracts collect bridge validator's signatures to approve and facilitate relay operations.

The POA bridge smart contracts are intended to work with [the bridge process implemented on NodeJS](https://github.com/poanetwork/token-bridge).
Please refer to the bridge process documentation to configure and deploy the bridge.
Expand Down Expand Up @@ -36,6 +36,7 @@ The POA bridge contracts consist of several components:
* The **Foreign Bridge** smart contract. This is deployed in the Ethereum Mainnet.
* Depending on the type of relay operations the following components are also used:
* in `NATIVE-TO-ERC` mode: the ERC20 token (in fact, the ERC677 extension is used) is deployed on the Foreign network;
* in `AMB-NATIVE-TO-ERC` mode: the ERC20 token (in fact, the ERC677 extension is used) is deployed on the Foreign network;
* in `ERC-TO-ERC` mode: the ERC20 token (in fact, the ERC677 extension is used) is deployed on the Home network;
* in `AMB-ERC-TO-ERC` mode: the ERC20 token (in fact, the ERC677 extension is used) is deployed on the Home network;
* in `ERC-TO-NATIVE` mode: The home network nodes must support consensus engine that allows using a smart contract for block reward calculation;
Expand All @@ -59,16 +60,17 @@ Responsibilities and roles of the bridge:
- **User** role:
- sends assets to Bridge contracts:
- in `NATIVE-TO-ERC` mode: send native coins to the Home Bridge to receive ERC20 tokens from the Foreign Bridge, send ERC20 tokens to the Foreign Bridge to unlock native coins from the Home Bridge;
- in `ERC-TO-ERC` mode: transfer ERC20 tokens to the Foreign Bridge to mint ERC20 tokens on the Home Network, transfer ERC20 tokens to the Home Bridge to unlock ERC20 tokens on Foreign networks;
- in `ERC-TO-ERC` mode: transfer ERC20 tokens to the Foreign Bridge to mint ERC20 tokens on the Home Network, transfer ERC20 tokens to the Home Bridge to unlock ERC20 tokens on Foreign networks;
- in `ERC-TO-NATIVE` mode: send ERC20 tokens to the Foreign Bridge to receive native coins from the Home Bridge, send native coins to the Home Bridge to unlock ERC20 tokens from the Foreign Bridge;
- in `ARBITRARY-MESSAGE` mode: Invoke Home/Foreign Bridge to send a message that will be executed on the other Network as an arbitrary contract method invocation;
- in `AMB-ERC-TO-ERC` mode: transfer ERC20 tokens to the Foreign Mediator which will interact with Foreign AMB Bridge to mint ERC20 tokens on the Home Network, transfer ERC20 tokens to the Home Mediator which will interact with Home AMB Bridge to unlock ERC20 tokens on Foreign network.
- in `AMB-NATIVE-TO-ERC` mode: send native coins to the Home Mediator which will interact with Home AMB Bridge to mint ERC20 tokens on the Foreign Network, transfer ERC20 tokens to the Foreign Mediator which will interact with Foreign AMB Bridge to unlock native coins from Home network.

## Usage

There are two ways to deploy contracts:
* install and use NodeJS
* use Docker to deploy
* use Docker to deploy

### Deployment with NodeJS

Expand Down Expand Up @@ -101,7 +103,7 @@ npm run flatten
The flattened contracts can be found in the `flats` directory.

### Deployment in the Docker environment
[Docker](https://www.docker.com/community-edition) and [Docker Compose](https://docs.docker.com/compose/install/) can be used to deploy contracts without NodeJS installed on the system.
[Docker](https://www.docker.com/community-edition) and [Docker Compose](https://docs.docker.com/compose/install/) can be used to deploy contracts without NodeJS installed on the system.
If you are on Linux, we recommend you [create a docker group and add your user to it](https://docs.docker.com/install/linux/linux-postinstall/), so that you can use the CLI without `sudo`.

#### Prepare the docker container
Expand Down
15 changes: 15 additions & 0 deletions REWARD_MANAGEMENT.md
Expand Up @@ -72,3 +72,18 @@ Fees are calculated and distributed on Home network. Validators will receive ERC
Fees are calculated and distributed on Home network. Validators will receive ERC20 tokens.
![ERC-ERC-HomeToForeign](https://user-images.githubusercontent.com/4614574/59939670-0cc32480-942f-11e9-9693-727125555c97.png)

## AMB-NATIVE-TO-ERC
Configuration:
```
HOME_REWARDABLE=ONE_DIRECTION
FOREIGN_REWARDABLE=ONE_DIRECTION
```
### Home to Foreign transfer
Fees are calculated and distributed on Foreign network. The reward accounts will receive ERC20 tokens.
![AMB-NATIVE-TO-ERC677-Home-Foreign](https://user-images.githubusercontent.com/4614574/74660965-dd0f1c80-5175-11ea-8d6c-51b8bd85f844.png)

### Foreign to Home transfer
Fees are calculated and distributed on Home network. The reward accounts will receive native tokens.
![AMB-NATIVE-TO-ERC677-Foreign-Home](https://user-images.githubusercontent.com/4614574/74660986-e6988480-5175-11ea-9216-7f008a6fdaf0.png)


5 changes: 5 additions & 0 deletions contracts/interfaces/IMediatorFeeManager.sol
@@ -0,0 +1,5 @@
pragma solidity 0.4.24;

interface IMediatorFeeManager {
function calculateFee(uint256) external view returns (uint256);
}
20 changes: 20 additions & 0 deletions contracts/libraries/Address.sol
@@ -0,0 +1,20 @@
pragma solidity 0.4.24;

import "../upgradeable_contracts/Sacrifice.sol";

/**
* @title Address
* @dev Helper methods for Address type.
*/
library Address {
/**
* @dev Try to send native tokens to the address. If it fails, it will force the transfer by creating a selfdestruct contract
* @param _receiver address that will receive the native tokens
* @param _value the amount of native tokens to send
*/
function safeSendValue(address _receiver, uint256 _value) internal {
if (!_receiver.send(_value)) {
(new Sacrifice).value(_value)(_receiver);
}
}
}
18 changes: 18 additions & 0 deletions contracts/mocks/FeeReceiverMock.sol
@@ -0,0 +1,18 @@
pragma solidity 0.4.24;

import "openzeppelin-solidity/contracts/token/ERC20/ERC20Basic.sol";

contract FeeReceiverMock {
address public mediator;
address public token;

constructor(address _mediator, address _token) public {
mediator = _mediator;
token = _token;
}

function onTokenTransfer(address, uint256 _value, bytes) external returns (bool) {
ERC20Basic(token).transfer(mediator, _value);
return true;
}
}
11 changes: 11 additions & 0 deletions contracts/mocks/RevertFallback.sol
@@ -1,5 +1,7 @@
pragma solidity 0.4.24;

import "../libraries/Address.sol";

contract RevertFallback {
function() public payable {
revert();
Expand All @@ -8,4 +10,13 @@ contract RevertFallback {
function receiveEth() public payable {
// solhint-disable-previous-line no-empty-blocks
}

function sendEth(address _receiver, uint256 _value) public {
// solhint-disable-next-line check-send-result
require(_receiver.send(_value));
}

function safeSendEth(address _receiver, uint256 _value) public {
Address.safeSendValue(_receiver, _value);
}
}
4 changes: 3 additions & 1 deletion contracts/upgradeability/ClassicEternalStorageProxy.sol
Expand Up @@ -7,7 +7,9 @@ import "./EternalStorageProxy.sol";
* @dev This proxy holds the storage of the token contract and delegates every call to the current implementation set.
* Besides, it allows to upgrade the token's behaviour towards further implementations, and provides basic
* authorization control functionalities.
* The only differency between this contract and EternalStorageProxy is a provided support of pre-Byzantium environment.
* The only difference between this contract and EternalStorageProxy is a provided support of pre-Byzantium environment.
* The fallback proxy will return values with a default size of 32. If a method will be returning a different size,
* the size needs to be stored for the signature so the method getSize() can get it.
*/
contract ClassicEternalStorageProxy is EternalStorageProxy {
// solhint-disable-next-line no-complex-fallback
Expand Down
184 changes: 184 additions & 0 deletions contracts/upgradeable_contracts/BaseMediatorFeeManager.sol
@@ -0,0 +1,184 @@
pragma solidity 0.4.24;

import "openzeppelin-solidity/contracts/ownership/Ownable.sol";
import "openzeppelin-solidity/contracts/math/SafeMath.sol";

/**
* @title BaseMediatorFeeManager
* @dev Base fee manager to handle fees for AMB mediators.
*/
contract BaseMediatorFeeManager is Ownable {
using SafeMath for uint256;

event FeeUpdated(uint256 fee);

// This is not a real fee value but a relative value used to calculate the fee percentage.
// 1 ether = 100% of the value.
uint256 internal constant MAX_FEE = 1 ether;
uint256 internal constant MAX_REWARD_ACCOUNTS = 50;

uint256 public fee;
address[] internal rewardAccounts;
address internal mediatorContract;

modifier validFee(uint256 _fee) {
require(_fee < MAX_FEE);
/* solcov ignore next */
_;
}

/**
* @dev Stores the initial parameters of the fee manager.
* @param _owner address of the owner of the fee manager contract.
* @param _fee the fee percentage amount.
* @param _rewardAccountList list of addresses that will receive the fee rewards.
*/
constructor(address _owner, uint256 _fee, address[] _rewardAccountList, address _mediatorContract) public {
require(_rewardAccountList.length > 0 && _rewardAccountList.length <= MAX_REWARD_ACCOUNTS);
_transferOwnership(_owner);
_setFee(_fee);
mediatorContract = _mediatorContract;

for (uint256 i = 0; i < _rewardAccountList.length; i++) {
require(isValidAccount(_rewardAccountList[i]));
}
rewardAccounts = _rewardAccountList;
}

/**
* @dev Calculates the fee amount to be subtracted from the value.
* @param _value the base value from which fees are calculated
*/
function calculateFee(uint256 _value) external view returns (uint256) {
return _value.mul(fee).div(MAX_FEE);
}

/**
* @dev Stores the fee percentage amount for the mediator operations.
* @param _fee the fee percentage
*/
function _setFee(uint256 _fee) internal validFee(_fee) {
fee = _fee;
emit FeeUpdated(_fee);
}

/**
* @dev Sets the fee percentage amount for the mediator operations. Only the owner can call this method.
* @param _fee the fee percentage
*/
function setFee(uint256 _fee) external onlyOwner {
_setFee(_fee);
}

function isValidAccount(address _account) internal returns (bool) {
return _account != address(0) && _account != mediatorContract;
}

/**
* @dev Adds a new account to the list of accounts to receive rewards for the operations.
* Only the owner can call this method.
* @param _account new reward account
*/
function addRewardAccount(address _account) external onlyOwner {
require(isValidAccount(_account));
require(!isRewardAccount(_account));
require(rewardAccounts.length.add(1) < MAX_REWARD_ACCOUNTS);
rewardAccounts.push(_account);
}

/**
* @dev Removes an account from the list of accounts to receive rewards for the operations.
* Only the owner can call this method.
* finds the element, swaps it with the last element, and then deletes it;
* @param _account to be removed
* return boolean whether the element was found and deleted
*/
function removeRewardAccount(address _account) external onlyOwner returns (bool) {
uint256 numOfAccounts = rewardAccountsCount();
for (uint256 i = 0; i < numOfAccounts; i++) {
if (rewardAccounts[i] == _account) {
rewardAccounts[i] = rewardAccounts[numOfAccounts - 1];
delete rewardAccounts[numOfAccounts - 1];
rewardAccounts.length--;
return true;
}
}
// If account is not found and removed, the transactions is reverted
revert();
}

/**
* @dev Tells the amount of accounts in the list of reward accounts.
* @return amount of accounts.
*/
function rewardAccountsCount() public view returns (uint256) {
return rewardAccounts.length;
}

/**
* @dev Tells if the account is part of the list of reward accounts.
* @param _account to check if is part of the list.
* @return true if the account is in the list
*/
function isRewardAccount(address _account) internal view returns (bool) {
for (uint256 i = 0; i < rewardAccountsCount(); i++) {
if (rewardAccounts[i] == _account) {
return true;
}
}
return false;
}

/**
* @dev Tells the list of accounts that receives rewards for the operations.
* @return the list of reward accounts
*/
function rewardAccountsList() public view returns (address[]) {
return rewardAccounts;
}

/**
* @dev ERC677 transfer callback function, received fee is distributed.
* @param _value amount of transferred tokens
*/
function onTokenTransfer(address, uint256 _value, bytes) external returns (bool) {
distributeFee(_value);
return true;
}

/**
* @dev Distributes the provided amount of fees proportionally to the list of reward accounts.
* In case the fees cannot be equally distributed, the remaining difference will be distributed to an account
* in a semi-random way.
* @param _fee total amount to be distributed to the list of reward accounts.
*/
function distributeFee(uint256 _fee) internal {
uint256 numOfAccounts = rewardAccountsCount();
uint256 feePerAccount = _fee.div(numOfAccounts);
uint256 randomAccountIndex;
uint256 diff = _fee.sub(feePerAccount.mul(numOfAccounts));
if (diff > 0) {
randomAccountIndex = random(numOfAccounts);
}

for (uint256 i = 0; i < numOfAccounts; i++) {
uint256 feeToDistribute = feePerAccount;
if (diff > 0 && randomAccountIndex == i) {
feeToDistribute = feeToDistribute.add(diff);
}
onFeeDistribution(rewardAccounts[i], feeToDistribute);
}
}

/**
* @dev Calculates a random number based on the block number.
* @param _count the max value for the random number.
* @return a number between 0 and _count.
*/
function random(uint256 _count) internal view returns (uint256) {
return uint256(blockhash(block.number.sub(1))) % _count;
}

/* solcov ignore next */
function onFeeDistribution(address _rewardAddress, uint256 _fee) internal;
}

0 comments on commit 0a7b10e

Please sign in to comment.