Add support of multiple bridges in bridge token (#391)

This commit is contained in:
Kirill Fedoseev 2020-03-24 11:56:11 +03:00 committed by GitHub
parent 367b35b6f5
commit b5d30e09a0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 376 additions and 30 deletions

View File

@ -8,7 +8,7 @@ import "./interfaces/IBurnableMintableERC677Token.sol";
import "./upgradeable_contracts/Claimable.sol";
contract ERC677BridgeToken is IBurnableMintableERC677Token, DetailedERC20, BurnableToken, MintableToken, Claimable {
address public bridgeContract;
address internal bridgeContractAddr;
event ContractFallbackCallFailed(address from, address to, uint256 value);
@ -16,9 +16,13 @@ contract ERC677BridgeToken is IBurnableMintableERC677Token, DetailedERC20, Burna
// solhint-disable-previous-line no-empty-blocks
}
function bridgeContract() external view returns (address) {
return bridgeContractAddr;
}
function setBridgeContract(address _bridgeContract) external onlyOwner {
require(AddressUtils.isContract(_bridgeContract));
bridgeContract = _bridgeContract;
bridgeContractAddr = _bridgeContract;
}
modifier validRecipient(address _recipient) {
@ -59,11 +63,15 @@ contract ERC677BridgeToken is IBurnableMintableERC677Token, DetailedERC20, Burna
function callAfterTransfer(address _from, address _to, uint256 _value) internal {
if (AddressUtils.isContract(_to) && !contractFallback(_from, _to, _value, new bytes(0))) {
require(_to != bridgeContract);
require(!isBridge(_to));
emit ContractFallbackCallFailed(_from, _to, _value);
}
}
function isBridge(address _address) public view returns (bool) {
return _address == bridgeContractAddr;
}
function contractFallback(address _from, address _to, uint256 _value, bytes _data) private returns (bool) {
return _to.call(abi.encodeWithSignature("onTokenTransfer(address,uint256,bytes)", _from, _value, _data));
}

View File

@ -1,12 +1,15 @@
pragma solidity 0.4.24;
import "./ERC677BridgeToken.sol";
import "./ERC677MultiBridgeToken.sol";
contract ERC677BridgeTokenRewardable is ERC677BridgeToken {
contract ERC677BridgeTokenRewardable is ERC677MultiBridgeToken {
address public blockRewardContract;
address public stakingContract;
constructor(string _name, string _symbol, uint8 _decimals) public ERC677BridgeToken(_name, _symbol, _decimals) {
constructor(string _name, string _symbol, uint8 _decimals)
public
ERC677MultiBridgeToken(_name, _symbol, _decimals)
{
// solhint-disable-previous-line no-empty-blocks
}

View File

@ -0,0 +1,109 @@
pragma solidity 0.4.24;
import "./ERC677BridgeToken.sol";
/**
* @title ERC677MultiBridgeToken
* @dev This contract extends ERC677BridgeToken to support several bridge simulteniously
*/
contract ERC677MultiBridgeToken is ERC677BridgeToken {
address public constant F_ADDR = 0xFFfFfFffFFfffFFfFFfFFFFFffFFFffffFfFFFfF;
uint256 internal constant MAX_BRIDGES = 50;
mapping(address => address) public bridgePointers;
uint256 public bridgeCount;
event BridgeAdded(address indexed bridge);
event BridgeRemoved(address indexed bridge);
constructor(string _name, string _symbol, uint8 _decimals) public ERC677BridgeToken(_name, _symbol, _decimals) {
bridgePointers[F_ADDR] = F_ADDR; // empty bridge contracts list
}
/**
* @dev Removes unused function from ERC677BridgeToken contract
*/
function setBridgeContract(address) external {
revert();
}
/**
* @dev Removes unused getter from ERC677BridgeToken contract
*/
function bridgeContract() external view returns (address) {
revert();
}
/**
* @dev Adds one more bridge contract into the list
* @param _bridge bridge contract address
*/
function addBridge(address _bridge) external onlyOwner {
require(bridgeCount < MAX_BRIDGES);
require(AddressUtils.isContract(_bridge));
require(!isBridge(_bridge));
address firstBridge = bridgePointers[F_ADDR];
require(firstBridge != address(0));
bridgePointers[F_ADDR] = _bridge;
bridgePointers[_bridge] = firstBridge;
bridgeCount = bridgeCount.add(1);
emit BridgeAdded(_bridge);
}
/**
* @dev Removes one existing bridge contract from the list
* @param _bridge bridge contract address
*/
function removeBridge(address _bridge) external onlyOwner {
require(isBridge(_bridge));
address nextBridge = bridgePointers[_bridge];
address index = F_ADDR;
address next = bridgePointers[index];
require(next != address(0));
while (next != _bridge) {
index = next;
next = bridgePointers[index];
require(next != F_ADDR && next != address(0));
}
bridgePointers[index] = nextBridge;
delete bridgePointers[_bridge];
bridgeCount = bridgeCount.sub(1);
emit BridgeRemoved(_bridge);
}
/**
* @dev Returns all recorded bridge contract addresses
* @return address[] bridge contract addresses
*/
function bridgeList() external view returns (address[]) {
address[] memory list = new address[](bridgeCount);
uint256 counter = 0;
address nextBridge = bridgePointers[F_ADDR];
require(nextBridge != address(0));
while (nextBridge != F_ADDR) {
list[counter] = nextBridge;
nextBridge = bridgePointers[nextBridge];
counter++;
require(nextBridge != address(0));
}
return list;
}
/**
* @dev Checks if given address is included into bridge contracts list
* @param _address bridge contract address
* @return bool true, if given address is a known bridge contract
*/
function isBridge(address _address) public view returns (bool) {
return _address != F_ADDR && bridgePointers[_address] != address(0);
}
}

View File

@ -0,0 +1,211 @@
const ERC677MultiBridgeToken = artifacts.require('ERC677MultiBridgeToken.sol')
const StubContract = artifacts.require('RevertFallback.sol')
const { expect } = require('chai')
const { ERROR_MSG, ZERO_ADDRESS, F_ADDRESS, BN } = require('./setup')
const { expectEventInLogs } = require('./helpers/helpers')
const MAX_BRIDGES = 50
const ZERO = new BN(0)
contract('ERC677MultiBridgeToken', async accounts => {
let token
const owner = accounts[0]
const contracts = []
before(async () => {
for (let i = 0; i < MAX_BRIDGES + 1; i++) {
contracts.push((await StubContract.new()).address)
}
})
beforeEach(async () => {
token = await ERC677MultiBridgeToken.new('Test token', 'TEST', 18)
})
describe('constructor', async () => {
it('should initialize contract', async () => {
expect(await token.symbol()).to.be.equal('TEST')
expect(await token.decimals()).to.be.bignumber.equal('18')
expect(await token.name()).to.be.equal('Test token')
expect(await token.totalSupply()).to.be.bignumber.equal(ZERO)
expect(await token.bridgeCount()).to.be.bignumber.equal(ZERO)
})
})
describe('#addBridge', async () => {
it('should add one bridge', async () => {
expect(await token.isBridge(contracts[0])).to.be.equal(false)
expect(await token.bridgeCount()).to.be.bignumber.equal('0')
const { logs } = await token.addBridge(contracts[0], { from: owner }).should.be.fulfilled
expect(await token.isBridge(contracts[0])).to.be.equal(true)
expect(await token.bridgeCount()).to.be.bignumber.equal('1')
expectEventInLogs(logs, 'BridgeAdded', { bridge: contracts[0] })
})
it('should add two bridges', async () => {
expect(await token.isBridge(contracts[0])).to.be.equal(false)
expect(await token.isBridge(contracts[1])).to.be.equal(false)
expect(await token.bridgeCount()).to.be.bignumber.equal('0')
false.should.be.equal(await token.isBridge(contracts[0]))
await token.addBridge(contracts[0], { from: owner }).should.be.fulfilled
await token.addBridge(contracts[1], { from: owner }).should.be.fulfilled
expect(await token.isBridge(contracts[0])).to.be.equal(true)
expect(await token.isBridge(contracts[1])).to.be.equal(true)
expect(await token.bridgeCount()).to.be.bignumber.equal('2')
})
it('should add max allowed number of bridges', async () => {
for (let i = 0; i < MAX_BRIDGES; i++) {
await token.addBridge(contracts[i], { from: owner }).should.be.fulfilled
expect(await token.isBridge(contracts[i])).to.be.equal(true)
}
expect(await token.bridgeCount()).to.be.bignumber.equal(MAX_BRIDGES.toString())
await token.addBridge(contracts[MAX_BRIDGES], { from: owner }).should.be.rejected
})
it('should not allow to add already existing bridge', async () => {
await token.addBridge(contracts[0], { from: owner }).should.be.fulfilled
await token.addBridge(contracts[0], { from: owner }).should.be.rejectedWith(ERROR_MSG)
expect(await token.bridgeCount()).to.be.bignumber.equal('1')
})
it('should not allow to add 0xf as bridge address', async () => {
await token.addBridge(F_ADDRESS, { from: owner }).should.be.rejectedWith(ERROR_MSG)
})
it('should not allow to add 0x0 as bridge address', async () => {
await token.addBridge(ZERO_ADDRESS, { from: owner }).should.be.rejectedWith(ERROR_MSG)
})
it('should not allow to add if not an owner', async () => {
await token.addBridge(contracts[0], { from: accounts[2] }).should.be.rejectedWith(ERROR_MSG)
})
})
describe('#removeBridge', async () => {
it('should remove bridges one by one', async () => {
await token.addBridge(contracts[0], { from: owner }).should.be.fulfilled
await token.addBridge(contracts[1], { from: owner }).should.be.fulfilled
await token.addBridge(contracts[2], { from: owner }).should.be.fulfilled
await token.addBridge(contracts[3], { from: owner }).should.be.fulfilled
expect(await token.bridgeCount()).to.be.bignumber.equal('4')
expect(await token.isBridge(contracts[0])).to.be.equal(true)
expect(await token.isBridge(contracts[1])).to.be.equal(true)
expect(await token.isBridge(contracts[2])).to.be.equal(true)
expect(await token.isBridge(contracts[3])).to.be.equal(true)
const { logs } = await token.removeBridge(contracts[1], { from: owner }).should.be.fulfilled
expect(await token.bridgeCount()).to.be.bignumber.equal('3')
expect(await token.isBridge(contracts[0])).to.be.equal(true)
expect(await token.isBridge(contracts[1])).to.be.equal(false)
expect(await token.isBridge(contracts[2])).to.be.equal(true)
expect(await token.isBridge(contracts[3])).to.be.equal(true)
expectEventInLogs(logs, 'BridgeRemoved', { bridge: contracts[1] })
await token.removeBridge(contracts[3], { from: owner }).should.be.fulfilled
expect(await token.bridgeCount()).to.be.bignumber.equal('2')
expect(await token.isBridge(contracts[0])).to.be.equal(true)
expect(await token.isBridge(contracts[1])).to.be.equal(false)
expect(await token.isBridge(contracts[2])).to.be.equal(true)
expect(await token.isBridge(contracts[3])).to.be.equal(false)
await token.removeBridge(contracts[0], { from: owner }).should.be.fulfilled
expect(await token.bridgeCount()).to.be.bignumber.equal('1')
expect(await token.isBridge(contracts[0])).to.be.equal(false)
expect(await token.isBridge(contracts[1])).to.be.equal(false)
expect(await token.isBridge(contracts[2])).to.be.equal(true)
expect(await token.isBridge(contracts[3])).to.be.equal(false)
await token.removeBridge(contracts[2], { from: owner }).should.be.fulfilled
expect(await token.bridgeCount()).to.be.bignumber.equal('0')
expect(await token.isBridge(contracts[0])).to.be.equal(false)
expect(await token.isBridge(contracts[1])).to.be.equal(false)
expect(await token.isBridge(contracts[2])).to.be.equal(false)
expect(await token.isBridge(contracts[3])).to.be.equal(false)
})
it('should not allow to remove not existing bridge', async () => {
await token.addBridge(contracts[0], { from: owner }).should.be.fulfilled
await token.addBridge(contracts[1], { from: owner }).should.be.fulfilled
expect(await token.bridgeCount()).to.be.bignumber.equal('2')
expect(await token.isBridge(contracts[0])).to.be.equal(true)
expect(await token.isBridge(contracts[1])).to.be.equal(true)
await token.removeBridge(contracts[2], { from: owner }).should.be.rejectedWith(ERROR_MSG)
await token.removeBridge(contracts[0], { from: owner }).should.be.fulfilled
await token.removeBridge(contracts[0], { from: owner }).should.be.rejectedWith(ERROR_MSG)
await token.removeBridge(contracts[1], { from: owner }).should.be.fulfilled
await token.removeBridge(contracts[1], { from: owner }).should.be.rejectedWith(ERROR_MSG)
expect(await token.bridgeCount()).to.be.bignumber.equal('0')
expect(await token.isBridge(contracts[0])).to.be.equal(false)
expect(await token.isBridge(contracts[1])).to.be.equal(false)
})
it('should not allow to remove if not an owner', async () => {
await token.addBridge(contracts[0], { from: owner }).should.be.fulfilled
await token.addBridge(contracts[0], { from: accounts[2] }).should.be.rejectedWith(ERROR_MSG)
})
})
describe('#bridgeList', async () => {
it('should return empty bridge list', async () => {
expect(await token.bridgeList()).to.be.eql([])
})
it('should expand bridge list when adding bridges', async () => {
expect(await token.bridgeList()).to.be.eql([])
await token.addBridge(contracts[0], { from: owner }).should.be.fulfilled
expect(await token.bridgeList()).to.be.eql([contracts[0]])
await token.addBridge(contracts[1], { from: owner }).should.be.fulfilled
expect(await token.bridgeList()).to.be.eql([contracts[1], contracts[0]])
await token.addBridge(contracts[2], { from: owner }).should.be.fulfilled
expect(await token.bridgeList()).to.be.eql([contracts[2], contracts[1], contracts[0]])
})
it('should shrink bridge list when removing bridges', async () => {
await token.addBridge(contracts[0], { from: owner }).should.be.fulfilled
await token.addBridge(contracts[1], { from: owner }).should.be.fulfilled
await token.addBridge(contracts[2], { from: owner }).should.be.fulfilled
await token.addBridge(contracts[3], { from: owner }).should.be.fulfilled
expect(await token.bridgeList()).to.be.eql([contracts[3], contracts[2], contracts[1], contracts[0]])
await token.removeBridge(contracts[1], { from: owner }).should.be.fulfilled
expect(await token.bridgeList()).to.be.eql([contracts[3], contracts[2], contracts[0]])
await token.removeBridge(contracts[3], { from: owner }).should.be.fulfilled
expect(await token.bridgeList()).to.be.eql([contracts[2], contracts[0]])
await token.removeBridge(contracts[0], { from: owner }).should.be.fulfilled
expect(await token.bridgeList()).to.be.eql([contracts[2]])
await token.removeBridge(contracts[2], { from: owner }).should.be.fulfilled
expect(await token.bridgeList()).to.be.eql([])
})
})
describe('#setBridgeContract', async () => {
it('should always revert', async () => {
await token.setBridgeContract(contracts[0], { from: owner }).should.be.rejectedWith(ERROR_MSG)
})
})
describe('#bridgeContract', async () => {
it('should always revert', async () => {
await token.bridgeContract().should.be.rejectedWith(ERROR_MSG)
})
})
})

View File

@ -27,6 +27,21 @@ async function testERC677BridgeToken(accounts, rewardable) {
const owner = accounts[0]
const user = accounts[1]
const tokenContract = rewardable ? POA20RewardableMock : POA20
async function addBridge(token, bridge, options = { from: owner }) {
if (rewardable) {
return token.addBridge(bridge, options)
}
return token.setBridgeContract(bridge, options)
}
async function isBridge(token, bridge) {
if (rewardable) {
return token.isBridge(bridge)
}
return bridge === (await token.bridgeContract())
}
beforeEach(async () => {
token = await tokenContract.new('POA ERC20 Foundation', 'POA20', 18)
})
@ -46,32 +61,32 @@ async function testERC677BridgeToken(accounts, rewardable) {
describe('#bridgeContract', async () => {
it('can set bridge contract', async () => {
const homeErcToErcContract = await HomeErcToErcBridge.new()
;(await token.bridgeContract()).should.be.equal(ZERO_ADDRESS)
expect(await isBridge(token, homeErcToErcContract.address)).to.be.equal(false)
await token.setBridgeContract(homeErcToErcContract.address).should.be.fulfilled
;(await token.bridgeContract()).should.be.equal(homeErcToErcContract.address)
await addBridge(token, homeErcToErcContract.address).should.be.fulfilled
expect(await isBridge(token, homeErcToErcContract.address)).to.be.equal(true)
})
it('only owner can set bridge contract', async () => {
const homeErcToErcContract = await HomeErcToErcBridge.new()
;(await token.bridgeContract()).should.be.equal(ZERO_ADDRESS)
expect(await isBridge(token, homeErcToErcContract.address)).to.be.equal(false)
await token.setBridgeContract(homeErcToErcContract.address, { from: user }).should.be.rejectedWith(ERROR_MSG)
;(await token.bridgeContract()).should.be.equal(ZERO_ADDRESS)
await addBridge(token, homeErcToErcContract.address, { from: user }).should.be.rejectedWith(ERROR_MSG)
expect(await isBridge(token, homeErcToErcContract.address)).to.be.equal(false)
await token.setBridgeContract(homeErcToErcContract.address, { from: owner }).should.be.fulfilled
;(await token.bridgeContract()).should.be.equal(homeErcToErcContract.address)
await addBridge(token, homeErcToErcContract.address, { from: owner }).should.be.fulfilled
expect(await isBridge(token, homeErcToErcContract.address)).to.be.equal(true)
})
it('fail to set invalid bridge contract address', async () => {
const invalidContractAddress = '0xaaB52d66283F7A1D5978bcFcB55721ACB467384b'
;(await token.bridgeContract()).should.be.equal(ZERO_ADDRESS)
expect(await isBridge(token, invalidContractAddress)).to.be.equal(false)
await token.setBridgeContract(invalidContractAddress).should.be.rejectedWith(ERROR_MSG)
;(await token.bridgeContract()).should.be.equal(ZERO_ADDRESS)
await addBridge(token, invalidContractAddress).should.be.rejectedWith(ERROR_MSG)
expect(await isBridge(token, invalidContractAddress)).to.be.equal(false)
await token.setBridgeContract(ZERO_ADDRESS).should.be.rejectedWith(ERROR_MSG)
;(await token.bridgeContract()).should.be.equal(ZERO_ADDRESS)
await addBridge(token, ZERO_ADDRESS).should.be.rejectedWith(ERROR_MSG)
expect(await isBridge(token, invalidContractAddress)).to.be.equal(false)
})
})
@ -265,7 +280,7 @@ async function testERC677BridgeToken(accounts, rewardable) {
})
it('sends tokens to bridge contract', async () => {
await token.setBridgeContract(homeErcToErcContract.address).should.be.fulfilled
await addBridge(token, homeErcToErcContract.address).should.be.fulfilled
await token.mint(user, oneEther, { from: owner }).should.be.fulfilled
const result = await token.transfer(homeErcToErcContract.address, minPerTx, { from: user }).should.be.fulfilled
@ -275,7 +290,7 @@ async function testERC677BridgeToken(accounts, rewardable) {
value: minPerTx
})
await token.setBridgeContract(foreignNativeToErcBridge.address).should.be.fulfilled
await addBridge(token, foreignNativeToErcBridge.address).should.be.fulfilled
const result2 = await token.transfer(foreignNativeToErcBridge.address, minPerTx, {
from: user
}).should.be.fulfilled
@ -287,7 +302,7 @@ async function testERC677BridgeToken(accounts, rewardable) {
})
it('sends tokens to contract that does not contains onTokenTransfer method', async () => {
await token.setBridgeContract(homeErcToErcContract.address).should.be.fulfilled
await addBridge(token, homeErcToErcContract.address).should.be.fulfilled
await token.mint(user, oneEther, { from: owner }).should.be.fulfilled
const result = await token.transfer(validatorContract.address, minPerTx, { from: user }).should.be.fulfilled
@ -307,10 +322,10 @@ async function testERC677BridgeToken(accounts, rewardable) {
const lessThanMin = ether('0.0001')
await token.mint(user, oneEther, { from: owner }).should.be.fulfilled
await token.setBridgeContract(homeErcToErcContract.address).should.be.fulfilled
await addBridge(token, homeErcToErcContract.address).should.be.fulfilled
await token.transfer(homeErcToErcContract.address, lessThanMin, { from: user }).should.be.rejectedWith(ERROR_MSG)
await token.setBridgeContract(foreignNativeToErcBridge.address).should.be.fulfilled
await addBridge(token, foreignNativeToErcBridge.address).should.be.fulfilled
await token
.transfer(foreignNativeToErcBridge.address, lessThanMin, { from: user })
.should.be.rejectedWith(ERROR_MSG)
@ -343,7 +358,7 @@ async function testERC677BridgeToken(accounts, rewardable) {
const amount = ether('1')
const user2 = accounts[2]
await token.setBridgeContract(receiver.address).should.be.fulfilled
await addBridge(token, receiver.address).should.be.fulfilled
expect(await receiver.from()).to.be.equal(ZERO_ADDRESS)
expect(await receiver.value()).to.be.bignumber.equal(ZERO)
@ -480,7 +495,7 @@ async function testERC677BridgeToken(accounts, rewardable) {
})
it('sends tokens to bridge contract', async () => {
await token.setBridgeContract(homeErcToErcContract.address).should.be.fulfilled
await addBridge(token, homeErcToErcContract.address).should.be.fulfilled
await token.mint(user, oneEther, { from: owner }).should.be.fulfilled
const result = await token.transferAndCall(homeErcToErcContract.address, minPerTx, '0x', {
@ -492,7 +507,7 @@ async function testERC677BridgeToken(accounts, rewardable) {
value: minPerTx
})
await token.setBridgeContract(foreignNativeToErcBridge.address).should.be.fulfilled
await addBridge(token, foreignNativeToErcBridge.address).should.be.fulfilled
const result2 = await token.transferAndCall(foreignNativeToErcBridge.address, minPerTx, '0x', { from: user })
.should.be.fulfilled
expectEventInLogs(result2.logs, 'Transfer', {
@ -503,7 +518,7 @@ async function testERC677BridgeToken(accounts, rewardable) {
})
it('fail to sends tokens to contract that does not contains onTokenTransfer method', async () => {
await token.setBridgeContract(homeErcToErcContract.address).should.be.fulfilled
await addBridge(token, homeErcToErcContract.address).should.be.fulfilled
await token.mint(user, oneEther, { from: owner }).should.be.fulfilled
await token
@ -515,12 +530,12 @@ async function testERC677BridgeToken(accounts, rewardable) {
const lessThanMin = ether('0.0001')
await token.mint(user, oneEther, { from: owner }).should.be.fulfilled
await token.setBridgeContract(homeErcToErcContract.address).should.be.fulfilled
await addBridge(token, homeErcToErcContract.address).should.be.fulfilled
await token
.transferAndCall(homeErcToErcContract.address, lessThanMin, '0x', { from: user })
.should.be.rejectedWith(ERROR_MSG)
await token.setBridgeContract(foreignNativeToErcBridge.address).should.be.fulfilled
await addBridge(token, foreignNativeToErcBridge.address).should.be.fulfilled
await token
.transferAndCall(foreignNativeToErcBridge.address, lessThanMin, '0x', { from: user })
.should.be.rejectedWith(ERROR_MSG)