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/core/test/extensions/dutch_auction.ts | 486 +++++++++++++++++++++ .../extensions/DutchAuction/DutchAuction.sol | 205 --------- .../contracts/test/extensions/dutch_auction.ts | 486 --------------------- 4 files changed, 691 insertions(+), 691 deletions(-) create mode 100644 contracts/core/contracts/extensions/DutchAuction/DutchAuction.sol create mode 100644 contracts/core/test/extensions/dutch_auction.ts delete mode 100644 packages/contracts/contracts/extensions/DutchAuction/DutchAuction.sol delete mode 100644 packages/contracts/test/extensions/dutch_auction.ts diff --git a/contracts/core/contracts/extensions/DutchAuction/DutchAuction.sol b/contracts/core/contracts/extensions/DutchAuction/DutchAuction.sol new file mode 100644 index 000000000..abe8309cf --- /dev/null +++ b/contracts/core/contracts/extensions/DutchAuction/DutchAuction.sol @@ -0,0 +1,205 @@ +/* + + 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/contracts/core/test/extensions/dutch_auction.ts b/contracts/core/test/extensions/dutch_auction.ts new file mode 100644 index 000000000..c133d8c60 --- /dev/null +++ b/contracts/core/test/extensions/dutch_auction.ts @@ -0,0 +1,486 @@ +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); + }); + }); + }); +}); 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