feat: Dutch Auction

This commit is contained in:
Jacob Evans
2018-11-05 10:31:24 +01:00
parent f076cdb832
commit da8c27c286
8 changed files with 482 additions and 1 deletions

View File

@@ -25,6 +25,7 @@
"DummyERC721Token",
"DummyMultipleReturnERC20Token",
"DummyNoReturnERC20Token",
"DutchAuction",
"ERC20Proxy",
"ERC20Token",
"ERC721Token",

View File

@@ -34,7 +34,7 @@
},
"config": {
"abis":
"generated-artifacts/@(AssetProxyOwner|DummyERC20Token|DummyERC721Receiver|DummyERC721Token|DummyMultipleReturnERC20Token|DummyNoReturnERC20Token|ERC20Token|ERC20Proxy|ERC721Token|ERC721Proxy|Forwarder|Exchange|ExchangeWrapper|IAssetData|IAssetProxy|InvalidERC721Receiver|MixinAuthorizable|MultiAssetProxy|MultiSigWallet|MultiSigWalletWithTimeLock|OrderValidator|ReentrantERC20Token|TestAssetProxyOwner|TestAssetProxyDispatcher|TestConstants|TestExchangeInternals|TestLibBytes|TestLibs|TestSignatureValidator|TestStaticCallReceiver|Validator|Wallet|Whitelist|WETH9|ZRXToken).json"
"generated-artifacts/@(AssetProxyOwner|DummyERC20Token|DummyERC721Receiver|DummyERC721Token|DummyMultipleReturnERC20Token|DummyNoReturnERC20Token|DutchAuction|ERC20Token|ERC20Proxy|ERC721Token|ERC721Proxy|Forwarder|Exchange|ExchangeWrapper|IAssetData|IAssetProxy|InvalidERC721Receiver|MixinAuthorizable|MultiAssetProxy|MultiSigWallet|MultiSigWalletWithTimeLock|OrderValidator|ReentrantERC20Token|TestAssetProxyOwner|TestAssetProxyDispatcher|TestConstants|TestExchangeInternals|TestLibBytes|TestLibs|TestSignatureValidator|TestStaticCallReceiver|Validator|Wallet|Whitelist|WETH9|ZRXToken).json"
},
"repository": {
"type": "git",

View File

@@ -6,6 +6,7 @@ import * as DummyERC721Receiver from '../../generated-artifacts/DummyERC721Recei
import * as DummyERC721Token from '../../generated-artifacts/DummyERC721Token.json';
import * as DummyMultipleReturnERC20Token from '../../generated-artifacts/DummyMultipleReturnERC20Token.json';
import * as DummyNoReturnERC20Token from '../../generated-artifacts/DummyNoReturnERC20Token.json';
import * as DutchAuction from '../../generated-artifacts/DutchAuction.json';
import * as ERC20Proxy from '../../generated-artifacts/ERC20Proxy.json';
import * as ERC20Token from '../../generated-artifacts/ERC20Token.json';
import * as ERC721Proxy from '../../generated-artifacts/ERC721Proxy.json';
@@ -45,6 +46,7 @@ export const artifacts = {
DummyERC721Token: DummyERC721Token as ContractArtifact,
DummyMultipleReturnERC20Token: DummyMultipleReturnERC20Token as ContractArtifact,
DummyNoReturnERC20Token: DummyNoReturnERC20Token as ContractArtifact,
DutchAuction: DutchAuction as ContractArtifact,
ERC20Proxy: ERC20Proxy as ContractArtifact,
ERC20Token: ERC20Token as ContractArtifact,
ERC721Proxy: ERC721Proxy as ContractArtifact,

View File

@@ -4,6 +4,7 @@ export * from '../../generated-wrappers/dummy_erc721_receiver';
export * from '../../generated-wrappers/dummy_erc721_token';
export * from '../../generated-wrappers/dummy_multiple_return_erc20_token';
export * from '../../generated-wrappers/dummy_no_return_erc20_token';
export * from '../../generated-wrappers/dutch_auction';
export * from '../../generated-wrappers/erc20_proxy';
export * from '../../generated-wrappers/erc721_proxy';
export * from '../../generated-wrappers/erc20_token';

View File

@@ -86,6 +86,7 @@ export enum ContractName {
ZRXToken = 'ZRXToken',
DummyERC20Token = 'DummyERC20Token',
EtherToken = 'WETH9',
DutchAuction = 'DutchAuction',
AssetProxyOwner = 'AssetProxyOwner',
AccountLevels = 'AccountLevels',
EtherDelta = 'EtherDelta',

View File

@@ -13,6 +13,7 @@
"./generated-artifacts/DummyERC721Token.json",
"./generated-artifacts/DummyMultipleReturnERC20Token.json",
"./generated-artifacts/DummyNoReturnERC20Token.json",
"./generated-artifacts/DutchAuction.json",
"./generated-artifacts/ERC20Proxy.json",
"./generated-artifacts/ERC20Token.json",
"./generated-artifacts/ERC721Proxy.json",

View File

@@ -0,0 +1,160 @@
/*
Copyright 2018 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.4.24;
pragma experimental ABIEncoderV2;
import "../../protocol/Exchange/interfaces/IExchange.sol";
import "../../protocol/Exchange/libs/LibOrder.sol";
import "../../tokens/ERC20Token/IERC20Token.sol";
import "../../utils/LibBytes/LibBytes.sol";
contract DutchAuction {
using LibBytes for bytes;
// solhint-disable var-name-mixedcase
IExchange internal EXCHANGE;
struct AuctionDetails {
uint256 beginTime; // Auction begin time in seconds
uint256 endTime; // Auction end time in seconds
uint256 beginPrice; // Auction begin price
uint256 endPrice; // Auction end price
uint256 currentPrice; // Current auction price at block.timestamp
uint256 currentTime; // block.timestamp
}
constructor (address _exchange)
public
{
EXCHANGE = IExchange(_exchange);
}
/// @dev Packs the begin time and price parameters of an auction into uint256.
/// This is stored as the salt value of the sale order.
/// @param beginTime Begin time of the auction (32 bits)
/// @param beginPrice Starting price of the auction (224 bits)
/// @return Encoded Auction Parameters packed into a uint256
function encodeParameters(
uint256 beginTime,
uint256 beginPrice
)
external
view
returns (uint256 encodedParameters)
{
require(beginTime <= 2**32, "INVALID_BEGIN_TIME");
require(beginPrice <= 2**224, "INVALID_BEGIN_PRICE");
encodedParameters = beginTime;
encodedParameters |= beginPrice<<32;
return encodedParameters;
}
/// @dev Performs a match of the two orders at the price point given the current block time and the auction
/// start time (encoded in the salt).
/// The Sellers order is a signed order at the lowest price at the end of the auction. Excess from the match
/// is transferred to the seller.
/// @param buyOrder The Buyer's order
/// @param sellOrder The Seller's order
/// @param buySignature Proof that order was created by the left maker.
/// @param sellSignature Proof that order was created by the right maker.
/// @return matchedFillResults Amounts filled and fees paid by maker and taker of matched orders.
function matchOrders(
LibOrder.Order memory buyOrder,
LibOrder.Order memory sellOrder,
bytes memory buySignature,
bytes memory sellSignature
)
public
returns (LibFillResults.MatchedFillResults memory matchedFillResults)
{
AuctionDetails memory auctionDetails = getAuctionDetails(sellOrder);
// Ensure the auction has not yet started
// solhint-disable-next-line not-rely-on-time
require(block.timestamp >= auctionDetails.beginTime, "AUCTION_NOT_STARTED");
// Ensure the auction has not expired. This will fail later in 0x but we can save gas by failing early
// solhint-disable-next-line not-rely-on-time
require(sellOrder.expirationTimeSeconds > block.timestamp, "AUCTION_EXPIRED");
// Ensure the auction goes from high to low
require(auctionDetails.beginPrice > auctionDetails.endPrice, "INVALID_PRICE");
// Validate the buyer amount is greater than the current auction price
require(buyOrder.makerAssetAmount >= auctionDetails.currentPrice, "INVALID_PRICE");
// Match orders, maximally filling `buyOrder`
matchedFillResults = EXCHANGE.matchOrders(
buyOrder,
sellOrder,
buySignature,
sellSignature
);
// Return any spread to the seller
uint256 leftMakerAssetSpreadAmount = matchedFillResults.leftMakerAssetSpreadAmount;
if (leftMakerAssetSpreadAmount > 0) {
bytes memory assetData = sellOrder.takerAssetData;
address token = assetData.readAddress(16);
address makerAddress = sellOrder.makerAddress;
IERC20Token(token).transfer(makerAddress, leftMakerAssetSpreadAmount);
}
return matchedFillResults;
}
/// @dev Decodes the packed parameters into beginTime and beginPrice.
/// @param encodedParameters the encoded parameters
/// @return beginTime and beginPrice decoded
function decodeParameters(
uint256 encodedParameters
)
public
view
returns (uint256 beginTime, uint256 beginPrice)
{
beginTime = encodedParameters & 0x00000000000000000000000fffffffff;
beginPrice = encodedParameters>>32;
return (beginTime, beginPrice);
}
/// @dev Calculates the Auction Details for the given order
/// @param order The sell order
/// @return AuctionDetails
function getAuctionDetails(
LibOrder.Order memory order
)
public
returns (AuctionDetails memory auctionDetails)
{
// solhint-disable-next-line indent
(uint256 auctionBeginTimeSeconds, uint256 auctionBeginPrice) = decodeParameters(order.salt);
require(order.expirationTimeSeconds > auctionBeginTimeSeconds, "INVALID_BEGIN_TIME");
uint256 auctionDurationSeconds = order.expirationTimeSeconds-auctionBeginTimeSeconds;
// solhint-disable-next-line not-rely-on-time
uint256 currentDurationSeconds = order.expirationTimeSeconds-block.timestamp;
uint256 minPrice = order.takerAssetAmount;
uint256 priceDiff = auctionBeginPrice-minPrice;
uint256 currentPrice = minPrice + (currentDurationSeconds*priceDiff/auctionDurationSeconds);
auctionDetails.beginTime = auctionBeginTimeSeconds;
auctionDetails.endTime = order.expirationTimeSeconds;
auctionDetails.beginPrice = auctionBeginPrice;
auctionDetails.endPrice = minPrice;
auctionDetails.currentPrice = currentPrice;
// solhint-disable-next-line not-rely-on-time
auctionDetails.currentTime = block.timestamp;
return auctionDetails;
}
}

View File

@@ -0,0 +1,315 @@
import { BlockchainLifecycle } from '@0x/dev-utils';
import { assetDataUtils } from '@0x/order-utils';
import { SignedOrder } from '@0x/types';
import { BigNumber } from '@0x/utils';
import { Web3Wrapper } from '@0x/web3-wrapper';
import * as chai from 'chai';
import { DummyERC20TokenContract } from '../../generated-wrappers/dummy_erc20_token';
import { DummyERC721TokenContract } from '../../generated-wrappers/dummy_erc721_token';
import { DutchAuctionContract } from '../../generated-wrappers/dutch_auction';
import { ExchangeContract } from '../../generated-wrappers/exchange';
import { WETH9Contract } from '../../generated-wrappers/weth9';
import { artifacts } from '../../src/artifacts';
import { expectTransactionFailedAsync } from '../utils/assertions';
import { getLatestBlockTimestampAsync } from '../utils/block_timestamp';
import { chaiSetup } from '../utils/chai_setup';
import { constants } from '../utils/constants';
import { ERC20Wrapper } from '../utils/erc20_wrapper';
import { ERC721Wrapper } from '../utils/erc721_wrapper';
import { ExchangeWrapper } from '../utils/exchange_wrapper';
import { OrderFactory } from '../utils/order_factory';
import { ContractName, ERC20BalancesByOwner } from '../utils/types';
import { provider, txDefaults, web3Wrapper } from '../utils/web3_wrapper';
chaiSetup.configure();
const expect = chai.expect;
const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper);
const DECIMALS_DEFAULT = 18;
describe(ContractName.DutchAuction, () => {
let makerAddress: string;
let owner: string;
let takerAddress: string;
let feeRecipientAddress: string;
let defaultMakerAssetAddress: string;
let weth: DummyERC20TokenContract;
let zrxToken: DummyERC20TokenContract;
let erc20TokenA: DummyERC20TokenContract;
let erc721Token: DummyERC721TokenContract;
let dutchAuctionContract: DutchAuctionContract;
let wethContract: WETH9Contract;
let exchangeWrapper: ExchangeWrapper;
let sellerOrderFactory: OrderFactory;
let buyerOrderFactory: OrderFactory;
let erc20Wrapper: ERC20Wrapper;
let erc20Balances: ERC20BalancesByOwner;
let tenMinutesInSeconds: number;
let currentBlockTimestamp: number;
let auctionBeginTime: BigNumber;
let auctionBeginPrice: BigNumber;
let encodedParams: BigNumber;
let sellOrder: SignedOrder;
let buyOrder: SignedOrder;
let erc721MakerAssetIds: BigNumber[];
before(async () => {
await blockchainLifecycle.startAsync();
const accounts = await web3Wrapper.getAvailableAddressesAsync();
const usedAddresses = ([owner, makerAddress, takerAddress, feeRecipientAddress] = accounts);
erc20Wrapper = new ERC20Wrapper(provider, usedAddresses, owner);
const numDummyErc20ToDeploy = 2;
[erc20TokenA, zrxToken] = await erc20Wrapper.deployDummyTokensAsync(
numDummyErc20ToDeploy,
constants.DUMMY_TOKEN_DECIMALS,
);
const erc20Proxy = await erc20Wrapper.deployProxyAsync();
await erc20Wrapper.setBalancesAndAllowancesAsync();
const erc721Wrapper = new ERC721Wrapper(provider, usedAddresses, owner);
[erc721Token] = await erc721Wrapper.deployDummyTokensAsync();
const erc721Proxy = await erc721Wrapper.deployProxyAsync();
await erc721Wrapper.setBalancesAndAllowancesAsync();
const erc721Balances = await erc721Wrapper.getBalancesAsync();
erc721MakerAssetIds = erc721Balances[makerAddress][erc721Token.address];
wethContract = await WETH9Contract.deployFrom0xArtifactAsync(artifacts.WETH9, provider, txDefaults);
weth = new DummyERC20TokenContract(wethContract.abi, wethContract.address, provider);
erc20Wrapper.addDummyTokenContract(weth);
const zrxAssetData = assetDataUtils.encodeERC20AssetData(zrxToken.address);
const exchangeInstance = await ExchangeContract.deployFrom0xArtifactAsync(
artifacts.Exchange,
provider,
txDefaults,
zrxAssetData,
);
exchangeWrapper = new ExchangeWrapper(exchangeInstance, provider);
await exchangeWrapper.registerAssetProxyAsync(erc20Proxy.address, owner);
await exchangeWrapper.registerAssetProxyAsync(erc721Proxy.address, owner);
await erc20Proxy.addAuthorizedAddress.sendTransactionAsync(exchangeInstance.address, {
from: owner,
});
await erc721Proxy.addAuthorizedAddress.sendTransactionAsync(exchangeInstance.address, {
from: owner,
});
const dutchAuctionInstance = await DutchAuctionContract.deployFrom0xArtifactAsync(
artifacts.DutchAuction,
provider,
txDefaults,
exchangeInstance.address,
);
dutchAuctionContract = new DutchAuctionContract(
dutchAuctionInstance.abi,
dutchAuctionInstance.address,
provider,
);
defaultMakerAssetAddress = erc20TokenA.address;
const defaultTakerAssetAddress = wethContract.address;
// Set up taker WETH balance and allowance
await web3Wrapper.awaitTransactionSuccessAsync(
await wethContract.deposit.sendTransactionAsync({
from: takerAddress,
value: Web3Wrapper.toBaseUnitAmount(new BigNumber(50), DECIMALS_DEFAULT),
}),
);
await web3Wrapper.awaitTransactionSuccessAsync(
await wethContract.approve.sendTransactionAsync(
erc20Proxy.address,
constants.UNLIMITED_ALLOWANCE_IN_BASE_UNITS,
{ from: takerAddress },
),
);
web3Wrapper.abiDecoder.addABI(exchangeInstance.abi);
web3Wrapper.abiDecoder.addABI(zrxToken.abi);
erc20Wrapper.addTokenOwnerAddress(dutchAuctionContract.address);
tenMinutesInSeconds = 10 * 60;
currentBlockTimestamp = await getLatestBlockTimestampAsync();
auctionBeginTime = new BigNumber(currentBlockTimestamp).minus(tenMinutesInSeconds);
auctionBeginPrice = Web3Wrapper.toBaseUnitAmount(new BigNumber(10), DECIMALS_DEFAULT);
encodedParams = await dutchAuctionContract.encodeParameters.callAsync(auctionBeginTime, auctionBeginPrice);
const sellerDefaultOrderParams = {
salt: encodedParams, // Set the encoded params as the salt for the seller order
exchangeAddress: exchangeInstance.address,
makerAddress,
feeRecipientAddress,
senderAddress: dutchAuctionContract.address,
makerAssetData: assetDataUtils.encodeERC20AssetData(defaultMakerAssetAddress),
takerAssetData: assetDataUtils.encodeERC20AssetData(defaultTakerAssetAddress),
makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(200), DECIMALS_DEFAULT),
takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(2), DECIMALS_DEFAULT),
makerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(0), DECIMALS_DEFAULT),
takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(0), DECIMALS_DEFAULT),
};
const buyerDefaultOrderParams = {
...sellerDefaultOrderParams,
makerAddress: takerAddress,
makerAssetData: sellerDefaultOrderParams.takerAssetData,
takerAssetData: sellerDefaultOrderParams.makerAssetData,
makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), DECIMALS_DEFAULT),
takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(200), DECIMALS_DEFAULT),
};
const makerPrivateKey = constants.TESTRPC_PRIVATE_KEYS[accounts.indexOf(makerAddress)];
const takerPrivateKey = constants.TESTRPC_PRIVATE_KEYS[accounts.indexOf(takerAddress)];
sellerOrderFactory = new OrderFactory(makerPrivateKey, sellerDefaultOrderParams);
buyerOrderFactory = new OrderFactory(takerPrivateKey, buyerDefaultOrderParams);
});
after(async () => {
await blockchainLifecycle.revertAsync();
});
beforeEach(async () => {
await blockchainLifecycle.startAsync();
erc20Balances = await erc20Wrapper.getBalancesAsync();
tenMinutesInSeconds = 10 * 60;
currentBlockTimestamp = await getLatestBlockTimestampAsync();
auctionBeginTime = new BigNumber(currentBlockTimestamp).minus(tenMinutesInSeconds);
auctionBeginPrice = Web3Wrapper.toBaseUnitAmount(new BigNumber(10), DECIMALS_DEFAULT);
encodedParams = await dutchAuctionContract.encodeParameters.callAsync(auctionBeginTime, auctionBeginPrice);
sellOrder = await sellerOrderFactory.newSignedOrderAsync();
buyOrder = await buyerOrderFactory.newSignedOrderAsync();
});
afterEach(async () => {
await blockchainLifecycle.revertAsync();
});
describe('matchOrders', () => {
it('should encode and decode parameters', async () => {
const [decodedBegin, decodedBeginPrice] = await dutchAuctionContract.decodeParameters.callAsync(
encodedParams,
);
expect(decodedBegin).to.be.bignumber.equal(auctionBeginTime);
expect(decodedBeginPrice).to.be.bignumber.equal(auctionBeginPrice);
});
it('should be worth the begin price at the begining of the auction', async () => {
// TODO this is flakey
currentBlockTimestamp = await web3Wrapper.getBlockTimestampAsync('latest');
await web3Wrapper.increaseTimeAsync(1);
auctionBeginTime = new BigNumber(currentBlockTimestamp + 2);
encodedParams = await dutchAuctionContract.encodeParameters.callAsync(auctionBeginTime, auctionBeginPrice);
sellOrder = await sellerOrderFactory.newSignedOrderAsync({
salt: encodedParams,
});
const auctionDetails = await dutchAuctionContract.getAuctionDetails.callAsync(sellOrder);
expect(auctionDetails.currentPrice).to.be.bignumber.equal(auctionBeginPrice);
expect(auctionDetails.beginPrice).to.be.bignumber.equal(auctionBeginPrice);
});
it('should match orders and send excess to seller', async () => {
const txHash = await dutchAuctionContract.matchOrders.sendTransactionAsync(
buyOrder,
sellOrder,
buyOrder.signature,
sellOrder.signature,
{
from: takerAddress,
},
);
await web3Wrapper.awaitTransactionSuccessAsync(txHash);
const newBalances = await erc20Wrapper.getBalancesAsync();
expect(newBalances[dutchAuctionContract.address][weth.address]).to.be.bignumber.equal(
constants.ZERO_AMOUNT,
);
expect(newBalances[makerAddress][weth.address]).to.be.bignumber.equal(
erc20Balances[makerAddress][weth.address].plus(buyOrder.makerAssetAmount),
);
});
it('should have valid getAuctionDetails at a block in the future', async () => {
let auctionDetails = await dutchAuctionContract.getAuctionDetails.callAsync(sellOrder);
const beforePrice = auctionDetails.currentPrice;
// Increase block time
await web3Wrapper.increaseTimeAsync(60);
auctionDetails = await dutchAuctionContract.getAuctionDetails.callAsync(sellOrder);
const currentPrice = auctionDetails.currentPrice;
expect(beforePrice).to.be.bignumber.greaterThan(currentPrice);
buyOrder = await buyerOrderFactory.newSignedOrderAsync({
makerAssetAmount: currentPrice,
});
const txHash = await dutchAuctionContract.matchOrders.sendTransactionAsync(
buyOrder,
sellOrder,
buyOrder.signature,
sellOrder.signature,
{
from: takerAddress,
},
);
await web3Wrapper.awaitTransactionSuccessAsync(txHash);
const newBalances = await erc20Wrapper.getBalancesAsync();
expect(newBalances[makerAddress][weth.address]).to.be.bignumber.equal(
erc20Balances[makerAddress][weth.address].plus(currentPrice),
);
});
it('should revert when auction expires', async () => {
// Increase block time
await web3Wrapper.increaseTimeAsync(tenMinutesInSeconds);
return expectTransactionFailedAsync(
dutchAuctionContract.matchOrders.sendTransactionAsync(
buyOrder,
sellOrder,
buyOrder.signature,
sellOrder.signature,
{
from: takerAddress,
},
),
'AUCTION_EXPIRED' as any,
);
});
it('cannot be filled for less than the current price', async () => {
// Increase block time
await web3Wrapper.increaseTimeAsync(60);
buyOrder = await buyerOrderFactory.newSignedOrderAsync({
makerAssetAmount: sellOrder.takerAssetAmount,
});
return expectTransactionFailedAsync(
dutchAuctionContract.matchOrders.sendTransactionAsync(
buyOrder,
sellOrder,
buyOrder.signature,
sellOrder.signature,
{
from: takerAddress,
},
),
'INVALID_PRICE' as any,
);
});
describe('ERC721', () => {
it('should match orders when ERC721', async () => {
const makerAssetId = erc721MakerAssetIds[0];
sellOrder = await sellerOrderFactory.newSignedOrderAsync({
makerAssetAmount: new BigNumber(1),
makerAssetData: assetDataUtils.encodeERC721AssetData(erc721Token.address, makerAssetId),
});
buyOrder = await buyerOrderFactory.newSignedOrderAsync({
takerAssetAmount: new BigNumber(1),
takerAssetData: assetDataUtils.encodeERC721AssetData(erc721Token.address, makerAssetId),
});
const txHash = await dutchAuctionContract.matchOrders.sendTransactionAsync(
buyOrder,
sellOrder,
buyOrder.signature,
sellOrder.signature,
{
from: takerAddress,
},
);
await web3Wrapper.awaitTransactionSuccessAsync(txHash);
const newBalances = await erc20Wrapper.getBalancesAsync();
expect(newBalances[makerAddress][weth.address]).to.be.bignumber.equal(
erc20Balances[makerAddress][weth.address].plus(buyOrder.makerAssetAmount),
);
const newOwner = await erc721Token.ownerOf.callAsync(makerAssetId);
expect(newOwner).to.be.bignumber.equal(takerAddress);
});
});
});
});