aboutsummaryrefslogtreecommitdiffstats
path: root/contracts/extensions/test/dutch_auction.ts
diff options
context:
space:
mode:
Diffstat (limited to 'contracts/extensions/test/dutch_auction.ts')
-rw-r--r--contracts/extensions/test/dutch_auction.ts367
1 files changed, 367 insertions, 0 deletions
diff --git a/contracts/extensions/test/dutch_auction.ts b/contracts/extensions/test/dutch_auction.ts
new file mode 100644
index 000000000..34ff25c3d
--- /dev/null
+++ b/contracts/extensions/test/dutch_auction.ts
@@ -0,0 +1,367 @@
+import { DutchAuctionWrapper } from '@0x/contract-wrappers';
+import { ERC20Wrapper, ERC721Wrapper } from '@0x/contracts-asset-proxy';
+import { artifacts as erc20Artifacts, DummyERC20TokenContract, WETH9Contract } from '@0x/contracts-erc20';
+import { DummyERC721TokenContract } from '@0x/contracts-erc721';
+import { artifacts as exchangeArtifacts, ExchangeContract, ExchangeWrapper } from '@0x/contracts-exchange';
+import {
+ chaiSetup,
+ constants,
+ ContractName,
+ ERC20BalancesByOwner,
+ expectTransactionFailedAsync,
+ getLatestBlockTimestampAsync,
+ OrderFactory,
+ provider,
+ txDefaults,
+ web3Wrapper,
+} from '@0x/contracts-test-utils';
+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 * as _ from 'lodash';
+
+import { artifacts, DutchAuctionContract, DutchAuctionTestWrapper } from '../src';
+
+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;
+
+ let dutchAuctionTestWrapper: DutchAuctionTestWrapper;
+ let defaultERC20MakerAssetData: string;
+
+ 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(erc20Artifacts.WETH9, provider, txDefaults);
+ erc20Wrapper.addDummyTokenContract(wethContract as any);
+
+ const zrxAssetData = assetDataUtils.encodeERC20AssetData(zrxToken.address);
+ const exchangeInstance = await ExchangeContract.deployFrom0xArtifactAsync(
+ exchangeArtifacts.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,
+ );
+ dutchAuctionTestWrapper = new DutchAuctionTestWrapper(dutchAuctionInstance, 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: DutchAuctionWrapper.encodeDutchAuctionAssetData(
+ 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);
+ defaultERC20MakerAssetData = assetDataUtils.encodeERC20AssetData(defaultMakerAssetAddress);
+ });
+ 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);
+ const makerAssetData = DutchAuctionWrapper.encodeDutchAuctionAssetData(
+ defaultERC20MakerAssetData,
+ auctionBeginTimeSeconds,
+ auctionBeginAmount,
+ );
+ sellOrder = await sellerOrderFactory.newSignedOrderAsync({ makerAssetData });
+ const auctionDetails = await dutchAuctionTestWrapper.getAuctionDetailsAsync(sellOrder);
+ expect(auctionDetails.currentTimeSeconds).to.be.bignumber.lte(auctionBeginTimeSeconds);
+ 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);
+ const makerAssetData = DutchAuctionWrapper.encodeDutchAuctionAssetData(
+ defaultERC20MakerAssetData,
+ auctionBeginTimeSeconds,
+ auctionBeginAmount,
+ );
+ sellOrder = await sellerOrderFactory.newSignedOrderAsync({
+ makerAssetData,
+ expirationTimeSeconds: auctionEndTimeSeconds,
+ });
+ const auctionDetails = await dutchAuctionTestWrapper.getAuctionDetailsAsync(sellOrder);
+ expect(auctionDetails.currentTimeSeconds).to.be.bignumber.gte(auctionEndTimeSeconds);
+ 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 dutchAuctionTestWrapper.getAuctionDetailsAsync(sellOrder);
+ buyOrder = await buyerOrderFactory.newSignedOrderAsync({
+ makerAssetAmount: beforeAuctionDetails.currentAmount.times(2),
+ });
+ await dutchAuctionTestWrapper.matchOrdersAsync(buyOrder, sellOrder, takerAddress);
+ const afterAuctionDetails = await dutchAuctionTestWrapper.getAuctionDetailsAsync(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('maker fees on sellOrder are paid to the fee receipient', async () => {
+ sellOrder = await sellerOrderFactory.newSignedOrderAsync({
+ makerFee: new BigNumber(1),
+ });
+ await dutchAuctionTestWrapper.matchOrdersAsync(buyOrder, sellOrder, takerAddress);
+ const afterAuctionDetails = await dutchAuctionTestWrapper.getAuctionDetailsAsync(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),
+ });
+ await dutchAuctionTestWrapper.matchOrdersAsync(buyOrder, sellOrder, takerAddress);
+ const newBalances = await erc20Wrapper.getBalancesAsync();
+ const afterAuctionDetails = await dutchAuctionTestWrapper.getAuctionDetailsAsync(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);
+ const makerAssetData = DutchAuctionWrapper.encodeDutchAuctionAssetData(
+ defaultERC20MakerAssetData,
+ auctionBeginTimeSeconds,
+ auctionBeginAmount,
+ );
+ sellOrder = await sellerOrderFactory.newSignedOrderAsync({
+ expirationTimeSeconds: auctionEndTimeSeconds,
+ makerAssetData,
+ });
+ return expectTransactionFailedAsync(
+ dutchAuctionTestWrapper.matchOrdersAsync(buyOrder, sellOrder, takerAddress),
+ RevertReason.AuctionExpired,
+ );
+ });
+ it('cannot be filled for less than the current price', async () => {
+ buyOrder = await buyerOrderFactory.newSignedOrderAsync({
+ makerAssetAmount: sellOrder.takerAssetAmount,
+ });
+ return expectTransactionFailedAsync(
+ dutchAuctionTestWrapper.matchOrdersAsync(buyOrder, sellOrder, takerAddress),
+ RevertReason.AuctionInvalidAmount,
+ );
+ });
+ it('auction begin amount must be higher than final amount ', async () => {
+ sellOrder = await sellerOrderFactory.newSignedOrderAsync({
+ takerAssetAmount: auctionBeginAmount.plus(1),
+ });
+ return expectTransactionFailedAsync(
+ dutchAuctionTestWrapper.matchOrdersAsync(buyOrder, sellOrder, takerAddress),
+ RevertReason.AuctionInvalidAmount,
+ );
+ });
+ it('begin time is less than end time', async () => {
+ auctionBeginTimeSeconds = new BigNumber(auctionEndTimeSeconds).plus(tenMinutesInSeconds);
+ const makerAssetData = DutchAuctionWrapper.encodeDutchAuctionAssetData(
+ defaultERC20MakerAssetData,
+ auctionBeginTimeSeconds,
+ auctionBeginAmount,
+ );
+ sellOrder = await sellerOrderFactory.newSignedOrderAsync({
+ expirationTimeSeconds: auctionEndTimeSeconds,
+ makerAssetData,
+ });
+ return expectTransactionFailedAsync(
+ dutchAuctionTestWrapper.matchOrdersAsync(buyOrder, sellOrder, takerAddress),
+ RevertReason.AuctionInvalidBeginTime,
+ );
+ });
+ it('asset data contains auction parameters', async () => {
+ sellOrder = await sellerOrderFactory.newSignedOrderAsync({
+ makerAssetData: assetDataUtils.encodeERC20AssetData(defaultMakerAssetAddress),
+ });
+ return expectTransactionFailedAsync(
+ dutchAuctionTestWrapper.matchOrdersAsync(buyOrder, sellOrder, takerAddress),
+ RevertReason.InvalidAssetData,
+ );
+ });
+
+ describe('ERC721', () => {
+ it('should match orders when ERC721', async () => {
+ const makerAssetId = erc721MakerAssetIds[0];
+ const erc721MakerAssetData = assetDataUtils.encodeERC721AssetData(erc721Token.address, makerAssetId);
+ const makerAssetData = DutchAuctionWrapper.encodeDutchAuctionAssetData(
+ erc721MakerAssetData,
+ auctionBeginTimeSeconds,
+ auctionBeginAmount,
+ );
+ sellOrder = await sellerOrderFactory.newSignedOrderAsync({
+ makerAssetAmount: new BigNumber(1),
+ makerAssetData,
+ });
+ buyOrder = await buyerOrderFactory.newSignedOrderAsync({
+ takerAssetAmount: new BigNumber(1),
+ takerAssetData: sellOrder.makerAssetData,
+ });
+ await dutchAuctionTestWrapper.matchOrdersAsync(buyOrder, sellOrder, takerAddress);
+ const afterAuctionDetails = await dutchAuctionTestWrapper.getAuctionDetailsAsync(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);
+ });
+ });
+ });
+});