@0x/contracts-asset-proxy: Add Eth2DaiBridge and tests.

This commit is contained in:
Lawrence Forman
2019-09-28 01:29:06 -04:00
parent d5c8c076dc
commit 82ac8e29e3
8 changed files with 493 additions and 0 deletions

View File

@@ -21,6 +21,10 @@
{
"note": "Add `ERC20BridgeProxy`",
"pr": 2220
},
{
"note": "Add `Eth2DaiBridge`",
"pr": "TODO"
}
]
},

View File

@@ -0,0 +1,102 @@
/*
Copyright 2019 ZeroEx Intl.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
pragma solidity ^0.5.9;
pragma experimental ABIEncoderV2;
import "@0x/contracts-erc20/contracts/src/interfaces/IERC20Token.sol";
import "./ERC20Bridge.sol";
import "../interfaces/IEth2Dai.sol";
contract Eth2DaiBridge is
ERC20Bridge
{
/* Mainnet addresses */
address constant public ETH2DAI_ADDRESS = 0x39755357759cE0d7f32dC8dC45414CCa409AE24e;
address constant public WETH_ADDRESS = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
address constant public DAI_ADDRESS = 0x89d24A6b4CcB1B6fAA2625fE562bDD9a23260359;
constructor() public {
// Grant the Eth2Dai contract unlimited weth and dai allowances.
_getWethContract().approve(address(_getEth2DaiContract()), uint256(-1));
_getDaiContract().approve(address(_getEth2DaiContract()), uint256(-1));
}
// solhint-disable space-after-comma
function transfer(
bytes calldata /* bridgeData */,
address toTokenAddress,
address /* from */,
address to,
uint256 amount
)
external
returns (bytes4 success)
{
// The "from" token is the opposite of the "to" token.
IERC20Token fromToken = _getWethContract();
IERC20Token toToken = _getDaiContract();
// Swap them if necessary.
if (toTokenAddress == address(fromToken)) {
(fromToken, toToken) = (toToken, fromToken);
} else {
require(
toTokenAddress == address(toToken),
"INVALID_ETH2DAI_TOKEN"
);
}
// Try to sell all of this contract's `fromToken` balance.
uint256 boughtAmount = _getEth2DaiContract().sellAllAmount(
address(fromToken),
fromToken.balanceOf(address(this)),
address(toToken),
amount
);
// Transfer the converted `toToken`s to `to`.
toToken.transfer(to, boughtAmount);
return BRIDGE_SUCCESS;
}
/// @dev Overridable way to get the weth contract.
function _getWethContract()
internal
view
returns (IERC20Token)
{
return IERC20Token(WETH_ADDRESS);
}
/// @dev Overridable way to get the dai contract.
function _getDaiContract()
internal
view
returns (IERC20Token)
{
return IERC20Token(DAI_ADDRESS);
}
/// @dev Overridable way to get the eth2dai contract.
function _getEth2DaiContract()
internal
view
returns (IEth2Dai)
{
return IEth2Dai(ETH2DAI_ADDRESS);
}
}

View File

@@ -0,0 +1,32 @@
/*
Copyright 2019 ZeroEx Intl.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
pragma solidity ^0.5.9;
// solhint-disable func-param-name-mixedcase
interface IEth2Dai {
function sellAllAmount(
address pay_gem,
uint256 pay_amt,
address buy_gem,
uint256 min_fill_amount
)
external
returns (uint256 fill_amt);
}

View File

@@ -0,0 +1,197 @@
/*
Copyright 2019 ZeroEx Intl.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
pragma solidity ^0.5.9;
pragma experimental ABIEncoderV2;
import "@0x/contracts-erc20/contracts/src/interfaces/IERC20Token.sol";
import "../src/bridges/Eth2DaiBridge.sol";
import "../src/interfaces/IEth2Dai.sol";
// solhint-disable no-simple-event-func-name
/// @dev Interface that allows `TestToken` to call `raiseTransferEvent` on
/// the `TestEth2DaiBridge` contract.
interface IRaiseTransferEvent {
function raiseTransferEvent(
address from,
address to,
uint256 amount
)
external;
}
/// @dev A minimalist ERC20 token.
contract TestToken {
mapping (address => uint256) public balances;
mapping (address => mapping (address => uint256)) public allowances;
/// @dev Just calls `raiseTransferEvent()` on the caller.
function transfer(address to, uint256 amount)
external
returns (bool)
{
IRaiseTransferEvent(msg.sender).raiseTransferEvent(msg.sender, to, amount);
return true;
}
/// @dev Set the balance for `owner`.
function setBalance(address owner, uint256 balance)
external
{
balances[owner] = balance;
}
/// @dev Records allowance values.
function approve(address spender, uint256 allowance)
external
returns (bool)
{
allowances[msg.sender][spender] = allowance;
return true;
}
/// @dev Retrieve the balance for `owner`.
function balanceOf(address owner)
external
view
returns (uint256)
{
return balances[owner];
}
}
/// @dev Eth2DaiBridge overridden to mock tokens and
/// implement IEth2Dai.
contract TestEth2DaiBridge is
IEth2Dai,
Eth2DaiBridge
{
event SellAllAmount(
address sellToken,
uint256 sellTokenAmount,
address buyToken,
uint256 minimumFillAmount
);
event TokenTransfer(
address token,
address from,
address to,
uint256 amount
);
TestToken public wethToken = new TestToken();
TestToken public daiToken = new TestToken();
string private _nextRevertReason;
uint256 private _nextFillAmount;
/// @dev Set token balances for this contract.
function setTokenBalances(uint256 wethBalance, uint256 daiBalance)
external
{
wethToken.setBalance(address(this), wethBalance);
daiToken.setBalance(address(this), daiBalance);
}
/// @dev Set the behavior for `IEth2Dai.sellAllAmount()`.
function setFillBehavior(string calldata revertReason, uint256 fillAmount)
external
{
_nextRevertReason = revertReason;
_nextFillAmount = fillAmount;
}
/// @dev Implementation of `IEth2Dai.sellAllAmount()`
function sellAllAmount(
address sellTokenAddress,
uint256 sellTokenAmount,
address buyTokenAddress,
uint256 minimumFillAmount
)
external
returns (uint256 fillAmount)
{
emit SellAllAmount(
sellTokenAddress,
sellTokenAmount,
buyTokenAddress,
minimumFillAmount
);
if (bytes(_nextRevertReason).length != 0) {
revert(_nextRevertReason);
}
return _nextFillAmount;
}
function raiseTransferEvent(
address from,
address to,
uint256 amount
)
external
{
emit TokenTransfer(
msg.sender,
from,
to,
amount
);
}
/// @dev Retrieves the allowances of the test tokens.
function getEth2DaiTokenAllowances()
external
view
returns (uint256 wethAllowance, uint256 daiAllowance)
{
wethAllowance = wethToken.allowances(address(this), address(this));
daiAllowance = daiToken.allowances(address(this), address(this));
return (wethAllowance, daiAllowance);
}
// @dev Use `wethToken`.
function _getWethContract()
internal
view
returns (IERC20Token)
{
return IERC20Token(address(wethToken));
}
// @dev Use `daiToken`.
function _getDaiContract()
internal
view
returns (IERC20Token)
{
return IERC20Token(address(daiToken));
}
// @dev This contract will double as the Eth2Dai contract.
function _getEth2DaiContract()
internal
view
returns (IEth2Dai)
{
return IEth2Dai(address(this));
}
}

View File

@@ -9,17 +9,20 @@ import * as ERC1155Proxy from '../generated-artifacts/ERC1155Proxy.json';
import * as ERC20BridgeProxy from '../generated-artifacts/ERC20BridgeProxy.json';
import * as ERC20Proxy from '../generated-artifacts/ERC20Proxy.json';
import * as ERC721Proxy from '../generated-artifacts/ERC721Proxy.json';
import * as Eth2DaiBridge from '../generated-artifacts/Eth2DaiBridge.json';
import * as IAssetData from '../generated-artifacts/IAssetData.json';
import * as IAssetProxy from '../generated-artifacts/IAssetProxy.json';
import * as IAssetProxyDispatcher from '../generated-artifacts/IAssetProxyDispatcher.json';
import * as IAuthorizable from '../generated-artifacts/IAuthorizable.json';
import * as IERC20Bridge from '../generated-artifacts/IERC20Bridge.json';
import * as IEth2Dai from '../generated-artifacts/IEth2Dai.json';
import * as MixinAssetProxyDispatcher from '../generated-artifacts/MixinAssetProxyDispatcher.json';
import * as MixinAuthorizable from '../generated-artifacts/MixinAuthorizable.json';
import * as MultiAssetProxy from '../generated-artifacts/MultiAssetProxy.json';
import * as Ownable from '../generated-artifacts/Ownable.json';
import * as StaticCallProxy from '../generated-artifacts/StaticCallProxy.json';
import * as TestERC20Bridge from '../generated-artifacts/TestERC20Bridge.json';
import * as TestEth2DaiBridge from '../generated-artifacts/TestEth2DaiBridge.json';
import * as TestStaticCallTarget from '../generated-artifacts/TestStaticCallTarget.json';
export const artifacts = {
MixinAssetProxyDispatcher: MixinAssetProxyDispatcher as ContractArtifact,
@@ -36,6 +39,8 @@ export const artifacts = {
IAssetProxyDispatcher: IAssetProxyDispatcher as ContractArtifact,
IAuthorizable: IAuthorizable as ContractArtifact,
IERC20Bridge: IERC20Bridge as ContractArtifact,
IEth2Dai: IEth2Dai as ContractArtifact,
TestERC20Bridge: TestERC20Bridge as ContractArtifact,
TestEth2DaiBridge: TestEth2DaiBridge as ContractArtifact,
TestStaticCallTarget: TestStaticCallTarget as ContractArtifact,
};

View File

@@ -7,15 +7,18 @@ export * from '../generated-wrappers/erc1155_proxy';
export * from '../generated-wrappers/erc20_bridge_proxy';
export * from '../generated-wrappers/erc20_proxy';
export * from '../generated-wrappers/erc721_proxy';
export * from '../generated-wrappers/eth2_dai_bridge';
export * from '../generated-wrappers/i_asset_data';
export * from '../generated-wrappers/i_asset_proxy';
export * from '../generated-wrappers/i_asset_proxy_dispatcher';
export * from '../generated-wrappers/i_authorizable';
export * from '../generated-wrappers/i_erc20_bridge';
export * from '../generated-wrappers/i_eth2_dai';
export * from '../generated-wrappers/mixin_asset_proxy_dispatcher';
export * from '../generated-wrappers/mixin_authorizable';
export * from '../generated-wrappers/multi_asset_proxy';
export * from '../generated-wrappers/ownable';
export * from '../generated-wrappers/static_call_proxy';
export * from '../generated-wrappers/test_erc20_bridge';
export * from '../generated-wrappers/test_eth2_dai_bridge';
export * from '../generated-wrappers/test_static_call_target';

View File

@@ -0,0 +1,147 @@
import {
blockchainTests,
constants,
expect,
filterLogsToArguments,
getRandomInteger,
Numberish,
randomAddress,
TransactionHelper,
} from '@0x/contracts-test-utils';
import { BigNumber } from '@0x/utils';
import { DecodedLogs } from 'ethereum-types';
import * as _ from 'lodash';
import {
artifacts,
TestEth2DaiBridgeContract,
TestEth2DaiBridgeEvents,
TestEth2DaiBridgeSellAllAmountEventArgs,
TestEth2DaiBridgeTokenTransferEventArgs,
} from '../src';
blockchainTests.resets('Eth2DaiBridge unit tests', env => {
const txHelper = new TransactionHelper(env.web3Wrapper, artifacts);
let testContract: TestEth2DaiBridgeContract;
let daiTokenAddress: string;
let wethTokenAddress: string;
before(async () => {
testContract = await TestEth2DaiBridgeContract.deployFrom0xArtifactAsync(
artifacts.TestEth2DaiBridge,
env.provider,
env.txDefaults,
artifacts,
);
[daiTokenAddress, wethTokenAddress] = await Promise.all([
testContract.daiToken.callAsync(),
testContract.wethToken.callAsync(),
]);
});
describe('deployment', () => {
it('sets Eth2Dai allowances to maximum', async () => {
const [wethAllowance, daiAllowance] = await testContract.getEth2DaiTokenAllowances.callAsync();
expect(wethAllowance).to.bignumber.eq(constants.MAX_UINT256);
expect(daiAllowance).to.bignumber.eq(constants.MAX_UINT256);
});
});
describe('transfer()', () => {
interface TransferOpts {
toTokenAddress: string;
toAddress: string;
amount: Numberish;
fromTokenBalance: Numberish;
revertReason: string;
fillAmount: Numberish;
}
function createTransferOpts(opts?: Partial<TransferOpts>): TransferOpts {
return {
toTokenAddress: _.sampleSize([wethTokenAddress, daiTokenAddress], 1)[0],
toAddress: randomAddress(),
amount: getRandomInteger(1, 100e18),
revertReason: '',
fillAmount: getRandomInteger(1, 100e18),
fromTokenBalance: getRandomInteger(1, 100e18),
...opts,
};
}
async function transferAsync(opts?: Partial<TransferOpts>): Promise<[string, DecodedLogs]> {
const _opts = createTransferOpts(opts);
// Set the fill behavior.
await testContract.setFillBehavior.awaitTransactionSuccessAsync(
_opts.revertReason,
new BigNumber(_opts.fillAmount),
);
// Set the token balance for the token we're converting from.
await testContract.setTokenBalances.awaitTransactionSuccessAsync(
_opts.toTokenAddress === daiTokenAddress
? new BigNumber(_opts.fromTokenBalance)
: constants.ZERO_AMOUNT,
_opts.toTokenAddress === wethTokenAddress
? new BigNumber(_opts.fromTokenBalance)
: constants.ZERO_AMOUNT,
);
// Call transfer().
const [result, { logs }] = await txHelper.getResultAndReceiptAsync(
testContract.transfer,
'0x',
_opts.toTokenAddress,
randomAddress(),
_opts.toAddress,
new BigNumber(_opts.amount),
);
return [result, (logs as any) as DecodedLogs];
}
function getOppositeToken(tokenAddress: string): string {
if (tokenAddress === daiTokenAddress) {
return wethTokenAddress;
}
return daiTokenAddress;
}
it('returns magic bytes on success', async () => {
const BRIDGE_SUCCESS_RETURN_DATA = '0xb5d40d78';
const [result] = await transferAsync();
expect(result).to.eq(BRIDGE_SUCCESS_RETURN_DATA);
});
it('calls `Eth2Dai.sellAllAmount()`', async () => {
const opts = createTransferOpts();
const [, logs] = await transferAsync(opts);
const transfers = filterLogsToArguments<TestEth2DaiBridgeSellAllAmountEventArgs>(
logs,
TestEth2DaiBridgeEvents.SellAllAmount,
);
expect(transfers.length).to.eq(1);
expect(transfers[0].sellToken).to.eq(getOppositeToken(opts.toTokenAddress));
expect(transfers[0].buyToken).to.eq(opts.toTokenAddress);
expect(transfers[0].sellTokenAmount).to.bignumber.eq(opts.fromTokenBalance);
expect(transfers[0].minimumFillAmount).to.bignumber.eq(opts.amount);
});
it('transfers filled amount to `to`', async () => {
const opts = createTransferOpts();
const [, logs] = await transferAsync(opts);
const transfers = filterLogsToArguments<TestEth2DaiBridgeTokenTransferEventArgs>(
logs,
TestEth2DaiBridgeEvents.TokenTransfer,
);
expect(transfers.length).to.eq(1);
expect(transfers[0].token).to.eq(opts.toTokenAddress);
expect(transfers[0].from).to.eq(testContract.address);
expect(transfers[0].to).to.eq(opts.toAddress);
expect(transfers[0].amount).to.bignumber.eq(opts.fillAmount);
});
it('fails if `Eth2Dai.sellAllAmount()` reverts', async () => {
const opts = createTransferOpts({ revertReason: 'FOOBAR' });
const tx = transferAsync(opts);
return expect(tx).to.revertWith(opts.revertReason);
});
});
});

View File

@@ -7,17 +7,20 @@
"generated-artifacts/ERC20BridgeProxy.json",
"generated-artifacts/ERC20Proxy.json",
"generated-artifacts/ERC721Proxy.json",
"generated-artifacts/Eth2DaiBridge.json",
"generated-artifacts/IAssetData.json",
"generated-artifacts/IAssetProxy.json",
"generated-artifacts/IAssetProxyDispatcher.json",
"generated-artifacts/IAuthorizable.json",
"generated-artifacts/IERC20Bridge.json",
"generated-artifacts/IEth2Dai.json",
"generated-artifacts/MixinAssetProxyDispatcher.json",
"generated-artifacts/MixinAuthorizable.json",
"generated-artifacts/MultiAssetProxy.json",
"generated-artifacts/Ownable.json",
"generated-artifacts/StaticCallProxy.json",
"generated-artifacts/TestERC20Bridge.json",
"generated-artifacts/TestEth2DaiBridge.json",
"generated-artifacts/TestStaticCallTarget.json"
],
"exclude": ["./deploy/solc/solc_bin"]