diff options
author | Jacob Evans <jacob@dekz.net> | 2018-11-05 17:31:24 +0800 |
---|---|---|
committer | Jacob Evans <jacob@dekz.net> | 2018-12-04 06:13:58 +0800 |
commit | da8c27c286321af759125d7777f1c84d34498214 (patch) | |
tree | 8ea4858119a3a9ee352ca7b556e928ed12be30d9 /packages | |
parent | f076cdb8329693e9d7351ec88d2ac01ad42a2a3a (diff) | |
download | dexon-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.sol | 160 | ||||
-rw-r--r-- | packages/contracts/test/extensions/dutch_auction.ts | 315 |
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); + }); + }); + }); +}); |