@0x/contracts-asset-proxy
: Finish off UniswapBridge
tests.
This commit is contained in:
parent
b383781870
commit
c2261a6bbe
@ -21,18 +21,18 @@ pragma experimental ABIEncoderV2;
|
||||
|
||||
import "@0x/contracts-erc20/contracts/src/interfaces/IERC20Token.sol";
|
||||
import "@0x/contracts-erc20/contracts/src/interfaces/IEtherToken.sol";
|
||||
import "@0x/contracts-exchange/contracts/src/interfaces/IWallet.sol";
|
||||
import "../interfaces/IUniswapExchangeFactory.sol";
|
||||
import "../interfaces/IUniswapExchange.sol";
|
||||
import "./ERC20Bridge.sol";
|
||||
import "../interfaces/IWallet.sol";
|
||||
import "../interfaces/IERC20Bridge.sol";
|
||||
|
||||
|
||||
// solhint-disable space-after-comma
|
||||
// solhint-disable not-rely-on-time
|
||||
contract UniswapBridge is
|
||||
ERC20Bridge,
|
||||
IERC20Bridge,
|
||||
IWallet
|
||||
{
|
||||
bytes4 private constant LEGACY_WALLET_MAGIC_VALUE = 0xb0671381;
|
||||
/* Mainnet addresses */
|
||||
address constant public UNISWAP_EXCHANGE_FACTORY_ADDRESS = 0xc0a47dFe034B400B47bDaD5FecDa2621de6c4d95;
|
||||
address constant public WETH_ADDRESS = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
|
||||
@ -45,8 +45,15 @@ contract UniswapBridge is
|
||||
IEtherToken weth;
|
||||
}
|
||||
|
||||
/// @dev Whether we've granted an allowance to a spender for a token.
|
||||
mapping (address => mapping (address => bool)) private _hasAllowance;
|
||||
// solhint-disable no-empty-blocks
|
||||
/// @dev Payable fallback to receive ETH from uniswap.
|
||||
function ()
|
||||
external
|
||||
payable
|
||||
{}
|
||||
|
||||
/// @dev Whether we've granted an allowance to the exchange for a token.
|
||||
mapping (address => bool) private _hasAllowance;
|
||||
|
||||
/// @dev Callback for `IERC20Bridge`. Tries to buy `amount` of
|
||||
/// `toTokenAddress` tokens by selling the entirety of the `fromTokenAddress`
|
||||
@ -83,7 +90,7 @@ contract UniswapBridge is
|
||||
toTokenAddress
|
||||
);
|
||||
// Grant an allowance to the exchange.
|
||||
_grantAllowanceForTokens(address(state.exchange), [fromTokenAddress, toTokenAddress]);
|
||||
_grantExchangeAllowance(state.exchange);
|
||||
// Get our balance of `fromTokenAddress` token.
|
||||
state.fromTokenBalance = IERC20Token(fromTokenAddress).balanceOf(address(this));
|
||||
// Get the weth contract.
|
||||
@ -176,27 +183,16 @@ contract UniswapBridge is
|
||||
return IUniswapExchangeFactory(UNISWAP_EXCHANGE_FACTORY_ADDRESS);
|
||||
}
|
||||
|
||||
/// @dev Grants an unlimited allowance to `spender` for the tokens passed,
|
||||
/// if they're not WETH and we haven't already granted `spender` an
|
||||
/// allowance.
|
||||
/// @param spender The spender being granted an aloowance.
|
||||
/// @param tokenAddresses Array of token addresses.
|
||||
function _grantAllowanceForTokens(
|
||||
address spender,
|
||||
address[2] memory tokenAddresses
|
||||
)
|
||||
/// @dev Grants an unlimited allowance to the exchange for its token
|
||||
/// on behalf of this contract, if we haven't already done so.
|
||||
/// @param exchange The Uniswap token exchange.
|
||||
function _grantExchangeAllowance(IUniswapExchange exchange)
|
||||
private
|
||||
{
|
||||
address wethAddress = address(_getWethContract());
|
||||
mapping (address => bool) storage doesSpenderHaveAllowance = _hasAllowance[spender];
|
||||
for (uint256 i = 0; i < tokenAddresses.length; ++i) {
|
||||
address tokenAddress = tokenAddresses[i];
|
||||
if (tokenAddress != wethAddress) {
|
||||
if (!doesSpenderHaveAllowance[tokenAddress]) {
|
||||
IERC20Token(tokenAddress).approve(spender, uint256(-1));
|
||||
doesSpenderHaveAllowance[tokenAddress] = true;
|
||||
}
|
||||
}
|
||||
address tokenAddress = exchange.toTokenAddress();
|
||||
if (!_hasAllowance[tokenAddress]) {
|
||||
IERC20Token(tokenAddress).approve(address(exchange), uint256(-1));
|
||||
_hasAllowance[tokenAddress] = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -47,7 +47,6 @@ interface IUniswapExchange {
|
||||
uint256 deadline
|
||||
)
|
||||
external
|
||||
payable
|
||||
returns (uint256 ethBought);
|
||||
|
||||
/// @dev Buys at least `minTokensBought` tokens with the exchange token
|
||||
@ -68,4 +67,11 @@ interface IUniswapExchange {
|
||||
)
|
||||
external
|
||||
returns (uint256 tokensBought);
|
||||
|
||||
/// @dev Retrieves the token that is associated with this exchange.
|
||||
/// @return tokenAddress The token address.
|
||||
function toTokenAddress()
|
||||
external
|
||||
view
|
||||
returns (address tokenAddress);
|
||||
}
|
||||
|
@ -26,15 +26,9 @@ import "../src/interfaces/IUniswapExchangeFactory.sol";
|
||||
import "../src/interfaces/IUniswapExchange.sol";
|
||||
|
||||
|
||||
// solhint-disable no-simple-event-func-name
|
||||
contract TestEventsRaiser {
|
||||
|
||||
event SellAllAmount(
|
||||
address sellToken,
|
||||
uint256 sellTokenAmount,
|
||||
address buyToken,
|
||||
uint256 minimumFillAmount
|
||||
);
|
||||
|
||||
event TokenTransfer(
|
||||
address token,
|
||||
address from,
|
||||
@ -56,18 +50,21 @@ contract TestEventsRaiser {
|
||||
);
|
||||
|
||||
event EthToTokenTransferInput(
|
||||
address exchange,
|
||||
uint256 minTokensBought,
|
||||
uint256 deadline,
|
||||
address recipient
|
||||
);
|
||||
|
||||
event TokenToEthSwapInput(
|
||||
address exchange,
|
||||
uint256 tokensSold,
|
||||
uint256 minEthBought,
|
||||
uint256 deadline
|
||||
);
|
||||
|
||||
event TokenToTokenTransferInput(
|
||||
address exchange,
|
||||
uint256 tokensSold,
|
||||
uint256 minTokensBought,
|
||||
uint256 minEthBought,
|
||||
@ -84,6 +81,7 @@ contract TestEventsRaiser {
|
||||
external
|
||||
{
|
||||
emit EthToTokenTransferInput(
|
||||
msg.sender,
|
||||
minTokensBought,
|
||||
deadline,
|
||||
recipient
|
||||
@ -98,6 +96,7 @@ contract TestEventsRaiser {
|
||||
external
|
||||
{
|
||||
emit TokenToEthSwapInput(
|
||||
msg.sender,
|
||||
tokensSold,
|
||||
minEthBought,
|
||||
deadline
|
||||
@ -115,6 +114,7 @@ contract TestEventsRaiser {
|
||||
external
|
||||
{
|
||||
emit TokenToTokenTransferInput(
|
||||
msg.sender,
|
||||
tokensSold,
|
||||
minTokensBought,
|
||||
minEthBought,
|
||||
@ -158,21 +158,38 @@ contract TestEventsRaiser {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// @dev A minimalist ERC20/WETH token.
|
||||
contract TestToken {
|
||||
|
||||
using LibSafeMath for uint256;
|
||||
|
||||
mapping (address => uint256) public balances;
|
||||
string private _nextRevertReason;
|
||||
|
||||
/// @dev Calls `raiseTokenTransfer()` on the caller.
|
||||
/// @dev Set the balance for `owner`.
|
||||
function setBalance(address owner)
|
||||
external
|
||||
payable
|
||||
{
|
||||
balances[owner] = msg.value;
|
||||
}
|
||||
|
||||
/// @dev Set the revert reason for `transfer()`,
|
||||
/// `deposit()`, and `withdraw()`.
|
||||
function setRevertReason(string calldata reason)
|
||||
external
|
||||
{
|
||||
_nextRevertReason = reason;
|
||||
}
|
||||
|
||||
/// @dev Just calls `raiseTokenTransfer()` on the caller.
|
||||
function transfer(address to, uint256 amount)
|
||||
external
|
||||
returns (bool)
|
||||
{
|
||||
_revertIfReasonExists();
|
||||
TestEventsRaiser(msg.sender).raiseTokenTransfer(msg.sender, to, amount);
|
||||
balances[msg.sender] = balances[msg.sender].safeSub(amount);
|
||||
balances[to] = balances[to].safeAdd(amount);
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -185,20 +202,13 @@ contract TestToken {
|
||||
return true;
|
||||
}
|
||||
|
||||
/// @dev Set the balance for `owner`.
|
||||
function setBalance(address owner, uint256 balance)
|
||||
external
|
||||
payable
|
||||
{
|
||||
balances[owner] = balance;
|
||||
}
|
||||
|
||||
/// @dev `IWETH.deposit()` that increases balances and calls
|
||||
/// `raiseWethDeposit()` on the caller.
|
||||
function deposit()
|
||||
external
|
||||
payable
|
||||
{
|
||||
_revertIfReasonExists();
|
||||
balances[msg.sender] += balances[msg.sender].safeAdd(msg.value);
|
||||
TestEventsRaiser(msg.sender).raiseWethDeposit(msg.value);
|
||||
}
|
||||
@ -208,6 +218,7 @@ contract TestToken {
|
||||
function withdraw(uint256 amount)
|
||||
external
|
||||
{
|
||||
_revertIfReasonExists();
|
||||
balances[msg.sender] = balances[msg.sender].safeSub(amount);
|
||||
msg.sender.transfer(amount);
|
||||
TestEventsRaiser(msg.sender).raiseWethWithdraw(amount);
|
||||
@ -221,6 +232,15 @@ contract TestToken {
|
||||
{
|
||||
return balances[owner];
|
||||
}
|
||||
|
||||
function _revertIfReasonExists()
|
||||
private
|
||||
view
|
||||
{
|
||||
if (bytes(_nextRevertReason).length != 0) {
|
||||
revert(_nextRevertReason);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -229,21 +249,18 @@ contract TestExchange is
|
||||
{
|
||||
address public tokenAddress;
|
||||
string private _nextRevertReason;
|
||||
uint256 private _nextFillAmount;
|
||||
|
||||
constructor(address _tokenAddress) public {
|
||||
tokenAddress = _tokenAddress;
|
||||
}
|
||||
|
||||
function setFillBehavior(
|
||||
string calldata revertReason,
|
||||
uint256 fillAmount
|
||||
string calldata revertReason
|
||||
)
|
||||
external
|
||||
payable
|
||||
{
|
||||
_nextRevertReason = revertReason;
|
||||
_nextFillAmount = fillAmount;
|
||||
}
|
||||
|
||||
function ethToTokenTransferInput(
|
||||
@ -261,7 +278,7 @@ contract TestExchange is
|
||||
recipient
|
||||
);
|
||||
_revertIfReasonExists();
|
||||
return _nextFillAmount;
|
||||
return address(this).balance;
|
||||
}
|
||||
|
||||
function tokenToEthSwapInput(
|
||||
@ -270,7 +287,6 @@ contract TestExchange is
|
||||
uint256 deadline
|
||||
)
|
||||
external
|
||||
payable
|
||||
returns (uint256 ethBought)
|
||||
{
|
||||
TestEventsRaiser(msg.sender).raiseTokenToEthSwapInput(
|
||||
@ -279,7 +295,9 @@ contract TestExchange is
|
||||
deadline
|
||||
);
|
||||
_revertIfReasonExists();
|
||||
return _nextFillAmount;
|
||||
uint256 fillAmount = address(this).balance;
|
||||
msg.sender.transfer(fillAmount);
|
||||
return fillAmount;
|
||||
}
|
||||
|
||||
function tokenToTokenTransferInput(
|
||||
@ -302,11 +320,20 @@ contract TestExchange is
|
||||
toTokenAddress
|
||||
);
|
||||
_revertIfReasonExists();
|
||||
return _nextFillAmount;
|
||||
return address(this).balance;
|
||||
}
|
||||
|
||||
function toTokenAddress()
|
||||
external
|
||||
view
|
||||
returns (address _tokenAddress)
|
||||
{
|
||||
return tokenAddress;
|
||||
}
|
||||
|
||||
function _revertIfReasonExists()
|
||||
private
|
||||
view
|
||||
{
|
||||
if (bytes(_nextRevertReason).length != 0) {
|
||||
revert(_nextRevertReason);
|
||||
@ -321,50 +348,59 @@ contract TestUniswapBridge is
|
||||
TestEventsRaiser,
|
||||
UniswapBridge
|
||||
{
|
||||
|
||||
TestToken public wethToken = new TestToken();
|
||||
TestToken public wethToken;
|
||||
// Token address to TestToken instance.
|
||||
mapping (address => TestToken) private _testTokens;
|
||||
// Token address to TestExchange instance.
|
||||
mapping (address => TestExchange) private _testExchanges;
|
||||
|
||||
/// @dev Set token balances for this contract.
|
||||
function setTokenBalances(address tokenAddress, uint256 balance)
|
||||
external
|
||||
{
|
||||
TestToken token = _testTokens[tokenAddress];
|
||||
// Create the token if it doesn't exist.
|
||||
if (address(token) == address(0)) {
|
||||
_testTokens[tokenAddress] = token = new TestToken();
|
||||
}
|
||||
token.setBalance(address(this), balance);
|
||||
constructor() public {
|
||||
wethToken = new TestToken();
|
||||
_testTokens[address(wethToken)] = wethToken;
|
||||
}
|
||||
|
||||
/// @dev Set the behavior for a fill on a uniswap exchange.
|
||||
function setExchangeFillBehavior(
|
||||
address exchangeAddress,
|
||||
string calldata revertReason,
|
||||
uint256 fillAmount
|
||||
)
|
||||
/// @dev Sets the balance of this contract for an existing token.
|
||||
/// The wei attached will be the balance.
|
||||
function setTokenBalance(address tokenAddress)
|
||||
external
|
||||
payable
|
||||
{
|
||||
createExchange(exchangeAddress).setFillBehavior.value(msg.value)(
|
||||
revertReason,
|
||||
fillAmount
|
||||
);
|
||||
TestToken token = _testTokens[tokenAddress];
|
||||
token.deposit.value(msg.value)();
|
||||
}
|
||||
|
||||
/// @dev Create an exchange for a token.
|
||||
function createExchange(address tokenAddress)
|
||||
public
|
||||
returns (TestExchange exchangeAddress)
|
||||
/// @dev Sets the revert reason for an existing token.
|
||||
function setTokenRevertReason(address tokenAddress, string calldata revertReason)
|
||||
external
|
||||
{
|
||||
TestExchange exchange = _testExchanges[tokenAddress];
|
||||
if (address(exchange) == address(0)) {
|
||||
_testExchanges[tokenAddress] = exchange = new TestExchange(tokenAddress);
|
||||
TestToken token = _testTokens[tokenAddress];
|
||||
token.setRevertReason(revertReason);
|
||||
}
|
||||
|
||||
/// @dev Create a token and exchange (if they don't exist) for a new token
|
||||
/// and sets the exchange revert and fill behavior. The wei attached
|
||||
/// will be the fill amount for the exchange.
|
||||
/// @param tokenAddress The token address. If zero, one will be created.
|
||||
/// @param revertReason The revert reason for exchange operations.
|
||||
function createTokenAndExchange(
|
||||
address tokenAddress,
|
||||
string calldata revertReason
|
||||
)
|
||||
external
|
||||
payable
|
||||
returns (TestToken token, TestExchange exchange)
|
||||
{
|
||||
token = TestToken(tokenAddress);
|
||||
if (tokenAddress == address(0)) {
|
||||
token = new TestToken();
|
||||
}
|
||||
return exchange;
|
||||
_testTokens[address(token)] = token;
|
||||
exchange = _testExchanges[address(token)];
|
||||
if (address(exchange) == address(0)) {
|
||||
_testExchanges[address(token)] = exchange = new TestExchange(address(token));
|
||||
}
|
||||
exchange.setFillBehavior.value(msg.value)(revertReason);
|
||||
return (token, exchange);
|
||||
}
|
||||
|
||||
/// @dev `IUniswapExchangeFactory.getExchange`
|
||||
|
@ -2,13 +2,16 @@ import {
|
||||
blockchainTests,
|
||||
constants,
|
||||
expect,
|
||||
filterLogs,
|
||||
filterLogsToArguments,
|
||||
getRandomInteger,
|
||||
hexLeftPad,
|
||||
hexRandom,
|
||||
Numberish,
|
||||
randomAddress,
|
||||
TransactionHelper,
|
||||
} from '@0x/contracts-test-utils';
|
||||
import { AssetProxyId } from '@0x/types';
|
||||
import { BigNumber } from '@0x/utils';
|
||||
import { DecodedLogs } from 'ethereum-types';
|
||||
import * as _ from 'lodash';
|
||||
@ -16,15 +19,19 @@ import * as _ from 'lodash';
|
||||
import {
|
||||
artifacts,
|
||||
TestUniswapBridgeContract,
|
||||
TestUniswapBridgeEvents,
|
||||
TestUniswapBridgeSellAllAmountEventArgs,
|
||||
TestUniswapBridgeTokenTransferEventArgs,
|
||||
TestUniswapBridgeEthToTokenTransferInputEventArgs as EthToTokenTransferInputArgs,
|
||||
TestUniswapBridgeEvents as ContractEvents,
|
||||
TestUniswapBridgeTokenApproveEventArgs as TokenApproveArgs,
|
||||
TestUniswapBridgeTokenToEthSwapInputEventArgs as TokenToEthSwapInputArgs,
|
||||
TestUniswapBridgeTokenToTokenTransferInputEventArgs as TokenToTokenTransferInputArgs,
|
||||
TestUniswapBridgeTokenTransferEventArgs as TokenTransferArgs,
|
||||
TestUniswapBridgeWethDepositEventArgs as WethDepositArgs,
|
||||
TestUniswapBridgeWethWithdrawEventArgs as WethWithdrawArgs,
|
||||
} from '../src';
|
||||
|
||||
blockchainTests.resets('UniswapBridge unit tests', env => {
|
||||
blockchainTests.resets.only('UniswapBridge unit tests', env => {
|
||||
const txHelper = new TransactionHelper(env.web3Wrapper, artifacts);
|
||||
let testContract: TestUniswapBridgeContract;
|
||||
let daiTokenAddress: string;
|
||||
let wethTokenAddress: string;
|
||||
|
||||
before(async () => {
|
||||
@ -34,18 +41,7 @@ blockchainTests.resets('UniswapBridge unit tests', env => {
|
||||
env.txDefaults,
|
||||
artifacts,
|
||||
);
|
||||
[daiTokenAddress, wethTokenAddress] = await Promise.all([
|
||||
testContract.daiToken.callAsync(),
|
||||
testContract.wethToken.callAsync(),
|
||||
]);
|
||||
});
|
||||
|
||||
describe('deployment', () => {
|
||||
it('sets Uniswap allowances to maximum', async () => {
|
||||
const [wethAllowance, daiAllowance] = await testContract.getUniswapTokenAllowances.callAsync();
|
||||
expect(wethAllowance).to.bignumber.eq(constants.MAX_UINT256);
|
||||
expect(daiAllowance).to.bignumber.eq(constants.MAX_UINT256);
|
||||
});
|
||||
wethTokenAddress = await testContract.wethToken.callAsync();
|
||||
});
|
||||
|
||||
describe('isValidSignature()', () => {
|
||||
@ -56,125 +52,333 @@ blockchainTests.resets('UniswapBridge unit tests', env => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('transfer()', () => {
|
||||
interface TransferOpts {
|
||||
describe('withdrawTo()', () => {
|
||||
interface WithdrawToOpts {
|
||||
fromTokenAddress: string;
|
||||
toTokenAddress: string;
|
||||
fromTokenBalance: Numberish;
|
||||
toAddress: string;
|
||||
amount: Numberish;
|
||||
fromTokenBalance: Numberish;
|
||||
revertReason: string;
|
||||
fillAmount: Numberish;
|
||||
exchangeRevertReason: string;
|
||||
exchangeFillAmount: Numberish;
|
||||
toTokenRevertReason: string;
|
||||
fromTokenRevertReason: string;
|
||||
}
|
||||
|
||||
function createTransferOpts(opts?: Partial<TransferOpts>): TransferOpts {
|
||||
function createWithdrawToOpts(opts?: Partial<WithdrawToOpts>): WithdrawToOpts {
|
||||
return {
|
||||
toTokenAddress: _.sampleSize([wethTokenAddress, daiTokenAddress], 1)[0],
|
||||
fromTokenAddress: constants.NULL_ADDRESS,
|
||||
toTokenAddress: constants.NULL_ADDRESS,
|
||||
fromTokenBalance: getRandomInteger(1, 1e18),
|
||||
toAddress: randomAddress(),
|
||||
amount: getRandomInteger(1, 100e18),
|
||||
revertReason: '',
|
||||
fillAmount: getRandomInteger(1, 100e18),
|
||||
fromTokenBalance: getRandomInteger(1, 100e18),
|
||||
amount: getRandomInteger(1, 1e18),
|
||||
exchangeRevertReason: '',
|
||||
exchangeFillAmount: getRandomInteger(1, 1e18),
|
||||
toTokenRevertReason: '',
|
||||
fromTokenRevertReason: '',
|
||||
...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];
|
||||
interface WithdrawToResult {
|
||||
opts: WithdrawToOpts;
|
||||
result: string;
|
||||
logs: DecodedLogs;
|
||||
blockTime: number;
|
||||
}
|
||||
|
||||
function getOppositeToken(tokenAddress: string): string {
|
||||
if (tokenAddress === daiTokenAddress) {
|
||||
return wethTokenAddress;
|
||||
}
|
||||
return daiTokenAddress;
|
||||
async function withdrawToAsync(opts?: Partial<WithdrawToOpts>): Promise<WithdrawToResult> {
|
||||
const _opts = createWithdrawToOpts(opts);
|
||||
// Create the "from" token and exchange.
|
||||
[[_opts.fromTokenAddress]] = await txHelper.getResultAndReceiptAsync(
|
||||
testContract.createTokenAndExchange,
|
||||
_opts.fromTokenAddress,
|
||||
_opts.exchangeRevertReason,
|
||||
{ value: new BigNumber(_opts.exchangeFillAmount) },
|
||||
);
|
||||
// Create the "to" token and exchange.
|
||||
[[_opts.toTokenAddress]] = await txHelper.getResultAndReceiptAsync(
|
||||
testContract.createTokenAndExchange,
|
||||
_opts.toTokenAddress,
|
||||
_opts.exchangeRevertReason,
|
||||
{ value: new BigNumber(_opts.exchangeFillAmount) },
|
||||
);
|
||||
await testContract.setTokenRevertReason.awaitTransactionSuccessAsync(
|
||||
_opts.toTokenAddress,
|
||||
_opts.toTokenRevertReason,
|
||||
);
|
||||
await testContract.setTokenRevertReason.awaitTransactionSuccessAsync(
|
||||
_opts.fromTokenAddress,
|
||||
_opts.fromTokenRevertReason,
|
||||
);
|
||||
// Set the token balance for the token we're converting from.
|
||||
await testContract.setTokenBalance.awaitTransactionSuccessAsync(_opts.fromTokenAddress, {
|
||||
value: new BigNumber(_opts.fromTokenBalance),
|
||||
});
|
||||
// Call withdrawTo().
|
||||
const [result, receipt] = await txHelper.getResultAndReceiptAsync(
|
||||
testContract.withdrawTo,
|
||||
// The "to" token address.
|
||||
_opts.toTokenAddress,
|
||||
// The "from" address.
|
||||
randomAddress(),
|
||||
// The "to" address.
|
||||
_opts.toAddress,
|
||||
// The amount to transfer to "to"
|
||||
new BigNumber(_opts.amount),
|
||||
// ABI-encoded "from" token address.
|
||||
hexLeftPad(_opts.fromTokenAddress),
|
||||
);
|
||||
return {
|
||||
opts: _opts,
|
||||
result,
|
||||
logs: receipt.logs,
|
||||
blockTime: await env.web3Wrapper.getBlockTimestampAsync(receipt.blockNumber),
|
||||
};
|
||||
}
|
||||
|
||||
async function getExchangeForTokenAsync(tokenAddress: string): Promise<string> {
|
||||
return testContract.getExchange.callAsync(tokenAddress);
|
||||
}
|
||||
|
||||
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);
|
||||
const { result } = await withdrawToAsync();
|
||||
expect(result).to.eq(AssetProxyId.ERC20Bridge);
|
||||
});
|
||||
|
||||
it('calls `Uniswap.sellAllAmount()`', async () => {
|
||||
const opts = createTransferOpts();
|
||||
const [, logs] = await transferAsync(opts);
|
||||
const transfers = filterLogsToArguments<TestUniswapBridgeSellAllAmountEventArgs>(
|
||||
logs,
|
||||
TestUniswapBridgeEvents.SellAllAmount,
|
||||
it('just transfers tokens to `to` if the same tokens are in play', async () => {
|
||||
const [[tokenAddress]] = await txHelper.getResultAndReceiptAsync(
|
||||
testContract.createTokenAndExchange,
|
||||
constants.NULL_ADDRESS,
|
||||
'',
|
||||
);
|
||||
const { opts, result, logs } = await withdrawToAsync({
|
||||
fromTokenAddress: tokenAddress,
|
||||
toTokenAddress: tokenAddress,
|
||||
});
|
||||
expect(result).to.eq(AssetProxyId.ERC20Bridge);
|
||||
const transfers = filterLogsToArguments<TokenTransferArgs>(logs, ContractEvents.TokenTransfer);
|
||||
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('can swap DAI for WETH', async () => {
|
||||
const opts = createTransferOpts({ toTokenAddress: wethTokenAddress });
|
||||
const [, logs] = await transferAsync(opts);
|
||||
const transfers = filterLogsToArguments<TestUniswapBridgeSellAllAmountEventArgs>(
|
||||
logs,
|
||||
TestUniswapBridgeEvents.SellAllAmount,
|
||||
);
|
||||
expect(transfers.length).to.eq(1);
|
||||
expect(transfers[0].sellToken).to.eq(daiTokenAddress);
|
||||
expect(transfers[0].buyToken).to.eq(wethTokenAddress);
|
||||
});
|
||||
|
||||
it('can swap WETH for DAI', async () => {
|
||||
const opts = createTransferOpts({ toTokenAddress: daiTokenAddress });
|
||||
const [, logs] = await transferAsync(opts);
|
||||
const transfers = filterLogsToArguments<TestUniswapBridgeSellAllAmountEventArgs>(
|
||||
logs,
|
||||
TestUniswapBridgeEvents.SellAllAmount,
|
||||
);
|
||||
expect(transfers.length).to.eq(1);
|
||||
expect(transfers[0].sellToken).to.eq(wethTokenAddress);
|
||||
expect(transfers[0].buyToken).to.eq(daiTokenAddress);
|
||||
});
|
||||
|
||||
it('transfers filled amount to `to`', async () => {
|
||||
const opts = createTransferOpts();
|
||||
const [, logs] = await transferAsync(opts);
|
||||
const transfers = filterLogsToArguments<TestUniswapBridgeTokenTransferEventArgs>(
|
||||
logs,
|
||||
TestUniswapBridgeEvents.TokenTransfer,
|
||||
);
|
||||
expect(transfers.length).to.eq(1);
|
||||
expect(transfers[0].token).to.eq(opts.toTokenAddress);
|
||||
expect(transfers[0].token).to.eq(tokenAddress);
|
||||
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);
|
||||
expect(transfers[0].amount).to.bignumber.eq(opts.amount);
|
||||
});
|
||||
|
||||
it('fails if `Uniswap.sellAllAmount()` reverts', async () => {
|
||||
const opts = createTransferOpts({ revertReason: 'FOOBAR' });
|
||||
const tx = transferAsync(opts);
|
||||
return expect(tx).to.revertWith(opts.revertReason);
|
||||
describe('token -> token', () => {
|
||||
it('calls `IUniswapExchange.tokenToTokenTransferInput()', async () => {
|
||||
const { opts, logs, blockTime } = await withdrawToAsync();
|
||||
const exchangeAddress = await getExchangeForTokenAsync(opts.fromTokenAddress);
|
||||
const calls = filterLogsToArguments<TokenToTokenTransferInputArgs>(
|
||||
logs,
|
||||
ContractEvents.TokenToTokenTransferInput,
|
||||
);
|
||||
expect(calls.length).to.eq(1);
|
||||
expect(calls[0].exchange).to.eq(exchangeAddress);
|
||||
expect(calls[0].tokensSold).to.bignumber.eq(opts.fromTokenBalance);
|
||||
expect(calls[0].minTokensBought).to.bignumber.eq(0);
|
||||
expect(calls[0].minEthBought).to.bignumber.eq(0);
|
||||
expect(calls[0].deadline).to.bignumber.eq(blockTime);
|
||||
expect(calls[0].recipient).to.eq(opts.toAddress);
|
||||
expect(calls[0].toTokenAddress).to.eq(opts.toTokenAddress);
|
||||
});
|
||||
|
||||
it('sets allowance for "from" token', async () => {
|
||||
const { opts, logs } = await withdrawToAsync();
|
||||
const transfers = filterLogsToArguments<TokenApproveArgs>(logs, ContractEvents.TokenApprove);
|
||||
const exchangeAddress = await getExchangeForTokenAsync(opts.fromTokenAddress);
|
||||
expect(transfers.length).to.eq(1);
|
||||
expect(transfers[0].spender).to.eq(exchangeAddress);
|
||||
expect(transfers[0].allowance).to.bignumber.eq(constants.MAX_UINT256);
|
||||
});
|
||||
|
||||
it('sets allowance for "from" token only once', async () => {
|
||||
const { opts } = await withdrawToAsync();
|
||||
const { logs } = await withdrawToAsync(opts);
|
||||
const transfers = filterLogsToArguments<TokenApproveArgs>(logs, ContractEvents.TokenApprove);
|
||||
expect(transfers.length).to.eq(0);
|
||||
});
|
||||
|
||||
it('fails if "from" token does not exist', async () => {
|
||||
const tx = testContract.withdrawTo.awaitTransactionSuccessAsync(
|
||||
randomAddress(),
|
||||
randomAddress(),
|
||||
randomAddress(),
|
||||
getRandomInteger(1, 1e18),
|
||||
hexLeftPad(randomAddress()),
|
||||
);
|
||||
return expect(tx).to.revertWith('NO_UNISWAP_EXCHANGE_FOR_TOKEN');
|
||||
});
|
||||
|
||||
it('fails if the exchange fails', async () => {
|
||||
const revertReason = 'FOOBAR';
|
||||
const tx = withdrawToAsync({
|
||||
exchangeRevertReason: revertReason,
|
||||
});
|
||||
return expect(tx).to.revertWith(revertReason);
|
||||
});
|
||||
});
|
||||
|
||||
describe('token -> ETH', () => {
|
||||
it('calls `IUniswapExchange.tokenToEthSwapInput()`, `WETH.deposit()`, then `transfer()`', async () => {
|
||||
const { opts, logs, blockTime } = await withdrawToAsync({
|
||||
toTokenAddress: wethTokenAddress,
|
||||
});
|
||||
const exchangeAddress = await getExchangeForTokenAsync(opts.fromTokenAddress);
|
||||
let calls: any = filterLogs<TokenToEthSwapInputArgs>(logs, ContractEvents.TokenToEthSwapInput);
|
||||
expect(calls.length).to.eq(1);
|
||||
expect(calls[0].args.exchange).to.eq(exchangeAddress);
|
||||
expect(calls[0].args.tokensSold).to.bignumber.eq(opts.fromTokenBalance);
|
||||
expect(calls[0].args.minEthBought).to.bignumber.eq(0);
|
||||
expect(calls[0].args.deadline).to.bignumber.eq(blockTime);
|
||||
calls = filterLogs<WethDepositArgs>(
|
||||
logs.slice(calls[0].logIndex as number),
|
||||
ContractEvents.WethDeposit,
|
||||
);
|
||||
expect(calls.length).to.eq(1);
|
||||
expect(calls[0].args.amount).to.bignumber.eq(opts.exchangeFillAmount);
|
||||
calls = filterLogs<TokenTransferArgs>(
|
||||
logs.slice(calls[0].logIndex as number),
|
||||
ContractEvents.TokenTransfer,
|
||||
);
|
||||
expect(calls.length).to.eq(1);
|
||||
expect(calls[0].args.token).to.eq(opts.toTokenAddress);
|
||||
expect(calls[0].args.from).to.eq(testContract.address);
|
||||
expect(calls[0].args.to).to.eq(opts.toAddress);
|
||||
expect(calls[0].args.amount).to.bignumber.eq(opts.exchangeFillAmount);
|
||||
});
|
||||
|
||||
it('calls `IUniswapExchange.tokenToEthSwapInput()`', async () => {
|
||||
const { opts, logs, blockTime } = await withdrawToAsync({
|
||||
toTokenAddress: wethTokenAddress,
|
||||
});
|
||||
const calls = filterLogsToArguments<TokenToEthSwapInputArgs>(logs, ContractEvents.TokenToEthSwapInput);
|
||||
const exchangeAddress = await getExchangeForTokenAsync(opts.fromTokenAddress);
|
||||
expect(calls.length).to.eq(1);
|
||||
expect(calls[0].exchange).to.eq(exchangeAddress);
|
||||
expect(calls[0].tokensSold).to.bignumber.eq(opts.fromTokenBalance);
|
||||
expect(calls[0].minEthBought).to.bignumber.eq(0);
|
||||
expect(calls[0].deadline).to.bignumber.eq(blockTime);
|
||||
});
|
||||
|
||||
it('sets allowance for "from" token', async () => {
|
||||
const { opts, logs } = await withdrawToAsync({
|
||||
toTokenAddress: wethTokenAddress,
|
||||
});
|
||||
const transfers = filterLogsToArguments<TokenApproveArgs>(logs, ContractEvents.TokenApprove);
|
||||
const exchangeAddress = await getExchangeForTokenAsync(opts.fromTokenAddress);
|
||||
expect(transfers.length).to.eq(1);
|
||||
expect(transfers[0].spender).to.eq(exchangeAddress);
|
||||
expect(transfers[0].allowance).to.bignumber.eq(constants.MAX_UINT256);
|
||||
});
|
||||
|
||||
it('sets allowance for "from" token only once', async () => {
|
||||
const { opts } = await withdrawToAsync({
|
||||
toTokenAddress: wethTokenAddress,
|
||||
});
|
||||
const { logs } = await withdrawToAsync(opts);
|
||||
const transfers = filterLogsToArguments<TokenApproveArgs>(logs, ContractEvents.TokenApprove);
|
||||
expect(transfers.length).to.eq(0);
|
||||
});
|
||||
|
||||
it('fails if "from" token does not exist', async () => {
|
||||
const tx = testContract.withdrawTo.awaitTransactionSuccessAsync(
|
||||
randomAddress(),
|
||||
randomAddress(),
|
||||
randomAddress(),
|
||||
getRandomInteger(1, 1e18),
|
||||
hexLeftPad(wethTokenAddress),
|
||||
);
|
||||
return expect(tx).to.revertWith('NO_UNISWAP_EXCHANGE_FOR_TOKEN');
|
||||
});
|
||||
|
||||
it('fails if `WETH.deposit()` fails', async () => {
|
||||
const revertReason = 'FOOBAR';
|
||||
const tx = withdrawToAsync({
|
||||
toTokenAddress: wethTokenAddress,
|
||||
toTokenRevertReason: revertReason,
|
||||
});
|
||||
return expect(tx).to.revertWith(revertReason);
|
||||
});
|
||||
|
||||
it('fails if the exchange fails', async () => {
|
||||
const revertReason = 'FOOBAR';
|
||||
const tx = withdrawToAsync({
|
||||
toTokenAddress: wethTokenAddress,
|
||||
exchangeRevertReason: revertReason,
|
||||
});
|
||||
return expect(tx).to.revertWith(revertReason);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ETH -> token', () => {
|
||||
it('calls `WETH.withdraw()`, then `IUniswapExchange.ethToTokenTransferInput()`', async () => {
|
||||
const { opts, logs, blockTime } = await withdrawToAsync({
|
||||
fromTokenAddress: wethTokenAddress,
|
||||
});
|
||||
const exchangeAddress = await getExchangeForTokenAsync(opts.toTokenAddress);
|
||||
let calls: any = filterLogs<WethWithdrawArgs>(logs, ContractEvents.WethWithdraw);
|
||||
expect(calls.length).to.eq(1);
|
||||
expect(calls[0].args.amount).to.bignumber.eq(opts.fromTokenBalance);
|
||||
calls = filterLogs<EthToTokenTransferInputArgs>(
|
||||
logs.slice(calls[0].logIndex as number),
|
||||
ContractEvents.EthToTokenTransferInput,
|
||||
);
|
||||
expect(calls.length).to.eq(1);
|
||||
expect(calls[0].args.exchange).to.eq(exchangeAddress);
|
||||
expect(calls[0].args.minTokensBought).to.bignumber.eq(0);
|
||||
expect(calls[0].args.deadline).to.bignumber.eq(blockTime);
|
||||
expect(calls[0].args.recipient).to.eq(opts.toAddress);
|
||||
});
|
||||
|
||||
it('sets allowance for "to" token', async () => {
|
||||
const { opts, logs } = await withdrawToAsync({
|
||||
fromTokenAddress: wethTokenAddress,
|
||||
});
|
||||
const transfers = filterLogsToArguments<TokenApproveArgs>(logs, ContractEvents.TokenApprove);
|
||||
const exchangeAddress = await getExchangeForTokenAsync(opts.toTokenAddress);
|
||||
expect(transfers.length).to.eq(1);
|
||||
expect(transfers[0].spender).to.eq(exchangeAddress);
|
||||
expect(transfers[0].allowance).to.bignumber.eq(constants.MAX_UINT256);
|
||||
});
|
||||
|
||||
it('sets allowance for "from" token only', async () => {
|
||||
const { opts } = await withdrawToAsync({
|
||||
fromTokenAddress: wethTokenAddress,
|
||||
});
|
||||
const { logs } = await withdrawToAsync(opts);
|
||||
const transfers = filterLogsToArguments<TokenApproveArgs>(logs, ContractEvents.TokenApprove);
|
||||
expect(transfers.length).to.eq(0);
|
||||
});
|
||||
|
||||
it('fails if "to" token does not exist', async () => {
|
||||
const tx = testContract.withdrawTo.awaitTransactionSuccessAsync(
|
||||
wethTokenAddress,
|
||||
randomAddress(),
|
||||
randomAddress(),
|
||||
getRandomInteger(1, 1e18),
|
||||
hexLeftPad(randomAddress()),
|
||||
);
|
||||
return expect(tx).to.revertWith('NO_UNISWAP_EXCHANGE_FOR_TOKEN');
|
||||
});
|
||||
|
||||
it('fails if the `WETH.withdraw()` fails', async () => {
|
||||
const revertReason = 'FOOBAR';
|
||||
const tx = withdrawToAsync({
|
||||
fromTokenAddress: wethTokenAddress,
|
||||
fromTokenRevertReason: revertReason,
|
||||
});
|
||||
return expect(tx).to.revertWith(revertReason);
|
||||
});
|
||||
|
||||
it('fails if the exchange fails', async () => {
|
||||
const revertReason = 'FOOBAR';
|
||||
const tx = withdrawToAsync({
|
||||
fromTokenAddress: wethTokenAddress,
|
||||
exchangeRevertReason: revertReason,
|
||||
});
|
||||
return expect(tx).to.revertWith(revertReason);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
Loading…
x
Reference in New Issue
Block a user