From da8c27c286321af759125d7777f1c84d34498214 Mon Sep 17 00:00:00 2001 From: Jacob Evans Date: Mon, 5 Nov 2018 10:31:24 +0100 Subject: feat: Dutch Auction --- .../extensions/DutchAuction/DutchAuction.sol | 160 +++++++++++ .../contracts/test/extensions/dutch_auction.ts | 315 +++++++++++++++++++++ 2 files changed, 475 insertions(+) create mode 100644 packages/contracts/contracts/extensions/DutchAuction/DutchAuction.sol create mode 100644 packages/contracts/test/extensions/dutch_auction.ts (limited to 'packages') diff --git a/packages/contracts/contracts/extensions/DutchAuction/DutchAuction.sol b/packages/contracts/contracts/extensions/DutchAuction/DutchAuction.sol new file mode 100644 index 000000000..ed4158c25 --- /dev/null +++ b/packages/contracts/contracts/extensions/DutchAuction/DutchAuction.sol @@ -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; + } +} diff --git a/packages/contracts/test/extensions/dutch_auction.ts b/packages/contracts/test/extensions/dutch_auction.ts new file mode 100644 index 000000000..c61faf9d7 --- /dev/null +++ b/packages/contracts/test/extensions/dutch_auction.ts @@ -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); + }); + }); + }); +}); -- cgit From 98d9a9c6482e2170192f66350448703d2801d122 Mon Sep 17 00:00:00 2001 From: Jacob Evans Date: Fri, 16 Nov 2018 11:37:05 +1100 Subject: chore: time movement for geth --- .../extensions/DutchAuction/DutchAuction.sol | 26 ++++---- .../contracts/test/extensions/dutch_auction.ts | 73 ++++++++++++++++------ 2 files changed, 68 insertions(+), 31 deletions(-) (limited to 'packages') diff --git a/packages/contracts/contracts/extensions/DutchAuction/DutchAuction.sol b/packages/contracts/contracts/extensions/DutchAuction/DutchAuction.sol index ed4158c25..bb75fc188 100644 --- a/packages/contracts/contracts/extensions/DutchAuction/DutchAuction.sol +++ b/packages/contracts/contracts/extensions/DutchAuction/DutchAuction.sol @@ -86,11 +86,9 @@ contract DutchAuction { { 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"); + require(auctionDetails.currentTime >= 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"); + require(sellOrder.expirationTimeSeconds > auctionDetails.currentTime, "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 @@ -141,20 +139,24 @@ contract DutchAuction { (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); - + // solhint-disable-next-line not-rely-on-time + uint256 timestamp = block.timestamp; auctionDetails.beginTime = auctionBeginTimeSeconds; auctionDetails.endTime = order.expirationTimeSeconds; auctionDetails.beginPrice = auctionBeginPrice; auctionDetails.endPrice = minPrice; + auctionDetails.currentTime = timestamp; + + uint256 remainingDurationSeconds = order.expirationTimeSeconds-timestamp; + uint256 priceDelta = auctionBeginPrice-minPrice; + uint256 currentPrice = minPrice + (remainingDurationSeconds*priceDelta/auctionDurationSeconds); + // If the auction has not yet begun the current price is the auctionBeginPrice + currentPrice = timestamp < auctionBeginTimeSeconds ? auctionBeginPrice : currentPrice; + // If the auction has ended the current price is the minPrice + // auction end time is guaranteed by 0x Exchange to fail due to the order expiration + currentPrice = timestamp >= order.expirationTimeSeconds ? minPrice : currentPrice; auctionDetails.currentPrice = currentPrice; - // solhint-disable-next-line not-rely-on-time - auctionDetails.currentTime = block.timestamp; - return auctionDetails; } } diff --git a/packages/contracts/test/extensions/dutch_auction.ts b/packages/contracts/test/extensions/dutch_auction.ts index c61faf9d7..6a1e3ed45 100644 --- a/packages/contracts/test/extensions/dutch_auction.ts +++ b/packages/contracts/test/extensions/dutch_auction.ts @@ -27,7 +27,7 @@ const expect = chai.expect; const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); const DECIMALS_DEFAULT = 18; -describe(ContractName.DutchAuction, () => { +describe.only(ContractName.DutchAuction, () => { let makerAddress: string; let owner: string; let takerAddress: string; @@ -49,11 +49,26 @@ describe(ContractName.DutchAuction, () => { let tenMinutesInSeconds: number; let currentBlockTimestamp: number; let auctionBeginTime: BigNumber; + let auctionEndTime: BigNumber; let auctionBeginPrice: BigNumber; + let auctionEndPrice: BigNumber; let encodedParams: BigNumber; let sellOrder: SignedOrder; let buyOrder: SignedOrder; let erc721MakerAssetIds: BigNumber[]; + async function increaseTimeAsync(): Promise { + const timestampBefore = await getLatestBlockTimestampAsync(); + await web3Wrapper.increaseTimeAsync(5); + const timestampAfter = await getLatestBlockTimestampAsync(); + // HACK send some transactions + if (timestampAfter === timestampBefore) { + await web3Wrapper.sendTransactionAsync({ to: makerAddress, from: makerAddress, value: new BigNumber(1) }); + await web3Wrapper.sendTransactionAsync({ to: makerAddress, from: makerAddress, value: new BigNumber(1) }); + await web3Wrapper.sendTransactionAsync({ to: makerAddress, from: makerAddress, value: new BigNumber(1) }); + await web3Wrapper.sendTransactionAsync({ to: makerAddress, from: makerAddress, value: new BigNumber(1) }); + await web3Wrapper.sendTransactionAsync({ to: makerAddress, from: makerAddress, value: new BigNumber(1) }); + } + } before(async () => { await blockchainLifecycle.startAsync(); @@ -132,12 +147,19 @@ describe(ContractName.DutchAuction, () => { web3Wrapper.abiDecoder.addABI(exchangeInstance.abi); web3Wrapper.abiDecoder.addABI(zrxToken.abi); erc20Wrapper.addTokenOwnerAddress(dutchAuctionContract.address); + tenMinutesInSeconds = 10 * 60; currentBlockTimestamp = await getLatestBlockTimestampAsync(); + // Default auction begins 10 minutes ago auctionBeginTime = new BigNumber(currentBlockTimestamp).minus(tenMinutesInSeconds); + // Default auction ends 10 from now + auctionEndTime = new BigNumber(currentBlockTimestamp).plus(tenMinutesInSeconds); auctionBeginPrice = Web3Wrapper.toBaseUnitAmount(new BigNumber(10), DECIMALS_DEFAULT); + auctionEndPrice = Web3Wrapper.toBaseUnitAmount(new BigNumber(1), DECIMALS_DEFAULT); encodedParams = await dutchAuctionContract.encodeParameters.callAsync(auctionBeginTime, auctionBeginPrice); + const zero = Web3Wrapper.toBaseUnitAmount(new BigNumber(0), DECIMALS_DEFAULT); + // Default sell order and buy order are exact mirrors const sellerDefaultOrderParams = { salt: encodedParams, // Set the encoded params as the salt for the seller order exchangeAddress: exchangeInstance.address, @@ -147,18 +169,23 @@ describe(ContractName.DutchAuction, () => { 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), + takerAssetAmount: auctionEndPrice, + expirationTimeSeconds: auctionEndTime, + makerFee: zero, + takerFee: zero, }; + // Default buy order is for the auction begin price const buyerDefaultOrderParams = { ...sellerDefaultOrderParams, makerAddress: takerAddress, makerAssetData: sellerDefaultOrderParams.takerAssetData, takerAssetData: sellerDefaultOrderParams.makerAssetData, - makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), DECIMALS_DEFAULT), + makerAssetAmount: auctionBeginPrice, takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(200), DECIMALS_DEFAULT), }; + + encodedParams = await dutchAuctionContract.encodeParameters.callAsync(auctionBeginTime, auctionBeginPrice); + const makerPrivateKey = constants.TESTRPC_PRIVATE_KEYS[accounts.indexOf(makerAddress)]; const takerPrivateKey = constants.TESTRPC_PRIVATE_KEYS[accounts.indexOf(takerAddress)]; sellerOrderFactory = new OrderFactory(makerPrivateKey, sellerDefaultOrderParams); @@ -170,11 +197,6 @@ describe(ContractName.DutchAuction, () => { 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(); }); @@ -183,6 +205,7 @@ describe(ContractName.DutchAuction, () => { }); describe('matchOrders', () => { it('should encode and decode parameters', async () => { + encodedParams = await dutchAuctionContract.encodeParameters.callAsync(auctionBeginTime, auctionBeginPrice); const [decodedBegin, decodedBeginPrice] = await dutchAuctionContract.decodeParameters.callAsync( encodedParams, ); @@ -190,9 +213,6 @@ describe(ContractName.DutchAuction, () => { 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({ @@ -202,6 +222,18 @@ describe(ContractName.DutchAuction, () => { expect(auctionDetails.currentPrice).to.be.bignumber.equal(auctionBeginPrice); expect(auctionDetails.beginPrice).to.be.bignumber.equal(auctionBeginPrice); }); + it('should be be worth the end price at the end of the auction', async () => { + auctionBeginTime = new BigNumber(currentBlockTimestamp - 1000); + auctionEndTime = new BigNumber(currentBlockTimestamp - 100); + encodedParams = await dutchAuctionContract.encodeParameters.callAsync(auctionBeginTime, auctionBeginPrice); + sellOrder = await sellerOrderFactory.newSignedOrderAsync({ + salt: encodedParams, + expirationTimeSeconds: auctionEndTime, + }); + const auctionDetails = await dutchAuctionContract.getAuctionDetails.callAsync(sellOrder); + expect(auctionDetails.currentPrice).to.be.bignumber.equal(auctionEndPrice); + 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, @@ -224,11 +256,11 @@ describe(ContractName.DutchAuction, () => { 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); + await increaseTimeAsync(); auctionDetails = await dutchAuctionContract.getAuctionDetails.callAsync(sellOrder); const currentPrice = auctionDetails.currentPrice; expect(beforePrice).to.be.bignumber.greaterThan(currentPrice); + buyOrder = await buyerOrderFactory.newSignedOrderAsync({ makerAssetAmount: currentPrice, }); @@ -248,8 +280,12 @@ describe(ContractName.DutchAuction, () => { ); }); it('should revert when auction expires', async () => { - // Increase block time - await web3Wrapper.increaseTimeAsync(tenMinutesInSeconds); + auctionEndTime = new BigNumber(currentBlockTimestamp - 100); + encodedParams = await dutchAuctionContract.encodeParameters.callAsync(auctionBeginTime, auctionBeginPrice); + sellOrder = await sellerOrderFactory.newSignedOrderAsync({ + salt: encodedParams, + expirationTimeSeconds: auctionEndTime, + }); return expectTransactionFailedAsync( dutchAuctionContract.matchOrders.sendTransactionAsync( buyOrder, @@ -264,8 +300,7 @@ describe(ContractName.DutchAuction, () => { ); }); it('cannot be filled for less than the current price', async () => { - // Increase block time - await web3Wrapper.increaseTimeAsync(60); + await increaseTimeAsync(); buyOrder = await buyerOrderFactory.newSignedOrderAsync({ makerAssetAmount: sellOrder.takerAssetAmount, }); -- cgit From cb8601676d70ba648683f8c7df1eca25d670d663 Mon Sep 17 00:00:00 2001 From: Jacob Evans Date: Wed, 21 Nov 2018 16:04:13 +1100 Subject: chore: Rename price to amount. Encode parameters in makerAssetData --- .../extensions/DutchAuction/DutchAuction.sol | 100 ++++------- .../contracts/test/extensions/dutch_auction.ts | 183 +++++++++++++-------- 2 files changed, 149 insertions(+), 134 deletions(-) (limited to 'packages') diff --git a/packages/contracts/contracts/extensions/DutchAuction/DutchAuction.sol b/packages/contracts/contracts/extensions/DutchAuction/DutchAuction.sol index bb75fc188..a304184f1 100644 --- a/packages/contracts/contracts/extensions/DutchAuction/DutchAuction.sol +++ b/packages/contracts/contracts/extensions/DutchAuction/DutchAuction.sol @@ -32,12 +32,12 @@ contract DutchAuction { 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 + uint256 beginTimeSeconds; // Auction begin time in seconds + uint256 endTimeSeconds; // Auction end time in seconds + uint256 beginAmount; // Auction begin amount + uint256 endAmount; // Auction end amount + uint256 currentAmount; // Current auction amount at block.timestamp + uint256 currentTimeSeconds; // block.timestamp } constructor (address _exchange) @@ -46,35 +46,15 @@ contract DutchAuction { 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 + /// @dev Performs a match of the two orders at the amount given: the current block time, the auction + /// start time and the auction begin amount. + /// The Sellers order is a signed order at the lowest amount 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. + /// @return matchedFillResults amounts filled and fees paid by maker and taker of matched orders. function matchOrders( LibOrder.Order memory buyOrder, LibOrder.Order memory sellOrder, @@ -86,13 +66,13 @@ contract DutchAuction { { AuctionDetails memory auctionDetails = getAuctionDetails(sellOrder); // Ensure the auction has not yet started - require(auctionDetails.currentTime >= auctionDetails.beginTime, "AUCTION_NOT_STARTED"); + require(auctionDetails.currentTimeSeconds >= auctionDetails.beginTimeSeconds, "AUCTION_NOT_STARTED"); // Ensure the auction has not expired. This will fail later in 0x but we can save gas by failing early - require(sellOrder.expirationTimeSeconds > auctionDetails.currentTime, "AUCTION_EXPIRED"); + require(sellOrder.expirationTimeSeconds > auctionDetails.currentTimeSeconds, "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"); + require(auctionDetails.beginAmount > auctionDetails.endAmount, "INVALID_AMOUNT"); + // Validate the buyer amount is greater than the current auction amount + require(buyOrder.makerAssetAmount >= auctionDetails.currentAmount, "INVALID_AMOUNT"); // Match orders, maximally filling `buyOrder` matchedFillResults = EXCHANGE.matchOrders( buyOrder, @@ -103,6 +83,7 @@ contract DutchAuction { // Return any spread to the seller uint256 leftMakerAssetSpreadAmount = matchedFillResults.leftMakerAssetSpreadAmount; if (leftMakerAssetSpreadAmount > 0) { + // Assume auction is for ERC20 bytes memory assetData = sellOrder.takerAssetData; address token = assetData.readAddress(16); address makerAddress = sellOrder.makerAddress; @@ -111,21 +92,6 @@ contract DutchAuction { 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 @@ -135,28 +101,30 @@ contract DutchAuction { 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 makerAssetDataLength = order.makerAssetData.length; + // We assume auctionBeginTimeSeconds and auctionBeginAmount are appended to the makerAssetData + uint256 auctionBeginTimeSeconds = order.makerAssetData.readUint256(makerAssetDataLength-64); + uint256 auctionBeginAmount = order.makerAssetData.readUint256(makerAssetDataLength-32); + // require(order.expirationTimeSeconds > auctionBeginTimeSeconds, "INVALID_BEGIN_TIME"); uint256 auctionDurationSeconds = order.expirationTimeSeconds-auctionBeginTimeSeconds; - uint256 minPrice = order.takerAssetAmount; + uint256 minAmount = order.takerAssetAmount; // solhint-disable-next-line not-rely-on-time uint256 timestamp = block.timestamp; - auctionDetails.beginTime = auctionBeginTimeSeconds; - auctionDetails.endTime = order.expirationTimeSeconds; - auctionDetails.beginPrice = auctionBeginPrice; - auctionDetails.endPrice = minPrice; - auctionDetails.currentTime = timestamp; + auctionDetails.beginTimeSeconds = auctionBeginTimeSeconds; + auctionDetails.endTimeSeconds = order.expirationTimeSeconds; + auctionDetails.beginAmount = auctionBeginAmount; + auctionDetails.endAmount = minAmount; + auctionDetails.currentTimeSeconds = timestamp; uint256 remainingDurationSeconds = order.expirationTimeSeconds-timestamp; - uint256 priceDelta = auctionBeginPrice-minPrice; - uint256 currentPrice = minPrice + (remainingDurationSeconds*priceDelta/auctionDurationSeconds); - // If the auction has not yet begun the current price is the auctionBeginPrice - currentPrice = timestamp < auctionBeginTimeSeconds ? auctionBeginPrice : currentPrice; - // If the auction has ended the current price is the minPrice + uint256 amountDelta = auctionBeginAmount-minAmount; + uint256 currentAmount = minAmount + (remainingDurationSeconds*amountDelta/auctionDurationSeconds); + // If the auction has not yet begun the current amount is the auctionBeginAmount + currentAmount = timestamp < auctionBeginTimeSeconds ? auctionBeginAmount : currentAmount; + // If the auction has ended the current amount is the minAmount // auction end time is guaranteed by 0x Exchange to fail due to the order expiration - currentPrice = timestamp >= order.expirationTimeSeconds ? minPrice : currentPrice; - auctionDetails.currentPrice = currentPrice; + currentAmount = timestamp >= order.expirationTimeSeconds ? minAmount : currentAmount; + auctionDetails.currentAmount = currentAmount; return auctionDetails; } } diff --git a/packages/contracts/test/extensions/dutch_auction.ts b/packages/contracts/test/extensions/dutch_auction.ts index 6a1e3ed45..4ad5cfa19 100644 --- a/packages/contracts/test/extensions/dutch_auction.ts +++ b/packages/contracts/test/extensions/dutch_auction.ts @@ -1,9 +1,11 @@ import { BlockchainLifecycle } from '@0x/dev-utils'; -import { assetDataUtils } from '@0x/order-utils'; +import { assetDataUtils, generatePseudoRandomSalt } 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 ethAbi = require('ethereumjs-abi'); +import * as ethUtil from 'ethereumjs-util'; import { DummyERC20TokenContract } from '../../generated-wrappers/dummy_erc20_token'; import { DummyERC721TokenContract } from '../../generated-wrappers/dummy_erc721_token'; @@ -26,6 +28,7 @@ chaiSetup.configure(); const expect = chai.expect; const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); const DECIMALS_DEFAULT = 18; +const ZERO = Web3Wrapper.toBaseUnitAmount(new BigNumber(0), DECIMALS_DEFAULT); describe.only(ContractName.DutchAuction, () => { let makerAddress: string; @@ -34,13 +37,11 @@ describe.only(ContractName.DutchAuction, () => { 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; @@ -48,11 +49,10 @@ describe.only(ContractName.DutchAuction, () => { let erc20Balances: ERC20BalancesByOwner; let tenMinutesInSeconds: number; let currentBlockTimestamp: number; - let auctionBeginTime: BigNumber; - let auctionEndTime: BigNumber; - let auctionBeginPrice: BigNumber; - let auctionEndPrice: BigNumber; - let encodedParams: BigNumber; + let auctionBeginTimeSeconds: BigNumber; + let auctionEndTimeSeconds: BigNumber; + let auctionBeginAmount: BigNumber; + let auctionEndAmount: BigNumber; let sellOrder: SignedOrder; let buyOrder: SignedOrder; let erc721MakerAssetIds: BigNumber[]; @@ -60,7 +60,7 @@ describe.only(ContractName.DutchAuction, () => { const timestampBefore = await getLatestBlockTimestampAsync(); await web3Wrapper.increaseTimeAsync(5); const timestampAfter = await getLatestBlockTimestampAsync(); - // HACK send some transactions + // HACK send some transactions when a time increase isn't supported if (timestampAfter === timestampBefore) { await web3Wrapper.sendTransactionAsync({ to: makerAddress, from: makerAddress, value: new BigNumber(1) }); await web3Wrapper.sendTransactionAsync({ to: makerAddress, from: makerAddress, value: new BigNumber(1) }); @@ -70,6 +70,20 @@ describe.only(ContractName.DutchAuction, () => { } } + function extendMakerAssetData(makerAssetData: string, beginTimeSeconds: BigNumber, beginAmount: BigNumber): string { + return ethUtil.bufferToHex( + Buffer.concat([ + ethUtil.toBuffer(makerAssetData), + ethUtil.toBuffer( + (ethAbi as any).rawEncode( + ['uint256', 'uint256'], + [beginTimeSeconds.toString(), beginAmount.toString()], + ), + ), + ]), + ); + } + before(async () => { await blockchainLifecycle.startAsync(); const accounts = await web3Wrapper.getAvailableAddressesAsync(); @@ -93,8 +107,7 @@ describe.only(ContractName.DutchAuction, () => { erc721MakerAssetIds = erc721Balances[makerAddress][erc721Token.address]; wethContract = await WETH9Contract.deployFrom0xArtifactAsync(artifacts.WETH9, provider, txDefaults); - weth = new DummyERC20TokenContract(wethContract.abi, wethContract.address, provider); - erc20Wrapper.addDummyTokenContract(weth); + erc20Wrapper.addDummyTokenContract(wethContract as any); const zrxAssetData = assetDataUtils.encodeERC20AssetData(zrxToken.address); const exchangeInstance = await ExchangeContract.deployFrom0xArtifactAsync( @@ -103,9 +116,8 @@ describe.only(ContractName.DutchAuction, () => { txDefaults, zrxAssetData, ); - exchangeWrapper = new ExchangeWrapper(exchangeInstance, provider); + const exchangeWrapper = new ExchangeWrapper(exchangeInstance, provider); await exchangeWrapper.registerAssetProxyAsync(erc20Proxy.address, owner); - await exchangeWrapper.registerAssetProxyAsync(erc721Proxy.address, owner); await erc20Proxy.addAuthorizedAddress.sendTransactionAsync(exchangeInstance.address, { @@ -151,28 +163,30 @@ describe.only(ContractName.DutchAuction, () => { tenMinutesInSeconds = 10 * 60; currentBlockTimestamp = await getLatestBlockTimestampAsync(); // Default auction begins 10 minutes ago - auctionBeginTime = new BigNumber(currentBlockTimestamp).minus(tenMinutesInSeconds); + auctionBeginTimeSeconds = new BigNumber(currentBlockTimestamp).minus(tenMinutesInSeconds); // Default auction ends 10 from now - auctionEndTime = new BigNumber(currentBlockTimestamp).plus(tenMinutesInSeconds); - auctionBeginPrice = Web3Wrapper.toBaseUnitAmount(new BigNumber(10), DECIMALS_DEFAULT); - auctionEndPrice = Web3Wrapper.toBaseUnitAmount(new BigNumber(1), DECIMALS_DEFAULT); - encodedParams = await dutchAuctionContract.encodeParameters.callAsync(auctionBeginTime, auctionBeginPrice); - const zero = Web3Wrapper.toBaseUnitAmount(new BigNumber(0), DECIMALS_DEFAULT); + auctionEndTimeSeconds = new BigNumber(currentBlockTimestamp).plus(tenMinutesInSeconds); + auctionBeginAmount = Web3Wrapper.toBaseUnitAmount(new BigNumber(10), DECIMALS_DEFAULT); + auctionEndAmount = Web3Wrapper.toBaseUnitAmount(new BigNumber(1), DECIMALS_DEFAULT); // Default sell order and buy order are exact mirrors const sellerDefaultOrderParams = { - salt: encodedParams, // Set the encoded params as the salt for the seller order + salt: generatePseudoRandomSalt(), exchangeAddress: exchangeInstance.address, makerAddress, feeRecipientAddress, senderAddress: dutchAuctionContract.address, - makerAssetData: assetDataUtils.encodeERC20AssetData(defaultMakerAssetAddress), + makerAssetData: extendMakerAssetData( + assetDataUtils.encodeERC20AssetData(defaultMakerAssetAddress), + auctionBeginTimeSeconds, + auctionBeginAmount, + ), takerAssetData: assetDataUtils.encodeERC20AssetData(defaultTakerAssetAddress), makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(200), DECIMALS_DEFAULT), - takerAssetAmount: auctionEndPrice, - expirationTimeSeconds: auctionEndTime, - makerFee: zero, - takerFee: zero, + takerAssetAmount: auctionEndAmount, + expirationTimeSeconds: auctionEndTimeSeconds, + makerFee: ZERO, + takerFee: ZERO, }; // Default buy order is for the auction begin price const buyerDefaultOrderParams = { @@ -180,12 +194,9 @@ describe.only(ContractName.DutchAuction, () => { makerAddress: takerAddress, makerAssetData: sellerDefaultOrderParams.takerAssetData, takerAssetData: sellerDefaultOrderParams.makerAssetData, - makerAssetAmount: auctionBeginPrice, - takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(200), DECIMALS_DEFAULT), + makerAssetAmount: auctionBeginAmount, + takerAssetAmount: sellerDefaultOrderParams.makerAssetAmount, }; - - encodedParams = await dutchAuctionContract.encodeParameters.callAsync(auctionBeginTime, auctionBeginPrice); - const makerPrivateKey = constants.TESTRPC_PRIVATE_KEYS[accounts.indexOf(makerAddress)]; const takerPrivateKey = constants.TESTRPC_PRIVATE_KEYS[accounts.indexOf(takerAddress)]; sellerOrderFactory = new OrderFactory(makerPrivateKey, sellerDefaultOrderParams); @@ -204,35 +215,33 @@ describe.only(ContractName.DutchAuction, () => { await blockchainLifecycle.revertAsync(); }); describe('matchOrders', () => { - it('should encode and decode parameters', async () => { - encodedParams = await dutchAuctionContract.encodeParameters.callAsync(auctionBeginTime, auctionBeginPrice); - 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 () => { - auctionBeginTime = new BigNumber(currentBlockTimestamp + 2); - encodedParams = await dutchAuctionContract.encodeParameters.callAsync(auctionBeginTime, auctionBeginPrice); + auctionBeginTimeSeconds = new BigNumber(currentBlockTimestamp + 2); sellOrder = await sellerOrderFactory.newSignedOrderAsync({ - salt: encodedParams, + makerAssetData: extendMakerAssetData( + assetDataUtils.encodeERC20AssetData(defaultMakerAssetAddress), + auctionBeginTimeSeconds, + auctionBeginAmount, + ), }); const auctionDetails = await dutchAuctionContract.getAuctionDetails.callAsync(sellOrder); - expect(auctionDetails.currentPrice).to.be.bignumber.equal(auctionBeginPrice); - expect(auctionDetails.beginPrice).to.be.bignumber.equal(auctionBeginPrice); + expect(auctionDetails.currentAmount).to.be.bignumber.equal(auctionBeginAmount); + expect(auctionDetails.beginAmount).to.be.bignumber.equal(auctionBeginAmount); }); it('should be be worth the end price at the end of the auction', async () => { - auctionBeginTime = new BigNumber(currentBlockTimestamp - 1000); - auctionEndTime = new BigNumber(currentBlockTimestamp - 100); - encodedParams = await dutchAuctionContract.encodeParameters.callAsync(auctionBeginTime, auctionBeginPrice); + auctionBeginTimeSeconds = new BigNumber(currentBlockTimestamp - 1000); + auctionEndTimeSeconds = new BigNumber(currentBlockTimestamp - 100); sellOrder = await sellerOrderFactory.newSignedOrderAsync({ - salt: encodedParams, - expirationTimeSeconds: auctionEndTime, + makerAssetData: extendMakerAssetData( + assetDataUtils.encodeERC20AssetData(defaultMakerAssetAddress), + auctionBeginTimeSeconds, + auctionBeginAmount, + ), + expirationTimeSeconds: auctionEndTimeSeconds, }); const auctionDetails = await dutchAuctionContract.getAuctionDetails.callAsync(sellOrder); - expect(auctionDetails.currentPrice).to.be.bignumber.equal(auctionEndPrice); - expect(auctionDetails.beginPrice).to.be.bignumber.equal(auctionBeginPrice); + expect(auctionDetails.currentAmount).to.be.bignumber.equal(auctionEndAmount); + expect(auctionDetails.beginAmount).to.be.bignumber.equal(auctionBeginAmount); }); it('should match orders and send excess to seller', async () => { const txHash = await dutchAuctionContract.matchOrders.sendTransactionAsync( @@ -246,23 +255,23 @@ describe.only(ContractName.DutchAuction, () => { ); await web3Wrapper.awaitTransactionSuccessAsync(txHash); const newBalances = await erc20Wrapper.getBalancesAsync(); - expect(newBalances[dutchAuctionContract.address][weth.address]).to.be.bignumber.equal( + expect(newBalances[dutchAuctionContract.address][wethContract.address]).to.be.bignumber.equal( constants.ZERO_AMOUNT, ); - expect(newBalances[makerAddress][weth.address]).to.be.bignumber.equal( - erc20Balances[makerAddress][weth.address].plus(buyOrder.makerAssetAmount), + expect(newBalances[makerAddress][wethContract.address]).to.be.bignumber.equal( + erc20Balances[makerAddress][wethContract.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; + const beforeAmount = auctionDetails.currentAmount; await increaseTimeAsync(); auctionDetails = await dutchAuctionContract.getAuctionDetails.callAsync(sellOrder); - const currentPrice = auctionDetails.currentPrice; - expect(beforePrice).to.be.bignumber.greaterThan(currentPrice); + const currentAmount = auctionDetails.currentAmount; + expect(beforeAmount).to.be.bignumber.greaterThan(currentAmount); buyOrder = await buyerOrderFactory.newSignedOrderAsync({ - makerAssetAmount: currentPrice, + makerAssetAmount: currentAmount, }); const txHash = await dutchAuctionContract.matchOrders.sendTransactionAsync( buyOrder, @@ -275,16 +284,14 @@ describe.only(ContractName.DutchAuction, () => { ); await web3Wrapper.awaitTransactionSuccessAsync(txHash); const newBalances = await erc20Wrapper.getBalancesAsync(); - expect(newBalances[makerAddress][weth.address]).to.be.bignumber.equal( - erc20Balances[makerAddress][weth.address].plus(currentPrice), + expect(newBalances[makerAddress][wethContract.address]).to.be.bignumber.equal( + erc20Balances[makerAddress][wethContract.address].plus(currentAmount), ); }); it('should revert when auction expires', async () => { - auctionEndTime = new BigNumber(currentBlockTimestamp - 100); - encodedParams = await dutchAuctionContract.encodeParameters.callAsync(auctionBeginTime, auctionBeginPrice); + auctionEndTimeSeconds = new BigNumber(currentBlockTimestamp - 100); sellOrder = await sellerOrderFactory.newSignedOrderAsync({ - salt: encodedParams, - expirationTimeSeconds: auctionEndTime, + expirationTimeSeconds: auctionEndTimeSeconds, }); return expectTransactionFailedAsync( dutchAuctionContract.matchOrders.sendTransactionAsync( @@ -314,7 +321,43 @@ describe.only(ContractName.DutchAuction, () => { from: takerAddress, }, ), - 'INVALID_PRICE' as any, + 'INVALID_AMOUNT' as any, + ); + }); + it('cannot match an expired auction', async () => { + auctionBeginTimeSeconds = new BigNumber(currentBlockTimestamp - 1000); + auctionEndTimeSeconds = new BigNumber(currentBlockTimestamp - 100); + sellOrder = await sellerOrderFactory.newSignedOrderAsync({ + expirationTimeSeconds: auctionEndTimeSeconds, + }); + return expectTransactionFailedAsync( + dutchAuctionContract.matchOrders.sendTransactionAsync( + buyOrder, + sellOrder, + buyOrder.signature, + sellOrder.signature, + { + from: takerAddress, + }, + ), + 'AUCTION_EXPIRED' as any, + ); + }); + it('auction begin amount must be higher than final amount ', async () => { + sellOrder = await sellerOrderFactory.newSignedOrderAsync({ + takerAssetAmount: auctionBeginAmount.plus(1), + }); + return expectTransactionFailedAsync( + dutchAuctionContract.matchOrders.sendTransactionAsync( + buyOrder, + sellOrder, + buyOrder.signature, + sellOrder.signature, + { + from: takerAddress, + }, + ), + 'INVALID_AMOUNT' as any, ); }); describe('ERC721', () => { @@ -322,11 +365,15 @@ describe.only(ContractName.DutchAuction, () => { const makerAssetId = erc721MakerAssetIds[0]; sellOrder = await sellerOrderFactory.newSignedOrderAsync({ makerAssetAmount: new BigNumber(1), - makerAssetData: assetDataUtils.encodeERC721AssetData(erc721Token.address, makerAssetId), + makerAssetData: extendMakerAssetData( + assetDataUtils.encodeERC721AssetData(erc721Token.address, makerAssetId), + auctionBeginTimeSeconds, + auctionBeginAmount, + ), }); buyOrder = await buyerOrderFactory.newSignedOrderAsync({ takerAssetAmount: new BigNumber(1), - takerAssetData: assetDataUtils.encodeERC721AssetData(erc721Token.address, makerAssetId), + takerAssetData: sellOrder.makerAssetData, }); const txHash = await dutchAuctionContract.matchOrders.sendTransactionAsync( buyOrder, @@ -339,8 +386,8 @@ describe.only(ContractName.DutchAuction, () => { ); 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), + expect(newBalances[makerAddress][wethContract.address]).to.be.bignumber.equal( + erc20Balances[makerAddress][wethContract.address].plus(buyOrder.makerAssetAmount), ); const newOwner = await erc721Token.ownerOf.callAsync(makerAssetId); expect(newOwner).to.be.bignumber.equal(takerAddress); -- cgit From 7934d77defe6510081090398ea352f5ac3d4e3df Mon Sep 17 00:00:00 2001 From: Jacob Evans Date: Wed, 21 Nov 2018 16:14:59 +1100 Subject: chore: taker address and zrx fee test --- .../contracts/test/extensions/dutch_auction.ts | 47 +++++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) (limited to 'packages') diff --git a/packages/contracts/test/extensions/dutch_auction.ts b/packages/contracts/test/extensions/dutch_auction.ts index 4ad5cfa19..dbd956c0f 100644 --- a/packages/contracts/test/extensions/dutch_auction.ts +++ b/packages/contracts/test/extensions/dutch_auction.ts @@ -175,7 +175,8 @@ describe.only(ContractName.DutchAuction, () => { exchangeAddress: exchangeInstance.address, makerAddress, feeRecipientAddress, - senderAddress: dutchAuctionContract.address, + // taker address or sender address should be set to the ducth auction contract + takerAddress: dutchAuctionContract.address, makerAssetData: extendMakerAssetData( assetDataUtils.encodeERC20AssetData(defaultMakerAssetAddress), auctionBeginTimeSeconds, @@ -288,6 +289,50 @@ describe.only(ContractName.DutchAuction, () => { erc20Balances[makerAddress][wethContract.address].plus(currentAmount), ); }); + it('maker fees on sellOrder are paid to the fee receipient', async () => { + sellOrder = await sellerOrderFactory.newSignedOrderAsync({ + makerFee: new BigNumber(1), + }); + 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][wethContract.address]).to.be.bignumber.equal( + erc20Balances[makerAddress][wethContract.address].plus(buyOrder.makerAssetAmount), + ); + expect(newBalances[feeRecipientAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[feeRecipientAddress][zrxToken.address].plus(sellOrder.makerFee), + ); + }); + it('maker fees on buyOrder are paid to the fee receipient', async () => { + buyOrder = await buyerOrderFactory.newSignedOrderAsync({ + makerFee: new BigNumber(1), + }); + 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][wethContract.address]).to.be.bignumber.equal( + erc20Balances[makerAddress][wethContract.address].plus(buyOrder.makerAssetAmount), + ); + expect(newBalances[feeRecipientAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[feeRecipientAddress][zrxToken.address].plus(buyOrder.makerFee), + ); + }); it('should revert when auction expires', async () => { auctionEndTimeSeconds = new BigNumber(currentBlockTimestamp - 100); sellOrder = await sellerOrderFactory.newSignedOrderAsync({ -- cgit From 54aaee8e42dda50dc325451387b98c9f75bcb6ab Mon Sep 17 00:00:00 2001 From: Jacob Evans Date: Wed, 21 Nov 2018 16:34:38 +1100 Subject: chore: update documentation --- .../extensions/DutchAuction/DutchAuction.sol | 37 ++++++++++++++-------- .../contracts/test/extensions/dutch_auction.ts | 2 +- 2 files changed, 25 insertions(+), 14 deletions(-) (limited to 'packages') diff --git a/packages/contracts/contracts/extensions/DutchAuction/DutchAuction.sol b/packages/contracts/contracts/extensions/DutchAuction/DutchAuction.sol index a304184f1..225e33929 100644 --- a/packages/contracts/contracts/extensions/DutchAuction/DutchAuction.sol +++ b/packages/contracts/contracts/extensions/DutchAuction/DutchAuction.sol @@ -32,11 +32,11 @@ contract DutchAuction { IExchange internal EXCHANGE; struct AuctionDetails { - uint256 beginTimeSeconds; // Auction begin time in seconds - uint256 endTimeSeconds; // Auction end time in seconds - uint256 beginAmount; // Auction begin amount - uint256 endAmount; // Auction end amount - uint256 currentAmount; // Current auction amount at block.timestamp + uint256 beginTimeSeconds; // Auction begin time in seconds: sellOrder.makerAssetData + uint256 endTimeSeconds; // Auction end time in seconds: sellOrder.expiryTimeSeconds + uint256 beginAmount; // Auction begin amount: sellOrder.makerAssetData + uint256 endAmount; // Auction end amount: sellOrder.takerAssetAmount + uint256 currentAmount; // Calculated amount given block.timestamp uint256 currentTimeSeconds; // block.timestamp } @@ -46,14 +46,25 @@ contract DutchAuction { EXCHANGE = IExchange(_exchange); } - /// @dev Performs a match of the two orders at the amount given: the current block time, the auction - /// start time and the auction begin amount. - /// The Sellers order is a signed order at the lowest amount 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. + /// @dev Matches the buy and sell orders at an amount given the following: the current block time, the auction + /// start time and the auction begin amount. The sell order is a an order at the lowest amount + /// at the end of the auction. Excess from the match is transferred to the seller. + /// Over time the price moves from beginAmount to endAmount given the current block.timestamp. + /// sellOrder.expiryTimeSeconds is the end time of the auction. + /// sellOrder.takerAssetAmount is the end amount of the auction (lowest possible amount). + /// sellOrder.makerAssetData is the ABI encoded Asset Proxy data with the following data appended + /// buyOrder.makerAssetData is the buyers bid on the auction, must meet the amount for the current block timestamp + /// (uint256 beginTimeSeconds, uint256 beginAmount). + /// This function reverts in the following scenarios: + /// * Auction has not started (auctionDetails.currentTimeSeconds < auctionDetails.beginTimeSeconds) + /// * Auction has expired (auctionDetails.endTimeSeconds < auctionDetails.currentTimeSeconds) + /// * Amount is invalid: Buy order amount is too low (buyOrder.makerAssetAmount < auctionDetails.currentAmount) + /// * Amount is invalid: Invalid begin amount (auctionDetails.beginAmount > auctionDetails.endAmount) + /// * Any failure in the 0x Match Orders + /// @param buyOrder The Buyer's order. This order is for the current expected price of the auction. + /// @param sellOrder The Seller's order. This order is for the lowest amount (at the end of the auction). + /// @param buySignature Proof that order was created by the buyer. + /// @param sellSignature Proof that order was created by the seller. /// @return matchedFillResults amounts filled and fees paid by maker and taker of matched orders. function matchOrders( LibOrder.Order memory buyOrder, diff --git a/packages/contracts/test/extensions/dutch_auction.ts b/packages/contracts/test/extensions/dutch_auction.ts index dbd956c0f..207da6796 100644 --- a/packages/contracts/test/extensions/dutch_auction.ts +++ b/packages/contracts/test/extensions/dutch_auction.ts @@ -30,7 +30,7 @@ const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); const DECIMALS_DEFAULT = 18; const ZERO = Web3Wrapper.toBaseUnitAmount(new BigNumber(0), DECIMALS_DEFAULT); -describe.only(ContractName.DutchAuction, () => { +describe(ContractName.DutchAuction, () => { let makerAddress: string; let owner: string; let takerAddress: string; -- cgit From a5eb1bcc20d62c222010e838944a084dde0867da Mon Sep 17 00:00:00 2001 From: Jacob Evans Date: Thu, 22 Nov 2018 15:26:39 +1100 Subject: chore: Return excess to buyer --- .../extensions/DutchAuction/DutchAuction.sol | 17 ++++-- .../contracts/test/extensions/dutch_auction.ts | 70 ++++++++++++++-------- 2 files changed, 58 insertions(+), 29 deletions(-) (limited to 'packages') diff --git a/packages/contracts/contracts/extensions/DutchAuction/DutchAuction.sol b/packages/contracts/contracts/extensions/DutchAuction/DutchAuction.sol index 225e33929..dea836da9 100644 --- a/packages/contracts/contracts/extensions/DutchAuction/DutchAuction.sol +++ b/packages/contracts/contracts/extensions/DutchAuction/DutchAuction.sol @@ -94,11 +94,20 @@ contract DutchAuction { // Return any spread to the seller uint256 leftMakerAssetSpreadAmount = matchedFillResults.leftMakerAssetSpreadAmount; if (leftMakerAssetSpreadAmount > 0) { - // Assume auction is for ERC20 + // Calculate the excess from the buy order. This can occur if the buyer sends in a higher + // amount than the calculated current amount + uint256 buyerExcessAmount = buyOrder.makerAssetAmount-auctionDetails.currentAmount; + uint256 sellerExcessAmount = leftMakerAssetSpreadAmount-buyerExcessAmount; bytes memory assetData = sellOrder.takerAssetData; address token = assetData.readAddress(16); - address makerAddress = sellOrder.makerAddress; - IERC20Token(token).transfer(makerAddress, leftMakerAssetSpreadAmount); + if (sellerExcessAmount > 0) { + address makerAddress = sellOrder.makerAddress; + IERC20Token(token).transfer(makerAddress, sellerExcessAmount); + } + if (buyerExcessAmount > 0) { + address takerAddress = buyOrder.makerAddress; + IERC20Token(token).transfer(takerAddress, buyerExcessAmount); + } } return matchedFillResults; } @@ -116,7 +125,7 @@ contract DutchAuction { // We assume auctionBeginTimeSeconds and auctionBeginAmount are appended to the makerAssetData uint256 auctionBeginTimeSeconds = order.makerAssetData.readUint256(makerAssetDataLength-64); uint256 auctionBeginAmount = order.makerAssetData.readUint256(makerAssetDataLength-32); - // require(order.expirationTimeSeconds > auctionBeginTimeSeconds, "INVALID_BEGIN_TIME"); + require(order.expirationTimeSeconds > auctionBeginTimeSeconds, "INVALID_BEGIN_TIME"); uint256 auctionDurationSeconds = order.expirationTimeSeconds-auctionBeginTimeSeconds; uint256 minAmount = order.takerAssetAmount; // solhint-disable-next-line not-rely-on-time diff --git a/packages/contracts/test/extensions/dutch_auction.ts b/packages/contracts/test/extensions/dutch_auction.ts index 207da6796..28e72858c 100644 --- a/packages/contracts/test/extensions/dutch_auction.ts +++ b/packages/contracts/test/extensions/dutch_auction.ts @@ -28,9 +28,8 @@ chaiSetup.configure(); const expect = chai.expect; const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); const DECIMALS_DEFAULT = 18; -const ZERO = Web3Wrapper.toBaseUnitAmount(new BigNumber(0), DECIMALS_DEFAULT); -describe(ContractName.DutchAuction, () => { +describe.only(ContractName.DutchAuction, () => { let makerAddress: string; let owner: string; let takerAddress: string; @@ -186,8 +185,8 @@ describe(ContractName.DutchAuction, () => { makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(200), DECIMALS_DEFAULT), takerAssetAmount: auctionEndAmount, expirationTimeSeconds: auctionEndTimeSeconds, - makerFee: ZERO, - takerFee: ZERO, + makerFee: constants.ZERO_AMOUNT, + takerFee: constants.ZERO_AMOUNT, }; // Default buy order is for the auction begin price const buyerDefaultOrderParams = { @@ -244,23 +243,36 @@ describe(ContractName.DutchAuction, () => { expect(auctionDetails.currentAmount).to.be.bignumber.equal(auctionEndAmount); expect(auctionDetails.beginAmount).to.be.bignumber.equal(auctionBeginAmount); }); - it('should match orders and send excess to seller', async () => { - const txHash = await dutchAuctionContract.matchOrders.sendTransactionAsync( - buyOrder, + it('should match orders at current amount and send excess to buyer', async () => { + let auctionDetails = await dutchAuctionContract.getAuctionDetails.callAsync(sellOrder); + buyOrder = await buyerOrderFactory.newSignedOrderAsync({ + makerAssetAmount: auctionDetails.currentAmount.times(2), + }); + const receipt = await web3Wrapper.awaitTransactionSuccessAsync( + await dutchAuctionContract.matchOrders.sendTransactionAsync( + buyOrder, + sellOrder, + buyOrder.signature, + sellOrder.signature, + { + from: takerAddress, + }, + ), + ); + auctionDetails = await dutchAuctionContract.getAuctionDetails.callAsync( sellOrder, - buyOrder.signature, - sellOrder.signature, - { - from: takerAddress, - }, + {}, + parseInt(receipt.blockNumber as any, 16), ); - await web3Wrapper.awaitTransactionSuccessAsync(txHash); const newBalances = await erc20Wrapper.getBalancesAsync(); expect(newBalances[dutchAuctionContract.address][wethContract.address]).to.be.bignumber.equal( constants.ZERO_AMOUNT, ); expect(newBalances[makerAddress][wethContract.address]).to.be.bignumber.equal( - erc20Balances[makerAddress][wethContract.address].plus(buyOrder.makerAssetAmount), + erc20Balances[makerAddress][wethContract.address].plus(auctionDetails.currentAmount), + ); + expect(newBalances[takerAddress][wethContract.address]).to.be.bignumber.equal( + erc20Balances[takerAddress][wethContract.address].minus(auctionDetails.currentAmount), ); }); it('should have valid getAuctionDetails at a block in the future', async () => { @@ -293,6 +305,7 @@ describe(ContractName.DutchAuction, () => { sellOrder = await sellerOrderFactory.newSignedOrderAsync({ makerFee: new BigNumber(1), }); + const auctionDetails = await dutchAuctionContract.getAuctionDetails.callAsync(sellOrder); const txHash = await dutchAuctionContract.matchOrders.sendTransactionAsync( buyOrder, sellOrder, @@ -305,7 +318,7 @@ describe(ContractName.DutchAuction, () => { await web3Wrapper.awaitTransactionSuccessAsync(txHash); const newBalances = await erc20Wrapper.getBalancesAsync(); expect(newBalances[makerAddress][wethContract.address]).to.be.bignumber.equal( - erc20Balances[makerAddress][wethContract.address].plus(buyOrder.makerAssetAmount), + erc20Balances[makerAddress][wethContract.address].plus(auctionDetails.currentAmount), ); expect(newBalances[feeRecipientAddress][zrxToken.address]).to.be.bignumber.equal( erc20Balances[feeRecipientAddress][zrxToken.address].plus(sellOrder.makerFee), @@ -315,6 +328,7 @@ describe(ContractName.DutchAuction, () => { buyOrder = await buyerOrderFactory.newSignedOrderAsync({ makerFee: new BigNumber(1), }); + const auctionDetails = await dutchAuctionContract.getAuctionDetails.callAsync(sellOrder); const txHash = await dutchAuctionContract.matchOrders.sendTransactionAsync( buyOrder, sellOrder, @@ -327,7 +341,7 @@ describe(ContractName.DutchAuction, () => { await web3Wrapper.awaitTransactionSuccessAsync(txHash); const newBalances = await erc20Wrapper.getBalancesAsync(); expect(newBalances[makerAddress][wethContract.address]).to.be.bignumber.equal( - erc20Balances[makerAddress][wethContract.address].plus(buyOrder.makerAssetAmount), + erc20Balances[makerAddress][wethContract.address].plus(auctionDetails.currentAmount), ); expect(newBalances[feeRecipientAddress][zrxToken.address]).to.be.bignumber.equal( erc20Balances[feeRecipientAddress][zrxToken.address].plus(buyOrder.makerFee), @@ -420,19 +434,25 @@ describe(ContractName.DutchAuction, () => { takerAssetAmount: new BigNumber(1), takerAssetData: sellOrder.makerAssetData, }); - const txHash = await dutchAuctionContract.matchOrders.sendTransactionAsync( - buyOrder, + const receipt = await web3Wrapper.awaitTransactionSuccessAsync( + await dutchAuctionContract.matchOrders.sendTransactionAsync( + buyOrder, + sellOrder, + buyOrder.signature, + sellOrder.signature, + { + from: takerAddress, + }, + ), + ); + const auctionDetails = await dutchAuctionContract.getAuctionDetails.callAsync( sellOrder, - buyOrder.signature, - sellOrder.signature, - { - from: takerAddress, - }, + {}, + parseInt(receipt.blockNumber as any, 16), ); - await web3Wrapper.awaitTransactionSuccessAsync(txHash); const newBalances = await erc20Wrapper.getBalancesAsync(); expect(newBalances[makerAddress][wethContract.address]).to.be.bignumber.equal( - erc20Balances[makerAddress][wethContract.address].plus(buyOrder.makerAssetAmount), + erc20Balances[makerAddress][wethContract.address].plus(auctionDetails.currentAmount), ); const newOwner = await erc721Token.ownerOf.callAsync(makerAssetId); expect(newOwner).to.be.bignumber.equal(takerAddress); -- cgit From afa24aa122f0217b64f853ec33a8f3813cc80e1f Mon Sep 17 00:00:00 2001 From: Jacob Evans Date: Thu, 22 Nov 2018 15:40:34 +1100 Subject: chore: Work around for Ganache timestamp bug --- .../contracts/test/extensions/dutch_auction.ts | 31 ++++++++++------------ 1 file changed, 14 insertions(+), 17 deletions(-) (limited to 'packages') diff --git a/packages/contracts/test/extensions/dutch_auction.ts b/packages/contracts/test/extensions/dutch_auction.ts index 28e72858c..ae614cbd8 100644 --- a/packages/contracts/test/extensions/dutch_auction.ts +++ b/packages/contracts/test/extensions/dutch_auction.ts @@ -244,11 +244,11 @@ describe.only(ContractName.DutchAuction, () => { expect(auctionDetails.beginAmount).to.be.bignumber.equal(auctionBeginAmount); }); it('should match orders at current amount and send excess to buyer', async () => { - let auctionDetails = await dutchAuctionContract.getAuctionDetails.callAsync(sellOrder); + const auctionDetails = await dutchAuctionContract.getAuctionDetails.callAsync(sellOrder); buyOrder = await buyerOrderFactory.newSignedOrderAsync({ makerAssetAmount: auctionDetails.currentAmount.times(2), }); - const receipt = await web3Wrapper.awaitTransactionSuccessAsync( + await web3Wrapper.awaitTransactionSuccessAsync( await dutchAuctionContract.matchOrders.sendTransactionAsync( buyOrder, sellOrder, @@ -259,16 +259,14 @@ describe.only(ContractName.DutchAuction, () => { }, ), ); - auctionDetails = await dutchAuctionContract.getAuctionDetails.callAsync( - sellOrder, - {}, - parseInt(receipt.blockNumber as any, 16), - ); const newBalances = await erc20Wrapper.getBalancesAsync(); expect(newBalances[dutchAuctionContract.address][wethContract.address]).to.be.bignumber.equal( constants.ZERO_AMOUNT, ); - expect(newBalances[makerAddress][wethContract.address]).to.be.bignumber.equal( + // HACK gte used here due to a bug in ganache where the timestamp can change + // between multiple calls to the same block. Which can move the amount in our case + // ref: https://github.com/trufflesuite/ganache-core/issues/111 + expect(newBalances[makerAddress][wethContract.address]).to.be.bignumber.gte( erc20Balances[makerAddress][wethContract.address].plus(auctionDetails.currentAmount), ); expect(newBalances[takerAddress][wethContract.address]).to.be.bignumber.equal( @@ -317,7 +315,7 @@ describe.only(ContractName.DutchAuction, () => { ); await web3Wrapper.awaitTransactionSuccessAsync(txHash); const newBalances = await erc20Wrapper.getBalancesAsync(); - expect(newBalances[makerAddress][wethContract.address]).to.be.bignumber.equal( + expect(newBalances[makerAddress][wethContract.address]).to.be.bignumber.gte( erc20Balances[makerAddress][wethContract.address].plus(auctionDetails.currentAmount), ); expect(newBalances[feeRecipientAddress][zrxToken.address]).to.be.bignumber.equal( @@ -340,7 +338,7 @@ describe.only(ContractName.DutchAuction, () => { ); await web3Wrapper.awaitTransactionSuccessAsync(txHash); const newBalances = await erc20Wrapper.getBalancesAsync(); - expect(newBalances[makerAddress][wethContract.address]).to.be.bignumber.equal( + expect(newBalances[makerAddress][wethContract.address]).to.be.bignumber.gte( erc20Balances[makerAddress][wethContract.address].plus(auctionDetails.currentAmount), ); expect(newBalances[feeRecipientAddress][zrxToken.address]).to.be.bignumber.equal( @@ -434,7 +432,8 @@ describe.only(ContractName.DutchAuction, () => { takerAssetAmount: new BigNumber(1), takerAssetData: sellOrder.makerAssetData, }); - const receipt = await web3Wrapper.awaitTransactionSuccessAsync( + const auctionDetails = await dutchAuctionContract.getAuctionDetails.callAsync(sellOrder); + await web3Wrapper.awaitTransactionSuccessAsync( await dutchAuctionContract.matchOrders.sendTransactionAsync( buyOrder, sellOrder, @@ -445,13 +444,11 @@ describe.only(ContractName.DutchAuction, () => { }, ), ); - const auctionDetails = await dutchAuctionContract.getAuctionDetails.callAsync( - sellOrder, - {}, - parseInt(receipt.blockNumber as any, 16), - ); const newBalances = await erc20Wrapper.getBalancesAsync(); - expect(newBalances[makerAddress][wethContract.address]).to.be.bignumber.equal( + // HACK gte used here due to a bug in ganache where the timestamp can change + // between multiple calls to the same block. Which can move the amount in our case + // ref: https://github.com/trufflesuite/ganache-core/issues/111 + expect(newBalances[makerAddress][wethContract.address]).to.be.bignumber.gte( erc20Balances[makerAddress][wethContract.address].plus(auctionDetails.currentAmount), ); const newOwner = await erc721Token.ownerOf.callAsync(makerAssetId); -- cgit From 6b1fea602ed762dc882872a80616f404e9a52525 Mon Sep 17 00:00:00 2001 From: Jacob Evans Date: Tue, 27 Nov 2018 15:44:22 +1100 Subject: chore: Clean up documentation --- .../extensions/DutchAuction/DutchAuction.sol | 46 +++++++++++++++++----- .../contracts/test/extensions/dutch_auction.ts | 42 ++++++++++---------- 2 files changed, 58 insertions(+), 30 deletions(-) (limited to 'packages') diff --git a/packages/contracts/contracts/extensions/DutchAuction/DutchAuction.sol b/packages/contracts/contracts/extensions/DutchAuction/DutchAuction.sol index dea836da9..7115e5bd5 100644 --- a/packages/contracts/contracts/extensions/DutchAuction/DutchAuction.sol +++ b/packages/contracts/contracts/extensions/DutchAuction/DutchAuction.sol @@ -32,8 +32,8 @@ contract DutchAuction { IExchange internal EXCHANGE; struct AuctionDetails { - uint256 beginTimeSeconds; // Auction begin time in seconds: sellOrder.makerAssetData - uint256 endTimeSeconds; // Auction end time in seconds: sellOrder.expiryTimeSeconds + uint256 beginTimeSeconds; // Auction begin unix timestamp: sellOrder.makerAssetData + uint256 endTimeSeconds; // Auction end unix timestamp: sellOrder.expiryTimeSeconds uint256 beginAmount; // Auction begin amount: sellOrder.makerAssetData uint256 endAmount; // Auction end amount: sellOrder.takerAssetAmount uint256 currentAmount; // Calculated amount given block.timestamp @@ -80,8 +80,6 @@ contract DutchAuction { require(auctionDetails.currentTimeSeconds >= auctionDetails.beginTimeSeconds, "AUCTION_NOT_STARTED"); // Ensure the auction has not expired. This will fail later in 0x but we can save gas by failing early require(sellOrder.expirationTimeSeconds > auctionDetails.currentTimeSeconds, "AUCTION_EXPIRED"); - // Ensure the auction goes from high to low - require(auctionDetails.beginAmount > auctionDetails.endAmount, "INVALID_AMOUNT"); // Validate the buyer amount is greater than the current auction amount require(buyOrder.makerAssetAmount >= auctionDetails.currentAmount, "INVALID_AMOUNT"); // Match orders, maximally filling `buyOrder` @@ -91,19 +89,34 @@ contract DutchAuction { buySignature, sellSignature ); - // Return any spread to the seller + // The difference in sellOrder.takerAssetAmount and current amount is given as spread to the matcher + // This may include additional spread from the buyOrder.makerAssetAmount and the currentAmount. + // e.g currentAmount is 30, sellOrder.takerAssetAmount is 10 and buyOrder.makerAssetamount is 40. + // 10 (40-30) is returned to the buyer, 20 (30-10) sent to the seller and 10 has previously + // been transferred to the seller during matchOrders uint256 leftMakerAssetSpreadAmount = matchedFillResults.leftMakerAssetSpreadAmount; if (leftMakerAssetSpreadAmount > 0) { + // ERC20 Asset data itself is encoded as follows: + // + // | Area | Offset | Length | Contents | + // |----------|--------|---------|-------------------------------------| + // | Header | 0 | 4 | function selector | + // | Params | | 1 * 32 | function parameters: | + // | | 4 | 12 + 20 | 1. token address | + bytes memory assetData = sellOrder.takerAssetData; + address token = assetData.readAddress(16); // Calculate the excess from the buy order. This can occur if the buyer sends in a higher // amount than the calculated current amount uint256 buyerExcessAmount = buyOrder.makerAssetAmount-auctionDetails.currentAmount; uint256 sellerExcessAmount = leftMakerAssetSpreadAmount-buyerExcessAmount; - bytes memory assetData = sellOrder.takerAssetData; - address token = assetData.readAddress(16); + // Return the difference between auctionDetails.currentAmount and sellOrder.takerAssetAmount + // to the seller if (sellerExcessAmount > 0) { address makerAddress = sellOrder.makerAddress; IERC20Token(token).transfer(makerAddress, sellerExcessAmount); } + // Return the difference between buyOrder.makerAssetAmount and auctionDetails.currentAmount + // to the buyer if (buyerExcessAmount > 0) { address takerAddress = buyOrder.makerAddress; IERC20Token(token).transfer(takerAddress, buyerExcessAmount); @@ -122,12 +135,26 @@ contract DutchAuction { returns (AuctionDetails memory auctionDetails) { uint256 makerAssetDataLength = order.makerAssetData.length; - // We assume auctionBeginTimeSeconds and auctionBeginAmount are appended to the makerAssetData + // It is unknown the encoded data of makerAssetData, we assume the last 64 bytes + // are the Auction Details encoding. + // Auction Details is encoded as follows: + // + // | Area | Offset | Length | Contents | + // |----------|--------|---------|-------------------------------------| + // | Params | | 2 * 32 | parameters: | + // | | -64 | 32 | 1. auction begin unix timestamp | + // | | -32 | 32 | 2. auction begin begin amount | + // ERC20 asset data length is 4+32, 64 for auction details results in min length if 100 + require(makerAssetDataLength > 10, "INVALID_ASSET_DATA"); uint256 auctionBeginTimeSeconds = order.makerAssetData.readUint256(makerAssetDataLength-64); uint256 auctionBeginAmount = order.makerAssetData.readUint256(makerAssetDataLength-32); + // Ensure the auction has a valid begin time require(order.expirationTimeSeconds > auctionBeginTimeSeconds, "INVALID_BEGIN_TIME"); uint256 auctionDurationSeconds = order.expirationTimeSeconds-auctionBeginTimeSeconds; + // Ensure the auction goes from high to low uint256 minAmount = order.takerAssetAmount; + require(auctionBeginAmount > minAmount, "INVALID_AMOUNT"); + uint256 amountDelta = auctionBeginAmount-minAmount; // solhint-disable-next-line not-rely-on-time uint256 timestamp = block.timestamp; auctionDetails.beginTimeSeconds = auctionBeginTimeSeconds; @@ -137,8 +164,9 @@ contract DutchAuction { auctionDetails.currentTimeSeconds = timestamp; uint256 remainingDurationSeconds = order.expirationTimeSeconds-timestamp; - uint256 amountDelta = auctionBeginAmount-minAmount; uint256 currentAmount = minAmount + (remainingDurationSeconds*amountDelta/auctionDurationSeconds); + // Check the bounds where we SafeMath was avoivded so the auction details can be queried prior + // and after the auction time. // If the auction has not yet begun the current amount is the auctionBeginAmount currentAmount = timestamp < auctionBeginTimeSeconds ? auctionBeginAmount : currentAmount; // If the auction has ended the current amount is the minAmount diff --git a/packages/contracts/test/extensions/dutch_auction.ts b/packages/contracts/test/extensions/dutch_auction.ts index ae614cbd8..b3408e27d 100644 --- a/packages/contracts/test/extensions/dutch_auction.ts +++ b/packages/contracts/test/extensions/dutch_auction.ts @@ -6,6 +6,7 @@ import { Web3Wrapper } from '@0x/web3-wrapper'; import * as chai from 'chai'; import ethAbi = require('ethereumjs-abi'); import * as ethUtil from 'ethereumjs-util'; +import * as _ from 'lodash'; import { DummyERC20TokenContract } from '../../generated-wrappers/dummy_erc20_token'; import { DummyERC721TokenContract } from '../../generated-wrappers/dummy_erc721_token'; @@ -29,7 +30,7 @@ const expect = chai.expect; const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); const DECIMALS_DEFAULT = 18; -describe.only(ContractName.DutchAuction, () => { +describe(ContractName.DutchAuction, () => { let makerAddress: string; let owner: string; let takerAddress: string; @@ -46,7 +47,6 @@ describe.only(ContractName.DutchAuction, () => { let buyerOrderFactory: OrderFactory; let erc20Wrapper: ERC20Wrapper; let erc20Balances: ERC20BalancesByOwner; - let tenMinutesInSeconds: number; let currentBlockTimestamp: number; let auctionBeginTimeSeconds: BigNumber; let auctionEndTimeSeconds: BigNumber; @@ -55,6 +55,8 @@ describe.only(ContractName.DutchAuction, () => { let sellOrder: SignedOrder; let buyOrder: SignedOrder; let erc721MakerAssetIds: BigNumber[]; + const tenMinutesInSeconds = 10 * 60; + async function increaseTimeAsync(): Promise { const timestampBefore = await getLatestBlockTimestampAsync(); await web3Wrapper.increaseTimeAsync(5); @@ -62,10 +64,6 @@ describe.only(ContractName.DutchAuction, () => { // HACK send some transactions when a time increase isn't supported if (timestampAfter === timestampBefore) { await web3Wrapper.sendTransactionAsync({ to: makerAddress, from: makerAddress, value: new BigNumber(1) }); - await web3Wrapper.sendTransactionAsync({ to: makerAddress, from: makerAddress, value: new BigNumber(1) }); - await web3Wrapper.sendTransactionAsync({ to: makerAddress, from: makerAddress, value: new BigNumber(1) }); - await web3Wrapper.sendTransactionAsync({ to: makerAddress, from: makerAddress, value: new BigNumber(1) }); - await web3Wrapper.sendTransactionAsync({ to: makerAddress, from: makerAddress, value: new BigNumber(1) }); } } @@ -159,7 +157,6 @@ describe.only(ContractName.DutchAuction, () => { web3Wrapper.abiDecoder.addABI(zrxToken.abi); erc20Wrapper.addTokenOwnerAddress(dutchAuctionContract.address); - tenMinutesInSeconds = 10 * 60; currentBlockTimestamp = await getLatestBlockTimestampAsync(); // Default auction begins 10 minutes ago auctionBeginTimeSeconds = new BigNumber(currentBlockTimestamp).minus(tenMinutesInSeconds); @@ -229,8 +226,8 @@ describe.only(ContractName.DutchAuction, () => { expect(auctionDetails.beginAmount).to.be.bignumber.equal(auctionBeginAmount); }); it('should be be worth the end price at the end of the auction', async () => { - auctionBeginTimeSeconds = new BigNumber(currentBlockTimestamp - 1000); - auctionEndTimeSeconds = new BigNumber(currentBlockTimestamp - 100); + auctionBeginTimeSeconds = new BigNumber(currentBlockTimestamp - tenMinutesInSeconds * 2); + auctionEndTimeSeconds = new BigNumber(currentBlockTimestamp - tenMinutesInSeconds); sellOrder = await sellerOrderFactory.newSignedOrderAsync({ makerAssetData: extendMakerAssetData( assetDataUtils.encodeERC20AssetData(defaultMakerAssetAddress), @@ -244,9 +241,9 @@ describe.only(ContractName.DutchAuction, () => { expect(auctionDetails.beginAmount).to.be.bignumber.equal(auctionBeginAmount); }); it('should match orders at current amount and send excess to buyer', async () => { - const auctionDetails = await dutchAuctionContract.getAuctionDetails.callAsync(sellOrder); + const beforeAuctionDetails = await dutchAuctionContract.getAuctionDetails.callAsync(sellOrder); buyOrder = await buyerOrderFactory.newSignedOrderAsync({ - makerAssetAmount: auctionDetails.currentAmount.times(2), + makerAssetAmount: beforeAuctionDetails.currentAmount.times(2), }); await web3Wrapper.awaitTransactionSuccessAsync( await dutchAuctionContract.matchOrders.sendTransactionAsync( @@ -259,6 +256,7 @@ describe.only(ContractName.DutchAuction, () => { }, ), ); + const afterAuctionDetails = await dutchAuctionContract.getAuctionDetails.callAsync(sellOrder); const newBalances = await erc20Wrapper.getBalancesAsync(); expect(newBalances[dutchAuctionContract.address][wethContract.address]).to.be.bignumber.equal( constants.ZERO_AMOUNT, @@ -267,13 +265,13 @@ describe.only(ContractName.DutchAuction, () => { // between multiple calls to the same block. Which can move the amount in our case // ref: https://github.com/trufflesuite/ganache-core/issues/111 expect(newBalances[makerAddress][wethContract.address]).to.be.bignumber.gte( - erc20Balances[makerAddress][wethContract.address].plus(auctionDetails.currentAmount), + erc20Balances[makerAddress][wethContract.address].plus(afterAuctionDetails.currentAmount), ); - expect(newBalances[takerAddress][wethContract.address]).to.be.bignumber.equal( - erc20Balances[takerAddress][wethContract.address].minus(auctionDetails.currentAmount), + expect(newBalances[takerAddress][wethContract.address]).to.be.bignumber.gte( + erc20Balances[takerAddress][wethContract.address].minus(afterAuctionDetails.currentAmount), ); }); - it('should have valid getAuctionDetails at a block in the future', async () => { + it('should have valid getAuctionDetails at some block in the future', async () => { let auctionDetails = await dutchAuctionContract.getAuctionDetails.callAsync(sellOrder); const beforeAmount = auctionDetails.currentAmount; await increaseTimeAsync(); @@ -291,6 +289,8 @@ describe.only(ContractName.DutchAuction, () => { sellOrder.signature, { from: takerAddress, + // HACK geth seems to miscalculate the gas required intermittently + gas: 400000, }, ); await web3Wrapper.awaitTransactionSuccessAsync(txHash); @@ -303,7 +303,6 @@ describe.only(ContractName.DutchAuction, () => { sellOrder = await sellerOrderFactory.newSignedOrderAsync({ makerFee: new BigNumber(1), }); - const auctionDetails = await dutchAuctionContract.getAuctionDetails.callAsync(sellOrder); const txHash = await dutchAuctionContract.matchOrders.sendTransactionAsync( buyOrder, sellOrder, @@ -314,9 +313,10 @@ describe.only(ContractName.DutchAuction, () => { }, ); await web3Wrapper.awaitTransactionSuccessAsync(txHash); + const afterAuctionDetails = await dutchAuctionContract.getAuctionDetails.callAsync(sellOrder); const newBalances = await erc20Wrapper.getBalancesAsync(); expect(newBalances[makerAddress][wethContract.address]).to.be.bignumber.gte( - erc20Balances[makerAddress][wethContract.address].plus(auctionDetails.currentAmount), + erc20Balances[makerAddress][wethContract.address].plus(afterAuctionDetails.currentAmount), ); expect(newBalances[feeRecipientAddress][zrxToken.address]).to.be.bignumber.equal( erc20Balances[feeRecipientAddress][zrxToken.address].plus(sellOrder.makerFee), @@ -326,7 +326,6 @@ describe.only(ContractName.DutchAuction, () => { buyOrder = await buyerOrderFactory.newSignedOrderAsync({ makerFee: new BigNumber(1), }); - const auctionDetails = await dutchAuctionContract.getAuctionDetails.callAsync(sellOrder); const txHash = await dutchAuctionContract.matchOrders.sendTransactionAsync( buyOrder, sellOrder, @@ -338,8 +337,9 @@ describe.only(ContractName.DutchAuction, () => { ); await web3Wrapper.awaitTransactionSuccessAsync(txHash); const newBalances = await erc20Wrapper.getBalancesAsync(); + const afterAuctionDetails = await dutchAuctionContract.getAuctionDetails.callAsync(sellOrder); expect(newBalances[makerAddress][wethContract.address]).to.be.bignumber.gte( - erc20Balances[makerAddress][wethContract.address].plus(auctionDetails.currentAmount), + erc20Balances[makerAddress][wethContract.address].plus(afterAuctionDetails.currentAmount), ); expect(newBalances[feeRecipientAddress][zrxToken.address]).to.be.bignumber.equal( erc20Balances[feeRecipientAddress][zrxToken.address].plus(buyOrder.makerFee), @@ -432,7 +432,6 @@ describe.only(ContractName.DutchAuction, () => { takerAssetAmount: new BigNumber(1), takerAssetData: sellOrder.makerAssetData, }); - const auctionDetails = await dutchAuctionContract.getAuctionDetails.callAsync(sellOrder); await web3Wrapper.awaitTransactionSuccessAsync( await dutchAuctionContract.matchOrders.sendTransactionAsync( buyOrder, @@ -444,12 +443,13 @@ describe.only(ContractName.DutchAuction, () => { }, ), ); + const afterAuctionDetails = await dutchAuctionContract.getAuctionDetails.callAsync(sellOrder); const newBalances = await erc20Wrapper.getBalancesAsync(); // HACK gte used here due to a bug in ganache where the timestamp can change // between multiple calls to the same block. Which can move the amount in our case // ref: https://github.com/trufflesuite/ganache-core/issues/111 expect(newBalances[makerAddress][wethContract.address]).to.be.bignumber.gte( - erc20Balances[makerAddress][wethContract.address].plus(auctionDetails.currentAmount), + erc20Balances[makerAddress][wethContract.address].plus(afterAuctionDetails.currentAmount), ); const newOwner = await erc721Token.ownerOf.callAsync(makerAssetId); expect(newOwner).to.be.bignumber.equal(takerAddress); -- cgit From a882a399378f75261e9fe68e6f20b1d4bb8d59c0 Mon Sep 17 00:00:00 2001 From: Jacob Evans Date: Fri, 30 Nov 2018 12:59:19 +1100 Subject: chore: Use SafeMath where possible --- .../extensions/DutchAuction/DutchAuction.sol | 49 +++++++++++++++------- 1 file changed, 34 insertions(+), 15 deletions(-) (limited to 'packages') diff --git a/packages/contracts/contracts/extensions/DutchAuction/DutchAuction.sol b/packages/contracts/contracts/extensions/DutchAuction/DutchAuction.sol index 7115e5bd5..60077746d 100644 --- a/packages/contracts/contracts/extensions/DutchAuction/DutchAuction.sol +++ b/packages/contracts/contracts/extensions/DutchAuction/DutchAuction.sol @@ -23,9 +23,12 @@ import "../../protocol/Exchange/interfaces/IExchange.sol"; import "../../protocol/Exchange/libs/LibOrder.sol"; import "../../tokens/ERC20Token/IERC20Token.sol"; import "../../utils/LibBytes/LibBytes.sol"; +import "../../utils/SafeMath/SafeMath.sol"; -contract DutchAuction { +contract DutchAuction is + SafeMath +{ using LibBytes for bytes; // solhint-disable var-name-mixedcase @@ -77,11 +80,20 @@ contract DutchAuction { { AuctionDetails memory auctionDetails = getAuctionDetails(sellOrder); // Ensure the auction has not yet started - require(auctionDetails.currentTimeSeconds >= auctionDetails.beginTimeSeconds, "AUCTION_NOT_STARTED"); + require( + auctionDetails.currentTimeSeconds >= auctionDetails.beginTimeSeconds, + "AUCTION_NOT_STARTED" + ); // Ensure the auction has not expired. This will fail later in 0x but we can save gas by failing early - require(sellOrder.expirationTimeSeconds > auctionDetails.currentTimeSeconds, "AUCTION_EXPIRED"); + require( + sellOrder.expirationTimeSeconds > auctionDetails.currentTimeSeconds, + "AUCTION_EXPIRED" + ); // Validate the buyer amount is greater than the current auction amount - require(buyOrder.makerAssetAmount >= auctionDetails.currentAmount, "INVALID_AMOUNT"); + require( + buyOrder.makerAssetAmount >= auctionDetails.currentAmount, + "INVALID_AMOUNT" + ); // Match orders, maximally filling `buyOrder` matchedFillResults = EXCHANGE.matchOrders( buyOrder, @@ -107,19 +119,17 @@ contract DutchAuction { address token = assetData.readAddress(16); // Calculate the excess from the buy order. This can occur if the buyer sends in a higher // amount than the calculated current amount - uint256 buyerExcessAmount = buyOrder.makerAssetAmount-auctionDetails.currentAmount; - uint256 sellerExcessAmount = leftMakerAssetSpreadAmount-buyerExcessAmount; + uint256 buyerExcessAmount = safeSub(buyOrder.makerAssetAmount, auctionDetails.currentAmount); + uint256 sellerExcessAmount = safeSub(leftMakerAssetSpreadAmount, buyerExcessAmount); // Return the difference between auctionDetails.currentAmount and sellOrder.takerAssetAmount // to the seller if (sellerExcessAmount > 0) { - address makerAddress = sellOrder.makerAddress; - IERC20Token(token).transfer(makerAddress, sellerExcessAmount); + IERC20Token(token).transfer(sellOrder.makerAddress, sellerExcessAmount); } // Return the difference between buyOrder.makerAssetAmount and auctionDetails.currentAmount // to the buyer if (buyerExcessAmount > 0) { - address takerAddress = buyOrder.makerAddress; - IERC20Token(token).transfer(takerAddress, buyerExcessAmount); + IERC20Token(token).transfer(buyOrder.makerAddress, buyerExcessAmount); } } return matchedFillResults; @@ -145,15 +155,24 @@ contract DutchAuction { // | | -64 | 32 | 1. auction begin unix timestamp | // | | -32 | 32 | 2. auction begin begin amount | // ERC20 asset data length is 4+32, 64 for auction details results in min length if 100 - require(makerAssetDataLength > 10, "INVALID_ASSET_DATA"); - uint256 auctionBeginTimeSeconds = order.makerAssetData.readUint256(makerAssetDataLength-64); - uint256 auctionBeginAmount = order.makerAssetData.readUint256(makerAssetDataLength-32); + require( + makerAssetDataLength > 10, + "INVALID_ASSET_DATA" + ); + uint256 auctionBeginTimeSeconds = order.makerAssetData.readUint256(makerAssetDataLength - 64); + uint256 auctionBeginAmount = order.makerAssetData.readUint256(makerAssetDataLength - 32); // Ensure the auction has a valid begin time - require(order.expirationTimeSeconds > auctionBeginTimeSeconds, "INVALID_BEGIN_TIME"); + require( + order.expirationTimeSeconds > auctionBeginTimeSeconds, + "INVALID_BEGIN_TIME" + ); uint256 auctionDurationSeconds = order.expirationTimeSeconds-auctionBeginTimeSeconds; // Ensure the auction goes from high to low uint256 minAmount = order.takerAssetAmount; - require(auctionBeginAmount > minAmount, "INVALID_AMOUNT"); + require( + auctionBeginAmount > minAmount, + "INVALID_AMOUNT" + ); uint256 amountDelta = auctionBeginAmount-minAmount; // solhint-disable-next-line not-rely-on-time uint256 timestamp = block.timestamp; -- cgit From 0bffc3d10e5b7c55fabfd4f0e5dcceefe468aa5f Mon Sep 17 00:00:00 2001 From: Jacob Evans Date: Fri, 30 Nov 2018 13:25:36 +1100 Subject: chore: Add RevertReasons for DutchAuction. Test revert reasons --- .../extensions/DutchAuction/DutchAuction.sol | 4 +- .../contracts/test/extensions/dutch_auction.ts | 51 +++++++++++++++++----- packages/types/CHANGELOG.json | 4 ++ packages/types/src/index.ts | 6 +++ 4 files changed, 51 insertions(+), 14 deletions(-) (limited to 'packages') diff --git a/packages/contracts/contracts/extensions/DutchAuction/DutchAuction.sol b/packages/contracts/contracts/extensions/DutchAuction/DutchAuction.sol index 60077746d..9b8fec54a 100644 --- a/packages/contracts/contracts/extensions/DutchAuction/DutchAuction.sol +++ b/packages/contracts/contracts/extensions/DutchAuction/DutchAuction.sol @@ -154,9 +154,9 @@ contract DutchAuction is // | Params | | 2 * 32 | parameters: | // | | -64 | 32 | 1. auction begin unix timestamp | // | | -32 | 32 | 2. auction begin begin amount | - // ERC20 asset data length is 4+32, 64 for auction details results in min length if 100 + // ERC20 asset data length is 4+32, 64 for auction details results in min length 100 require( - makerAssetDataLength > 10, + makerAssetDataLength >= 100, "INVALID_ASSET_DATA" ); uint256 auctionBeginTimeSeconds = order.makerAssetData.readUint256(makerAssetDataLength - 64); diff --git a/packages/contracts/test/extensions/dutch_auction.ts b/packages/contracts/test/extensions/dutch_auction.ts index b3408e27d..c133d8c60 100644 --- a/packages/contracts/test/extensions/dutch_auction.ts +++ b/packages/contracts/test/extensions/dutch_auction.ts @@ -1,6 +1,6 @@ import { BlockchainLifecycle } from '@0x/dev-utils'; import { assetDataUtils, generatePseudoRandomSalt } from '@0x/order-utils'; -import { SignedOrder } from '@0x/types'; +import { RevertReason, SignedOrder } from '@0x/types'; import { BigNumber } from '@0x/utils'; import { Web3Wrapper } from '@0x/web3-wrapper'; import * as chai from 'chai'; @@ -268,7 +268,7 @@ describe(ContractName.DutchAuction, () => { erc20Balances[makerAddress][wethContract.address].plus(afterAuctionDetails.currentAmount), ); expect(newBalances[takerAddress][wethContract.address]).to.be.bignumber.gte( - erc20Balances[takerAddress][wethContract.address].minus(afterAuctionDetails.currentAmount), + erc20Balances[takerAddress][wethContract.address].minus(beforeAuctionDetails.currentAmount), ); }); it('should have valid getAuctionDetails at some block in the future', async () => { @@ -346,9 +346,15 @@ describe(ContractName.DutchAuction, () => { ); }); it('should revert when auction expires', async () => { - auctionEndTimeSeconds = new BigNumber(currentBlockTimestamp - 100); + auctionBeginTimeSeconds = new BigNumber(currentBlockTimestamp - tenMinutesInSeconds * 2); + auctionEndTimeSeconds = new BigNumber(currentBlockTimestamp - tenMinutesInSeconds); sellOrder = await sellerOrderFactory.newSignedOrderAsync({ expirationTimeSeconds: auctionEndTimeSeconds, + makerAssetData: extendMakerAssetData( + assetDataUtils.encodeERC20AssetData(defaultMakerAssetAddress), + auctionBeginTimeSeconds, + auctionBeginAmount, + ), }); return expectTransactionFailedAsync( dutchAuctionContract.matchOrders.sendTransactionAsync( @@ -360,7 +366,7 @@ describe(ContractName.DutchAuction, () => { from: takerAddress, }, ), - 'AUCTION_EXPIRED' as any, + RevertReason.AuctionExpired, ); }); it('cannot be filled for less than the current price', async () => { @@ -378,14 +384,35 @@ describe(ContractName.DutchAuction, () => { from: takerAddress, }, ), - 'INVALID_AMOUNT' as any, + RevertReason.AuctionInvalidAmount, + ); + }); + it('auction begin amount must be higher than final amount ', async () => { + sellOrder = await sellerOrderFactory.newSignedOrderAsync({ + takerAssetAmount: auctionBeginAmount.plus(1), + }); + return expectTransactionFailedAsync( + dutchAuctionContract.matchOrders.sendTransactionAsync( + buyOrder, + sellOrder, + buyOrder.signature, + sellOrder.signature, + { + from: takerAddress, + }, + ), + RevertReason.AuctionInvalidAmount, ); }); - it('cannot match an expired auction', async () => { - auctionBeginTimeSeconds = new BigNumber(currentBlockTimestamp - 1000); - auctionEndTimeSeconds = new BigNumber(currentBlockTimestamp - 100); + it('begin time is less than end time', async () => { + auctionBeginTimeSeconds = new BigNumber(auctionEndTimeSeconds).plus(tenMinutesInSeconds); sellOrder = await sellerOrderFactory.newSignedOrderAsync({ expirationTimeSeconds: auctionEndTimeSeconds, + makerAssetData: extendMakerAssetData( + assetDataUtils.encodeERC20AssetData(defaultMakerAssetAddress), + auctionBeginTimeSeconds, + auctionBeginAmount, + ), }); return expectTransactionFailedAsync( dutchAuctionContract.matchOrders.sendTransactionAsync( @@ -397,12 +424,12 @@ describe(ContractName.DutchAuction, () => { from: takerAddress, }, ), - 'AUCTION_EXPIRED' as any, + RevertReason.AuctionInvalidBeginTime, ); }); - it('auction begin amount must be higher than final amount ', async () => { + it('asset data contains auction parameters', async () => { sellOrder = await sellerOrderFactory.newSignedOrderAsync({ - takerAssetAmount: auctionBeginAmount.plus(1), + makerAssetData: assetDataUtils.encodeERC20AssetData(defaultMakerAssetAddress), }); return expectTransactionFailedAsync( dutchAuctionContract.matchOrders.sendTransactionAsync( @@ -414,7 +441,7 @@ describe(ContractName.DutchAuction, () => { from: takerAddress, }, ), - 'INVALID_AMOUNT' as any, + RevertReason.InvalidAssetData, ); }); describe('ERC721', () => { diff --git a/packages/types/CHANGELOG.json b/packages/types/CHANGELOG.json index 53b24aff0..b09859101 100644 --- a/packages/types/CHANGELOG.json +++ b/packages/types/CHANGELOG.json @@ -5,6 +5,10 @@ { "note": "Add `LengthMismatch` and `LengthGreaterThan3Required` revert reasons", "pr": 1224 + }, + { + "note": "Add RevertReasons for DutchAuction contract", + "pr": 1225 } ] }, diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 26d8f8e22..6b728af71 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -237,6 +237,12 @@ export enum RevertReason { TxFullyConfirmed = 'TX_FULLY_CONFIRMED', TxNotFullyConfirmed = 'TX_NOT_FULLY_CONFIRMED', TimeLockIncomplete = 'TIME_LOCK_INCOMPLETE', + // DutchAuction + AuctionInvalidAmount = 'INVALID_AMOUNT', + AuctionExpired = 'AUCTION_EXPIRED', + AuctionNotStarted = 'AUCTION_NOT_STARTED', + AuctionInvalidBeginTime = 'INVALID_BEGIN_TIME', + InvalidAssetData = 'INVALID_ASSET_DATA', } export enum StatusCodes { -- cgit From 247266b969d5439abc372ede77a68b937809a6ab Mon Sep 17 00:00:00 2001 From: Jacob Evans Date: Tue, 4 Dec 2018 09:10:59 +1100 Subject: chore: SafeMath and if statement in getAuctionDetails --- .../extensions/DutchAuction/DutchAuction.sol | 28 ++++++++++++++-------- 1 file changed, 18 insertions(+), 10 deletions(-) (limited to 'packages') diff --git a/packages/contracts/contracts/extensions/DutchAuction/DutchAuction.sol b/packages/contracts/contracts/extensions/DutchAuction/DutchAuction.sol index 9b8fec54a..abe8309cf 100644 --- a/packages/contracts/contracts/extensions/DutchAuction/DutchAuction.sol +++ b/packages/contracts/contracts/extensions/DutchAuction/DutchAuction.sol @@ -114,7 +114,8 @@ contract DutchAuction is // |----------|--------|---------|-------------------------------------| // | Header | 0 | 4 | function selector | // | Params | | 1 * 32 | function parameters: | - // | | 4 | 12 + 20 | 1. token address | + // | | 4 | 12 | 1. token address padding | + // | | 16 | 20 | 2. token address | bytes memory assetData = sellOrder.takerAssetData; address token = assetData.readAddress(16); // Calculate the excess from the buy order. This can occur if the buyer sends in a higher @@ -183,15 +184,22 @@ contract DutchAuction is auctionDetails.currentTimeSeconds = timestamp; uint256 remainingDurationSeconds = order.expirationTimeSeconds-timestamp; - uint256 currentAmount = minAmount + (remainingDurationSeconds*amountDelta/auctionDurationSeconds); - // Check the bounds where we SafeMath was avoivded so the auction details can be queried prior - // and after the auction time. - // If the auction has not yet begun the current amount is the auctionBeginAmount - currentAmount = timestamp < auctionBeginTimeSeconds ? auctionBeginAmount : currentAmount; - // If the auction has ended the current amount is the minAmount - // auction end time is guaranteed by 0x Exchange to fail due to the order expiration - currentAmount = timestamp >= order.expirationTimeSeconds ? minAmount : currentAmount; - auctionDetails.currentAmount = currentAmount; + if (timestamp < auctionBeginTimeSeconds) { + // If the auction has not yet begun the current amount is the auctionBeginAmount + auctionDetails.currentAmount = auctionBeginAmount; + } else if (timestamp >= order.expirationTimeSeconds) { + // If the auction has ended the current amount is the minAmount. + // Auction end time is guaranteed by 0x Exchange due to the order expiration + auctionDetails.currentAmount = minAmount; + } else { + auctionDetails.currentAmount = safeAdd( + minAmount, + safeDiv( + safeMul(remainingDurationSeconds, amountDelta), + auctionDurationSeconds + ) + ); + } return auctionDetails; } } -- cgit From 098a531de8776df3776017001014078a21eb4029 Mon Sep 17 00:00:00 2001 From: Jacob Evans Date: Tue, 4 Dec 2018 09:58:37 +1100 Subject: chore: Move to new structure --- .../extensions/DutchAuction/DutchAuction.sol | 205 --------- .../contracts/test/extensions/dutch_auction.ts | 486 --------------------- 2 files changed, 691 deletions(-) delete mode 100644 packages/contracts/contracts/extensions/DutchAuction/DutchAuction.sol delete mode 100644 packages/contracts/test/extensions/dutch_auction.ts (limited to 'packages') diff --git a/packages/contracts/contracts/extensions/DutchAuction/DutchAuction.sol b/packages/contracts/contracts/extensions/DutchAuction/DutchAuction.sol deleted file mode 100644 index abe8309cf..000000000 --- a/packages/contracts/contracts/extensions/DutchAuction/DutchAuction.sol +++ /dev/null @@ -1,205 +0,0 @@ -/* - - 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"; -import "../../utils/SafeMath/SafeMath.sol"; - - -contract DutchAuction is - SafeMath -{ - using LibBytes for bytes; - - // solhint-disable var-name-mixedcase - IExchange internal EXCHANGE; - - struct AuctionDetails { - uint256 beginTimeSeconds; // Auction begin unix timestamp: sellOrder.makerAssetData - uint256 endTimeSeconds; // Auction end unix timestamp: sellOrder.expiryTimeSeconds - uint256 beginAmount; // Auction begin amount: sellOrder.makerAssetData - uint256 endAmount; // Auction end amount: sellOrder.takerAssetAmount - uint256 currentAmount; // Calculated amount given block.timestamp - uint256 currentTimeSeconds; // block.timestamp - } - - constructor (address _exchange) - public - { - EXCHANGE = IExchange(_exchange); - } - - /// @dev Matches the buy and sell orders at an amount given the following: the current block time, the auction - /// start time and the auction begin amount. The sell order is a an order at the lowest amount - /// at the end of the auction. Excess from the match is transferred to the seller. - /// Over time the price moves from beginAmount to endAmount given the current block.timestamp. - /// sellOrder.expiryTimeSeconds is the end time of the auction. - /// sellOrder.takerAssetAmount is the end amount of the auction (lowest possible amount). - /// sellOrder.makerAssetData is the ABI encoded Asset Proxy data with the following data appended - /// buyOrder.makerAssetData is the buyers bid on the auction, must meet the amount for the current block timestamp - /// (uint256 beginTimeSeconds, uint256 beginAmount). - /// This function reverts in the following scenarios: - /// * Auction has not started (auctionDetails.currentTimeSeconds < auctionDetails.beginTimeSeconds) - /// * Auction has expired (auctionDetails.endTimeSeconds < auctionDetails.currentTimeSeconds) - /// * Amount is invalid: Buy order amount is too low (buyOrder.makerAssetAmount < auctionDetails.currentAmount) - /// * Amount is invalid: Invalid begin amount (auctionDetails.beginAmount > auctionDetails.endAmount) - /// * Any failure in the 0x Match Orders - /// @param buyOrder The Buyer's order. This order is for the current expected price of the auction. - /// @param sellOrder The Seller's order. This order is for the lowest amount (at the end of the auction). - /// @param buySignature Proof that order was created by the buyer. - /// @param sellSignature Proof that order was created by the seller. - /// @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 - require( - auctionDetails.currentTimeSeconds >= auctionDetails.beginTimeSeconds, - "AUCTION_NOT_STARTED" - ); - // Ensure the auction has not expired. This will fail later in 0x but we can save gas by failing early - require( - sellOrder.expirationTimeSeconds > auctionDetails.currentTimeSeconds, - "AUCTION_EXPIRED" - ); - // Validate the buyer amount is greater than the current auction amount - require( - buyOrder.makerAssetAmount >= auctionDetails.currentAmount, - "INVALID_AMOUNT" - ); - // Match orders, maximally filling `buyOrder` - matchedFillResults = EXCHANGE.matchOrders( - buyOrder, - sellOrder, - buySignature, - sellSignature - ); - // The difference in sellOrder.takerAssetAmount and current amount is given as spread to the matcher - // This may include additional spread from the buyOrder.makerAssetAmount and the currentAmount. - // e.g currentAmount is 30, sellOrder.takerAssetAmount is 10 and buyOrder.makerAssetamount is 40. - // 10 (40-30) is returned to the buyer, 20 (30-10) sent to the seller and 10 has previously - // been transferred to the seller during matchOrders - uint256 leftMakerAssetSpreadAmount = matchedFillResults.leftMakerAssetSpreadAmount; - if (leftMakerAssetSpreadAmount > 0) { - // ERC20 Asset data itself is encoded as follows: - // - // | Area | Offset | Length | Contents | - // |----------|--------|---------|-------------------------------------| - // | Header | 0 | 4 | function selector | - // | Params | | 1 * 32 | function parameters: | - // | | 4 | 12 | 1. token address padding | - // | | 16 | 20 | 2. token address | - bytes memory assetData = sellOrder.takerAssetData; - address token = assetData.readAddress(16); - // Calculate the excess from the buy order. This can occur if the buyer sends in a higher - // amount than the calculated current amount - uint256 buyerExcessAmount = safeSub(buyOrder.makerAssetAmount, auctionDetails.currentAmount); - uint256 sellerExcessAmount = safeSub(leftMakerAssetSpreadAmount, buyerExcessAmount); - // Return the difference between auctionDetails.currentAmount and sellOrder.takerAssetAmount - // to the seller - if (sellerExcessAmount > 0) { - IERC20Token(token).transfer(sellOrder.makerAddress, sellerExcessAmount); - } - // Return the difference between buyOrder.makerAssetAmount and auctionDetails.currentAmount - // to the buyer - if (buyerExcessAmount > 0) { - IERC20Token(token).transfer(buyOrder.makerAddress, buyerExcessAmount); - } - } - return matchedFillResults; - } - - /// @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) - { - uint256 makerAssetDataLength = order.makerAssetData.length; - // It is unknown the encoded data of makerAssetData, we assume the last 64 bytes - // are the Auction Details encoding. - // Auction Details is encoded as follows: - // - // | Area | Offset | Length | Contents | - // |----------|--------|---------|-------------------------------------| - // | Params | | 2 * 32 | parameters: | - // | | -64 | 32 | 1. auction begin unix timestamp | - // | | -32 | 32 | 2. auction begin begin amount | - // ERC20 asset data length is 4+32, 64 for auction details results in min length 100 - require( - makerAssetDataLength >= 100, - "INVALID_ASSET_DATA" - ); - uint256 auctionBeginTimeSeconds = order.makerAssetData.readUint256(makerAssetDataLength - 64); - uint256 auctionBeginAmount = order.makerAssetData.readUint256(makerAssetDataLength - 32); - // Ensure the auction has a valid begin time - require( - order.expirationTimeSeconds > auctionBeginTimeSeconds, - "INVALID_BEGIN_TIME" - ); - uint256 auctionDurationSeconds = order.expirationTimeSeconds-auctionBeginTimeSeconds; - // Ensure the auction goes from high to low - uint256 minAmount = order.takerAssetAmount; - require( - auctionBeginAmount > minAmount, - "INVALID_AMOUNT" - ); - uint256 amountDelta = auctionBeginAmount-minAmount; - // solhint-disable-next-line not-rely-on-time - uint256 timestamp = block.timestamp; - auctionDetails.beginTimeSeconds = auctionBeginTimeSeconds; - auctionDetails.endTimeSeconds = order.expirationTimeSeconds; - auctionDetails.beginAmount = auctionBeginAmount; - auctionDetails.endAmount = minAmount; - auctionDetails.currentTimeSeconds = timestamp; - - uint256 remainingDurationSeconds = order.expirationTimeSeconds-timestamp; - if (timestamp < auctionBeginTimeSeconds) { - // If the auction has not yet begun the current amount is the auctionBeginAmount - auctionDetails.currentAmount = auctionBeginAmount; - } else if (timestamp >= order.expirationTimeSeconds) { - // If the auction has ended the current amount is the minAmount. - // Auction end time is guaranteed by 0x Exchange due to the order expiration - auctionDetails.currentAmount = minAmount; - } else { - auctionDetails.currentAmount = safeAdd( - minAmount, - safeDiv( - safeMul(remainingDurationSeconds, amountDelta), - auctionDurationSeconds - ) - ); - } - return auctionDetails; - } -} diff --git a/packages/contracts/test/extensions/dutch_auction.ts b/packages/contracts/test/extensions/dutch_auction.ts deleted file mode 100644 index c133d8c60..000000000 --- a/packages/contracts/test/extensions/dutch_auction.ts +++ /dev/null @@ -1,486 +0,0 @@ -import { BlockchainLifecycle } from '@0x/dev-utils'; -import { assetDataUtils, generatePseudoRandomSalt } from '@0x/order-utils'; -import { RevertReason, SignedOrder } from '@0x/types'; -import { BigNumber } from '@0x/utils'; -import { Web3Wrapper } from '@0x/web3-wrapper'; -import * as chai from 'chai'; -import ethAbi = require('ethereumjs-abi'); -import * as ethUtil from 'ethereumjs-util'; -import * as _ from 'lodash'; - -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 zrxToken: DummyERC20TokenContract; - let erc20TokenA: DummyERC20TokenContract; - let erc721Token: DummyERC721TokenContract; - let dutchAuctionContract: DutchAuctionContract; - let wethContract: WETH9Contract; - - let sellerOrderFactory: OrderFactory; - let buyerOrderFactory: OrderFactory; - let erc20Wrapper: ERC20Wrapper; - let erc20Balances: ERC20BalancesByOwner; - let currentBlockTimestamp: number; - let auctionBeginTimeSeconds: BigNumber; - let auctionEndTimeSeconds: BigNumber; - let auctionBeginAmount: BigNumber; - let auctionEndAmount: BigNumber; - let sellOrder: SignedOrder; - let buyOrder: SignedOrder; - let erc721MakerAssetIds: BigNumber[]; - const tenMinutesInSeconds = 10 * 60; - - async function increaseTimeAsync(): Promise { - const timestampBefore = await getLatestBlockTimestampAsync(); - await web3Wrapper.increaseTimeAsync(5); - const timestampAfter = await getLatestBlockTimestampAsync(); - // HACK send some transactions when a time increase isn't supported - if (timestampAfter === timestampBefore) { - await web3Wrapper.sendTransactionAsync({ to: makerAddress, from: makerAddress, value: new BigNumber(1) }); - } - } - - function extendMakerAssetData(makerAssetData: string, beginTimeSeconds: BigNumber, beginAmount: BigNumber): string { - return ethUtil.bufferToHex( - Buffer.concat([ - ethUtil.toBuffer(makerAssetData), - ethUtil.toBuffer( - (ethAbi as any).rawEncode( - ['uint256', 'uint256'], - [beginTimeSeconds.toString(), beginAmount.toString()], - ), - ), - ]), - ); - } - - 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); - erc20Wrapper.addDummyTokenContract(wethContract as any); - - const zrxAssetData = assetDataUtils.encodeERC20AssetData(zrxToken.address); - const exchangeInstance = await ExchangeContract.deployFrom0xArtifactAsync( - artifacts.Exchange, - provider, - txDefaults, - zrxAssetData, - ); - const 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); - - currentBlockTimestamp = await getLatestBlockTimestampAsync(); - // Default auction begins 10 minutes ago - auctionBeginTimeSeconds = new BigNumber(currentBlockTimestamp).minus(tenMinutesInSeconds); - // Default auction ends 10 from now - auctionEndTimeSeconds = new BigNumber(currentBlockTimestamp).plus(tenMinutesInSeconds); - auctionBeginAmount = Web3Wrapper.toBaseUnitAmount(new BigNumber(10), DECIMALS_DEFAULT); - auctionEndAmount = Web3Wrapper.toBaseUnitAmount(new BigNumber(1), DECIMALS_DEFAULT); - - // Default sell order and buy order are exact mirrors - const sellerDefaultOrderParams = { - salt: generatePseudoRandomSalt(), - exchangeAddress: exchangeInstance.address, - makerAddress, - feeRecipientAddress, - // taker address or sender address should be set to the ducth auction contract - takerAddress: dutchAuctionContract.address, - makerAssetData: extendMakerAssetData( - assetDataUtils.encodeERC20AssetData(defaultMakerAssetAddress), - auctionBeginTimeSeconds, - auctionBeginAmount, - ), - takerAssetData: assetDataUtils.encodeERC20AssetData(defaultTakerAssetAddress), - makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(200), DECIMALS_DEFAULT), - takerAssetAmount: auctionEndAmount, - expirationTimeSeconds: auctionEndTimeSeconds, - makerFee: constants.ZERO_AMOUNT, - takerFee: constants.ZERO_AMOUNT, - }; - // Default buy order is for the auction begin price - const buyerDefaultOrderParams = { - ...sellerDefaultOrderParams, - makerAddress: takerAddress, - makerAssetData: sellerDefaultOrderParams.takerAssetData, - takerAssetData: sellerDefaultOrderParams.makerAssetData, - makerAssetAmount: auctionBeginAmount, - takerAssetAmount: sellerDefaultOrderParams.makerAssetAmount, - }; - 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(); - sellOrder = await sellerOrderFactory.newSignedOrderAsync(); - buyOrder = await buyerOrderFactory.newSignedOrderAsync(); - }); - afterEach(async () => { - await blockchainLifecycle.revertAsync(); - }); - describe('matchOrders', () => { - it('should be worth the begin price at the begining of the auction', async () => { - auctionBeginTimeSeconds = new BigNumber(currentBlockTimestamp + 2); - sellOrder = await sellerOrderFactory.newSignedOrderAsync({ - makerAssetData: extendMakerAssetData( - assetDataUtils.encodeERC20AssetData(defaultMakerAssetAddress), - auctionBeginTimeSeconds, - auctionBeginAmount, - ), - }); - const auctionDetails = await dutchAuctionContract.getAuctionDetails.callAsync(sellOrder); - expect(auctionDetails.currentAmount).to.be.bignumber.equal(auctionBeginAmount); - expect(auctionDetails.beginAmount).to.be.bignumber.equal(auctionBeginAmount); - }); - it('should be be worth the end price at the end of the auction', async () => { - auctionBeginTimeSeconds = new BigNumber(currentBlockTimestamp - tenMinutesInSeconds * 2); - auctionEndTimeSeconds = new BigNumber(currentBlockTimestamp - tenMinutesInSeconds); - sellOrder = await sellerOrderFactory.newSignedOrderAsync({ - makerAssetData: extendMakerAssetData( - assetDataUtils.encodeERC20AssetData(defaultMakerAssetAddress), - auctionBeginTimeSeconds, - auctionBeginAmount, - ), - expirationTimeSeconds: auctionEndTimeSeconds, - }); - const auctionDetails = await dutchAuctionContract.getAuctionDetails.callAsync(sellOrder); - expect(auctionDetails.currentAmount).to.be.bignumber.equal(auctionEndAmount); - expect(auctionDetails.beginAmount).to.be.bignumber.equal(auctionBeginAmount); - }); - it('should match orders at current amount and send excess to buyer', async () => { - const beforeAuctionDetails = await dutchAuctionContract.getAuctionDetails.callAsync(sellOrder); - buyOrder = await buyerOrderFactory.newSignedOrderAsync({ - makerAssetAmount: beforeAuctionDetails.currentAmount.times(2), - }); - await web3Wrapper.awaitTransactionSuccessAsync( - await dutchAuctionContract.matchOrders.sendTransactionAsync( - buyOrder, - sellOrder, - buyOrder.signature, - sellOrder.signature, - { - from: takerAddress, - }, - ), - ); - const afterAuctionDetails = await dutchAuctionContract.getAuctionDetails.callAsync(sellOrder); - const newBalances = await erc20Wrapper.getBalancesAsync(); - expect(newBalances[dutchAuctionContract.address][wethContract.address]).to.be.bignumber.equal( - constants.ZERO_AMOUNT, - ); - // HACK gte used here due to a bug in ganache where the timestamp can change - // between multiple calls to the same block. Which can move the amount in our case - // ref: https://github.com/trufflesuite/ganache-core/issues/111 - expect(newBalances[makerAddress][wethContract.address]).to.be.bignumber.gte( - erc20Balances[makerAddress][wethContract.address].plus(afterAuctionDetails.currentAmount), - ); - expect(newBalances[takerAddress][wethContract.address]).to.be.bignumber.gte( - erc20Balances[takerAddress][wethContract.address].minus(beforeAuctionDetails.currentAmount), - ); - }); - it('should have valid getAuctionDetails at some block in the future', async () => { - let auctionDetails = await dutchAuctionContract.getAuctionDetails.callAsync(sellOrder); - const beforeAmount = auctionDetails.currentAmount; - await increaseTimeAsync(); - auctionDetails = await dutchAuctionContract.getAuctionDetails.callAsync(sellOrder); - const currentAmount = auctionDetails.currentAmount; - expect(beforeAmount).to.be.bignumber.greaterThan(currentAmount); - - buyOrder = await buyerOrderFactory.newSignedOrderAsync({ - makerAssetAmount: currentAmount, - }); - const txHash = await dutchAuctionContract.matchOrders.sendTransactionAsync( - buyOrder, - sellOrder, - buyOrder.signature, - sellOrder.signature, - { - from: takerAddress, - // HACK geth seems to miscalculate the gas required intermittently - gas: 400000, - }, - ); - await web3Wrapper.awaitTransactionSuccessAsync(txHash); - const newBalances = await erc20Wrapper.getBalancesAsync(); - expect(newBalances[makerAddress][wethContract.address]).to.be.bignumber.equal( - erc20Balances[makerAddress][wethContract.address].plus(currentAmount), - ); - }); - it('maker fees on sellOrder are paid to the fee receipient', async () => { - sellOrder = await sellerOrderFactory.newSignedOrderAsync({ - makerFee: new BigNumber(1), - }); - const txHash = await dutchAuctionContract.matchOrders.sendTransactionAsync( - buyOrder, - sellOrder, - buyOrder.signature, - sellOrder.signature, - { - from: takerAddress, - }, - ); - await web3Wrapper.awaitTransactionSuccessAsync(txHash); - const afterAuctionDetails = await dutchAuctionContract.getAuctionDetails.callAsync(sellOrder); - const newBalances = await erc20Wrapper.getBalancesAsync(); - expect(newBalances[makerAddress][wethContract.address]).to.be.bignumber.gte( - erc20Balances[makerAddress][wethContract.address].plus(afterAuctionDetails.currentAmount), - ); - expect(newBalances[feeRecipientAddress][zrxToken.address]).to.be.bignumber.equal( - erc20Balances[feeRecipientAddress][zrxToken.address].plus(sellOrder.makerFee), - ); - }); - it('maker fees on buyOrder are paid to the fee receipient', async () => { - buyOrder = await buyerOrderFactory.newSignedOrderAsync({ - makerFee: new BigNumber(1), - }); - const txHash = await dutchAuctionContract.matchOrders.sendTransactionAsync( - buyOrder, - sellOrder, - buyOrder.signature, - sellOrder.signature, - { - from: takerAddress, - }, - ); - await web3Wrapper.awaitTransactionSuccessAsync(txHash); - const newBalances = await erc20Wrapper.getBalancesAsync(); - const afterAuctionDetails = await dutchAuctionContract.getAuctionDetails.callAsync(sellOrder); - expect(newBalances[makerAddress][wethContract.address]).to.be.bignumber.gte( - erc20Balances[makerAddress][wethContract.address].plus(afterAuctionDetails.currentAmount), - ); - expect(newBalances[feeRecipientAddress][zrxToken.address]).to.be.bignumber.equal( - erc20Balances[feeRecipientAddress][zrxToken.address].plus(buyOrder.makerFee), - ); - }); - it('should revert when auction expires', async () => { - auctionBeginTimeSeconds = new BigNumber(currentBlockTimestamp - tenMinutesInSeconds * 2); - auctionEndTimeSeconds = new BigNumber(currentBlockTimestamp - tenMinutesInSeconds); - sellOrder = await sellerOrderFactory.newSignedOrderAsync({ - expirationTimeSeconds: auctionEndTimeSeconds, - makerAssetData: extendMakerAssetData( - assetDataUtils.encodeERC20AssetData(defaultMakerAssetAddress), - auctionBeginTimeSeconds, - auctionBeginAmount, - ), - }); - return expectTransactionFailedAsync( - dutchAuctionContract.matchOrders.sendTransactionAsync( - buyOrder, - sellOrder, - buyOrder.signature, - sellOrder.signature, - { - from: takerAddress, - }, - ), - RevertReason.AuctionExpired, - ); - }); - it('cannot be filled for less than the current price', async () => { - await increaseTimeAsync(); - buyOrder = await buyerOrderFactory.newSignedOrderAsync({ - makerAssetAmount: sellOrder.takerAssetAmount, - }); - return expectTransactionFailedAsync( - dutchAuctionContract.matchOrders.sendTransactionAsync( - buyOrder, - sellOrder, - buyOrder.signature, - sellOrder.signature, - { - from: takerAddress, - }, - ), - RevertReason.AuctionInvalidAmount, - ); - }); - it('auction begin amount must be higher than final amount ', async () => { - sellOrder = await sellerOrderFactory.newSignedOrderAsync({ - takerAssetAmount: auctionBeginAmount.plus(1), - }); - return expectTransactionFailedAsync( - dutchAuctionContract.matchOrders.sendTransactionAsync( - buyOrder, - sellOrder, - buyOrder.signature, - sellOrder.signature, - { - from: takerAddress, - }, - ), - RevertReason.AuctionInvalidAmount, - ); - }); - it('begin time is less than end time', async () => { - auctionBeginTimeSeconds = new BigNumber(auctionEndTimeSeconds).plus(tenMinutesInSeconds); - sellOrder = await sellerOrderFactory.newSignedOrderAsync({ - expirationTimeSeconds: auctionEndTimeSeconds, - makerAssetData: extendMakerAssetData( - assetDataUtils.encodeERC20AssetData(defaultMakerAssetAddress), - auctionBeginTimeSeconds, - auctionBeginAmount, - ), - }); - return expectTransactionFailedAsync( - dutchAuctionContract.matchOrders.sendTransactionAsync( - buyOrder, - sellOrder, - buyOrder.signature, - sellOrder.signature, - { - from: takerAddress, - }, - ), - RevertReason.AuctionInvalidBeginTime, - ); - }); - it('asset data contains auction parameters', async () => { - sellOrder = await sellerOrderFactory.newSignedOrderAsync({ - makerAssetData: assetDataUtils.encodeERC20AssetData(defaultMakerAssetAddress), - }); - return expectTransactionFailedAsync( - dutchAuctionContract.matchOrders.sendTransactionAsync( - buyOrder, - sellOrder, - buyOrder.signature, - sellOrder.signature, - { - from: takerAddress, - }, - ), - RevertReason.InvalidAssetData, - ); - }); - describe('ERC721', () => { - it('should match orders when ERC721', async () => { - const makerAssetId = erc721MakerAssetIds[0]; - sellOrder = await sellerOrderFactory.newSignedOrderAsync({ - makerAssetAmount: new BigNumber(1), - makerAssetData: extendMakerAssetData( - assetDataUtils.encodeERC721AssetData(erc721Token.address, makerAssetId), - auctionBeginTimeSeconds, - auctionBeginAmount, - ), - }); - buyOrder = await buyerOrderFactory.newSignedOrderAsync({ - takerAssetAmount: new BigNumber(1), - takerAssetData: sellOrder.makerAssetData, - }); - await web3Wrapper.awaitTransactionSuccessAsync( - await dutchAuctionContract.matchOrders.sendTransactionAsync( - buyOrder, - sellOrder, - buyOrder.signature, - sellOrder.signature, - { - from: takerAddress, - }, - ), - ); - const afterAuctionDetails = await dutchAuctionContract.getAuctionDetails.callAsync(sellOrder); - const newBalances = await erc20Wrapper.getBalancesAsync(); - // HACK gte used here due to a bug in ganache where the timestamp can change - // between multiple calls to the same block. Which can move the amount in our case - // ref: https://github.com/trufflesuite/ganache-core/issues/111 - expect(newBalances[makerAddress][wethContract.address]).to.be.bignumber.gte( - erc20Balances[makerAddress][wethContract.address].plus(afterAuctionDetails.currentAmount), - ); - const newOwner = await erc721Token.ownerOf.callAsync(makerAssetId); - expect(newOwner).to.be.bignumber.equal(takerAddress); - }); - }); - }); -}); -- cgit