Dutch Auction Contract Wrapper

This commit is contained in:
Greg Hysen 2018-12-19 16:08:59 -08:00
parent 7dda953bc9
commit c850046ea0
5 changed files with 158 additions and 137 deletions

View File

@ -1,4 +1,13 @@
[ [
{
"version": "1.2.0",
"changes": [
{
"note": "Added Dutch Auction Wrapper",
"pr": 1465
}
]
},
{ {
"version": "1.1.0", "version": "1.1.0",
"changes": [ "changes": [

View File

@ -29,13 +29,13 @@ import { RevertReason, SignedOrder } from '@0x/types';
import { BigNumber } from '@0x/utils'; import { BigNumber } from '@0x/utils';
import { Web3Wrapper } from '@0x/web3-wrapper'; import { Web3Wrapper } from '@0x/web3-wrapper';
import * as chai from 'chai'; import * as chai from 'chai';
import ethAbi = require('ethereumjs-abi');
import * as ethUtil from 'ethereumjs-util';
import * as _ from 'lodash'; import * as _ from 'lodash';
import { DutchAuctionContract } from '../../generated-wrappers/dutch_auction'; import { DutchAuctionContract } from '../../generated-wrappers/dutch_auction';
import { artifacts } from '../../src/artifacts'; import { artifacts } from '../../src/artifacts';
import { DutchAuctionWrapper } from '../utils/dutch_auction_wrapper';
chaiSetup.configure(); chaiSetup.configure();
const expect = chai.expect; const expect = chai.expect;
const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper);
@ -68,19 +68,9 @@ describe(ContractName.DutchAuction, () => {
let erc721MakerAssetIds: BigNumber[]; let erc721MakerAssetIds: BigNumber[];
const tenMinutesInSeconds = 10 * 60; const tenMinutesInSeconds = 10 * 60;
function extendMakerAssetData(makerAssetData: string, beginTimeSeconds: BigNumber, beginAmount: BigNumber): string { let dutchAuctionInstance: DutchAuctionContract;
return ethUtil.bufferToHex( let dutchAuctionWrapper: DutchAuctionWrapper;
Buffer.concat([ let defaultMakerAssetData: string;
ethUtil.toBuffer(makerAssetData),
ethUtil.toBuffer(
(ethAbi as any).rawEncode(
['uint256', 'uint256'],
[beginTimeSeconds.toString(), beginAmount.toString()],
),
),
]),
);
}
before(async () => { before(async () => {
await blockchainLifecycle.startAsync(); await blockchainLifecycle.startAsync();
@ -136,6 +126,7 @@ describe(ContractName.DutchAuction, () => {
dutchAuctionInstance.address, dutchAuctionInstance.address,
provider, provider,
); );
dutchAuctionWrapper = new DutchAuctionWrapper(dutchAuctionInstance, provider);
defaultMakerAssetAddress = erc20TokenA.address; defaultMakerAssetAddress = erc20TokenA.address;
const defaultTakerAssetAddress = wethContract.address; const defaultTakerAssetAddress = wethContract.address;
@ -174,7 +165,7 @@ describe(ContractName.DutchAuction, () => {
feeRecipientAddress, feeRecipientAddress,
// taker address or sender address should be set to the ducth auction contract // taker address or sender address should be set to the ducth auction contract
takerAddress: dutchAuctionContract.address, takerAddress: dutchAuctionContract.address,
makerAssetData: extendMakerAssetData( makerAssetData: assetDataUtils.encodeDutchAuctionAssetData(
assetDataUtils.encodeERC20AssetData(defaultMakerAssetAddress), assetDataUtils.encodeERC20AssetData(defaultMakerAssetAddress),
auctionBeginTimeSeconds, auctionBeginTimeSeconds,
auctionBeginAmount, auctionBeginAmount,
@ -199,6 +190,7 @@ describe(ContractName.DutchAuction, () => {
const takerPrivateKey = constants.TESTRPC_PRIVATE_KEYS[accounts.indexOf(takerAddress)]; const takerPrivateKey = constants.TESTRPC_PRIVATE_KEYS[accounts.indexOf(takerAddress)];
sellerOrderFactory = new OrderFactory(makerPrivateKey, sellerDefaultOrderParams); sellerOrderFactory = new OrderFactory(makerPrivateKey, sellerDefaultOrderParams);
buyerOrderFactory = new OrderFactory(takerPrivateKey, buyerDefaultOrderParams); buyerOrderFactory = new OrderFactory(takerPrivateKey, buyerDefaultOrderParams);
defaultMakerAssetData = assetDataUtils.encodeERC20AssetData(defaultMakerAssetAddress);
}); });
after(async () => { after(async () => {
await blockchainLifecycle.revertAsync(); await blockchainLifecycle.revertAsync();
@ -215,49 +207,41 @@ describe(ContractName.DutchAuction, () => {
describe('matchOrders', () => { describe('matchOrders', () => {
it('should be worth the begin price at the begining of the auction', async () => { it('should be worth the begin price at the begining of the auction', async () => {
auctionBeginTimeSeconds = new BigNumber(currentBlockTimestamp + 2); auctionBeginTimeSeconds = new BigNumber(currentBlockTimestamp + 2);
sellOrder = await sellerOrderFactory.newSignedOrderAsync({ const makerAssetData = assetDataUtils.encodeDutchAuctionAssetData(
makerAssetData: extendMakerAssetData( defaultMakerAssetData,
assetDataUtils.encodeERC20AssetData(defaultMakerAssetAddress), auctionBeginTimeSeconds,
auctionBeginTimeSeconds, auctionBeginAmount,
auctionBeginAmount, );
), sellOrder = await sellerOrderFactory.newSignedOrderAsync({ makerAssetData });
}); const auctionDetails = await dutchAuctionWrapper.getAuctionDetailsAsync(sellOrder);
const auctionDetails = await dutchAuctionContract.getAuctionDetails.callAsync(sellOrder); expect(auctionDetails.currentTimeSeconds).to.be.bignumber.lte(auctionBeginTimeSeconds);
expect(auctionDetails.currentAmount).to.be.bignumber.equal(auctionBeginAmount); expect(auctionDetails.currentAmount).to.be.bignumber.equal(auctionBeginAmount);
expect(auctionDetails.beginAmount).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 () => { it('should be be worth the end price at the end of the auction', async () => {
auctionBeginTimeSeconds = new BigNumber(currentBlockTimestamp - tenMinutesInSeconds * 2); auctionBeginTimeSeconds = new BigNumber(currentBlockTimestamp - tenMinutesInSeconds * 2);
auctionEndTimeSeconds = new BigNumber(currentBlockTimestamp - tenMinutesInSeconds); auctionEndTimeSeconds = new BigNumber(currentBlockTimestamp - tenMinutesInSeconds);
const makerAssetData = assetDataUtils.encodeDutchAuctionAssetData(
defaultMakerAssetData,
auctionBeginTimeSeconds,
auctionBeginAmount,
);
sellOrder = await sellerOrderFactory.newSignedOrderAsync({ sellOrder = await sellerOrderFactory.newSignedOrderAsync({
makerAssetData: extendMakerAssetData( makerAssetData,
assetDataUtils.encodeERC20AssetData(defaultMakerAssetAddress),
auctionBeginTimeSeconds,
auctionBeginAmount,
),
expirationTimeSeconds: auctionEndTimeSeconds, expirationTimeSeconds: auctionEndTimeSeconds,
}); });
const auctionDetails = await dutchAuctionContract.getAuctionDetails.callAsync(sellOrder); const auctionDetails = await dutchAuctionWrapper.getAuctionDetailsAsync(sellOrder);
expect(auctionDetails.currentTimeSeconds).to.be.bignumber.gte(auctionEndTimeSeconds);
expect(auctionDetails.currentAmount).to.be.bignumber.equal(auctionEndAmount); expect(auctionDetails.currentAmount).to.be.bignumber.equal(auctionEndAmount);
expect(auctionDetails.beginAmount).to.be.bignumber.equal(auctionBeginAmount); expect(auctionDetails.beginAmount).to.be.bignumber.equal(auctionBeginAmount);
}); });
it('should match orders at current amount and send excess to buyer', async () => { it('should match orders at current amount and send excess to buyer', async () => {
const beforeAuctionDetails = await dutchAuctionContract.getAuctionDetails.callAsync(sellOrder); const beforeAuctionDetails = await dutchAuctionWrapper.getAuctionDetailsAsync(sellOrder);
buyOrder = await buyerOrderFactory.newSignedOrderAsync({ buyOrder = await buyerOrderFactory.newSignedOrderAsync({
makerAssetAmount: beforeAuctionDetails.currentAmount.times(2), makerAssetAmount: beforeAuctionDetails.currentAmount.times(2),
}); });
await web3Wrapper.awaitTransactionSuccessAsync( await dutchAuctionWrapper.matchOrdersAsync(buyOrder, sellOrder, takerAddress);
await dutchAuctionContract.matchOrders.sendTransactionAsync( const afterAuctionDetails = await dutchAuctionWrapper.getAuctionDetailsAsync(sellOrder);
buyOrder,
sellOrder,
buyOrder.signature,
sellOrder.signature,
{
from: takerAddress,
},
),
);
const afterAuctionDetails = await dutchAuctionContract.getAuctionDetails.callAsync(sellOrder);
const newBalances = await erc20Wrapper.getBalancesAsync(); const newBalances = await erc20Wrapper.getBalancesAsync();
expect(newBalances[dutchAuctionContract.address][wethContract.address]).to.be.bignumber.equal( expect(newBalances[dutchAuctionContract.address][wethContract.address]).to.be.bignumber.equal(
constants.ZERO_AMOUNT, constants.ZERO_AMOUNT,
@ -276,17 +260,8 @@ describe(ContractName.DutchAuction, () => {
sellOrder = await sellerOrderFactory.newSignedOrderAsync({ sellOrder = await sellerOrderFactory.newSignedOrderAsync({
makerFee: new BigNumber(1), makerFee: new BigNumber(1),
}); });
const txHash = await dutchAuctionContract.matchOrders.sendTransactionAsync( await dutchAuctionWrapper.matchOrdersAsync(buyOrder, sellOrder, takerAddress);
buyOrder, const afterAuctionDetails = await dutchAuctionWrapper.getAuctionDetailsAsync(sellOrder);
sellOrder,
buyOrder.signature,
sellOrder.signature,
{
from: takerAddress,
},
);
await web3Wrapper.awaitTransactionSuccessAsync(txHash);
const afterAuctionDetails = await dutchAuctionContract.getAuctionDetails.callAsync(sellOrder);
const newBalances = await erc20Wrapper.getBalancesAsync(); const newBalances = await erc20Wrapper.getBalancesAsync();
expect(newBalances[makerAddress][wethContract.address]).to.be.bignumber.gte( expect(newBalances[makerAddress][wethContract.address]).to.be.bignumber.gte(
erc20Balances[makerAddress][wethContract.address].plus(afterAuctionDetails.currentAmount), erc20Balances[makerAddress][wethContract.address].plus(afterAuctionDetails.currentAmount),
@ -299,18 +274,9 @@ describe(ContractName.DutchAuction, () => {
buyOrder = await buyerOrderFactory.newSignedOrderAsync({ buyOrder = await buyerOrderFactory.newSignedOrderAsync({
makerFee: new BigNumber(1), makerFee: new BigNumber(1),
}); });
const txHash = await dutchAuctionContract.matchOrders.sendTransactionAsync( await dutchAuctionWrapper.matchOrdersAsync(buyOrder, sellOrder, takerAddress);
buyOrder,
sellOrder,
buyOrder.signature,
sellOrder.signature,
{
from: takerAddress,
},
);
await web3Wrapper.awaitTransactionSuccessAsync(txHash);
const newBalances = await erc20Wrapper.getBalancesAsync(); const newBalances = await erc20Wrapper.getBalancesAsync();
const afterAuctionDetails = await dutchAuctionContract.getAuctionDetails.callAsync(sellOrder); const afterAuctionDetails = await dutchAuctionWrapper.getAuctionDetailsAsync(sellOrder);
expect(newBalances[makerAddress][wethContract.address]).to.be.bignumber.gte( expect(newBalances[makerAddress][wethContract.address]).to.be.bignumber.gte(
erc20Balances[makerAddress][wethContract.address].plus(afterAuctionDetails.currentAmount), erc20Balances[makerAddress][wethContract.address].plus(afterAuctionDetails.currentAmount),
); );
@ -321,24 +287,17 @@ describe(ContractName.DutchAuction, () => {
it('should revert when auction expires', async () => { it('should revert when auction expires', async () => {
auctionBeginTimeSeconds = new BigNumber(currentBlockTimestamp - tenMinutesInSeconds * 2); auctionBeginTimeSeconds = new BigNumber(currentBlockTimestamp - tenMinutesInSeconds * 2);
auctionEndTimeSeconds = new BigNumber(currentBlockTimestamp - tenMinutesInSeconds); auctionEndTimeSeconds = new BigNumber(currentBlockTimestamp - tenMinutesInSeconds);
const makerAssetData = assetDataUtils.encodeDutchAuctionAssetData(
defaultMakerAssetData,
auctionBeginTimeSeconds,
auctionBeginAmount,
);
sellOrder = await sellerOrderFactory.newSignedOrderAsync({ sellOrder = await sellerOrderFactory.newSignedOrderAsync({
expirationTimeSeconds: auctionEndTimeSeconds, expirationTimeSeconds: auctionEndTimeSeconds,
makerAssetData: extendMakerAssetData( makerAssetData,
assetDataUtils.encodeERC20AssetData(defaultMakerAssetAddress),
auctionBeginTimeSeconds,
auctionBeginAmount,
),
}); });
return expectTransactionFailedAsync( return expectTransactionFailedAsync(
dutchAuctionContract.matchOrders.sendTransactionAsync( dutchAuctionWrapper.matchOrdersAsync(buyOrder, sellOrder, takerAddress),
buyOrder,
sellOrder,
buyOrder.signature,
sellOrder.signature,
{
from: takerAddress,
},
),
RevertReason.AuctionExpired, RevertReason.AuctionExpired,
); );
}); });
@ -347,15 +306,7 @@ describe(ContractName.DutchAuction, () => {
makerAssetAmount: sellOrder.takerAssetAmount, makerAssetAmount: sellOrder.takerAssetAmount,
}); });
return expectTransactionFailedAsync( return expectTransactionFailedAsync(
dutchAuctionContract.matchOrders.sendTransactionAsync( dutchAuctionWrapper.matchOrdersAsync(buyOrder, sellOrder, takerAddress),
buyOrder,
sellOrder,
buyOrder.signature,
sellOrder.signature,
{
from: takerAddress,
},
),
RevertReason.AuctionInvalidAmount, RevertReason.AuctionInvalidAmount,
); );
}); });
@ -364,38 +315,23 @@ describe(ContractName.DutchAuction, () => {
takerAssetAmount: auctionBeginAmount.plus(1), takerAssetAmount: auctionBeginAmount.plus(1),
}); });
return expectTransactionFailedAsync( return expectTransactionFailedAsync(
dutchAuctionContract.matchOrders.sendTransactionAsync( dutchAuctionWrapper.matchOrdersAsync(buyOrder, sellOrder, takerAddress),
buyOrder,
sellOrder,
buyOrder.signature,
sellOrder.signature,
{
from: takerAddress,
},
),
RevertReason.AuctionInvalidAmount, RevertReason.AuctionInvalidAmount,
); );
}); });
it('begin time is less than end time', async () => { it('begin time is less than end time', async () => {
auctionBeginTimeSeconds = new BigNumber(auctionEndTimeSeconds).plus(tenMinutesInSeconds); auctionBeginTimeSeconds = new BigNumber(auctionEndTimeSeconds).plus(tenMinutesInSeconds);
const makerAssetData = assetDataUtils.encodeDutchAuctionAssetData(
defaultMakerAssetData,
auctionBeginTimeSeconds,
auctionBeginAmount,
);
sellOrder = await sellerOrderFactory.newSignedOrderAsync({ sellOrder = await sellerOrderFactory.newSignedOrderAsync({
expirationTimeSeconds: auctionEndTimeSeconds, expirationTimeSeconds: auctionEndTimeSeconds,
makerAssetData: extendMakerAssetData( makerAssetData,
assetDataUtils.encodeERC20AssetData(defaultMakerAssetAddress),
auctionBeginTimeSeconds,
auctionBeginAmount,
),
}); });
return expectTransactionFailedAsync( return expectTransactionFailedAsync(
dutchAuctionContract.matchOrders.sendTransactionAsync( dutchAuctionWrapper.matchOrdersAsync(buyOrder, sellOrder, takerAddress),
buyOrder,
sellOrder,
buyOrder.signature,
sellOrder.signature,
{
from: takerAddress,
},
),
RevertReason.AuctionInvalidBeginTime, RevertReason.AuctionInvalidBeginTime,
); );
}); });
@ -404,45 +340,30 @@ describe(ContractName.DutchAuction, () => {
makerAssetData: assetDataUtils.encodeERC20AssetData(defaultMakerAssetAddress), makerAssetData: assetDataUtils.encodeERC20AssetData(defaultMakerAssetAddress),
}); });
return expectTransactionFailedAsync( return expectTransactionFailedAsync(
dutchAuctionContract.matchOrders.sendTransactionAsync( dutchAuctionWrapper.matchOrdersAsync(buyOrder, sellOrder, takerAddress),
buyOrder,
sellOrder,
buyOrder.signature,
sellOrder.signature,
{
from: takerAddress,
},
),
RevertReason.InvalidAssetData, RevertReason.InvalidAssetData,
); );
}); });
describe('ERC721', () => { describe('ERC721', () => {
it('should match orders when ERC721', async () => { it('should match orders when ERC721', async () => {
const makerAssetId = erc721MakerAssetIds[0]; const makerAssetId = erc721MakerAssetIds[0];
const erc721MakerAssetData = assetDataUtils.encodeERC721AssetData(erc721Token.address, makerAssetId);
const makerAssetData = assetDataUtils.encodeDutchAuctionAssetData(
erc721MakerAssetData,
auctionBeginTimeSeconds,
auctionBeginAmount,
);
sellOrder = await sellerOrderFactory.newSignedOrderAsync({ sellOrder = await sellerOrderFactory.newSignedOrderAsync({
makerAssetAmount: new BigNumber(1), makerAssetAmount: new BigNumber(1),
makerAssetData: extendMakerAssetData( makerAssetData,
assetDataUtils.encodeERC721AssetData(erc721Token.address, makerAssetId),
auctionBeginTimeSeconds,
auctionBeginAmount,
),
}); });
buyOrder = await buyerOrderFactory.newSignedOrderAsync({ buyOrder = await buyerOrderFactory.newSignedOrderAsync({
takerAssetAmount: new BigNumber(1), takerAssetAmount: new BigNumber(1),
takerAssetData: sellOrder.makerAssetData, takerAssetData: sellOrder.makerAssetData,
}); });
await web3Wrapper.awaitTransactionSuccessAsync( await dutchAuctionWrapper.matchOrdersAsync(buyOrder, sellOrder, takerAddress);
await dutchAuctionContract.matchOrders.sendTransactionAsync( const afterAuctionDetails = await dutchAuctionWrapper.getAuctionDetailsAsync(sellOrder);
buyOrder,
sellOrder,
buyOrder.signature,
sellOrder.signature,
{
from: takerAddress,
},
),
);
const afterAuctionDetails = await dutchAuctionContract.getAuctionDetails.callAsync(sellOrder);
const newBalances = await erc20Wrapper.getBalancesAsync(); const newBalances = await erc20Wrapper.getBalancesAsync();
// HACK gte used here due to a bug in ganache where the timestamp can change // 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 // between multiple calls to the same block. Which can move the amount in our case

View File

@ -0,0 +1,62 @@
import { artifacts as protocolArtifacts } from '@0x/contracts-protocol';
import { LogDecoder } from '@0x/contracts-test-utils';
import { artifacts as tokensArtifacts } from '@0x/contracts-tokens';
import { DutchAuctionDetails, SignedOrder } from '@0x/types';
import { Web3Wrapper } from '@0x/web3-wrapper';
import { Provider, TransactionReceiptWithDecodedLogs } from 'ethereum-types';
import * as _ from 'lodash';
import { DutchAuctionContract } from '../../generated-wrappers/dutch_auction';
import { artifacts } from '../../src/artifacts';
export class DutchAuctionWrapper {
private readonly _dutchAuctionContract: DutchAuctionContract;
private readonly _web3Wrapper: Web3Wrapper;
private readonly _logDecoder: LogDecoder;
constructor(contractInstance: DutchAuctionContract, provider: Provider) {
this._dutchAuctionContract = contractInstance;
this._web3Wrapper = new Web3Wrapper(provider);
this._logDecoder = new LogDecoder(this._web3Wrapper, {
...artifacts,
...tokensArtifacts,
...protocolArtifacts,
});
}
/**
* 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.
* @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 from Address the transaction is being sent from.
* @return Transaction receipt with decoded logs.
*/
public async matchOrdersAsync(
buyOrder: SignedOrder,
sellOrder: SignedOrder,
from: string,
): Promise<TransactionReceiptWithDecodedLogs> {
const txHash = await this._dutchAuctionContract.matchOrders.sendTransactionAsync(
buyOrder,
sellOrder,
buyOrder.signature,
sellOrder.signature,
{
from,
},
);
const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash);
return tx;
}
/**
* Calculates the Auction Details for the given order
* @param sellOrder The Seller's order. This order is for the lowest amount (at the end of the auction).
* @return The dutch auction details.
*/
public async getAuctionDetailsAsync(sellOrder: SignedOrder): Promise<DutchAuctionDetails> {
const afterAuctionDetails = await this._dutchAuctionContract.getAuctionDetails.callAsync(sellOrder);
return afterAuctionDetails;
}
}

View File

@ -305,4 +305,24 @@ export const assetDataUtils = {
throw new Error(`Unrecognized asset proxy id: ${assetProxyId}`); throw new Error(`Unrecognized asset proxy id: ${assetProxyId}`);
} }
}, },
/**
* Dutch auction details are encoded with the asset data for a 0x order. This function produces a hex
* encoded assetData string, containing information both about the asset being traded and the
* dutch auction; which is usable in the makerAssetData or takerAssetData fields in a 0x order.
* @param assetData Hex encoded assetData string for the asset being auctioned.
* @param beginTimeSeconds Begin time of the dutch auction.
* @param beginAmount Starting amount being sold in the dutch auction.
* @return The hex encoded assetData string.
*/
encodeDutchAuctionAssetData(assetData: string, beginTimeSeconds: BigNumber, beginAmount: BigNumber): string {
const assetDataBuffer = ethUtil.toBuffer(assetData);
const abiEncodedAuctionData = (ethAbi as any).rawEncode(
['uint256', 'uint256'],
[beginTimeSeconds.toString(), beginAmount.toString()],
);
const abiEncodedAuctionDataBuffer = ethUtil.toBuffer(abiEncodedAuctionData);
const dutchAuctionDataBuffer = Buffer.concat([assetDataBuffer, abiEncodedAuctionDataBuffer]);
const dutchAuctionData = ethUtil.bufferToHex(dutchAuctionDataBuffer);
return dutchAuctionData;
},
}; };

View File

@ -676,3 +676,12 @@ export interface SimpleEvmOutput {
export interface SimpleEvmBytecodeOutput { export interface SimpleEvmBytecodeOutput {
object: string; object: string;
} }
export interface DutchAuctionDetails {
beginTimeSeconds: BigNumber;
endTimeSeconds: BigNumber;
beginAmount: BigNumber;
endAmount: BigNumber;
currentAmount: BigNumber;
currentTimeSeconds: BigNumber;
}