Added more integration tests for DydxBridge with the Exchange (demonstrates use cases)

This commit is contained in:
Greg Hysen 2019-12-18 13:37:17 -08:00
parent d4e46c5a9c
commit 4415e00b38
5 changed files with 219 additions and 58 deletions

View File

@ -19,9 +19,49 @@
pragma solidity ^0.5.9; pragma solidity ^0.5.9;
pragma experimental ABIEncoderV2; pragma experimental ABIEncoderV2;
import "@0x/contracts-erc20/contracts/src/interfaces/IERC20Token.sol";
import "../src/bridges/DydxBridge.sol"; import "../src/bridges/DydxBridge.sol";
contract TestDydxBridgeToken {
uint256 private constant INIT_HOLDER_BALANCE = 10 * 10**18; // 10 tokens
mapping (address => uint256) private _balances;
/// @dev Sets initial balance of token holders.
constructor(address[] memory holders)
public
{
for (uint256 i = 0; i != holders.length; ++i) {
_balances[holders[i]] = INIT_HOLDER_BALANCE;
}
_balances[msg.sender] = INIT_HOLDER_BALANCE;
}
/// @dev Basic transferFrom implementation.
function transferFrom(address from, address to, uint256 amount)
external
returns (bool)
{
if (_balances[from] < amount || _balances[to] + amount < _balances[to]) {
return false;
}
_balances[from] -= amount;
_balances[to] += amount;
return true;
}
/// @dev Returns balance of `holder`.
function balanceOf(address holder)
external
view
returns (uint256)
{
return _balances[holder];
}
}
// solhint-disable space-after-comma // solhint-disable space-after-comma
contract TestDydxBridge is contract TestDydxBridge is
IDydx, IDydx,
@ -29,8 +69,8 @@ contract TestDydxBridge is
{ {
address private constant ALWAYS_REVERT_ADDRESS = address(1); address private constant ALWAYS_REVERT_ADDRESS = address(1);
mapping (address => uint256) private balances; address private _testTokenAddress;
bool private shouldRevertOnOperate; bool private _shouldRevertOnOperate;
event OperateAccount( event OperateAccount(
address owner, address owner,
@ -51,6 +91,13 @@ contract TestDydxBridge is
bytes data bytes data
); );
constructor(address[] memory holders)
public
{
// Deploy a test token. This represents the asset being deposited/withdrawn from dydx.
_testTokenAddress = address(new TestDydxBridgeToken(holders));
}
/// @dev Simulates `operate` in dydx contract. /// @dev Simulates `operate` in dydx contract.
/// Emits events so that arguments can be validated client-side. /// Emits events so that arguments can be validated client-side.
function operate( function operate(
@ -59,7 +106,7 @@ contract TestDydxBridge is
) )
external external
{ {
if (shouldRevertOnOperate) { if (_shouldRevertOnOperate) {
revert("TestDydxBridge/SHOULD_REVERT_ON_OPERATE"); revert("TestDydxBridge/SHOULD_REVERT_ON_OPERATE");
} }
@ -86,9 +133,23 @@ contract TestDydxBridge is
); );
if (actions[i].actionType == IDydx.ActionType.Withdraw) { if (actions[i].actionType == IDydx.ActionType.Withdraw) {
balances[actions[i].otherAddress] += actions[i].amount.value; require(
IERC20Token(_testTokenAddress).transferFrom(
address(this),
actions[i].otherAddress,
actions[i].amount.value
),
"TestDydxBridge/WITHDRAW_FAILED"
);
} else if (actions[i].actionType == IDydx.ActionType.Deposit) { } else if (actions[i].actionType == IDydx.ActionType.Deposit) {
balances[actions[i].otherAddress] -= actions[i].amount.value; require(
IERC20Token(_testTokenAddress).transferFrom(
actions[i].otherAddress,
address(this),
actions[i].amount.value
),
"TestDydxBridge/DEPOSIT_FAILED"
);
} else { } else {
revert("TestDydxBridge/UNSUPPORTED_ACTION"); revert("TestDydxBridge/UNSUPPORTED_ACTION");
} }
@ -99,16 +160,15 @@ contract TestDydxBridge is
function setRevertOnOperate(bool shouldRevert) function setRevertOnOperate(bool shouldRevert)
external external
{ {
shouldRevertOnOperate = shouldRevert; _shouldRevertOnOperate = shouldRevert;
} }
/// @dev Returns balance of `holder`. /// @dev Returns test token.
function balanceOf(address holder) function getTestToken()
external external
view returns (address)
returns (uint256)
{ {
return balances[holder]; return _testTokenAddress;
} }
/// @dev overrides `_getDydxAddress()` from `DeploymentConstants` to return this address. /// @dev overrides `_getDydxAddress()` from `DeploymentConstants` to return this address.

View File

@ -12,18 +12,17 @@ import { TestDydxBridgeContract, TestDydxBridgeEvents } from './wrappers';
blockchainTests.resets('DydxBridge unit tests', env => { blockchainTests.resets('DydxBridge unit tests', env => {
const defaultAccountNumber = new BigNumber(1); const defaultAccountNumber = new BigNumber(1);
const marketId = new BigNumber(2); const marketId = new BigNumber(2);
let tokenAddress: string;
const defaultAmount = new BigNumber(4); const defaultAmount = new BigNumber(4);
const notAuthorized = '0x0000000000000000000000000000000000000001'; const notAuthorized = '0x0000000000000000000000000000000000000001';
const defaultDepositAction = { const defaultDepositAction = {
actionType: DydxBridgeActionType.Deposit as number, actionType: DydxBridgeActionType.Deposit,
accountId: constants.ZERO_AMOUNT, accountId: constants.ZERO_AMOUNT,
marketId, marketId,
conversionRateNumerator: constants.ZERO_AMOUNT, conversionRateNumerator: constants.ZERO_AMOUNT,
conversionRateDenominator: constants.ZERO_AMOUNT, conversionRateDenominator: constants.ZERO_AMOUNT,
}; };
const defaultWithdrawAction = { const defaultWithdrawAction = {
actionType: DydxBridgeActionType.Withdraw as number, actionType: DydxBridgeActionType.Withdraw,
accountId: constants.ZERO_AMOUNT, accountId: constants.ZERO_AMOUNT,
marketId, marketId,
conversionRateNumerator: constants.ZERO_AMOUNT, conversionRateNumerator: constants.ZERO_AMOUNT,
@ -48,6 +47,7 @@ blockchainTests.resets('DydxBridge unit tests', env => {
env.provider, env.provider,
env.txDefaults, env.txDefaults,
artifacts, artifacts,
[accountOwner, receiver],
); );
// Deploy test erc20 bridge proxy // Deploy test erc20 bridge proxy
@ -61,7 +61,6 @@ blockchainTests.resets('DydxBridge unit tests', env => {
// Setup asset data encoder // Setup asset data encoder
assetDataEncoder = new IAssetDataContract(constants.NULL_ADDRESS, env.provider); assetDataEncoder = new IAssetDataContract(constants.NULL_ADDRESS, env.provider);
tokenAddress = testContract.address;
}); });
describe('bridgeTransferFrom()', () => { describe('bridgeTransferFrom()', () => {
@ -289,8 +288,9 @@ blockchainTests.resets('DydxBridge unit tests', env => {
let assetData: string; let assetData: string;
before(async () => { before(async () => {
const testTokenAddress = await testContract.getTestToken().callAsync();
assetData = assetDataEncoder assetData = assetDataEncoder
.ERC20Bridge(tokenAddress, testContract.address, dydxBridgeDataEncoder.encode({ bridgeData })) .ERC20Bridge(testTokenAddress, testContract.address, dydxBridgeDataEncoder.encode({ bridgeData }))
.getABIEncodedTransactionData(); .getABIEncodedTransactionData();
}); });

View File

@ -10,11 +10,13 @@ export async function deployDydxBridgeAsync(
deployment: DeploymentManager, deployment: DeploymentManager,
environment: BlockchainTestsEnvironment, environment: BlockchainTestsEnvironment,
): Promise<TestDydxBridgeContract> { ): Promise<TestDydxBridgeContract> {
const tokenHolders = deployment.accounts;
const dydxBridge = await TestDydxBridgeContract.deployFrom0xArtifactAsync( const dydxBridge = await TestDydxBridgeContract.deployFrom0xArtifactAsync(
assetProxyArtifacts.TestDydxBridge, assetProxyArtifacts.TestDydxBridge,
environment.provider, environment.provider,
deployment.txDefaults, deployment.txDefaults,
assetProxyArtifacts, assetProxyArtifacts,
tokenHolders,
); );
return dydxBridge; return dydxBridge;
} }

View File

@ -106,16 +106,16 @@ blockchainTests.resets.fork('Mainnet dydx bridge tests', env => {
log.event === 'LogDeposit' log.event === 'LogDeposit'
? expectedDepositEvents[nextExpectedDepositEventIdx++] ? expectedDepositEvents[nextExpectedDepositEventIdx++]
: expectedWithdrawEvents[nextExpectedWithdrawEventIdx++]; : expectedWithdrawEvents[nextExpectedWithdrawEventIdx++];
expect(expectedEvent.accountOwner, 'accountOwner').to.equal(log.args.accountOwner); expect(log.args.accountOwner, 'accountOwner').to.equal(expectedEvent.accountOwner);
expect(expectedEvent.accountNumber, 'accountNumber').to.bignumber.equal(log.args.accountNumber); expect(log.args.accountNumber, 'accountNumber').to.bignumber.equal(expectedEvent.accountNumber);
expect(expectedEvent.market, 'market').to.bignumber.equal(log.args.market); expect(log.args.market, 'market').to.bignumber.equal(expectedEvent.market);
expect(expectedEvent.from, 'from').to.equal(log.args.from); expect(log.args.from, 'from').to.equal(expectedEvent.from);
// We only check the first update field because it's the delta balance (amount deposited). // We only check the first update field because it's the delta balance (amount deposited).
// The next field is the new total, which depends on interest rates at the time of execution. // The next field is the new total, which depends on interest rates at the time of execution.
expect(expectedEvent.update[0][0], 'update sign').to.equal(log.args.update[0][0]); expect(log.args.update[0][0], 'update sign').to.equal(expectedEvent.update[0][0]);
const updateValueHex = log.args.update[0][1]._hex; const updateValueHex = log.args.update[0][1]._hex;
const updateValueBn = new BigNumber(updateValueHex, 16); const updateValueBn = new BigNumber(updateValueHex, 16);
expect(expectedEvent.update[0][1], 'update value').to.bignumber.equal(updateValueBn); expect(updateValueBn, 'update value').to.bignumber.equal(expectedEvent.update[0][1]);
} }
}; };

View File

@ -1,8 +1,9 @@
import { DydxBridgeActionType, dydxBridgeDataEncoder, TestDydxBridgeContract } from '@0x/contracts-asset-proxy'; import { DydxBridgeActionType, dydxBridgeDataEncoder, TestDydxBridgeContract } from '@0x/contracts-asset-proxy';
import { DummyERC20TokenContract } from '@0x/contracts-erc20'; import { DummyERC20TokenContract } from '@0x/contracts-erc20';
import { blockchainTests, constants, describe, expect, toBaseUnitAmount } from '@0x/contracts-test-utils'; import { blockchainTests, constants, describe, expect, toBaseUnitAmount } from '@0x/contracts-test-utils';
import { SignedOrder } from '@0x/order-utils';
import { BigNumber } from '@0x/utils'; import { BigNumber } from '@0x/utils';
import { DecodedLogArgs, LogEntry, LogWithDecodedArgs } from 'ethereum-types'; import { DecodedLogArgs, LogWithDecodedArgs } from 'ethereum-types';
import * as _ from 'lodash'; import * as _ from 'lodash';
import { deployDydxBridgeAsync } from '../bridges/deploy_dydx_bridge'; import { deployDydxBridgeAsync } from '../bridges/deploy_dydx_bridge';
@ -16,19 +17,18 @@ blockchainTests.resets('Exchange fills dydx orders', env => {
let makerToken: DummyERC20TokenContract; let makerToken: DummyERC20TokenContract;
let takerToken: DummyERC20TokenContract; let takerToken: DummyERC20TokenContract;
const marketId = new BigNumber(3); const marketId = new BigNumber(3);
const makerAssetAmount = toBaseUnitAmount(6); const dydxConversionRateNumerator = toBaseUnitAmount(6);
const takerAssetAmount = toBaseUnitAmount(1); const dydxConversionRateDenominator = toBaseUnitAmount(1);
let maker: Maker; let maker: Maker;
let taker: Taker; let taker: Taker;
let orderConfig: Partial<SignedOrder>;
let dydxBridgeProxyAssetData: string;
const defaultDepositAction = { const defaultDepositAction = {
actionType: DydxBridgeActionType.Deposit as number, actionType: DydxBridgeActionType.Deposit as number,
accountId: constants.ZERO_AMOUNT, accountId: constants.ZERO_AMOUNT,
marketId, marketId,
// The bridge is passed the `makerAssetFillAmount` and we conversionRateNumerator: dydxConversionRateNumerator,
// want to compute the input `takerAssetFillAmount` conversionRateDenominator: dydxConversionRateDenominator,
// => multiply by `takerAssetAmount` / `makerAssetAmount`.
conversionRateNumerator: takerAssetAmount,
conversionRateDenominator: makerAssetAmount,
}; };
const defaultWithdrawAction = { const defaultWithdrawAction = {
actionType: DydxBridgeActionType.Withdraw as number, actionType: DydxBridgeActionType.Withdraw as number,
@ -49,16 +49,17 @@ blockchainTests.resets('Exchange fills dydx orders', env => {
}); });
testContract = await deployDydxBridgeAsync(deployment, env); testContract = await deployDydxBridgeAsync(deployment, env);
const encodedBridgeData = dydxBridgeDataEncoder.encode({ bridgeData }); const encodedBridgeData = dydxBridgeDataEncoder.encode({ bridgeData });
const bridgeProxyAssetData = deployment.assetDataEncoder const testTokenAddress = await testContract.getTestToken().callAsync();
.ERC20Bridge(testContract.address, testContract.address, encodedBridgeData) dydxBridgeProxyAssetData = deployment.assetDataEncoder
.ERC20Bridge(testTokenAddress, testContract.address, encodedBridgeData)
.getABIEncodedTransactionData(); .getABIEncodedTransactionData();
[makerToken, takerToken] = deployment.tokens.erc20; [makerToken, takerToken] = deployment.tokens.erc20;
// Configure Maker & Taker. // Configure default order parameters.
const orderConfig = { orderConfig = {
makerAssetAmount, makerAssetAmount: toBaseUnitAmount(1),
takerAssetAmount, takerAssetAmount: toBaseUnitAmount(1),
makerAssetData: bridgeProxyAssetData, makerAssetData: deployment.assetDataEncoder.ERC20Token(makerToken.address).getABIEncodedTransactionData(),
takerAssetData: deployment.assetDataEncoder.ERC20Token(takerToken.address).getABIEncodedTransactionData(), takerAssetData: deployment.assetDataEncoder.ERC20Token(takerToken.address).getABIEncodedTransactionData(),
// Not important for this test. // Not important for this test.
feeRecipientAddress: constants.NULL_ADDRESS, feeRecipientAddress: constants.NULL_ADDRESS,
@ -68,14 +69,19 @@ blockchainTests.resets('Exchange fills dydx orders', env => {
takerFeeAssetData: deployment.assetDataEncoder takerFeeAssetData: deployment.assetDataEncoder
.ERC20Token(takerToken.address) .ERC20Token(takerToken.address)
.getABIEncodedTransactionData(), .getABIEncodedTransactionData(),
makerFee: constants.ZERO_AMOUNT, makerFee: toBaseUnitAmount(1),
takerFee: constants.ZERO_AMOUNT, takerFee: toBaseUnitAmount(1),
}; };
// Configure maker.
maker = new Maker({ maker = new Maker({
name: 'Maker', name: 'Maker',
deployment, deployment,
orderConfig, orderConfig,
}); });
await maker.configureERC20TokenAsync(makerToken, deployment.assetProxies.erc20Proxy.address);
// Configure taker.
taker = new Taker({ taker = new Taker({
name: 'Taker', name: 'Taker',
deployment, deployment,
@ -88,46 +94,139 @@ blockchainTests.resets('Exchange fills dydx orders', env => {
}); });
describe('fillOrder', () => { describe('fillOrder', () => {
const verifyEvents = (logs: Array<LogWithDecodedArgs<DecodedLogArgs> | LogEntry>): void => { interface DydxFillResults {
makerAssetFilledAmount: BigNumber;
takerAssetFilledAmount: BigNumber;
makerFeePaid: BigNumber;
takerFeePaid: BigNumber;
amountDepositedIntoDydx: BigNumber;
amountWithdrawnFromDydx: BigNumber;
}
const fillOrder = async (
signedOrder: SignedOrder,
customTakerAssetFillAmount?: BigNumber,
): Promise<DydxFillResults> => {
// Fill order
const takerAssetFillAmount =
customTakerAssetFillAmount !== undefined ? customTakerAssetFillAmount : signedOrder.takerAssetAmount;
const tx = await taker.fillOrderAsync(signedOrder, takerAssetFillAmount);
// Extract values from fill event. // Extract values from fill event.
// tslint:disable no-unnecessary-type-assertion // tslint:disable no-unnecessary-type-assertion
const fillEvent = _.find(logs, log => { const fillEvent = _.find(tx.logs, log => {
return (log as any).event === 'Fill'; return (log as any).event === 'Fill';
}) as LogWithDecodedArgs<DecodedLogArgs>; }) as LogWithDecodedArgs<DecodedLogArgs>;
const makerAssetFilledAmount = fillEvent.args.makerAssetFilledAmount;
const takerAssetFilledAmount = fillEvent.args.takerAssetFilledAmount;
// Extract amount deposited into dydx from maker. // Extract amount deposited into dydx from maker.
const dydxDepositEvent = _.find(logs, log => { const dydxDepositEvent = _.find(tx.logs, log => {
return ( return (
(log as any).event === 'OperateAction' && (log as any).event === 'OperateAction' &&
(log as any).args.actionType === DydxBridgeActionType.Deposit (log as any).args.actionType === DydxBridgeActionType.Deposit
); );
}) as LogWithDecodedArgs<DecodedLogArgs>; }) as LogWithDecodedArgs<DecodedLogArgs>;
const amountDepositedIntoDydx = dydxDepositEvent.args.amountValue;
// Extract amount withdrawn from dydx to taker. // Extract amount withdrawn from dydx to taker.
const dydxWithdrawEvent = _.find(logs, log => { const dydxWithdrawEvent = _.find(tx.logs, log => {
return ( return (
(log as any).event === 'OperateAction' && (log as any).event === 'OperateAction' &&
(log as any).args.actionType === DydxBridgeActionType.Withdraw (log as any).args.actionType === DydxBridgeActionType.Withdraw
); );
}) as LogWithDecodedArgs<DecodedLogArgs>; }) as LogWithDecodedArgs<DecodedLogArgs>;
const amountWithdrawnFromDydx = dydxWithdrawEvent.args.amountValue;
// Assert fill amounts match amounts deposited/withdrawn from dydx. // Return values of interest for assertions.
expect(makerAssetFilledAmount).to.bignumber.equal(amountWithdrawnFromDydx); return {
expect(takerAssetFilledAmount).to.bignumber.equal(amountDepositedIntoDydx); makerAssetFilledAmount: fillEvent.args.makerAssetFilledAmount,
takerAssetFilledAmount: fillEvent.args.takerAssetFilledAmount,
makerFeePaid: fillEvent.args.makerFeePaid,
takerFeePaid: fillEvent.args.takerFeePaid,
amountDepositedIntoDydx: dydxDepositEvent.args.amountValue,
amountWithdrawnFromDydx: dydxWithdrawEvent.args.amountValue,
};
}; };
it('should successfully fill a dydx order', async () => { it('should successfully fill a dydx order (DydxBridge used in makerAssetData)', async () => {
const signedOrder = await maker.signOrderAsync(); const signedOrder = await maker.signOrderAsync({
const tx = await taker.fillOrderAsync(signedOrder, signedOrder.takerAssetAmount); // Invert the dydx conversion rate when using the bridge in the makerAssetData.
verifyEvents(tx.logs); makerAssetData: dydxBridgeProxyAssetData,
makerAssetAmount: dydxConversionRateDenominator,
takerAssetAmount: dydxConversionRateNumerator,
});
const dydxFillResults = await fillOrder(signedOrder);
expect(
dydxFillResults.makerAssetFilledAmount,
'makerAssetFilledAmount should equal amountWithdrawnFromDydx',
).to.bignumber.equal(dydxFillResults.amountWithdrawnFromDydx);
expect(
dydxFillResults.takerAssetFilledAmount,
'takerAssetFilledAmount should equal amountDepositedIntoDydx',
).to.bignumber.equal(dydxFillResults.amountDepositedIntoDydx);
}); });
it('should partially fill a dydx order', async () => { it('should successfully fill a dydx order (DydxBridge used in takerAssetData)', async () => {
const signedOrder = await maker.signOrderAsync(); const signedOrder = await maker.signOrderAsync({
const tx = await taker.fillOrderAsync(signedOrder, signedOrder.takerAssetAmount.div(2)); // Match the dydx conversion rate when using the bridge in the takerAssetData.
verifyEvents(tx.logs); takerAssetData: dydxBridgeProxyAssetData,
makerAssetAmount: dydxConversionRateNumerator,
takerAssetAmount: dydxConversionRateDenominator,
});
const dydxFillResults = await fillOrder(signedOrder);
expect(
dydxFillResults.makerAssetFilledAmount,
'makerAssetFilledAmount should equal amountDepositedIntoDydx',
).to.bignumber.equal(dydxFillResults.amountDepositedIntoDydx);
expect(
dydxFillResults.takerAssetFilledAmount,
'takerAssetFilledAmount should equal amountWithdrawnFromDydx',
).to.bignumber.equal(dydxFillResults.amountWithdrawnFromDydx);
});
it('should successfully fill a dydx order (DydxBridge used in makerFeeAssetData)', async () => {
const signedOrder = await maker.signOrderAsync({
// Invert the dydx conversion rate when using the bridge in the makerFeeAssetData.
makerFeeAssetData: dydxBridgeProxyAssetData,
makerFee: dydxConversionRateDenominator,
takerFee: dydxConversionRateNumerator,
});
const dydxFillResults = await fillOrder(signedOrder);
expect(
dydxFillResults.makerFeePaid,
'makerFeePaid should equal amountWithdrawnFromDydx',
).to.bignumber.equal(dydxFillResults.amountWithdrawnFromDydx);
expect(
dydxFillResults.takerFeePaid,
'takerFeePaid should equal amountDepositedIntoDydx',
).to.bignumber.equal(dydxFillResults.amountDepositedIntoDydx);
});
it('should successfully fill a dydx order (DydxBridge used in takerFeeAssetData)', async () => {
const signedOrder = await maker.signOrderAsync({
// Match the dydx conversion rate when using the bridge in the takerFeeAssetData.
takerFeeAssetData: dydxBridgeProxyAssetData,
makerFee: dydxConversionRateNumerator,
takerFee: dydxConversionRateDenominator,
});
const dydxFillResults = await fillOrder(signedOrder);
expect(
dydxFillResults.makerFeePaid,
'makerFeePaid should equal amountDepositedIntoDydx',
).to.bignumber.equal(dydxFillResults.amountDepositedIntoDydx);
expect(
dydxFillResults.takerFeePaid,
'takerFeePaid should equal amountWithdrawnFromDydx',
).to.bignumber.equal(dydxFillResults.amountWithdrawnFromDydx);
});
it('should partially fill a dydx order (DydxBridge used in makerAssetData)', async () => {
const signedOrder = await maker.signOrderAsync({
// Invert the dydx conversion rate when using the bridge in the makerAssetData.
makerAssetData: dydxBridgeProxyAssetData,
makerAssetAmount: dydxConversionRateDenominator,
takerAssetAmount: dydxConversionRateNumerator,
});
const dydxFillResults = await fillOrder(signedOrder, signedOrder.takerAssetAmount.div(2));
expect(
dydxFillResults.makerAssetFilledAmount,
'makerAssetFilledAmount should equal amountWithdrawnFromDydx',
).to.bignumber.equal(dydxFillResults.amountWithdrawnFromDydx);
expect(
dydxFillResults.takerAssetFilledAmount,
'takerAssetFilledAmount should equal amountDepositedIntoDydx',
).to.bignumber.equal(dydxFillResults.amountDepositedIntoDydx);
}); });
}); });
}); });