protocol/contracts/zero-ex/test/features/erc721_orders_test.ts
mzhu25 c1177416f5
[Final] ERC721 and ERC1155 Orders (#429)
* add LibERC721Order.sol

* Add ERC721 interface to vendor/

* Add ERC721OrdersFeature interface

* Storage lib for ERC721 orders feature

* Implement basic functionality for ERC721 orders (buy, sell, cancel, etc)

* Add isValidERC721OrderSignature to interface

* implement onERC721Received

* Implement batchBuyERC721s

* left/right orders -> sell/buy orders

* Add missing @return comments

* Implement matching functions

* Use SafeMath where necessary

* add rich errors for ERC721OrdersFeature

* Add comments

* Add presign support for ERC721 orders

* Cancel using just the order nonce

* Add IERC721OrdersFeature to IZeroEx

* Add taker callback

* Assembly optimizations in LibERC721Order

* Add ERC721Orders TS class

* create zero-ex/contracts/test/integration/ and tokens/ directories

* TestMintableERC721Token

* tmp

* address feedback from original PR (#391)

* address feedback from original PR

* Update contracts/zero-ex/contracts/src/features/ERC721OrdersFeature.sol

Co-authored-by: Kim Persson <kimpers@users.noreply.github.com>

* address review feedback and improve order parameter naming

* Add batchCancel function

* Emit order fields in preSign

* Fix tests

Co-authored-by: Lawrence Forman <me@merklejerk.com>
Co-authored-by: Kim Persson <kimpers@users.noreply.github.com>
Co-authored-by: Michael Zhu <mchl.zhu.96@gmail.com>

* Remove revertIfIncomplete from batchMatch

* Sanity check maker address in preSign

* ERC1155OrdersFeature contracts

* Commence refactor, abstract base contract

* ERC721OrdersFeature inherits from NFTOrders

* Refactor ERC1155OrdersFeature to inherit from NFTOrders

* Fix order hashing

* Fix ERC721OrdersFeature tests

* Typos

* Remove maker address from preSigned mapping

* disable dex sampler tests

* Refactor TS tooling

* Address PR feedback

* Rearrange event fields to better align with struct fields

* Update comments

* update AbiEncoder.create params

* Add ERC1155Order to protocol-utils

* Add ERC1155OrdersFeeature tests

* Bump package versions and regenerate contract wrappers

* Add ERC165Feature

* NFT orders: address audit findings (#417)

* CVF-1: use pragma solidity ^0.6 instead of ^0.6.5

* CVF-11: fix inaccurate comment

* CVF-16: Enable taker callbacks for batchBuyERC1155s

* CVF-17: use internal call if revertIfIncomplete is true

* CVF-21: avoid duplicate SLOAD

* CVF-23: merge if statements

* CVF-24: Reorder status checks to be consistent with ERC721OrdersFeature

* CVF-25: Update unclear comment (canonical hash -> EIP-712 hash)

* CVF-31: Document keys of orderState mapping

* CVF-45: DRY up fees/properties hashing

* CVF-47, CVF-50, CVF-57: calculate properties.length once; hash propertyStructHashArray in-place using assembly

* CVF-56: More descriptive names for assembly variables

* CVF-71: Update confusing comment about rounding in _payFees

* CVF-72: Move ETH assertions outside of loop in _payFees

* CVF-74: Move property validation loop to else branch

* CVF-82: Update inaccurate comment

* CVF-86: Enable taker callbacks for batchBuyERC721s

* CVF-87: use internal call if revertIfIncomplete is true

* CVF-89: Perform token mismatch checks before stateful operations

* CVF-90, CVF-91: Defer ERC20 token mismatch check

* CVF-93: Add inline comments for _payFees parameters in matchERC721Orders

* CVF-94: Fix comment (Step 7 -> Step 5)

* CVF-98: Use binary & operator instead of mod

* CVF-99: Update unclear comment (canonical hash -> EIP-712 hash)

* CVF-65, CVF-66, CVF-67: Copy params.ethAvailable into local variable; check that ethSpent does not exceed ethAvailable; remove ethAvailable < erc20FillAmount check

* CVF-52, CVF-55, CVF-59: calculate fees.length once; hash feeStructHashArray in-place using assembly

* CVF-14, CVF-32: OrderState struct; separate storage mapping for 1155 cancellations so orders can be cancelled by nonce

* Update changelogs, IZeroEx artifact/wrapper

Co-authored-by: Lawrence Forman <lawrence@0xproject.com>
Co-authored-by: Lawrence Forman <me@merklejerk.com>
Co-authored-by: Kim Persson <kimpers@users.noreply.github.com>
2022-02-22 10:00:22 -08:00

1732 lines
81 KiB
TypeScript

import {
blockchainTests,
constants,
describe,
expect,
getRandomInteger,
randomAddress,
verifyEventsFromLogs,
} from '@0x/contracts-test-utils';
import { ERC721Order, NFTOrder, RevertErrors, SIGNATURE_ABI, SignatureType } from '@0x/protocol-utils';
import { AbiEncoder, BigNumber, hexUtils, NULL_BYTES, StringRevertError } from '@0x/utils';
import {
IOwnableFeatureContract,
IZeroExContract,
IZeroExERC721OrderFilledEventArgs,
IZeroExEvents,
} from '../../src/wrappers';
import { artifacts } from '../artifacts';
import { abis } from '../utils/abis';
import { fullMigrateAsync } from '../utils/migration';
import { getRandomERC721Order } from '../utils/nft_orders';
import {
ERC721OrdersFeatureContract,
TestFeeRecipientContract,
TestMintableERC20TokenContract,
TestMintableERC721TokenContract,
TestNFTOrderPresignerContract,
TestPropertyValidatorContract,
TestWethContract,
} from '../wrappers';
blockchainTests.resets('ERC721OrdersFeature', env => {
const { NULL_ADDRESS, MAX_UINT256, ZERO_AMOUNT: ZERO } = constants;
const ETH_TOKEN_ADDRESS = '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee';
let owner: string;
let maker: string;
let taker: string;
let otherMaker: string;
let otherTaker: string;
let matcher: string;
let feeRecipient: TestFeeRecipientContract;
let zeroEx: IZeroExContract;
let weth: TestWethContract;
let erc20Token: TestMintableERC20TokenContract;
let erc721Token: TestMintableERC721TokenContract;
async function sendEtherAsync(to: string, amount: BigNumber): Promise<void> {
await env.web3Wrapper.awaitTransactionSuccessAsync(
await env.web3Wrapper.sendTransactionAsync({
...env.txDefaults,
to,
from: owner,
value: amount,
}),
);
}
before(async () => {
// Useful for ETH balance accounting
const txDefaults = { ...env.txDefaults, gasPrice: 0 };
[owner, maker, taker, otherMaker, otherTaker, matcher] = await env.getAccountAddressesAsync();
weth = await TestWethContract.deployFrom0xArtifactAsync(
artifacts.TestWeth,
env.provider,
txDefaults,
artifacts,
);
erc20Token = await TestMintableERC20TokenContract.deployFrom0xArtifactAsync(
artifacts.TestMintableERC20Token,
env.provider,
txDefaults,
artifacts,
);
erc721Token = await TestMintableERC721TokenContract.deployFrom0xArtifactAsync(
artifacts.TestMintableERC721Token,
env.provider,
txDefaults,
artifacts,
);
zeroEx = await fullMigrateAsync(owner, env.provider, txDefaults, {}, { wethAddress: weth.address });
zeroEx = new IZeroExContract(zeroEx.address, env.provider, txDefaults, abis);
const featureImpl = await ERC721OrdersFeatureContract.deployFrom0xArtifactAsync(
artifacts.ERC721OrdersFeature,
env.provider,
txDefaults,
artifacts,
zeroEx.address,
weth.address,
);
await new IOwnableFeatureContract(zeroEx.address, env.provider, txDefaults, abis)
.migrate(featureImpl.address, featureImpl.migrate().getABIEncodedTransactionData(), owner)
.awaitTransactionSuccessAsync();
await Promise.all([
erc20Token.approve(zeroEx.address, MAX_UINT256).awaitTransactionSuccessAsync({
from: maker,
}),
erc20Token.approve(zeroEx.address, MAX_UINT256).awaitTransactionSuccessAsync({
from: otherMaker,
}),
erc20Token.approve(zeroEx.address, MAX_UINT256).awaitTransactionSuccessAsync({
from: taker,
}),
erc20Token.approve(zeroEx.address, MAX_UINT256).awaitTransactionSuccessAsync({
from: otherTaker,
}),
weth.approve(zeroEx.address, MAX_UINT256).awaitTransactionSuccessAsync({
from: maker,
}),
weth.approve(zeroEx.address, MAX_UINT256).awaitTransactionSuccessAsync({
from: otherMaker,
}),
weth.approve(zeroEx.address, MAX_UINT256).awaitTransactionSuccessAsync({
from: taker,
}),
weth.approve(zeroEx.address, MAX_UINT256).awaitTransactionSuccessAsync({
from: otherTaker,
}),
erc721Token.setApprovalForAll(zeroEx.address, true).awaitTransactionSuccessAsync({
from: maker,
}),
erc721Token.setApprovalForAll(zeroEx.address, true).awaitTransactionSuccessAsync({
from: otherMaker,
}),
erc721Token.setApprovalForAll(zeroEx.address, true).awaitTransactionSuccessAsync({
from: taker,
}),
erc721Token.setApprovalForAll(zeroEx.address, true).awaitTransactionSuccessAsync({
from: otherTaker,
}),
]);
feeRecipient = await TestFeeRecipientContract.deployFrom0xArtifactAsync(
artifacts.TestFeeRecipient,
env.provider,
txDefaults,
artifacts,
);
});
async function mintAssetsAsync(
order: ERC721Order,
tokenId: BigNumber = order.erc721TokenId,
_taker: string = taker,
): Promise<void> {
const totalFeeAmount = order.fees.length > 0 ? BigNumber.sum(...order.fees.map(fee => fee.amount)) : ZERO;
if (order.direction === NFTOrder.TradeDirection.SellNFT) {
await erc721Token.mint(order.maker, tokenId).awaitTransactionSuccessAsync();
if (order.erc20Token !== ETH_TOKEN_ADDRESS) {
await erc20Token
.mint(_taker, order.erc20TokenAmount.plus(totalFeeAmount))
.awaitTransactionSuccessAsync();
}
} else {
await erc721Token.mint(_taker, tokenId).awaitTransactionSuccessAsync();
if (order.erc20Token === weth.address) {
await weth.deposit().awaitTransactionSuccessAsync({
from: order.maker,
value: order.erc20TokenAmount.plus(totalFeeAmount),
});
} else {
await erc20Token
.mint(order.maker, order.erc20TokenAmount.plus(totalFeeAmount))
.awaitTransactionSuccessAsync();
}
}
}
async function assertBalancesAsync(
order: ERC721Order,
tokenId: BigNumber = order.erc721TokenId,
_taker: string = taker,
): Promise<void> {
const token = order.erc20Token === weth.address ? weth : erc20Token;
if (order.direction === NFTOrder.TradeDirection.SellNFT) {
const makerBalance = await token.balanceOf(order.maker).callAsync();
expect(makerBalance).to.bignumber.equal(order.erc20TokenAmount);
const erc721Owner = await erc721Token.ownerOf(tokenId).callAsync();
expect(erc721Owner).to.equal(_taker);
} else {
const erc20Balance = await token.balanceOf(_taker).callAsync();
expect(erc20Balance).to.bignumber.equal(order.erc20TokenAmount);
const erc721Owner = await erc721Token.ownerOf(tokenId).callAsync();
expect(erc721Owner).to.equal(order.maker);
}
if (order.fees.length > 0) {
await Promise.all(
order.fees.map(async fee => {
const feeRecipientBalance = await token.balanceOf(fee.recipient).callAsync();
expect(feeRecipientBalance).to.bignumber.equal(fee.amount);
}),
);
}
}
function getTestERC721Order(fields: Partial<ERC721Order> = {}): ERC721Order {
return getRandomERC721Order({
maker,
verifyingContract: zeroEx.address,
chainId: 1337,
erc20Token: erc20Token.address,
erc721Token: erc721Token.address,
taker: NULL_ADDRESS,
...fields,
});
}
function createERC721OrderFilledEvent(
order: ERC721Order,
_taker: string = taker,
erc721TokenId: BigNumber = order.erc721TokenId,
): IZeroExERC721OrderFilledEventArgs {
return {
direction: order.direction,
maker: order.maker,
taker,
nonce: order.nonce,
erc20Token: order.erc20Token,
erc20TokenAmount: order.erc20TokenAmount,
erc721Token: order.erc721Token,
erc721TokenId,
matcher: NULL_ADDRESS,
};
}
describe('getERC721OrderHash()', () => {
it('returns the correct hash for order with no fees or properties', async () => {
const order = getTestERC721Order();
const hash = await zeroEx.getERC721OrderHash(order).callAsync();
expect(hash).to.eq(order.getHash());
});
it('returns the correct hash for order with null property', async () => {
const order = getTestERC721Order({
erc721TokenProperties: [
{
propertyValidator: NULL_ADDRESS,
propertyData: NULL_BYTES,
},
],
});
const hash = await zeroEx.getERC721OrderHash(order).callAsync();
expect(hash).to.eq(order.getHash());
});
it('returns the correct hash for order with 1 fee, 1 property', async () => {
const order = getTestERC721Order({
fees: [
{
recipient: randomAddress(),
amount: getRandomInteger(0, MAX_UINT256),
feeData: hexUtils.random(),
},
],
erc721TokenProperties: [
{
propertyValidator: randomAddress(),
propertyData: hexUtils.random(),
},
],
});
const hash = await zeroEx.getERC721OrderHash(order).callAsync();
expect(hash).to.eq(order.getHash());
});
it('returns the correct hash for order with 2 fees, 2 properties', async () => {
const order = getTestERC721Order({
fees: [
{
recipient: randomAddress(),
amount: getRandomInteger(0, MAX_UINT256),
feeData: hexUtils.random(),
},
{
recipient: randomAddress(),
amount: getRandomInteger(0, MAX_UINT256),
feeData: hexUtils.random(),
},
],
erc721TokenProperties: [
{
propertyValidator: randomAddress(),
propertyData: hexUtils.random(),
},
{
propertyValidator: randomAddress(),
propertyData: hexUtils.random(),
},
],
});
const hash = await zeroEx.getERC721OrderHash(order).callAsync();
expect(hash).to.eq(order.getHash());
});
});
describe('validateERC721OrderSignature', () => {
it('succeeds for a valid EthSign signature', async () => {
const order = getTestERC721Order();
const signature = await order.getSignatureWithProviderAsync(env.provider);
await zeroEx.validateERC721OrderSignature(order, signature).callAsync();
});
it('reverts for an invalid EthSign signature', async () => {
const order = getTestERC721Order();
const signature = await order.getSignatureWithProviderAsync(
env.provider,
SignatureType.EthSign,
otherMaker,
);
const tx = zeroEx.validateERC721OrderSignature(order, signature).callAsync();
expect(tx).to.revertWith(new RevertErrors.NFTOrders.InvalidSignerError(maker, otherMaker));
});
it('succeeds for a valid EIP-712 signature', async () => {
const order = getTestERC721Order();
const signature = await order.getSignatureWithProviderAsync(env.provider, SignatureType.EIP712);
await zeroEx.validateERC721OrderSignature(order, signature).callAsync();
});
it('reverts for an invalid EIP-712 signature', async () => {
const order = getTestERC721Order();
const signature = await order.getSignatureWithProviderAsync(env.provider, SignatureType.EIP712, otherMaker);
const tx = zeroEx.validateERC721OrderSignature(order, signature).callAsync();
expect(tx).to.revertWith(new RevertErrors.NFTOrders.InvalidSignerError(maker, otherMaker));
});
});
describe('cancelERC721Order', () => {
it('can cancel an order', async () => {
const order = getTestERC721Order();
const tx = await zeroEx.cancelERC721Order(order.nonce).awaitTransactionSuccessAsync({
from: maker,
});
verifyEventsFromLogs(tx.logs, [{ maker, nonce: order.nonce }], IZeroExEvents.ERC721OrderCancelled);
const orderStatus = await zeroEx.getERC721OrderStatus(order).callAsync();
expect(orderStatus).to.equal(NFTOrder.OrderStatus.Unfillable);
const bitVector = await zeroEx
.getERC721OrderStatusBitVector(maker, order.nonce.dividedToIntegerBy(256))
.callAsync();
const flag = new BigNumber(2).exponentiatedBy(order.nonce.mod(256));
expect(bitVector).to.bignumber.equal(flag);
});
it('cancelling an order twice silently succeeds', async () => {
const order = getTestERC721Order();
await zeroEx.cancelERC721Order(order.nonce).awaitTransactionSuccessAsync({
from: maker,
});
const tx = await zeroEx.cancelERC721Order(order.nonce).awaitTransactionSuccessAsync({
from: maker,
});
verifyEventsFromLogs(tx.logs, [{ maker, nonce: order.nonce }], IZeroExEvents.ERC721OrderCancelled);
const orderStatus = await zeroEx.getERC721OrderStatus(order).callAsync();
expect(orderStatus).to.equal(NFTOrder.OrderStatus.Unfillable);
const bitVector = await zeroEx
.getERC721OrderStatusBitVector(maker, order.nonce.dividedToIntegerBy(256))
.callAsync();
const flag = new BigNumber(2).exponentiatedBy(order.nonce.mod(256));
expect(bitVector).to.bignumber.equal(flag);
});
});
describe('sellERC721', () => {
it('can fill a ERC721 buy order', async () => {
const order = getTestERC721Order({
direction: NFTOrder.TradeDirection.BuyNFT,
});
const signature = await order.getSignatureWithProviderAsync(env.provider);
await mintAssetsAsync(order);
const tx = await zeroEx
.sellERC721(order, signature, order.erc721TokenId, false, NULL_BYTES)
.awaitTransactionSuccessAsync({
from: taker,
});
await assertBalancesAsync(order);
verifyEventsFromLogs(tx.logs, [createERC721OrderFilledEvent(order)], IZeroExEvents.ERC721OrderFilled);
const bitVector = await zeroEx
.getERC721OrderStatusBitVector(maker, order.nonce.dividedToIntegerBy(256))
.callAsync();
const flag = new BigNumber(2).exponentiatedBy(order.nonce.mod(256));
expect(bitVector).to.bignumber.equal(flag);
});
it('cannot fill the same order twice', async () => {
const order = getTestERC721Order({
direction: NFTOrder.TradeDirection.BuyNFT,
});
const signature = await order.getSignatureWithProviderAsync(env.provider);
await mintAssetsAsync(order);
await zeroEx
.sellERC721(order, signature, order.erc721TokenId, false, NULL_BYTES)
.awaitTransactionSuccessAsync({
from: taker,
});
const tx = zeroEx
.sellERC721(order, signature, order.erc721TokenId, false, NULL_BYTES)
.awaitTransactionSuccessAsync({
from: taker,
});
return expect(tx).to.revertWith(
new RevertErrors.NFTOrders.OrderNotFillableError(maker, order.nonce, NFTOrder.OrderStatus.Unfillable),
);
});
it('can fill two orders from the same maker with different nonces', async () => {
const order1 = getTestERC721Order({
direction: NFTOrder.TradeDirection.BuyNFT,
nonce: ZERO,
});
const signature1 = await order1.getSignatureWithProviderAsync(env.provider);
await mintAssetsAsync(order1);
await zeroEx
.sellERC721(order1, signature1, order1.erc721TokenId, false, NULL_BYTES)
.awaitTransactionSuccessAsync({
from: taker,
});
const order2 = getTestERC721Order({
direction: NFTOrder.TradeDirection.BuyNFT,
nonce: new BigNumber(1),
});
const signature2 = await order2.getSignatureWithProviderAsync(env.provider);
await mintAssetsAsync(order2);
await zeroEx
.sellERC721(order2, signature2, order2.erc721TokenId, false, NULL_BYTES)
.awaitTransactionSuccessAsync({
from: taker,
});
const bitVector = await zeroEx.getERC721OrderStatusBitVector(maker, ZERO).callAsync();
expect(bitVector).to.bignumber.equal(3); // 0...00011
});
it('cannot fill a cancelled order', async () => {
const order = getTestERC721Order({
direction: NFTOrder.TradeDirection.BuyNFT,
});
const signature = await order.getSignatureWithProviderAsync(env.provider);
await mintAssetsAsync(order);
await zeroEx.cancelERC721Order(order.nonce).awaitTransactionSuccessAsync({
from: maker,
});
const tx = zeroEx
.sellERC721(order, signature, order.erc721TokenId, false, NULL_BYTES)
.awaitTransactionSuccessAsync({
from: taker,
});
return expect(tx).to.revertWith(
new RevertErrors.NFTOrders.OrderNotFillableError(maker, order.nonce, NFTOrder.OrderStatus.Unfillable),
);
});
it('cannot fill an invalid order (erc20Token == ETH)', async () => {
const order = getTestERC721Order({
direction: NFTOrder.TradeDirection.BuyNFT,
erc20Token: ETH_TOKEN_ADDRESS,
});
const signature = await order.getSignatureWithProviderAsync(env.provider);
await erc721Token.mint(taker, order.erc721TokenId).awaitTransactionSuccessAsync();
const tx = zeroEx
.sellERC721(order, signature, order.erc721TokenId, false, NULL_BYTES)
.awaitTransactionSuccessAsync({
from: taker,
});
return expect(tx).to.revertWith('NFTOrders::_validateBuyOrder/NATIVE_TOKEN_NOT_ALLOWED');
});
it('cannot fill an expired order', async () => {
const order = getTestERC721Order({
direction: NFTOrder.TradeDirection.BuyNFT,
expiry: new BigNumber(Math.floor(Date.now() / 1000 - 1)),
});
const signature = await order.getSignatureWithProviderAsync(env.provider);
await mintAssetsAsync(order);
const tx = zeroEx
.sellERC721(order, signature, order.erc721TokenId, false, NULL_BYTES)
.awaitTransactionSuccessAsync({
from: taker,
});
return expect(tx).to.revertWith(
new RevertErrors.NFTOrders.OrderNotFillableError(maker, order.nonce, NFTOrder.OrderStatus.Expired),
);
});
it('reverts if a sell order is provided', async () => {
const order = getTestERC721Order({
direction: NFTOrder.TradeDirection.SellNFT,
});
const signature = await order.getSignatureWithProviderAsync(env.provider);
await mintAssetsAsync(order);
const tx = zeroEx
.sellERC721(order, signature, order.erc721TokenId, false, NULL_BYTES)
.awaitTransactionSuccessAsync({
from: taker,
});
return expect(tx).to.revertWith('NFTOrders::_validateBuyOrder/WRONG_TRADE_DIRECTION');
});
it('reverts if the taker is not the taker address specified in the order', async () => {
const order = getTestERC721Order({
direction: NFTOrder.TradeDirection.BuyNFT,
taker,
});
const signature = await order.getSignatureWithProviderAsync(env.provider);
await mintAssetsAsync(order, order.erc721TokenId, otherTaker);
const tx = zeroEx
.sellERC721(order, signature, order.erc721TokenId, false, NULL_BYTES)
.awaitTransactionSuccessAsync({
from: otherTaker,
});
return expect(tx).to.revertWith(new RevertErrors.NFTOrders.OnlyTakerError(otherTaker, taker));
});
it('succeeds if the taker is the taker address specified in the order', async () => {
const order = getTestERC721Order({
direction: NFTOrder.TradeDirection.BuyNFT,
taker,
});
const signature = await order.getSignatureWithProviderAsync(env.provider);
await mintAssetsAsync(order);
await zeroEx
.sellERC721(order, signature, order.erc721TokenId, false, NULL_BYTES)
.awaitTransactionSuccessAsync({
from: taker,
});
await assertBalancesAsync(order);
});
it('reverts if an invalid signature is provided', async () => {
const order = getTestERC721Order({
direction: NFTOrder.TradeDirection.BuyNFT,
});
const signature = await order.getSignatureWithProviderAsync(env.provider, SignatureType.EIP712, otherMaker);
await mintAssetsAsync(order);
const tx = zeroEx
.sellERC721(order, signature, order.erc721TokenId, false, NULL_BYTES)
.awaitTransactionSuccessAsync({
from: taker,
});
return expect(tx).to.revertWith(new RevertErrors.NFTOrders.InvalidSignerError(maker, otherMaker));
});
it('reverts if `unwrapNativeToken` is true and `erc20Token` is not WETH', async () => {
const order = getTestERC721Order({
direction: NFTOrder.TradeDirection.BuyNFT,
});
const signature = await order.getSignatureWithProviderAsync(env.provider);
await mintAssetsAsync(order);
const tx = zeroEx
.sellERC721(order, signature, order.erc721TokenId, true, NULL_BYTES)
.awaitTransactionSuccessAsync({
from: taker,
});
return expect(tx).to.revertWith(
new RevertErrors.NFTOrders.ERC20TokenMismatchError(order.erc20Token, weth.address),
);
});
it('sends ETH to taker if `unwrapNativeToken` is true and `erc20Token` is WETH', async () => {
const order = getTestERC721Order({
direction: NFTOrder.TradeDirection.BuyNFT,
erc20Token: weth.address,
});
const signature = await order.getSignatureWithProviderAsync(env.provider);
await mintAssetsAsync(order);
const takerEthBalanceBefore = await env.web3Wrapper.getBalanceInWeiAsync(taker);
await zeroEx
.sellERC721(order, signature, order.erc721TokenId, true, NULL_BYTES)
.awaitTransactionSuccessAsync({
from: taker,
});
const takerEthBalanceAfter = await env.web3Wrapper.getBalanceInWeiAsync(taker);
expect(takerEthBalanceAfter.minus(takerEthBalanceBefore)).to.bignumber.equal(order.erc20TokenAmount);
const erc721Owner = await erc721Token.ownerOf(order.erc721TokenId).callAsync();
expect(erc721Owner).to.equal(maker);
});
describe('fees', () => {
it('single fee to EOA', async () => {
const order = getTestERC721Order({
direction: NFTOrder.TradeDirection.BuyNFT,
fees: [
{
recipient: otherMaker,
amount: new BigNumber(111),
feeData: constants.NULL_BYTES,
},
],
});
const signature = await order.getSignatureWithProviderAsync(env.provider);
await mintAssetsAsync(order);
await zeroEx
.sellERC721(order, signature, order.erc721TokenId, false, NULL_BYTES)
.awaitTransactionSuccessAsync({
from: taker,
});
await assertBalancesAsync(order);
});
it('single fee, successful callback', async () => {
const order = getTestERC721Order({
direction: NFTOrder.TradeDirection.BuyNFT,
fees: [
{
recipient: feeRecipient.address,
amount: new BigNumber(111),
feeData: hexUtils.random(),
},
],
});
const signature = await order.getSignatureWithProviderAsync(env.provider);
await mintAssetsAsync(order);
await zeroEx
.sellERC721(order, signature, order.erc721TokenId, false, NULL_BYTES)
.awaitTransactionSuccessAsync({
from: taker,
});
await assertBalancesAsync(order);
});
it('single fee, callback reverts', async () => {
const order = getTestERC721Order({
direction: NFTOrder.TradeDirection.BuyNFT,
fees: [
{
recipient: feeRecipient.address,
amount: new BigNumber(333),
feeData: hexUtils.random(),
},
],
});
const signature = await order.getSignatureWithProviderAsync(env.provider);
await mintAssetsAsync(order);
const tx = zeroEx
.sellERC721(order, signature, order.erc721TokenId, false, NULL_BYTES)
.awaitTransactionSuccessAsync({
from: taker,
});
return expect(tx).to.revertWith('TestFeeRecipient::receiveZeroExFeeCallback/REVERT');
});
it('single fee, callback returns invalid value', async () => {
const order = getTestERC721Order({
direction: NFTOrder.TradeDirection.BuyNFT,
fees: [
{
recipient: feeRecipient.address,
amount: new BigNumber(666),
feeData: hexUtils.random(),
},
],
});
const signature = await order.getSignatureWithProviderAsync(env.provider);
await mintAssetsAsync(order);
const tx = zeroEx
.sellERC721(order, signature, order.erc721TokenId, false, NULL_BYTES)
.awaitTransactionSuccessAsync({
from: taker,
});
return expect(tx).to.revertWith('NFTOrders::_payFees/CALLBACK_FAILED');
});
it('multiple fees to EOAs', async () => {
const order = getTestERC721Order({
direction: NFTOrder.TradeDirection.BuyNFT,
fees: [
{
recipient: otherMaker,
amount: new BigNumber(111),
feeData: constants.NULL_BYTES,
},
{
recipient: otherTaker,
amount: new BigNumber(222),
feeData: constants.NULL_BYTES,
},
],
});
const signature = await order.getSignatureWithProviderAsync(env.provider);
await mintAssetsAsync(order);
await zeroEx
.sellERC721(order, signature, order.erc721TokenId, false, NULL_BYTES)
.awaitTransactionSuccessAsync({
from: taker,
});
await assertBalancesAsync(order);
});
});
describe('properties', () => {
let propertyValidator: TestPropertyValidatorContract;
before(async () => {
propertyValidator = await TestPropertyValidatorContract.deployFrom0xArtifactAsync(
artifacts.TestPropertyValidator,
env.provider,
env.txDefaults,
artifacts,
);
});
it('Checks tokenId if no properties are provided', async () => {
const order = getTestERC721Order({
direction: NFTOrder.TradeDirection.BuyNFT,
});
const signature = await order.getSignatureWithProviderAsync(env.provider);
await mintAssetsAsync(order, order.erc721TokenId.plus(1));
const tx = zeroEx
.sellERC721(order, signature, order.erc721TokenId.plus(1), false, NULL_BYTES)
.awaitTransactionSuccessAsync({
from: taker,
});
return expect(tx).to.revertWith(
new RevertErrors.NFTOrders.TokenIdMismatchError(order.erc721TokenId.plus(1), order.erc721TokenId),
);
});
it('Null property', async () => {
const order = getTestERC721Order({
direction: NFTOrder.TradeDirection.BuyNFT,
erc721TokenId: ZERO,
erc721TokenProperties: [
{
propertyValidator: NULL_ADDRESS,
propertyData: NULL_BYTES,
},
],
});
const tokenId = getRandomInteger(0, MAX_UINT256);
const signature = await order.getSignatureWithProviderAsync(env.provider);
await mintAssetsAsync(order, tokenId);
await zeroEx.sellERC721(order, signature, tokenId, false, NULL_BYTES).awaitTransactionSuccessAsync({
from: taker,
});
await assertBalancesAsync(order, tokenId);
});
it('Reverts if property validation fails', async () => {
const order = getTestERC721Order({
direction: NFTOrder.TradeDirection.BuyNFT,
erc721TokenId: ZERO,
erc721TokenProperties: [
{
propertyValidator: propertyValidator.address,
propertyData: NULL_BYTES,
},
],
});
const tokenId = getRandomInteger(0, MAX_UINT256);
const signature = await order.getSignatureWithProviderAsync(env.provider);
await mintAssetsAsync(order, tokenId);
const tx = zeroEx
.sellERC721(order, signature, tokenId, false, NULL_BYTES)
.awaitTransactionSuccessAsync({
from: taker,
});
return expect(tx).to.revertWith(
new RevertErrors.NFTOrders.PropertyValidationFailedError(
propertyValidator.address,
order.erc721Token,
tokenId,
NULL_BYTES,
new StringRevertError('TestPropertyValidator::validateProperty/REVERT').encode(),
),
);
});
it('Successful property validation', async () => {
const order = getTestERC721Order({
direction: NFTOrder.TradeDirection.BuyNFT,
erc721TokenId: ZERO,
erc721TokenProperties: [
{
propertyValidator: propertyValidator.address,
propertyData: hexUtils.random(),
},
],
});
const tokenId = getRandomInteger(0, MAX_UINT256);
const signature = await order.getSignatureWithProviderAsync(env.provider);
await mintAssetsAsync(order, tokenId);
await zeroEx.sellERC721(order, signature, tokenId, false, NULL_BYTES).awaitTransactionSuccessAsync({
from: taker,
});
await assertBalancesAsync(order, tokenId);
});
});
});
describe('onERC721Received', () => {
let dataEncoder: AbiEncoder.DataType;
before(() => {
dataEncoder = AbiEncoder.create(
[
{
name: 'order',
type: 'tuple',
components: ERC721Order.STRUCT_ABI,
},
{
name: 'signature',
type: 'tuple',
components: SIGNATURE_ABI,
},
{ name: 'unwrapNativeToken', type: 'bool' },
],
[
{
name: 'property',
type: 'tuple',
internalType: 'Property',
components: [
{
name: 'propertyValidator',
type: 'address',
},
{ name: 'propertyData', type: 'bytes' },
],
},
{
name: 'fee',
type: 'tuple',
internalType: 'Fee',
components: [
{ name: 'recipient', type: 'address' },
{ name: 'amount', type: 'uint256' },
{ name: 'feeData', type: 'bytes' },
],
},
],
);
});
it('throws if data is not encoded correctly', async () => {
const order = getTestERC721Order({
direction: NFTOrder.TradeDirection.BuyNFT,
});
await mintAssetsAsync(order);
const tx = erc721Token
.safeTransferFrom2(taker, zeroEx.address, order.erc721TokenId, hexUtils.random())
.awaitTransactionSuccessAsync({
from: taker,
});
return expect(tx).to.be.rejected();
});
it('reverts if msg.sender != order.erc721Token', async () => {
const order = getTestERC721Order({
direction: NFTOrder.TradeDirection.BuyNFT,
});
const signature = await order.getSignatureWithProviderAsync(env.provider);
await mintAssetsAsync(order);
const tx = zeroEx
.onERC721Received(
taker,
taker,
order.erc721TokenId,
dataEncoder.encode({
order,
signature,
unwrapNativeToken: false,
}),
)
.awaitTransactionSuccessAsync({
from: taker,
});
return expect(tx).to.revertWith(
new RevertErrors.NFTOrders.ERC721TokenMismatchError(taker, order.erc721Token),
);
});
it('reverts if transferred tokenId does not match order.erc721TokenId', async () => {
const order = getTestERC721Order({
direction: NFTOrder.TradeDirection.BuyNFT,
});
const signature = await order.getSignatureWithProviderAsync(env.provider);
await mintAssetsAsync(order, order.erc721TokenId.plus(1));
const tx = erc721Token
.safeTransferFrom2(
taker,
zeroEx.address,
order.erc721TokenId.plus(1),
dataEncoder.encode({
order,
signature,
unwrapNativeToken: false,
}),
)
.awaitTransactionSuccessAsync({
from: taker,
});
return expect(tx).to.revertWith(
new RevertErrors.NFTOrders.TokenIdMismatchError(order.erc721TokenId.plus(1), order.erc721TokenId),
);
});
it('can sell ERC721 without approval', async () => {
const order = getTestERC721Order({
direction: NFTOrder.TradeDirection.BuyNFT,
});
const signature = await order.getSignatureWithProviderAsync(env.provider);
await mintAssetsAsync(order);
// revoke approval
await erc721Token.setApprovalForAll(zeroEx.address, false).awaitTransactionSuccessAsync({
from: taker,
});
await erc721Token
.safeTransferFrom2(
taker,
zeroEx.address,
order.erc721TokenId,
dataEncoder.encode({
order,
signature,
unwrapNativeToken: false,
}),
)
.awaitTransactionSuccessAsync({
from: taker,
});
await assertBalancesAsync(order);
});
});
describe('buyERC721', () => {
it('can fill a ERC721 sell order', async () => {
const order = getTestERC721Order({
direction: NFTOrder.TradeDirection.SellNFT,
});
const signature = await order.getSignatureWithProviderAsync(env.provider);
await mintAssetsAsync(order);
const tx = await zeroEx.buyERC721(order, signature, NULL_BYTES).awaitTransactionSuccessAsync({
from: taker,
});
await assertBalancesAsync(order);
verifyEventsFromLogs(tx.logs, [createERC721OrderFilledEvent(order)], IZeroExEvents.ERC721OrderFilled);
const bitVector = await zeroEx
.getERC721OrderStatusBitVector(maker, order.nonce.dividedToIntegerBy(256))
.callAsync();
const flag = new BigNumber(2).exponentiatedBy(order.nonce.mod(256));
expect(bitVector).to.bignumber.equal(flag);
});
it('cannot fill the same order twice', async () => {
const order = getTestERC721Order({
direction: NFTOrder.TradeDirection.SellNFT,
});
const signature = await order.getSignatureWithProviderAsync(env.provider);
await mintAssetsAsync(order);
await zeroEx.buyERC721(order, signature, NULL_BYTES).awaitTransactionSuccessAsync({
from: taker,
});
const tx = zeroEx.buyERC721(order, signature, NULL_BYTES).awaitTransactionSuccessAsync({
from: taker,
});
return expect(tx).to.revertWith(
new RevertErrors.NFTOrders.OrderNotFillableError(maker, order.nonce, NFTOrder.OrderStatus.Unfillable),
);
});
it('cannot fill a cancelled order', async () => {
const order = getTestERC721Order({
direction: NFTOrder.TradeDirection.SellNFT,
});
const signature = await order.getSignatureWithProviderAsync(env.provider);
await mintAssetsAsync(order);
await zeroEx.cancelERC721Order(order.nonce).awaitTransactionSuccessAsync({
from: maker,
});
const tx = zeroEx.buyERC721(order, signature, NULL_BYTES).awaitTransactionSuccessAsync({
from: taker,
});
return expect(tx).to.revertWith(
new RevertErrors.NFTOrders.OrderNotFillableError(maker, order.nonce, NFTOrder.OrderStatus.Unfillable),
);
});
it('cannot fill an expired order', async () => {
const order = getTestERC721Order({
direction: NFTOrder.TradeDirection.SellNFT,
expiry: new BigNumber(Math.floor(Date.now() / 1000 - 1)),
});
const signature = await order.getSignatureWithProviderAsync(env.provider);
await mintAssetsAsync(order);
const tx = zeroEx.buyERC721(order, signature, NULL_BYTES).awaitTransactionSuccessAsync({
from: taker,
});
return expect(tx).to.revertWith(
new RevertErrors.NFTOrders.OrderNotFillableError(maker, order.nonce, NFTOrder.OrderStatus.Expired),
);
});
it('reverts if a buy order is provided', async () => {
const order = getTestERC721Order({
direction: NFTOrder.TradeDirection.BuyNFT,
});
const signature = await order.getSignatureWithProviderAsync(env.provider);
await mintAssetsAsync(order);
const tx = zeroEx.buyERC721(order, signature, NULL_BYTES).awaitTransactionSuccessAsync({
from: taker,
});
return expect(tx).to.revertWith('NFTOrders::_validateSellOrder/WRONG_TRADE_DIRECTION');
});
it('reverts if the taker is not the taker address specified in the order', async () => {
const order = getTestERC721Order({
direction: NFTOrder.TradeDirection.SellNFT,
taker,
});
const signature = await order.getSignatureWithProviderAsync(env.provider);
await mintAssetsAsync(order, order.erc721TokenId, otherTaker);
const tx = zeroEx.buyERC721(order, signature, NULL_BYTES).awaitTransactionSuccessAsync({
from: otherTaker,
});
return expect(tx).to.revertWith(new RevertErrors.NFTOrders.OnlyTakerError(otherTaker, taker));
});
it('succeeds if the taker is the taker address specified in the order', async () => {
const order = getTestERC721Order({
direction: NFTOrder.TradeDirection.SellNFT,
taker,
});
const signature = await order.getSignatureWithProviderAsync(env.provider);
await mintAssetsAsync(order);
await zeroEx.buyERC721(order, signature, NULL_BYTES).awaitTransactionSuccessAsync({
from: taker,
});
await assertBalancesAsync(order);
});
it('reverts if an invalid signature is provided', async () => {
const order = getTestERC721Order({
direction: NFTOrder.TradeDirection.SellNFT,
});
const signature = await order.getSignatureWithProviderAsync(env.provider, SignatureType.EIP712, otherMaker);
await mintAssetsAsync(order);
const tx = zeroEx.buyERC721(order, signature, NULL_BYTES).awaitTransactionSuccessAsync({
from: taker,
});
return expect(tx).to.revertWith(new RevertErrors.NFTOrders.InvalidSignerError(maker, otherMaker));
});
describe('ETH', () => {
it('can fill an order with ETH (and refunds excess ETH)', async () => {
const order = getTestERC721Order({
erc20Token: ETH_TOKEN_ADDRESS,
direction: NFTOrder.TradeDirection.SellNFT,
});
const signature = await order.getSignatureWithProviderAsync(env.provider);
await mintAssetsAsync(order);
const takerEthBalanceBefore = await env.web3Wrapper.getBalanceInWeiAsync(taker);
const makerEthBalanceBefore = await env.web3Wrapper.getBalanceInWeiAsync(maker);
const tx = await zeroEx.buyERC721(order, signature, NULL_BYTES).awaitTransactionSuccessAsync({
from: taker,
value: order.erc20TokenAmount.plus(1),
});
const takerEthBalanceAfter = await env.web3Wrapper.getBalanceInWeiAsync(taker);
const makerEthBalanceAfter = await env.web3Wrapper.getBalanceInWeiAsync(maker);
expect(takerEthBalanceBefore.minus(takerEthBalanceAfter)).to.bignumber.equal(order.erc20TokenAmount);
expect(makerEthBalanceAfter.minus(makerEthBalanceBefore)).to.bignumber.equal(order.erc20TokenAmount);
verifyEventsFromLogs(
tx.logs,
[
{
_from: maker,
_to: taker,
_tokenId: order.erc721TokenId,
},
],
'Transfer',
);
});
it('can fill a WETH order with ETH', async () => {
const order = getTestERC721Order({
erc20Token: weth.address,
direction: NFTOrder.TradeDirection.SellNFT,
});
const signature = await order.getSignatureWithProviderAsync(env.provider);
await erc721Token.mint(maker, order.erc721TokenId).awaitTransactionSuccessAsync();
const takerEthBalanceBefore = await env.web3Wrapper.getBalanceInWeiAsync(taker);
await zeroEx.buyERC721(order, signature, NULL_BYTES).awaitTransactionSuccessAsync({
from: taker,
value: order.erc20TokenAmount,
});
const takerEthBalanceAfter = await env.web3Wrapper.getBalanceInWeiAsync(taker);
expect(takerEthBalanceBefore.minus(takerEthBalanceAfter)).to.bignumber.equal(order.erc20TokenAmount);
await assertBalancesAsync(order);
});
it('uses WETH if not enough ETH to fill WETH order', async () => {
const order = getTestERC721Order({
erc20Token: weth.address,
direction: NFTOrder.TradeDirection.SellNFT,
});
const signature = await order.getSignatureWithProviderAsync(env.provider);
await weth.deposit().awaitTransactionSuccessAsync({
from: taker,
value: order.erc20TokenAmount,
});
await erc721Token.mint(maker, order.erc721TokenId).awaitTransactionSuccessAsync();
const takerEthBalanceBefore = await env.web3Wrapper.getBalanceInWeiAsync(taker);
await zeroEx.buyERC721(order, signature, NULL_BYTES).awaitTransactionSuccessAsync({
from: taker,
value: order.erc20TokenAmount.minus(1),
});
const takerEthBalanceAfter = await env.web3Wrapper.getBalanceInWeiAsync(taker);
expect(takerEthBalanceAfter).to.bignumber.equal(takerEthBalanceBefore);
await assertBalancesAsync(order);
});
});
describe('fees', () => {
it('single fee to EOA', async () => {
const order = getTestERC721Order({
direction: NFTOrder.TradeDirection.SellNFT,
fees: [
{
recipient: otherMaker,
amount: new BigNumber(111),
feeData: constants.NULL_BYTES,
},
],
});
const signature = await order.getSignatureWithProviderAsync(env.provider);
await mintAssetsAsync(order);
await zeroEx.buyERC721(order, signature, NULL_BYTES).awaitTransactionSuccessAsync({
from: taker,
});
await assertBalancesAsync(order);
});
it('pays fees in ETH if erc20Token == ETH', async () => {
const order = getTestERC721Order({
direction: NFTOrder.TradeDirection.SellNFT,
erc20Token: ETH_TOKEN_ADDRESS,
fees: [
{
recipient: otherMaker,
amount: new BigNumber(111),
feeData: constants.NULL_BYTES,
},
],
});
const signature = await order.getSignatureWithProviderAsync(env.provider);
await mintAssetsAsync(order);
const takerEthBalanceBefore = await env.web3Wrapper.getBalanceInWeiAsync(taker);
const makerEthBalanceBefore = await env.web3Wrapper.getBalanceInWeiAsync(maker);
const feeRecipientEthBalanceBefore = await env.web3Wrapper.getBalanceInWeiAsync(otherMaker);
await zeroEx.buyERC721(order, signature, NULL_BYTES).awaitTransactionSuccessAsync({
from: taker,
value: order.erc20TokenAmount.plus(order.fees[0].amount).plus(1),
});
const takerEthBalanceAfter = await env.web3Wrapper.getBalanceInWeiAsync(taker);
const makerEthBalanceAfter = await env.web3Wrapper.getBalanceInWeiAsync(maker);
const feeRecipientEthBalanceAfter = await env.web3Wrapper.getBalanceInWeiAsync(otherMaker);
expect(takerEthBalanceBefore.minus(takerEthBalanceAfter)).to.bignumber.equal(
order.erc20TokenAmount.plus(order.fees[0].amount),
);
expect(makerEthBalanceAfter.minus(makerEthBalanceBefore)).to.bignumber.equal(order.erc20TokenAmount);
expect(feeRecipientEthBalanceAfter.minus(feeRecipientEthBalanceBefore)).to.bignumber.equal(
order.fees[0].amount,
);
const erc721Owner = await erc721Token.ownerOf(order.erc721TokenId).callAsync();
expect(erc721Owner).to.equal(taker);
});
it('pays fees in ETH if erc20Token == WETH but taker uses ETH', async () => {
const order = getTestERC721Order({
direction: NFTOrder.TradeDirection.SellNFT,
erc20Token: weth.address,
fees: [
{
recipient: otherMaker,
amount: new BigNumber(111),
feeData: constants.NULL_BYTES,
},
],
});
const signature = await order.getSignatureWithProviderAsync(env.provider);
await mintAssetsAsync(order);
const takerEthBalanceBefore = await env.web3Wrapper.getBalanceInWeiAsync(taker);
const feeRecipientEthBalanceBefore = await env.web3Wrapper.getBalanceInWeiAsync(otherMaker);
const tx = await zeroEx.buyERC721(order, signature, NULL_BYTES).awaitTransactionSuccessAsync({
from: taker,
value: order.erc20TokenAmount.plus(order.fees[0].amount).plus(1),
});
const takerEthBalanceAfter = await env.web3Wrapper.getBalanceInWeiAsync(taker);
const feeRecipientEthBalanceAfter = await env.web3Wrapper.getBalanceInWeiAsync(otherMaker);
expect(takerEthBalanceBefore.minus(takerEthBalanceAfter)).to.bignumber.equal(
order.erc20TokenAmount.plus(order.fees[0].amount),
);
expect(feeRecipientEthBalanceAfter.minus(feeRecipientEthBalanceBefore)).to.bignumber.equal(
order.fees[0].amount,
);
verifyEventsFromLogs(
tx.logs,
[
{
_from: maker,
_to: taker,
_tokenId: order.erc721TokenId,
},
{
token: weth.address,
from: zeroEx.address,
to: maker,
value: order.erc20TokenAmount,
},
],
'Transfer',
);
});
it('pays fees in WETH if taker uses WETH', async () => {
const order = getTestERC721Order({
direction: NFTOrder.TradeDirection.SellNFT,
erc20Token: weth.address,
fees: [
{
recipient: otherMaker,
amount: new BigNumber(111),
feeData: constants.NULL_BYTES,
},
],
});
const signature = await order.getSignatureWithProviderAsync(env.provider);
await erc721Token.mint(maker, order.erc721TokenId).awaitTransactionSuccessAsync();
await weth.deposit().awaitTransactionSuccessAsync({
from: taker,
value: order.erc20TokenAmount.plus(order.fees[0].amount),
});
await zeroEx.buyERC721(order, signature, NULL_BYTES).awaitTransactionSuccessAsync({
from: taker,
});
await assertBalancesAsync(order);
});
it('reverts if overspent ETH', async () => {
const order = getTestERC721Order({
direction: NFTOrder.TradeDirection.SellNFT,
erc20Token: ETH_TOKEN_ADDRESS,
fees: [
{
recipient: otherMaker,
amount: new BigNumber(111),
feeData: constants.NULL_BYTES,
},
],
});
await sendEtherAsync(zeroEx.address, order.fees[0].amount);
const signature = await order.getSignatureWithProviderAsync(env.provider);
await mintAssetsAsync(order);
const tx = zeroEx.buyERC721(order, signature, NULL_BYTES).awaitTransactionSuccessAsync({
from: taker,
value: order.erc20TokenAmount,
});
return expect(tx).to.revertWith(
new RevertErrors.NFTOrders.OverspentEthError(
order.erc20TokenAmount.plus(order.fees[0].amount),
order.erc20TokenAmount,
),
);
});
});
});
describe('batchBuyERC721s', () => {
it('reverts if arrays are different lengths', async () => {
const order = getTestERC721Order({
direction: NFTOrder.TradeDirection.SellNFT,
});
const signature = await order.getSignatureWithProviderAsync(env.provider);
await mintAssetsAsync(order);
const tx = zeroEx.batchBuyERC721s([order], [signature, signature], [], false).awaitTransactionSuccessAsync({
from: taker,
});
return expect(tx).to.revertWith('ERC721OrdersFeature::batchBuyERC721s/ARRAY_LENGTH_MISMATCH');
});
it('successfully fills multiple orders', async () => {
const order1 = getTestERC721Order({
direction: NFTOrder.TradeDirection.SellNFT,
});
const signature1 = await order1.getSignatureWithProviderAsync(env.provider);
await mintAssetsAsync(order1);
const order2 = getTestERC721Order({
direction: NFTOrder.TradeDirection.SellNFT,
erc20Token: weth.address,
});
const signature2 = await order2.getSignatureWithProviderAsync(env.provider);
await erc721Token.mint(maker, order2.erc721TokenId).awaitTransactionSuccessAsync();
await weth.deposit().sendTransactionAsync({
from: taker,
value: order2.erc20TokenAmount,
});
await zeroEx
.batchBuyERC721s([order1, order2], [signature1, signature2], [NULL_BYTES, NULL_BYTES], false)
.awaitTransactionSuccessAsync({
from: taker,
});
await assertBalancesAsync(order1);
await assertBalancesAsync(order2);
});
it('catches revert if one order fails', async () => {
const order1 = getTestERC721Order({
direction: NFTOrder.TradeDirection.SellNFT,
});
const signature1 = await order1.getSignatureWithProviderAsync(env.provider);
await mintAssetsAsync(order1);
const order2 = getTestERC721Order({
direction: NFTOrder.TradeDirection.SellNFT,
erc20Token: weth.address,
});
// invalid signature
const signature2 = await order2.getSignatureWithProviderAsync(
env.provider,
SignatureType.EIP712,
otherMaker,
);
await erc721Token.mint(maker, order2.erc721TokenId).awaitTransactionSuccessAsync();
await weth.deposit().sendTransactionAsync({
from: taker,
value: order2.erc20TokenAmount,
});
const tx = zeroEx.batchBuyERC721s(
[order1, order2],
[signature1, signature2],
[NULL_BYTES, NULL_BYTES],
false,
);
const successes = await tx.callAsync({
from: taker,
});
expect(successes).to.deep.equal([true, false]);
await tx.awaitTransactionSuccessAsync({
from: taker,
});
await assertBalancesAsync(order1);
const erc721Owner = await erc721Token.ownerOf(order2.erc721TokenId).callAsync();
expect(erc721Owner).to.equal(maker);
const takerWethBalance = await weth.balanceOf(taker).callAsync();
expect(takerWethBalance).to.bignumber.equal(order2.erc20TokenAmount);
});
it('bubbles up revert if one order fails and `revertIfIncomplete == true`', async () => {
const order1 = getTestERC721Order({
direction: NFTOrder.TradeDirection.SellNFT,
});
const signature1 = await order1.getSignatureWithProviderAsync(env.provider);
await mintAssetsAsync(order1);
const order2 = getTestERC721Order({
direction: NFTOrder.TradeDirection.SellNFT,
erc20Token: weth.address,
});
// invalid signature
const signature2 = await order2.getSignatureWithProviderAsync(
env.provider,
SignatureType.EIP712,
otherMaker,
);
await erc721Token.mint(maker, order2.erc721TokenId).awaitTransactionSuccessAsync();
await weth.deposit().sendTransactionAsync({
from: taker,
value: order2.erc20TokenAmount,
});
const tx = zeroEx
.batchBuyERC721s([order1, order2], [signature1, signature2], [NULL_BYTES, NULL_BYTES], true)
.awaitTransactionSuccessAsync({
from: taker,
});
return expect(tx).to.revertWith(new RevertErrors.NFTOrders.InvalidSignerError(order2.maker, otherMaker));
});
it('can fill multiple orders with ETH, refund excess ETH', async () => {
const order1 = getTestERC721Order({
direction: NFTOrder.TradeDirection.SellNFT,
erc20Token: ETH_TOKEN_ADDRESS,
});
const signature1 = await order1.getSignatureWithProviderAsync(env.provider);
await mintAssetsAsync(order1);
const order2 = getTestERC721Order({
direction: NFTOrder.TradeDirection.SellNFT,
erc20Token: weth.address,
});
const signature2 = await order2.getSignatureWithProviderAsync(env.provider);
await erc721Token.mint(maker, order2.erc721TokenId).awaitTransactionSuccessAsync();
const takerEthBalanceBefore = await env.web3Wrapper.getBalanceInWeiAsync(taker);
await zeroEx
.batchBuyERC721s([order1, order2], [signature1, signature2], [NULL_BYTES, NULL_BYTES], true)
.awaitTransactionSuccessAsync({
from: taker,
value: order1.erc20TokenAmount.plus(order2.erc20TokenAmount).plus(1),
});
const takerEthBalanceAfter = await env.web3Wrapper.getBalanceInWeiAsync(taker);
expect(takerEthBalanceBefore.minus(takerEthBalanceAfter)).to.bignumber.equal(
order1.erc20TokenAmount.plus(order2.erc20TokenAmount),
);
const erc721Owner1 = await erc721Token.ownerOf(order1.erc721TokenId).callAsync();
expect(erc721Owner1).to.bignumber.equal(taker);
const erc721Owner2 = await erc721Token.ownerOf(order2.erc721TokenId).callAsync();
expect(erc721Owner2).to.bignumber.equal(taker);
});
});
describe('preSignERC721Order', () => {
const PRESIGN_SIGNATURE = {
signatureType: SignatureType.PreSigned,
v: 0,
r: constants.NULL_BYTES32,
s: constants.NULL_BYTES32,
};
let contractMaker: TestNFTOrderPresignerContract;
before(async () => {
contractMaker = await TestNFTOrderPresignerContract.deployFrom0xArtifactAsync(
artifacts.TestNFTOrderPresigner,
env.provider,
env.txDefaults,
artifacts,
zeroEx.address,
);
await contractMaker.approveERC20(erc20Token.address).awaitTransactionSuccessAsync();
await contractMaker.approveERC721(erc721Token.address).awaitTransactionSuccessAsync();
});
it('can fill order that has been presigned by the maker', async () => {
const order = getTestERC721Order({
maker: contractMaker.address,
direction: NFTOrder.TradeDirection.BuyNFT,
});
await mintAssetsAsync(order);
await contractMaker.preSignERC721Order(order).awaitTransactionSuccessAsync();
await zeroEx
.sellERC721(order, PRESIGN_SIGNATURE, order.erc721TokenId, false, NULL_BYTES)
.awaitTransactionSuccessAsync({
from: taker,
});
await assertBalancesAsync(order);
});
it('cannot fill order that has not been presigned by the maker', async () => {
const order = getTestERC721Order({
maker: contractMaker.address,
direction: NFTOrder.TradeDirection.BuyNFT,
});
await mintAssetsAsync(order);
const tx = zeroEx
.sellERC721(order, PRESIGN_SIGNATURE, order.erc721TokenId, false, NULL_BYTES)
.awaitTransactionSuccessAsync({
from: taker,
});
return expect(tx).to.revertWith(
new RevertErrors.NFTOrders.InvalidSignerError(contractMaker.address, NULL_ADDRESS),
);
});
it('cannot fill order that was presigned then cancelled', async () => {
const order = getTestERC721Order({
maker: contractMaker.address,
direction: NFTOrder.TradeDirection.BuyNFT,
});
await mintAssetsAsync(order);
await contractMaker.preSignERC721Order(order).awaitTransactionSuccessAsync();
await contractMaker.cancelERC721Order(order.nonce).awaitTransactionSuccessAsync();
const tx = zeroEx
.sellERC721(order, PRESIGN_SIGNATURE, order.erc721TokenId, false, NULL_BYTES)
.awaitTransactionSuccessAsync({
from: taker,
});
return expect(tx).to.revertWith(
new RevertErrors.NFTOrders.OrderNotFillableError(
contractMaker.address,
order.nonce,
NFTOrder.OrderStatus.Unfillable,
),
);
});
});
describe('matchERC721Orders', () => {
it('cannot match two sell orders', async () => {
const order1 = getTestERC721Order({
direction: NFTOrder.TradeDirection.SellNFT,
});
const signature1 = await order1.getSignatureWithProviderAsync(env.provider);
const order2 = getTestERC721Order({
direction: NFTOrder.TradeDirection.SellNFT,
});
const signature2 = await order2.getSignatureWithProviderAsync(env.provider);
const tx = zeroEx.matchERC721Orders(order1, order2, signature1, signature2).awaitTransactionSuccessAsync({
from: matcher,
});
return expect(tx).to.revertWith('NFTOrders::_validateBuyOrder/WRONG_TRADE_DIRECTION');
});
it('cannot match two buy orders', async () => {
const order1 = getTestERC721Order({
direction: NFTOrder.TradeDirection.BuyNFT,
});
const signature1 = await order1.getSignatureWithProviderAsync(env.provider);
const order2 = getTestERC721Order({
direction: NFTOrder.TradeDirection.BuyNFT,
});
const signature2 = await order2.getSignatureWithProviderAsync(env.provider);
const tx = zeroEx.matchERC721Orders(order1, order2, signature1, signature2).awaitTransactionSuccessAsync({
from: matcher,
});
return expect(tx).to.revertWith('NFTOrders::_validateSellOrder/WRONG_TRADE_DIRECTION');
});
it('erc721TokenId must match', async () => {
const sellOrder = getTestERC721Order({
direction: NFTOrder.TradeDirection.SellNFT,
});
const sellSignature = await sellOrder.getSignatureWithProviderAsync(env.provider);
const buyOrder = getTestERC721Order({
direction: NFTOrder.TradeDirection.BuyNFT,
});
const buySignature = await buyOrder.getSignatureWithProviderAsync(env.provider);
const tx = zeroEx
.matchERC721Orders(sellOrder, buyOrder, sellSignature, buySignature)
.awaitTransactionSuccessAsync({
from: matcher,
});
return expect(tx).to.revertWith(
new RevertErrors.NFTOrders.TokenIdMismatchError(sellOrder.erc721TokenId, buyOrder.erc721TokenId),
);
});
it('erc721Token must match', async () => {
const sellOrder = getTestERC721Order({
direction: NFTOrder.TradeDirection.SellNFT,
});
const sellSignature = await sellOrder.getSignatureWithProviderAsync(env.provider);
const buyOrder = getTestERC721Order({
direction: NFTOrder.TradeDirection.BuyNFT,
erc721Token: erc20Token.address,
erc721TokenId: sellOrder.erc721TokenId,
});
const buySignature = await buyOrder.getSignatureWithProviderAsync(env.provider);
const tx = zeroEx
.matchERC721Orders(sellOrder, buyOrder, sellSignature, buySignature)
.awaitTransactionSuccessAsync({
from: matcher,
});
return expect(tx).to.revertWith(
new RevertErrors.NFTOrders.ERC721TokenMismatchError(sellOrder.erc721Token, buyOrder.erc721Token),
);
});
it('erc20Token must match', async () => {
const sellOrder = getTestERC721Order({
direction: NFTOrder.TradeDirection.SellNFT,
});
const sellSignature = await sellOrder.getSignatureWithProviderAsync(env.provider);
const buyOrder = getTestERC721Order({
direction: NFTOrder.TradeDirection.BuyNFT,
erc20Token: weth.address,
erc20TokenAmount: sellOrder.erc20TokenAmount,
erc721TokenId: sellOrder.erc721TokenId,
});
const buySignature = await buyOrder.getSignatureWithProviderAsync(env.provider);
await mintAssetsAsync(buyOrder, sellOrder.erc721TokenId, sellOrder.maker);
const tx = zeroEx
.matchERC721Orders(sellOrder, buyOrder, sellSignature, buySignature)
.awaitTransactionSuccessAsync({
from: matcher,
});
return expect(tx).to.revertWith(
new RevertErrors.NFTOrders.ERC20TokenMismatchError(sellOrder.erc20Token, buyOrder.erc20Token),
);
});
it('reverts if spread is negative', async () => {
const sellOrder = getTestERC721Order({
direction: NFTOrder.TradeDirection.SellNFT,
});
const sellSignature = await sellOrder.getSignatureWithProviderAsync(env.provider);
const buyOrder = getTestERC721Order({
direction: NFTOrder.TradeDirection.BuyNFT,
erc721TokenId: sellOrder.erc721TokenId,
erc20TokenAmount: sellOrder.erc20TokenAmount.minus(1),
});
const buySignature = await buyOrder.getSignatureWithProviderAsync(env.provider);
const tx = zeroEx
.matchERC721Orders(sellOrder, buyOrder, sellSignature, buySignature)
.awaitTransactionSuccessAsync({
from: matcher,
});
return expect(tx).to.revertWith(
new RevertErrors.NFTOrders.NegativeSpreadError(sellOrder.erc20TokenAmount, buyOrder.erc20TokenAmount),
);
});
it('matches two orders and sends profit to matcher', async () => {
const sellOrder = getTestERC721Order({
direction: NFTOrder.TradeDirection.SellNFT,
});
const sellSignature = await sellOrder.getSignatureWithProviderAsync(env.provider);
const spread = getRandomInteger(1, '1e18');
const buyOrder = getTestERC721Order({
maker: otherMaker,
direction: NFTOrder.TradeDirection.BuyNFT,
erc721TokenId: sellOrder.erc721TokenId,
erc20TokenAmount: sellOrder.erc20TokenAmount.plus(spread),
});
const buySignature = await buyOrder.getSignatureWithProviderAsync(env.provider);
await mintAssetsAsync(buyOrder, sellOrder.erc721TokenId, sellOrder.maker);
await zeroEx
.matchERC721Orders(sellOrder, buyOrder, sellSignature, buySignature)
.awaitTransactionSuccessAsync({
from: matcher,
});
await assertBalancesAsync(sellOrder, sellOrder.erc721TokenId, otherMaker);
const matcherBalance = await erc20Token.balanceOf(matcher).callAsync();
expect(matcherBalance).to.bignumber.equal(spread);
});
it('matches two ETH/WETH orders and sends profit to matcher', async () => {
const sellOrder = getTestERC721Order({
direction: NFTOrder.TradeDirection.SellNFT,
erc20Token: ETH_TOKEN_ADDRESS,
});
const sellSignature = await sellOrder.getSignatureWithProviderAsync(env.provider);
const spread = getRandomInteger(1, '1e18');
const buyOrder = getTestERC721Order({
maker: otherMaker,
direction: NFTOrder.TradeDirection.BuyNFT,
erc20Token: weth.address,
erc721TokenId: sellOrder.erc721TokenId,
erc20TokenAmount: sellOrder.erc20TokenAmount.plus(spread),
});
const buySignature = await buyOrder.getSignatureWithProviderAsync(env.provider);
await mintAssetsAsync(buyOrder, sellOrder.erc721TokenId, sellOrder.maker);
const sellerEthBalanceBefore = await env.web3Wrapper.getBalanceInWeiAsync(sellOrder.maker);
const matcherEthBalanceBefore = await env.web3Wrapper.getBalanceInWeiAsync(matcher);
await zeroEx
.matchERC721Orders(sellOrder, buyOrder, sellSignature, buySignature)
.awaitTransactionSuccessAsync({
from: matcher,
});
const erc721Owner = await erc721Token.ownerOf(sellOrder.erc721TokenId).callAsync();
expect(erc721Owner).to.equal(buyOrder.maker);
const sellerEthBalanceAfter = await env.web3Wrapper.getBalanceInWeiAsync(sellOrder.maker);
const matcherEthBalanceAfter = await env.web3Wrapper.getBalanceInWeiAsync(matcher);
expect(sellerEthBalanceAfter.minus(sellerEthBalanceBefore)).to.bignumber.equal(sellOrder.erc20TokenAmount);
expect(matcherEthBalanceAfter.minus(matcherEthBalanceBefore)).to.bignumber.equal(spread);
});
it('matches two orders (with fees) and sends profit to matcher', async () => {
const spread = getRandomInteger(1, '1e18');
const sellOrder = getTestERC721Order({
direction: NFTOrder.TradeDirection.SellNFT,
fees: [
{
recipient: otherTaker,
amount: getRandomInteger(1, spread),
feeData: NULL_BYTES,
},
],
});
const sellSignature = await sellOrder.getSignatureWithProviderAsync(env.provider);
const buyOrder = getTestERC721Order({
maker: otherMaker,
direction: NFTOrder.TradeDirection.BuyNFT,
erc721TokenId: sellOrder.erc721TokenId,
erc20TokenAmount: sellOrder.erc20TokenAmount.plus(spread),
});
const buySignature = await buyOrder.getSignatureWithProviderAsync(env.provider);
await mintAssetsAsync(buyOrder, sellOrder.erc721TokenId, sellOrder.maker);
await erc20Token.mint(buyOrder.maker, sellOrder.fees[0].amount).awaitTransactionSuccessAsync();
await zeroEx
.matchERC721Orders(sellOrder, buyOrder, sellSignature, buySignature)
.awaitTransactionSuccessAsync({
from: matcher,
});
await assertBalancesAsync(sellOrder, sellOrder.erc721TokenId, otherMaker);
const matcherBalance = await erc20Token.balanceOf(matcher).callAsync();
expect(matcherBalance).to.bignumber.equal(spread.minus(sellOrder.fees[0].amount));
});
it('matches two ETH/WETH (with fees) orders and sends profit to matcher', async () => {
const spread = getRandomInteger(1, '1e18');
const sellOrder = getTestERC721Order({
direction: NFTOrder.TradeDirection.SellNFT,
erc20Token: ETH_TOKEN_ADDRESS,
fees: [
{
recipient: otherTaker,
amount: getRandomInteger(1, spread),
feeData: NULL_BYTES,
},
],
});
const sellSignature = await sellOrder.getSignatureWithProviderAsync(env.provider);
const buyOrder = getTestERC721Order({
maker: otherMaker,
direction: NFTOrder.TradeDirection.BuyNFT,
erc20Token: weth.address,
erc721TokenId: sellOrder.erc721TokenId,
erc20TokenAmount: sellOrder.erc20TokenAmount.plus(spread),
});
const buySignature = await buyOrder.getSignatureWithProviderAsync(env.provider);
await mintAssetsAsync(buyOrder, sellOrder.erc721TokenId, sellOrder.maker);
await weth
.deposit()
.awaitTransactionSuccessAsync({ from: buyOrder.maker, value: sellOrder.fees[0].amount });
const sellerEthBalanceBefore = await env.web3Wrapper.getBalanceInWeiAsync(sellOrder.maker);
const matcherEthBalanceBefore = await env.web3Wrapper.getBalanceInWeiAsync(matcher);
await zeroEx
.matchERC721Orders(sellOrder, buyOrder, sellSignature, buySignature)
.awaitTransactionSuccessAsync({
from: matcher,
});
const erc721Owner = await erc721Token.ownerOf(sellOrder.erc721TokenId).callAsync();
expect(erc721Owner).to.equal(buyOrder.maker);
const sellerEthBalanceAfter = await env.web3Wrapper.getBalanceInWeiAsync(sellOrder.maker);
const matcherEthBalanceAfter = await env.web3Wrapper.getBalanceInWeiAsync(matcher);
expect(sellerEthBalanceAfter.minus(sellerEthBalanceBefore)).to.bignumber.equal(sellOrder.erc20TokenAmount);
expect(matcherEthBalanceAfter.minus(matcherEthBalanceBefore)).to.bignumber.equal(
spread.minus(sellOrder.fees[0].amount),
);
});
it('reverts if sell order fees exceed spread', async () => {
const spread = getRandomInteger(1, '1e18');
const sellOrder = getTestERC721Order({
direction: NFTOrder.TradeDirection.SellNFT,
fees: [
{
recipient: otherTaker,
amount: spread.plus(1),
feeData: NULL_BYTES,
},
],
});
const sellSignature = await sellOrder.getSignatureWithProviderAsync(env.provider);
const buyOrder = getTestERC721Order({
maker: otherMaker,
direction: NFTOrder.TradeDirection.BuyNFT,
erc721TokenId: sellOrder.erc721TokenId,
erc20TokenAmount: sellOrder.erc20TokenAmount.plus(spread),
});
const buySignature = await buyOrder.getSignatureWithProviderAsync(env.provider);
await mintAssetsAsync(buyOrder, sellOrder.erc721TokenId, sellOrder.maker);
await erc20Token.mint(buyOrder.maker, sellOrder.fees[0].amount).awaitTransactionSuccessAsync();
const tx = zeroEx
.matchERC721Orders(sellOrder, buyOrder, sellSignature, buySignature)
.awaitTransactionSuccessAsync({
from: matcher,
});
return expect(tx).to.revertWith(
new RevertErrors.NFTOrders.SellOrderFeesExceedSpreadError(sellOrder.fees[0].amount, spread),
);
});
it('reverts if sell order fees exceed spread (ETH/WETH)', async () => {
const spread = getRandomInteger(1, '1e18');
const sellOrder = getTestERC721Order({
direction: NFTOrder.TradeDirection.SellNFT,
erc20Token: ETH_TOKEN_ADDRESS,
fees: [
{
recipient: otherTaker,
amount: spread.plus(1),
feeData: NULL_BYTES,
},
],
});
await sendEtherAsync(zeroEx.address, sellOrder.fees[0].amount);
const sellSignature = await sellOrder.getSignatureWithProviderAsync(env.provider);
const buyOrder = getTestERC721Order({
maker: otherMaker,
direction: NFTOrder.TradeDirection.BuyNFT,
erc20Token: weth.address,
erc721TokenId: sellOrder.erc721TokenId,
erc20TokenAmount: sellOrder.erc20TokenAmount.plus(spread),
});
const buySignature = await buyOrder.getSignatureWithProviderAsync(env.provider);
await mintAssetsAsync(buyOrder, sellOrder.erc721TokenId, sellOrder.maker);
await weth
.deposit()
.awaitTransactionSuccessAsync({ from: buyOrder.maker, value: sellOrder.fees[0].amount });
const tx = zeroEx
.matchERC721Orders(sellOrder, buyOrder, sellSignature, buySignature)
.awaitTransactionSuccessAsync({
from: matcher,
});
return expect(tx).to.revertWith(
new RevertErrors.NFTOrders.SellOrderFeesExceedSpreadError(sellOrder.fees[0].amount, spread),
);
});
});
});