aboutsummaryrefslogtreecommitdiffstats
path: root/packages
diff options
context:
space:
mode:
authorJacob Evans <jacob@dekz.net>2018-11-05 17:31:24 +0800
committerJacob Evans <jacob@dekz.net>2018-12-04 06:13:58 +0800
commitda8c27c286321af759125d7777f1c84d34498214 (patch)
tree8ea4858119a3a9ee352ca7b556e928ed12be30d9 /packages
parentf076cdb8329693e9d7351ec88d2ac01ad42a2a3a (diff)
downloaddexon-sol-tools-da8c27c286321af759125d7777f1c84d34498214.tar
dexon-sol-tools-da8c27c286321af759125d7777f1c84d34498214.tar.gz
dexon-sol-tools-da8c27c286321af759125d7777f1c84d34498214.tar.bz2
dexon-sol-tools-da8c27c286321af759125d7777f1c84d34498214.tar.lz
dexon-sol-tools-da8c27c286321af759125d7777f1c84d34498214.tar.xz
dexon-sol-tools-da8c27c286321af759125d7777f1c84d34498214.tar.zst
dexon-sol-tools-da8c27c286321af759125d7777f1c84d34498214.zip
feat: Dutch Auction
Diffstat (limited to 'packages')
-rw-r--r--packages/contracts/contracts/extensions/DutchAuction/DutchAuction.sol160
-rw-r--r--packages/contracts/test/extensions/dutch_auction.ts315
2 files changed, 475 insertions, 0 deletions
diff --git a/packages/contracts/contracts/extensions/DutchAuction/DutchAuction.sol b/packages/contracts/contracts/extensions/DutchAuction/DutchAuction.sol
new file mode 100644
index 000000000..ed4158c25
--- /dev/null
+++ b/packages/contracts/contracts/extensions/DutchAuction/DutchAuction.sol
@@ -0,0 +1,160 @@
+/*
+
+ Copyright 2018 ZeroEx Intl.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+*/
+
+pragma solidity 0.4.24;
+pragma experimental ABIEncoderV2;
+
+import "../../protocol/Exchange/interfaces/IExchange.sol";
+import "../../protocol/Exchange/libs/LibOrder.sol";
+import "../../tokens/ERC20Token/IERC20Token.sol";
+import "../../utils/LibBytes/LibBytes.sol";
+
+
+contract DutchAuction {
+ using LibBytes for bytes;
+
+ // solhint-disable var-name-mixedcase
+ IExchange internal EXCHANGE;
+
+ struct AuctionDetails {
+ uint256 beginTime; // Auction begin time in seconds
+ uint256 endTime; // Auction end time in seconds
+ uint256 beginPrice; // Auction begin price
+ uint256 endPrice; // Auction end price
+ uint256 currentPrice; // Current auction price at block.timestamp
+ uint256 currentTime; // block.timestamp
+ }
+
+ constructor (address _exchange)
+ public
+ {
+ EXCHANGE = IExchange(_exchange);
+ }
+
+ /// @dev Packs the begin time and price parameters of an auction into uint256.
+ /// This is stored as the salt value of the sale order.
+ /// @param beginTime Begin time of the auction (32 bits)
+ /// @param beginPrice Starting price of the auction (224 bits)
+ /// @return Encoded Auction Parameters packed into a uint256
+ function encodeParameters(
+ uint256 beginTime,
+ uint256 beginPrice
+ )
+ external
+ view
+ returns (uint256 encodedParameters)
+ {
+ require(beginTime <= 2**32, "INVALID_BEGIN_TIME");
+ require(beginPrice <= 2**224, "INVALID_BEGIN_PRICE");
+ encodedParameters = beginTime;
+ encodedParameters |= beginPrice<<32;
+ return encodedParameters;
+ }
+
+ /// @dev Performs a match of the two orders at the price point given the current block time and the auction
+ /// start time (encoded in the salt).
+ /// The Sellers order is a signed order at the lowest price at the end of the auction. Excess from the match
+ /// is transferred to the seller.
+ /// @param buyOrder The Buyer's order
+ /// @param sellOrder The Seller's order
+ /// @param buySignature Proof that order was created by the left maker.
+ /// @param sellSignature Proof that order was created by the right maker.
+ /// @return matchedFillResults Amounts filled and fees paid by maker and taker of matched orders.
+ function matchOrders(
+ LibOrder.Order memory buyOrder,
+ LibOrder.Order memory sellOrder,
+ bytes memory buySignature,
+ bytes memory sellSignature
+ )
+ public
+ returns (LibFillResults.MatchedFillResults memory matchedFillResults)
+ {
+ AuctionDetails memory auctionDetails = getAuctionDetails(sellOrder);
+ // Ensure the auction has not yet started
+ // solhint-disable-next-line not-rely-on-time
+ require(block.timestamp >= auctionDetails.beginTime, "AUCTION_NOT_STARTED");
+ // Ensure the auction has not expired. This will fail later in 0x but we can save gas by failing early
+ // solhint-disable-next-line not-rely-on-time
+ require(sellOrder.expirationTimeSeconds > block.timestamp, "AUCTION_EXPIRED");
+ // Ensure the auction goes from high to low
+ require(auctionDetails.beginPrice > auctionDetails.endPrice, "INVALID_PRICE");
+ // Validate the buyer amount is greater than the current auction price
+ require(buyOrder.makerAssetAmount >= auctionDetails.currentPrice, "INVALID_PRICE");
+ // Match orders, maximally filling `buyOrder`
+ matchedFillResults = EXCHANGE.matchOrders(
+ buyOrder,
+ sellOrder,
+ buySignature,
+ sellSignature
+ );
+ // Return any spread to the seller
+ uint256 leftMakerAssetSpreadAmount = matchedFillResults.leftMakerAssetSpreadAmount;
+ if (leftMakerAssetSpreadAmount > 0) {
+ bytes memory assetData = sellOrder.takerAssetData;
+ address token = assetData.readAddress(16);
+ address makerAddress = sellOrder.makerAddress;
+ IERC20Token(token).transfer(makerAddress, leftMakerAssetSpreadAmount);
+ }
+ return matchedFillResults;
+ }
+
+ /// @dev Decodes the packed parameters into beginTime and beginPrice.
+ /// @param encodedParameters the encoded parameters
+ /// @return beginTime and beginPrice decoded
+ function decodeParameters(
+ uint256 encodedParameters
+ )
+ public
+ view
+ returns (uint256 beginTime, uint256 beginPrice)
+ {
+ beginTime = encodedParameters & 0x00000000000000000000000fffffffff;
+ beginPrice = encodedParameters>>32;
+ return (beginTime, beginPrice);
+ }
+
+ /// @dev Calculates the Auction Details for the given order
+ /// @param order The sell order
+ /// @return AuctionDetails
+ function getAuctionDetails(
+ LibOrder.Order memory order
+ )
+ public
+ returns (AuctionDetails memory auctionDetails)
+ {
+ // solhint-disable-next-line indent
+ (uint256 auctionBeginTimeSeconds, uint256 auctionBeginPrice) = decodeParameters(order.salt);
+ require(order.expirationTimeSeconds > auctionBeginTimeSeconds, "INVALID_BEGIN_TIME");
+ uint256 auctionDurationSeconds = order.expirationTimeSeconds-auctionBeginTimeSeconds;
+ // solhint-disable-next-line not-rely-on-time
+ uint256 currentDurationSeconds = order.expirationTimeSeconds-block.timestamp;
+ uint256 minPrice = order.takerAssetAmount;
+ uint256 priceDiff = auctionBeginPrice-minPrice;
+ uint256 currentPrice = minPrice + (currentDurationSeconds*priceDiff/auctionDurationSeconds);
+
+ auctionDetails.beginTime = auctionBeginTimeSeconds;
+ auctionDetails.endTime = order.expirationTimeSeconds;
+ auctionDetails.beginPrice = auctionBeginPrice;
+ auctionDetails.endPrice = minPrice;
+ auctionDetails.currentPrice = currentPrice;
+ // solhint-disable-next-line not-rely-on-time
+ auctionDetails.currentTime = block.timestamp;
+
+ return auctionDetails;
+ }
+}
diff --git a/packages/contracts/test/extensions/dutch_auction.ts b/packages/contracts/test/extensions/dutch_auction.ts
new file mode 100644
index 000000000..c61faf9d7
--- /dev/null
+++ b/packages/contracts/test/extensions/dutch_auction.ts
@@ -0,0 +1,315 @@
+import { BlockchainLifecycle } from '@0x/dev-utils';
+import { assetDataUtils } from '@0x/order-utils';
+import { SignedOrder } from '@0x/types';
+import { BigNumber } from '@0x/utils';
+import { Web3Wrapper } from '@0x/web3-wrapper';
+import * as chai from 'chai';
+
+import { DummyERC20TokenContract } from '../../generated-wrappers/dummy_erc20_token';
+import { DummyERC721TokenContract } from '../../generated-wrappers/dummy_erc721_token';
+import { DutchAuctionContract } from '../../generated-wrappers/dutch_auction';
+import { ExchangeContract } from '../../generated-wrappers/exchange';
+import { WETH9Contract } from '../../generated-wrappers/weth9';
+import { artifacts } from '../../src/artifacts';
+import { expectTransactionFailedAsync } from '../utils/assertions';
+import { getLatestBlockTimestampAsync } from '../utils/block_timestamp';
+import { chaiSetup } from '../utils/chai_setup';
+import { constants } from '../utils/constants';
+import { ERC20Wrapper } from '../utils/erc20_wrapper';
+import { ERC721Wrapper } from '../utils/erc721_wrapper';
+import { ExchangeWrapper } from '../utils/exchange_wrapper';
+import { OrderFactory } from '../utils/order_factory';
+import { ContractName, ERC20BalancesByOwner } from '../utils/types';
+import { provider, txDefaults, web3Wrapper } from '../utils/web3_wrapper';
+
+chaiSetup.configure();
+const expect = chai.expect;
+const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper);
+const DECIMALS_DEFAULT = 18;
+
+describe(ContractName.DutchAuction, () => {
+ let makerAddress: string;
+ let owner: string;
+ let takerAddress: string;
+ let feeRecipientAddress: string;
+ let defaultMakerAssetAddress: string;
+
+ let weth: DummyERC20TokenContract;
+ let zrxToken: DummyERC20TokenContract;
+ let erc20TokenA: DummyERC20TokenContract;
+ let erc721Token: DummyERC721TokenContract;
+ let dutchAuctionContract: DutchAuctionContract;
+ let wethContract: WETH9Contract;
+ let exchangeWrapper: ExchangeWrapper;
+
+ let sellerOrderFactory: OrderFactory;
+ let buyerOrderFactory: OrderFactory;
+ let erc20Wrapper: ERC20Wrapper;
+ let erc20Balances: ERC20BalancesByOwner;
+ let tenMinutesInSeconds: number;
+ let currentBlockTimestamp: number;
+ let auctionBeginTime: BigNumber;
+ let auctionBeginPrice: BigNumber;
+ let encodedParams: BigNumber;
+ let sellOrder: SignedOrder;
+ let buyOrder: SignedOrder;
+ let erc721MakerAssetIds: BigNumber[];
+
+ before(async () => {
+ await blockchainLifecycle.startAsync();
+ const accounts = await web3Wrapper.getAvailableAddressesAsync();
+ const usedAddresses = ([owner, makerAddress, takerAddress, feeRecipientAddress] = accounts);
+
+ erc20Wrapper = new ERC20Wrapper(provider, usedAddresses, owner);
+
+ const numDummyErc20ToDeploy = 2;
+ [erc20TokenA, zrxToken] = await erc20Wrapper.deployDummyTokensAsync(
+ numDummyErc20ToDeploy,
+ constants.DUMMY_TOKEN_DECIMALS,
+ );
+ const erc20Proxy = await erc20Wrapper.deployProxyAsync();
+ await erc20Wrapper.setBalancesAndAllowancesAsync();
+
+ const erc721Wrapper = new ERC721Wrapper(provider, usedAddresses, owner);
+ [erc721Token] = await erc721Wrapper.deployDummyTokensAsync();
+ const erc721Proxy = await erc721Wrapper.deployProxyAsync();
+ await erc721Wrapper.setBalancesAndAllowancesAsync();
+ const erc721Balances = await erc721Wrapper.getBalancesAsync();
+ erc721MakerAssetIds = erc721Balances[makerAddress][erc721Token.address];
+
+ wethContract = await WETH9Contract.deployFrom0xArtifactAsync(artifacts.WETH9, provider, txDefaults);
+ weth = new DummyERC20TokenContract(wethContract.abi, wethContract.address, provider);
+ erc20Wrapper.addDummyTokenContract(weth);
+
+ const zrxAssetData = assetDataUtils.encodeERC20AssetData(zrxToken.address);
+ const exchangeInstance = await ExchangeContract.deployFrom0xArtifactAsync(
+ artifacts.Exchange,
+ provider,
+ txDefaults,
+ zrxAssetData,
+ );
+ exchangeWrapper = new ExchangeWrapper(exchangeInstance, provider);
+ await exchangeWrapper.registerAssetProxyAsync(erc20Proxy.address, owner);
+
+ await exchangeWrapper.registerAssetProxyAsync(erc721Proxy.address, owner);
+
+ await erc20Proxy.addAuthorizedAddress.sendTransactionAsync(exchangeInstance.address, {
+ from: owner,
+ });
+ await erc721Proxy.addAuthorizedAddress.sendTransactionAsync(exchangeInstance.address, {
+ from: owner,
+ });
+
+ const dutchAuctionInstance = await DutchAuctionContract.deployFrom0xArtifactAsync(
+ artifacts.DutchAuction,
+ provider,
+ txDefaults,
+ exchangeInstance.address,
+ );
+ dutchAuctionContract = new DutchAuctionContract(
+ dutchAuctionInstance.abi,
+ dutchAuctionInstance.address,
+ provider,
+ );
+
+ defaultMakerAssetAddress = erc20TokenA.address;
+ const defaultTakerAssetAddress = wethContract.address;
+
+ // Set up taker WETH balance and allowance
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await wethContract.deposit.sendTransactionAsync({
+ from: takerAddress,
+ value: Web3Wrapper.toBaseUnitAmount(new BigNumber(50), DECIMALS_DEFAULT),
+ }),
+ );
+ await web3Wrapper.awaitTransactionSuccessAsync(
+ await wethContract.approve.sendTransactionAsync(
+ erc20Proxy.address,
+ constants.UNLIMITED_ALLOWANCE_IN_BASE_UNITS,
+ { from: takerAddress },
+ ),
+ );
+ web3Wrapper.abiDecoder.addABI(exchangeInstance.abi);
+ web3Wrapper.abiDecoder.addABI(zrxToken.abi);
+ erc20Wrapper.addTokenOwnerAddress(dutchAuctionContract.address);
+ tenMinutesInSeconds = 10 * 60;
+ currentBlockTimestamp = await getLatestBlockTimestampAsync();
+ auctionBeginTime = new BigNumber(currentBlockTimestamp).minus(tenMinutesInSeconds);
+ auctionBeginPrice = Web3Wrapper.toBaseUnitAmount(new BigNumber(10), DECIMALS_DEFAULT);
+ encodedParams = await dutchAuctionContract.encodeParameters.callAsync(auctionBeginTime, auctionBeginPrice);
+
+ const sellerDefaultOrderParams = {
+ salt: encodedParams, // Set the encoded params as the salt for the seller order
+ exchangeAddress: exchangeInstance.address,
+ makerAddress,
+ feeRecipientAddress,
+ senderAddress: dutchAuctionContract.address,
+ makerAssetData: assetDataUtils.encodeERC20AssetData(defaultMakerAssetAddress),
+ takerAssetData: assetDataUtils.encodeERC20AssetData(defaultTakerAssetAddress),
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(200), DECIMALS_DEFAULT),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(2), DECIMALS_DEFAULT),
+ makerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(0), DECIMALS_DEFAULT),
+ takerFee: Web3Wrapper.toBaseUnitAmount(new BigNumber(0), DECIMALS_DEFAULT),
+ };
+ const buyerDefaultOrderParams = {
+ ...sellerDefaultOrderParams,
+ makerAddress: takerAddress,
+ makerAssetData: sellerDefaultOrderParams.takerAssetData,
+ takerAssetData: sellerDefaultOrderParams.makerAssetData,
+ makerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(10), DECIMALS_DEFAULT),
+ takerAssetAmount: Web3Wrapper.toBaseUnitAmount(new BigNumber(200), DECIMALS_DEFAULT),
+ };
+ const makerPrivateKey = constants.TESTRPC_PRIVATE_KEYS[accounts.indexOf(makerAddress)];
+ const takerPrivateKey = constants.TESTRPC_PRIVATE_KEYS[accounts.indexOf(takerAddress)];
+ sellerOrderFactory = new OrderFactory(makerPrivateKey, sellerDefaultOrderParams);
+ buyerOrderFactory = new OrderFactory(takerPrivateKey, buyerDefaultOrderParams);
+ });
+ after(async () => {
+ await blockchainLifecycle.revertAsync();
+ });
+ beforeEach(async () => {
+ await blockchainLifecycle.startAsync();
+ erc20Balances = await erc20Wrapper.getBalancesAsync();
+ tenMinutesInSeconds = 10 * 60;
+ currentBlockTimestamp = await getLatestBlockTimestampAsync();
+ auctionBeginTime = new BigNumber(currentBlockTimestamp).minus(tenMinutesInSeconds);
+ auctionBeginPrice = Web3Wrapper.toBaseUnitAmount(new BigNumber(10), DECIMALS_DEFAULT);
+ encodedParams = await dutchAuctionContract.encodeParameters.callAsync(auctionBeginTime, auctionBeginPrice);
+ sellOrder = await sellerOrderFactory.newSignedOrderAsync();
+ buyOrder = await buyerOrderFactory.newSignedOrderAsync();
+ });
+ afterEach(async () => {
+ await blockchainLifecycle.revertAsync();
+ });
+ describe('matchOrders', () => {
+ it('should encode and decode parameters', async () => {
+ const [decodedBegin, decodedBeginPrice] = await dutchAuctionContract.decodeParameters.callAsync(
+ encodedParams,
+ );
+ expect(decodedBegin).to.be.bignumber.equal(auctionBeginTime);
+ expect(decodedBeginPrice).to.be.bignumber.equal(auctionBeginPrice);
+ });
+ it('should be worth the begin price at the begining of the auction', async () => {
+ // TODO this is flakey
+ currentBlockTimestamp = await web3Wrapper.getBlockTimestampAsync('latest');
+ await web3Wrapper.increaseTimeAsync(1);
+ auctionBeginTime = new BigNumber(currentBlockTimestamp + 2);
+ encodedParams = await dutchAuctionContract.encodeParameters.callAsync(auctionBeginTime, auctionBeginPrice);
+ sellOrder = await sellerOrderFactory.newSignedOrderAsync({
+ salt: encodedParams,
+ });
+ const auctionDetails = await dutchAuctionContract.getAuctionDetails.callAsync(sellOrder);
+ expect(auctionDetails.currentPrice).to.be.bignumber.equal(auctionBeginPrice);
+ expect(auctionDetails.beginPrice).to.be.bignumber.equal(auctionBeginPrice);
+ });
+ it('should match orders and send excess to seller', async () => {
+ const txHash = await dutchAuctionContract.matchOrders.sendTransactionAsync(
+ buyOrder,
+ sellOrder,
+ buyOrder.signature,
+ sellOrder.signature,
+ {
+ from: takerAddress,
+ },
+ );
+ await web3Wrapper.awaitTransactionSuccessAsync(txHash);
+ const newBalances = await erc20Wrapper.getBalancesAsync();
+ expect(newBalances[dutchAuctionContract.address][weth.address]).to.be.bignumber.equal(
+ constants.ZERO_AMOUNT,
+ );
+ expect(newBalances[makerAddress][weth.address]).to.be.bignumber.equal(
+ erc20Balances[makerAddress][weth.address].plus(buyOrder.makerAssetAmount),
+ );
+ });
+ it('should have valid getAuctionDetails at a block in the future', async () => {
+ let auctionDetails = await dutchAuctionContract.getAuctionDetails.callAsync(sellOrder);
+ const beforePrice = auctionDetails.currentPrice;
+ // Increase block time
+ await web3Wrapper.increaseTimeAsync(60);
+ auctionDetails = await dutchAuctionContract.getAuctionDetails.callAsync(sellOrder);
+ const currentPrice = auctionDetails.currentPrice;
+ expect(beforePrice).to.be.bignumber.greaterThan(currentPrice);
+ buyOrder = await buyerOrderFactory.newSignedOrderAsync({
+ makerAssetAmount: currentPrice,
+ });
+ const txHash = await dutchAuctionContract.matchOrders.sendTransactionAsync(
+ buyOrder,
+ sellOrder,
+ buyOrder.signature,
+ sellOrder.signature,
+ {
+ from: takerAddress,
+ },
+ );
+ await web3Wrapper.awaitTransactionSuccessAsync(txHash);
+ const newBalances = await erc20Wrapper.getBalancesAsync();
+ expect(newBalances[makerAddress][weth.address]).to.be.bignumber.equal(
+ erc20Balances[makerAddress][weth.address].plus(currentPrice),
+ );
+ });
+ it('should revert when auction expires', async () => {
+ // Increase block time
+ await web3Wrapper.increaseTimeAsync(tenMinutesInSeconds);
+ return expectTransactionFailedAsync(
+ dutchAuctionContract.matchOrders.sendTransactionAsync(
+ buyOrder,
+ sellOrder,
+ buyOrder.signature,
+ sellOrder.signature,
+ {
+ from: takerAddress,
+ },
+ ),
+ 'AUCTION_EXPIRED' as any,
+ );
+ });
+ it('cannot be filled for less than the current price', async () => {
+ // Increase block time
+ await web3Wrapper.increaseTimeAsync(60);
+ buyOrder = await buyerOrderFactory.newSignedOrderAsync({
+ makerAssetAmount: sellOrder.takerAssetAmount,
+ });
+ return expectTransactionFailedAsync(
+ dutchAuctionContract.matchOrders.sendTransactionAsync(
+ buyOrder,
+ sellOrder,
+ buyOrder.signature,
+ sellOrder.signature,
+ {
+ from: takerAddress,
+ },
+ ),
+ 'INVALID_PRICE' as any,
+ );
+ });
+ describe('ERC721', () => {
+ it('should match orders when ERC721', async () => {
+ const makerAssetId = erc721MakerAssetIds[0];
+ sellOrder = await sellerOrderFactory.newSignedOrderAsync({
+ makerAssetAmount: new BigNumber(1),
+ makerAssetData: assetDataUtils.encodeERC721AssetData(erc721Token.address, makerAssetId),
+ });
+ buyOrder = await buyerOrderFactory.newSignedOrderAsync({
+ takerAssetAmount: new BigNumber(1),
+ takerAssetData: assetDataUtils.encodeERC721AssetData(erc721Token.address, makerAssetId),
+ });
+ const txHash = await dutchAuctionContract.matchOrders.sendTransactionAsync(
+ buyOrder,
+ sellOrder,
+ buyOrder.signature,
+ sellOrder.signature,
+ {
+ from: takerAddress,
+ },
+ );
+ await web3Wrapper.awaitTransactionSuccessAsync(txHash);
+ const newBalances = await erc20Wrapper.getBalancesAsync();
+ expect(newBalances[makerAddress][weth.address]).to.be.bignumber.equal(
+ erc20Balances[makerAddress][weth.address].plus(buyOrder.makerAssetAmount),
+ );
+ const newOwner = await erc721Token.ownerOf.callAsync(makerAssetId);
+ expect(newOwner).to.be.bignumber.equal(takerAddress);
+ });
+ });
+ });
+});