aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJacob Evans <dekz@dekz.net>2018-12-04 09:44:19 +0800
committerGitHub <noreply@github.com>2018-12-04 09:44:19 +0800
commita1e985a1cac46ecbc54c7ef5b846fb5faf2bede2 (patch)
tree22ae572b251938bd45e8fe23b1127e4a4a1a5a99
parent8c146ce231ab0c3b6948afa21effe2a9bf228432 (diff)
parent098a531de8776df3776017001014078a21eb4029 (diff)
downloaddexon-0x-contracts-a1e985a1cac46ecbc54c7ef5b846fb5faf2bede2.tar
dexon-0x-contracts-a1e985a1cac46ecbc54c7ef5b846fb5faf2bede2.tar.gz
dexon-0x-contracts-a1e985a1cac46ecbc54c7ef5b846fb5faf2bede2.tar.bz2
dexon-0x-contracts-a1e985a1cac46ecbc54c7ef5b846fb5faf2bede2.tar.lz
dexon-0x-contracts-a1e985a1cac46ecbc54c7ef5b846fb5faf2bede2.tar.xz
dexon-0x-contracts-a1e985a1cac46ecbc54c7ef5b846fb5faf2bede2.tar.zst
dexon-0x-contracts-a1e985a1cac46ecbc54c7ef5b846fb5faf2bede2.zip
Merge pull request #1225 from 0xProject/feature/contracts/dutch-auction
Implement Basic Dutch Auction
-rw-r--r--contracts/core/compiler.json1
-rw-r--r--contracts/core/contracts/extensions/DutchAuction/DutchAuction.sol205
-rw-r--r--contracts/core/package.json2
-rw-r--r--contracts/core/src/artifacts/index.ts2
-rw-r--r--contracts/core/src/wrappers/index.ts1
-rw-r--r--contracts/core/test/extensions/dutch_auction.ts486
-rw-r--r--contracts/core/test/utils/types.ts1
-rw-r--r--contracts/core/tsconfig.json1
-rw-r--r--packages/types/CHANGELOG.json4
-rw-r--r--packages/types/src/index.ts6
10 files changed, 708 insertions, 1 deletions
diff --git a/contracts/core/compiler.json b/contracts/core/compiler.json
index c824e4645..a1a60c71d 100644
--- a/contracts/core/compiler.json
+++ b/contracts/core/compiler.json
@@ -25,6 +25,7 @@
"DummyERC721Token",
"DummyMultipleReturnERC20Token",
"DummyNoReturnERC20Token",
+ "DutchAuction",
"ERC20Proxy",
"ERC20Token",
"ERC721Token",
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/package.json b/contracts/core/package.json
index b20b0ef3f..457625791 100644
--- a/contracts/core/package.json
+++ b/contracts/core/package.json
@@ -34,7 +34,7 @@
},
"config": {
"abis":
- "generated-artifacts/@(AssetProxyOwner|DummyERC20Token|DummyERC721Receiver|DummyERC721Token|DummyMultipleReturnERC20Token|DummyNoReturnERC20Token|ERC20Token|ERC20Proxy|ERC721Token|ERC721Proxy|Forwarder|Exchange|ExchangeWrapper|IAssetData|IAssetProxy|InvalidERC721Receiver|MixinAuthorizable|MultiAssetProxy|MultiSigWallet|MultiSigWalletWithTimeLock|OrderValidator|ReentrantERC20Token|TestAssetProxyOwner|TestAssetProxyDispatcher|TestConstants|TestExchangeInternals|TestLibBytes|TestLibs|TestSignatureValidator|TestStaticCallReceiver|Validator|Wallet|Whitelist|WETH9|ZRXToken).json"
+ "generated-artifacts/@(AssetProxyOwner|DummyERC20Token|DummyERC721Receiver|DummyERC721Token|DummyMultipleReturnERC20Token|DummyNoReturnERC20Token|DutchAuction|ERC20Token|ERC20Proxy|ERC721Token|ERC721Proxy|Forwarder|Exchange|ExchangeWrapper|IAssetData|IAssetProxy|InvalidERC721Receiver|MixinAuthorizable|MultiAssetProxy|MultiSigWallet|MultiSigWalletWithTimeLock|OrderValidator|ReentrantERC20Token|TestAssetProxyOwner|TestAssetProxyDispatcher|TestConstants|TestExchangeInternals|TestLibBytes|TestLibs|TestSignatureValidator|TestStaticCallReceiver|Validator|Wallet|Whitelist|WETH9|ZRXToken).json"
},
"repository": {
"type": "git",
diff --git a/contracts/core/src/artifacts/index.ts b/contracts/core/src/artifacts/index.ts
index 97c1b6209..8a8c5f4d4 100644
--- a/contracts/core/src/artifacts/index.ts
+++ b/contracts/core/src/artifacts/index.ts
@@ -6,6 +6,7 @@ import * as DummyERC721Receiver from '../../generated-artifacts/DummyERC721Recei
import * as DummyERC721Token from '../../generated-artifacts/DummyERC721Token.json';
import * as DummyMultipleReturnERC20Token from '../../generated-artifacts/DummyMultipleReturnERC20Token.json';
import * as DummyNoReturnERC20Token from '../../generated-artifacts/DummyNoReturnERC20Token.json';
+import * as DutchAuction from '../../generated-artifacts/DutchAuction.json';
import * as ERC20Proxy from '../../generated-artifacts/ERC20Proxy.json';
import * as ERC20Token from '../../generated-artifacts/ERC20Token.json';
import * as ERC721Proxy from '../../generated-artifacts/ERC721Proxy.json';
@@ -45,6 +46,7 @@ export const artifacts = {
DummyERC721Token: DummyERC721Token as ContractArtifact,
DummyMultipleReturnERC20Token: DummyMultipleReturnERC20Token as ContractArtifact,
DummyNoReturnERC20Token: DummyNoReturnERC20Token as ContractArtifact,
+ DutchAuction: DutchAuction as ContractArtifact,
ERC20Proxy: ERC20Proxy as ContractArtifact,
ERC20Token: ERC20Token as ContractArtifact,
ERC721Proxy: ERC721Proxy as ContractArtifact,
diff --git a/contracts/core/src/wrappers/index.ts b/contracts/core/src/wrappers/index.ts
index 9ca676b56..e9e3f4e79 100644
--- a/contracts/core/src/wrappers/index.ts
+++ b/contracts/core/src/wrappers/index.ts
@@ -4,6 +4,7 @@ export * from '../../generated-wrappers/dummy_erc721_receiver';
export * from '../../generated-wrappers/dummy_erc721_token';
export * from '../../generated-wrappers/dummy_multiple_return_erc20_token';
export * from '../../generated-wrappers/dummy_no_return_erc20_token';
+export * from '../../generated-wrappers/dutch_auction';
export * from '../../generated-wrappers/erc20_proxy';
export * from '../../generated-wrappers/erc721_proxy';
export * from '../../generated-wrappers/erc20_token';
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<void> {
+ 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/contracts/core/test/utils/types.ts b/contracts/core/test/utils/types.ts
index 9fc9e1570..d738fcd4e 100644
--- a/contracts/core/test/utils/types.ts
+++ b/contracts/core/test/utils/types.ts
@@ -86,6 +86,7 @@ export enum ContractName {
ZRXToken = 'ZRXToken',
DummyERC20Token = 'DummyERC20Token',
EtherToken = 'WETH9',
+ DutchAuction = 'DutchAuction',
AssetProxyOwner = 'AssetProxyOwner',
AccountLevels = 'AccountLevels',
EtherDelta = 'EtherDelta',
diff --git a/contracts/core/tsconfig.json b/contracts/core/tsconfig.json
index e0f85079a..23b069110 100644
--- a/contracts/core/tsconfig.json
+++ b/contracts/core/tsconfig.json
@@ -13,6 +13,7 @@
"./generated-artifacts/DummyERC721Token.json",
"./generated-artifacts/DummyMultipleReturnERC20Token.json",
"./generated-artifacts/DummyNoReturnERC20Token.json",
+ "./generated-artifacts/DutchAuction.json",
"./generated-artifacts/ERC20Proxy.json",
"./generated-artifacts/ERC20Token.json",
"./generated-artifacts/ERC721Proxy.json",
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 {