Skip to content

Commit

Permalink
Add alternative receiver for ERC677-to-ERC677 on top of AMB (#301)
Browse files Browse the repository at this point in the history
* Add alternative receiver for amb-erc677-to-erc677
  • Loading branch information
patitonar authored and akolotov committed Oct 17, 2019
1 parent 36afcf0 commit 7d033bd
Show file tree
Hide file tree
Showing 10 changed files with 222 additions and 31 deletions.
6 changes: 6 additions & 0 deletions contracts/libraries/Bytes.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,10 @@ library Bytes {
result := mload(add(_bytes, 32))
}
}

function bytesToAddress(bytes _bytes) internal pure returns (address addr) {
assembly {
addr := mload(add(_bytes, 20))
}
}
}
10 changes: 3 additions & 7 deletions contracts/upgradeable_contracts/BaseERC677Bridge.sol
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,15 @@ contract BaseERC677Bridge is BasicTokenBridge, ERC677Receiver, ERC677Storage {
addressStorage[ERC677_TOKEN] = _token;
}

function onTokenTransfer(
address _from,
uint256 _value,
bytes /*_data*/
) external returns (bool) {
function onTokenTransfer(address _from, uint256 _value, bytes _data) external returns (bool) {
ERC677 token = erc677token();
require(msg.sender == address(token));
require(withinLimit(_value));
setTotalSpentPerDay(getCurrentDay(), totalSpentPerDay(getCurrentDay()).add(_value));
bridgeSpecificActionsOnTokenTransfer(token, _from, _value);
bridgeSpecificActionsOnTokenTransfer(token, _from, _value, _data);
return true;
}

/* solcov ignore next */
function bridgeSpecificActionsOnTokenTransfer(ERC677 _token, address _from, uint256 _value) internal;
function bridgeSpecificActionsOnTokenTransfer(ERC677 _token, address _from, uint256 _value, bytes _data) internal;
}
3 changes: 2 additions & 1 deletion contracts/upgradeable_contracts/ERC677Bridge.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ contract ERC677Bridge is BaseERC677Bridge {
function bridgeSpecificActionsOnTokenTransfer(
ERC677, /*_token*/
address _from,
uint256 _value
uint256 _value,
bytes /*_data*/
) internal {
fireEventOnTokenTransfer(_from, _value);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@ import "./ERC677Bridge.sol";
import "../interfaces/IBurnableMintableERC677Token.sol";

contract ERC677BridgeForBurnableMintableToken is ERC677Bridge {
function bridgeSpecificActionsOnTokenTransfer(ERC677 _token, address _from, uint256 _value) internal {
function bridgeSpecificActionsOnTokenTransfer(
ERC677 _token,
address _from,
uint256 _value,
bytes /*_data*/
) internal {
IBurnableMintableERC677Token(_token).burn(_value);
fireEventOnTokenTransfer(_from, _value);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,16 @@ contract BasicAMBErc677ToErc677 is
return isInitialized();
}

function chooseReceiver(address _from, bytes _data) internal view returns (address recipient) {
recipient = _from;
if (_data.length > 0) {
require(_data.length == 20);
recipient = Bytes.bytesToAddress(_data);
require(recipient != address(0));
require(recipient != mediatorContractOnOtherSide());
}
}

function passMessage(address _from, uint256 _value) internal {
bytes4 methodSelector = this.handleBridgedTokens.selector;
bytes memory data = abi.encodeWithSelector(methodSelector, _from, _value, nonce());
Expand All @@ -79,21 +89,29 @@ contract BasicAMBErc677ToErc677 is
bridgeContract().requireToPassMessage(mediatorContractOnOtherSide(), data, requestGasLimit());
}

function relayTokens(uint256 _value) external {
function relayTokens(address _from, address _receiver, uint256 _value) external {
require(_from == msg.sender || _from == _receiver);
_relayTokens(_from, _receiver, _value);
}

function _relayTokens(address _from, address _receiver, uint256 _value) internal {
// This lock is to prevent calling passMessage twice if a ERC677 token is used.
// When transferFrom is called, after the transfer, the ERC677 token will call onTokenTransfer from this contract
// which will call passMessage.
require(!lock());
ERC677 token = erc677token();
address from = msg.sender;
address to = address(this);
require(withinLimit(_value));
setTotalSpentPerDay(getCurrentDay(), totalSpentPerDay(getCurrentDay()).add(_value));

setLock(true);
token.transferFrom(from, to, _value);
token.transferFrom(_from, to, _value);
setLock(false);
bridgeSpecificActionsOnTokenTransfer(token, from, _value);
bridgeSpecificActionsOnTokenTransfer(token, _from, _value, abi.encodePacked(_receiver));
}

function relayTokens(address _receiver, uint256 _value) external {
_relayTokens(msg.sender, _receiver, _value);
}

function getBridgeInterfacesVersion() external pure returns (uint64 major, uint64 minor, uint64 patch) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,11 @@ contract ForeignAMBErc677ToErc677 is BasicAMBErc677ToErc677 {
function bridgeSpecificActionsOnTokenTransfer(
ERC677, /* _token */
address _from,
uint256 _value
uint256 _value,
bytes _data
) internal {
if (!lock()) {
passMessage(_from, _value);
passMessage(chooseReceiver(_from, _data), _value);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ contract HomeAMBErc677ToErc677 is BasicAMBErc677ToErc677 {
IBurnableMintableERC677Token(erc677token()).mint(_recipient, value);
}

function bridgeSpecificActionsOnTokenTransfer(ERC677 _token, address _from, uint256 _value) internal {
function bridgeSpecificActionsOnTokenTransfer(ERC677 _token, address _from, uint256 _value, bytes _data) internal {
if (!lock()) {
IBurnableMintableERC677Token(_token).burn(_value);
passMessage(_from, _value);
passMessage(chooseReceiver(_from, _data), _value);
}
}

Expand Down
119 changes: 113 additions & 6 deletions test/amb_erc677_to_erc677/AMBErc677ToErc677Behavior.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -498,6 +498,7 @@ function shouldBehaveLikeBasicAMBErc677ToErc677(otherSideMediatorContract, accou
describe('relayTokens', () => {
let contract
let erc20Token
const user2 = accounts[2]
beforeEach(async function() {
bridgeContract = await AMBMock.new()
await bridgeContract.setMaxGasPerTx(maxGasPerTx)
Expand Down Expand Up @@ -525,12 +526,118 @@ function shouldBehaveLikeBasicAMBErc677ToErc677(otherSideMediatorContract, accou
expect(await erc20Token.allowance(user, contract.address)).to.be.bignumber.equal(value)

// When
await contract.relayTokens(value, { from: user }).should.be.fulfilled
await contract.relayTokens(user, value, { from: user }).should.be.fulfilled

// Then
const events = await getEvents(bridgeContract, { event: 'MockedEvent' })
expect(events.length).to.be.equal(1)
})
it('should allow user to specify a itself as receiver', async () => {
// Given
await contract.initialize(
bridgeContract.address,
mediatorContract.address,
erc20Token.address,
[dailyLimit, maxPerTx, minPerTx],
[executionDailyLimit, executionMaxPerTx],
maxGasPerTx,
decimalShiftZero,
owner
).should.be.fulfilled

const value = oneEther
await erc20Token.approve(contract.address, value, { from: user }).should.be.fulfilled
expect(await erc20Token.allowance(user, contract.address)).to.be.bignumber.equal(value)

// When
await contract.methods['relayTokens(address,address,uint256)'](user, user, value, { from: user }).should.be
.fulfilled

// Then
const events = await getEvents(bridgeContract, { event: 'MockedEvent' })
expect(events.length).to.be.equal(1)
expect(events[0].returnValues.encodedData.includes(strip0x(user).toLowerCase())).to.be.equal(true)
})
it('should allow to specify a different receiver', async () => {
// Given
await contract.initialize(
bridgeContract.address,
mediatorContract.address,
erc20Token.address,
[dailyLimit, maxPerTx, minPerTx],
[executionDailyLimit, executionMaxPerTx],
maxGasPerTx,
decimalShiftZero,
owner
).should.be.fulfilled

const value = oneEther
await erc20Token.approve(contract.address, value, { from: user }).should.be.fulfilled
expect(await erc20Token.allowance(user, contract.address)).to.be.bignumber.equal(value)

// When
await contract.methods['relayTokens(address,address,uint256)'](user, user2, value, { from: user }).should.be
.fulfilled

// Then
const events = await getEvents(bridgeContract, { event: 'MockedEvent' })
expect(events.length).to.be.equal(1)
expect(events[0].returnValues.encodedData.includes(strip0x(user2).toLowerCase())).to.be.equal(true)
})
it('should allow to specify a different receiver without specifying sender', async () => {
// Given
await contract.initialize(
bridgeContract.address,
mediatorContract.address,
erc20Token.address,
[dailyLimit, maxPerTx, minPerTx],
[executionDailyLimit, executionMaxPerTx],
maxGasPerTx,
decimalShiftZero,
owner
).should.be.fulfilled

const value = oneEther
await erc20Token.approve(contract.address, value, { from: user }).should.be.fulfilled
expect(await erc20Token.allowance(user, contract.address)).to.be.bignumber.equal(value)

// When
await contract.relayTokens(user2, value, { from: user }).should.be.fulfilled

// Then
const events = await getEvents(bridgeContract, { event: 'MockedEvent' })
expect(events.length).to.be.equal(1)
expect(events[0].returnValues.encodedData.includes(strip0x(user2).toLowerCase())).to.be.equal(true)
})
it('should allow to complete a transfer approved by other user', async () => {
// Given
await contract.initialize(
bridgeContract.address,
mediatorContract.address,
erc20Token.address,
[dailyLimit, maxPerTx, minPerTx],
[executionDailyLimit, executionMaxPerTx],
maxGasPerTx,
decimalShiftZero,
owner
).should.be.fulfilled

const value = oneEther
await erc20Token.approve(contract.address, value, { from: user }).should.be.fulfilled
expect(await erc20Token.allowance(user, contract.address)).to.be.bignumber.equal(value)

// When
await contract.methods['relayTokens(address,address,uint256)'](user, user2, value, {
from: user2
}).should.be.rejectedWith(ERROR_MSG)
await contract.methods['relayTokens(address,address,uint256)'](user, user, value, { from: user2 }).should.be
.fulfilled

// Then
const events = await getEvents(bridgeContract, { event: 'MockedEvent' })
expect(events.length).to.be.equal(1)
expect(events[0].returnValues.encodedData.includes(strip0x(user).toLowerCase())).to.be.equal(true)
})
it('should fail if user did not approve the transfer', async () => {
await contract.initialize(
bridgeContract.address,
Expand All @@ -543,7 +650,7 @@ function shouldBehaveLikeBasicAMBErc677ToErc677(otherSideMediatorContract, accou
owner
).should.be.fulfilled

await contract.relayTokens(oneEther, { from: user }).should.be.rejectedWith(ERROR_MSG)
await contract.relayTokens(user, oneEther, { from: user }).should.be.rejectedWith(ERROR_MSG)
})
it('should fail if value is not within limits', async () => {
await contract.initialize(
Expand All @@ -561,7 +668,7 @@ function shouldBehaveLikeBasicAMBErc677ToErc677(otherSideMediatorContract, accou
await erc20Token.approve(contract.address, value, { from: user }).should.be.fulfilled
expect(await erc20Token.allowance(user, contract.address)).to.be.bignumber.equal(value)

await contract.relayTokens(value, { from: user }).should.be.rejectedWith(ERROR_MSG)
await contract.relayTokens(user, value, { from: user }).should.be.rejectedWith(ERROR_MSG)
})
it('should prevent emitting the event twice when ERC677 used by relayTokens and ERC677 is owned by token manager', async function() {
// Given
Expand All @@ -588,7 +695,7 @@ function shouldBehaveLikeBasicAMBErc677ToErc677(otherSideMediatorContract, accou
expect(await erc677Token.allowance(user, contract.address)).to.be.bignumber.equal(value)

// When
await contract.relayTokens(value, { from: user }).should.be.fulfilled
await contract.relayTokens(user, value, { from: user }).should.be.fulfilled

// Then
const events = await getEvents(bridgeContract, { event: 'MockedEvent' })
Expand Down Expand Up @@ -617,7 +724,7 @@ function shouldBehaveLikeBasicAMBErc677ToErc677(otherSideMediatorContract, accou
expect(await erc677Token.allowance(user, contract.address)).to.be.bignumber.equal(value)

// When
await contract.relayTokens(value, { from: user }).should.be.fulfilled
await contract.relayTokens(user, value, { from: user }).should.be.fulfilled

// Then
const events = await getEvents(bridgeContract, { event: 'MockedEvent' })
Expand Down Expand Up @@ -761,7 +868,7 @@ function shouldBehaveLikeBasicAMBErc677ToErc677(otherSideMediatorContract, accou
expect(await erc677Token.totalSupply()).to.be.bignumber.equal(twoEthers)

// User transfer tokens
const transferTx = await erc677Token.transferAndCall(contract.address, oneEther, '0x00', { from: user }).should.be
const transferTx = await erc677Token.transferAndCall(contract.address, oneEther, '0x', { from: user }).should.be
.fulfilled

expect(await erc677Token.balanceOf(user)).to.be.bignumber.equal(oneEther)
Expand Down
37 changes: 33 additions & 4 deletions test/amb_erc677_to_erc677/foreign_bridge.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const AMBMock = artifacts.require('AMBMock.sol')
const { expect } = require('chai')
const { shouldBehaveLikeBasicAMBErc677ToErc677 } = require('./AMBErc677ToErc677Behavior.test')
const { ether } = require('../helpers/helpers')
const { getEvents } = require('../helpers/helpers')
const { getEvents, strip0x } = require('../helpers/helpers')
const { ERROR_MSG, toBN } = require('../setup')

const ZERO = toBN(0)
Expand Down Expand Up @@ -70,19 +70,48 @@ contract('ForeignAMBErc677ToErc677', async accounts => {
expect(initialEvents.length).to.be.equal(0)

// only token address can call it
await foreignBridge.onTokenTransfer(user, halfEther, '0x00', { from: owner }).should.be.rejectedWith(ERROR_MSG)
await foreignBridge.onTokenTransfer(user, halfEther, '0x', { from: owner }).should.be.rejectedWith(ERROR_MSG)

// must be within limits
await erc677Token
.transferAndCall(foreignBridge.address, twoEthers, '0x00', { from: user })
.transferAndCall(foreignBridge.address, twoEthers, '0x', { from: user })
.should.be.rejectedWith(ERROR_MSG)

// When
await erc677Token.transferAndCall(foreignBridge.address, halfEther, '0x00', { from: user }).should.be.fulfilled
await erc677Token.transferAndCall(foreignBridge.address, halfEther, '0x', { from: user }).should.be.fulfilled

// Then
const events = await getEvents(ambBridgeContract, { event: 'UserRequestForAffirmation' })
expect(events.length).to.be.equal(1)
expect(events[0].returnValues.encodedData.includes(strip0x(user).toLowerCase())).to.be.equal(true)
expect(await foreignBridge.totalSpentPerDay(currentDay)).to.be.bignumber.equal(halfEther)
})
it('should be able to specify a different receiver', async () => {
// Given
const user2 = accounts[2]
const currentDay = await foreignBridge.getCurrentDay()
expect(await foreignBridge.totalSpentPerDay(currentDay)).to.be.bignumber.equal(ZERO)
const initialEvents = await getEvents(ambBridgeContract, { event: 'UserRequestForAffirmation' })
expect(initialEvents.length).to.be.equal(0)

// only token address can call it
await foreignBridge.onTokenTransfer(user, halfEther, '0x', { from: owner }).should.be.rejectedWith(ERROR_MSG)

// must be within limits
await erc677Token
.transferAndCall(foreignBridge.address, twoEthers, '0x', { from: user })
.should.be.rejectedWith(ERROR_MSG)

// When
await erc677Token
.transferAndCall(foreignBridge.address, halfEther, '0x00', { from: user })
.should.be.rejectedWith(ERROR_MSG)
await erc677Token.transferAndCall(foreignBridge.address, halfEther, user2, { from: user }).should.be.fulfilled

// Then
const events = await getEvents(ambBridgeContract, { event: 'UserRequestForAffirmation' })
expect(events.length).to.be.equal(1)
expect(events[0].returnValues.encodedData.includes(strip0x(user2).toLowerCase())).to.be.equal(true)
expect(await foreignBridge.totalSpentPerDay(currentDay)).to.be.bignumber.equal(halfEther)
})
})
Expand Down

0 comments on commit 7d033bd

Please sign in to comment.