update contracts and tests to support different maker assets

This commit is contained in:
Michael Zhu 2019-10-01 14:15:03 -07:00
parent 0ff8b12770
commit 8077123e9f
4 changed files with 143 additions and 95 deletions

View File

@ -27,10 +27,12 @@ import "@0x/contracts-exchange-libs/contracts/src/LibMath.sol";
import "@0x/contracts-exchange/contracts/src/interfaces/IExchange.sol"; import "@0x/contracts-exchange/contracts/src/interfaces/IExchange.sol";
import "./libs/LibConstants.sol"; import "./libs/LibConstants.sol";
import "./libs/LibForwarderRichErrors.sol"; import "./libs/LibForwarderRichErrors.sol";
import "./MixinAssets.sol";
contract MixinExchangeWrapper is contract MixinExchangeWrapper is
LibConstants LibConstants,
MixinAssets
{ {
using LibSafeMath for uint256; using LibSafeMath for uint256;
@ -72,14 +74,12 @@ contract MixinExchangeWrapper is
/// @param order A single order specification. /// @param order A single order specification.
/// @param signature Signature for the given order. /// @param signature Signature for the given order.
/// @param remainingTakerAssetFillAmount Remaining amount of WETH to sell. /// @param remainingTakerAssetFillAmount Remaining amount of WETH to sell.
/// @param protocolFee Amount of WETH that will be spent on the protocol fee for each order.
/// @return wethSpentAmount Amount of WETH spent on the given order. /// @return wethSpentAmount Amount of WETH spent on the given order.
/// @return makerAssetAcquiredAmount Amount of maker asset acquired from the given order. /// @return makerAssetAcquiredAmount Amount of maker asset acquired from the given order.
function _marketSellSingleOrder( function _marketSellSingleOrder(
LibOrder.Order memory order, LibOrder.Order memory order,
bytes memory signature, bytes memory signature,
uint256 remainingTakerAssetFillAmount, uint256 remainingTakerAssetFillAmount
uint256 protocolFee
) )
internal internal
returns ( returns (
@ -92,7 +92,7 @@ contract MixinExchangeWrapper is
// Attempt to sell the remaining amount of WETH // Attempt to sell the remaining amount of WETH
LibFillResults.FillResults memory singleFillResults = _fillOrderNoThrow( LibFillResults.FillResults memory singleFillResults = _fillOrderNoThrow(
order, order,
remainingTakerAssetFillAmount.safeSub(protocolFee), remainingTakerAssetFillAmount,
signature signature
); );
@ -110,7 +110,7 @@ contract MixinExchangeWrapper is
uint256 takerAssetFillAmount = LibMath.getPartialAmountCeil( uint256 takerAssetFillAmount = LibMath.getPartialAmountCeil(
order.takerAssetAmount, order.takerAssetAmount,
order.takerAssetAmount.safeAdd(order.takerFee), order.takerAssetAmount.safeAdd(order.takerFee),
remainingTakerAssetFillAmount.safeSub(protocolFee) remainingTakerAssetFillAmount
); );
LibFillResults.FillResults memory singleFillResults = _fillOrderNoThrow( LibFillResults.FillResults memory singleFillResults = _fillOrderNoThrow(
@ -161,7 +161,8 @@ contract MixinExchangeWrapper is
// The remaining amount of WETH to sell // The remaining amount of WETH to sell
uint256 remainingTakerAssetFillAmount = wethSellAmount uint256 remainingTakerAssetFillAmount = wethSellAmount
.safeSub(totalWethSpentAmount); .safeSub(totalWethSpentAmount)
.safeSub(protocolFee);
( (
uint256 wethSpentAmount, uint256 wethSpentAmount,
@ -169,10 +170,11 @@ contract MixinExchangeWrapper is
) = _marketSellSingleOrder( ) = _marketSellSingleOrder(
orders[i], orders[i],
signatures[i], signatures[i],
remainingTakerAssetFillAmount, remainingTakerAssetFillAmount
protocolFee
); );
_transferAssetToSender(orders[i].makerAssetData, makerAssetAcquiredAmount);
totalWethSpentAmount = totalWethSpentAmount totalWethSpentAmount = totalWethSpentAmount
.safeAdd(wethSpentAmount); .safeAdd(wethSpentAmount);
totalMakerAssetAcquiredAmount = totalMakerAssetAcquiredAmount totalMakerAssetAcquiredAmount = totalMakerAssetAcquiredAmount
@ -294,6 +296,8 @@ contract MixinExchangeWrapper is
remainingMakerAssetFillAmount remainingMakerAssetFillAmount
); );
_transferAssetToSender(orders[i].makerAssetData, makerAssetAcquiredAmount);
totalWethSpentAmount = totalWethSpentAmount totalWethSpentAmount = totalWethSpentAmount
.safeAdd(wethSpentAmount); .safeAdd(wethSpentAmount);
totalMakerAssetAcquiredAmount = totalMakerAssetAcquiredAmount totalMakerAssetAcquiredAmount = totalMakerAssetAcquiredAmount

View File

@ -28,7 +28,6 @@ import "./libs/LibConstants.sol";
import "./libs/LibForwarderRichErrors.sol"; import "./libs/LibForwarderRichErrors.sol";
import "./interfaces/IAssets.sol"; import "./interfaces/IAssets.sol";
import "./interfaces/IForwarderCore.sol"; import "./interfaces/IForwarderCore.sol";
import "./MixinAssets.sol";
import "./MixinExchangeWrapper.sol"; import "./MixinExchangeWrapper.sol";
import "./MixinWeth.sol"; import "./MixinWeth.sol";
@ -38,7 +37,6 @@ contract MixinForwarderCore is
IAssets, IAssets,
IForwarderCore, IForwarderCore,
MixinWeth, MixinWeth,
MixinAssets,
MixinExchangeWrapper MixinExchangeWrapper
{ {
using LibBytes for bytes; using LibBytes for bytes;
@ -93,7 +91,8 @@ contract MixinForwarderCore is
msg.value msg.value
); );
// Spends up to wethSellAmount to fill orders and pay WETH order fees. // Spends up to wethSellAmount to fill orders, transfers purchased assets to msg.sender,
// and pays WETH order fees.
( (
wethSpentAmount, wethSpentAmount,
makerAssetAcquiredAmount makerAssetAcquiredAmount
@ -110,12 +109,6 @@ contract MixinForwarderCore is
feePercentage, feePercentage,
feeRecipient feeRecipient
); );
// Transfer purchased assets to msg.sender.
_transferAssetToSender(
orders[0].makerAssetData,
makerAssetAcquiredAmount
);
} }
/// @dev Attempt to buy makerAssetBuyAmount of makerAsset by selling ETH provided with transaction. /// @dev Attempt to buy makerAssetBuyAmount of makerAsset by selling ETH provided with transaction.
@ -148,9 +141,7 @@ contract MixinForwarderCore is
// Convert ETH to WETH. // Convert ETH to WETH.
_convertEthToWeth(); _convertEthToWeth();
// Attempt to fill the desired amount of makerAsset. Note that makerAssetAcquiredAmount < makerAssetBuyAmount // Attempts to fill the desired amount of makerAsset and trasnfer purchased assets to msg.sender.
// if any of the orders filled have an takerFee denominated in makerAsset, since these fees will be paid out
// from the Forwarder's temporary makerAsset balance.
( (
wethSpentAmount, wethSpentAmount,
makerAssetAcquiredAmount makerAssetAcquiredAmount
@ -167,11 +158,5 @@ contract MixinForwarderCore is
feePercentage, feePercentage,
feeRecipient feeRecipient
); );
// Transfer acquired assets to msg.sender.
_transferAssetToSender(
orders[0].makerAssetData,
makerAssetAcquiredAmount
);
} }
} }

View File

@ -154,7 +154,7 @@ blockchainTests(ContractName.Forwarder, env => {
wethAssetData, wethAssetData,
); );
forwarderWrapper = new ForwarderWrapper(forwarderContract, env.provider); forwarderWrapper = new ForwarderWrapper(forwarderContract, env.provider);
await forwarderWrapper.approveMakerAssetProxyAsync(assetDataUtils.encodeERC20AssetData(erc20Token.address), { await forwarderWrapper.approveMakerAssetProxyAsync(defaultOrderParams.makerAssetData, {
from: takerAddress, from: takerAddress,
}); });
erc20Wrapper.addTokenOwnerAddress(forwarderContract.address); erc20Wrapper.addTokenOwnerAddress(forwarderContract.address);
@ -203,7 +203,7 @@ blockchainTests(ContractName.Forwarder, env => {
blockchainTests.resets('marketSellOrdersWithEth without extra fees', () => { blockchainTests.resets('marketSellOrdersWithEth without extra fees', () => {
it('should fill a single order without a taker fee', async () => { it('should fill a single order without a taker fee', async () => {
const orderWithoutFee = await orderFactory.newSignedOrderAsync(); const orderWithoutFee = await orderFactory.newSignedOrderAsync();
await forwarderTestFactory.marketSellTestAsync([orderWithoutFee], 0.78, erc20Token); await forwarderTestFactory.marketSellTestAsync([orderWithoutFee], 0.78, [erc20Token]);
}); });
it('should fill multiple orders without taker fees', async () => { it('should fill multiple orders without taker fees', async () => {
const firstOrder = await orderFactory.newSignedOrderAsync(); const firstOrder = await orderFactory.newSignedOrderAsync();
@ -212,14 +212,14 @@ blockchainTests(ContractName.Forwarder, env => {
takerAssetAmount: Web3Wrapper.toBaseUnitAmount(21, DECIMALS_DEFAULT), takerAssetAmount: Web3Wrapper.toBaseUnitAmount(21, DECIMALS_DEFAULT),
}); });
const orders = [firstOrder, secondOrder]; const orders = [firstOrder, secondOrder];
await forwarderTestFactory.marketSellTestAsync(orders, 1.51, erc20Token); await forwarderTestFactory.marketSellTestAsync(orders, 1.51, [erc20Token]);
}); });
it('should fill a single order with a percentage fee', async () => { it('should fill a single order with a percentage fee', async () => {
const orderWithPercentageFee = await orderFactory.newSignedOrderAsync({ const orderWithPercentageFee = await orderFactory.newSignedOrderAsync({
takerFee: Web3Wrapper.toBaseUnitAmount(1, DECIMALS_DEFAULT), takerFee: Web3Wrapper.toBaseUnitAmount(1, DECIMALS_DEFAULT),
takerFeeAssetData: assetDataUtils.encodeERC20AssetData(defaultMakerAssetAddress), takerFeeAssetData: assetDataUtils.encodeERC20AssetData(defaultMakerAssetAddress),
}); });
await forwarderTestFactory.marketSellTestAsync([orderWithPercentageFee], 0.58, erc20Token); await forwarderTestFactory.marketSellTestAsync([orderWithPercentageFee], 0.58, [erc20Token]);
}); });
it('should fill multiple orders with percentage fees', async () => { it('should fill multiple orders with percentage fees', async () => {
const firstOrder = await orderFactory.newSignedOrderAsync({ const firstOrder = await orderFactory.newSignedOrderAsync({
@ -233,7 +233,7 @@ blockchainTests(ContractName.Forwarder, env => {
takerFeeAssetData: assetDataUtils.encodeERC20AssetData(defaultMakerAssetAddress), takerFeeAssetData: assetDataUtils.encodeERC20AssetData(defaultMakerAssetAddress),
}); });
const orders = [firstOrder, secondOrder]; const orders = [firstOrder, secondOrder];
await forwarderTestFactory.marketSellTestAsync(orders, 1.34, erc20Token); await forwarderTestFactory.marketSellTestAsync(orders, 1.34, [erc20Token]);
}); });
it('should fail to fill an order with a percentage fee if the asset proxy is not yet approved', async () => { it('should fail to fill an order with a percentage fee if the asset proxy is not yet approved', async () => {
const unapprovedAsset = assetDataUtils.encodeERC20AssetData(secondErc20Token.address); const unapprovedAsset = assetDataUtils.encodeERC20AssetData(secondErc20Token.address);
@ -280,7 +280,7 @@ blockchainTests(ContractName.Forwarder, env => {
takerFee: Web3Wrapper.toBaseUnitAmount(1, DECIMALS_DEFAULT), takerFee: Web3Wrapper.toBaseUnitAmount(1, DECIMALS_DEFAULT),
takerFeeAssetData: wethAssetData, takerFeeAssetData: wethAssetData,
}); });
await forwarderTestFactory.marketSellTestAsync([orderWithWethFee], 0.13, erc20Token); await forwarderTestFactory.marketSellTestAsync([orderWithWethFee], 0.13, [erc20Token]);
}); });
it('should fill multiple orders with WETH fees', async () => { it('should fill multiple orders with WETH fees', async () => {
const firstOrder = await orderFactory.newSignedOrderAsync({ const firstOrder = await orderFactory.newSignedOrderAsync({
@ -294,7 +294,7 @@ blockchainTests(ContractName.Forwarder, env => {
takerFeeAssetData: wethAssetData, takerFeeAssetData: wethAssetData,
}); });
const orders = [firstOrder, secondOrderWithWethFee]; const orders = [firstOrder, secondOrderWithWethFee];
await forwarderTestFactory.marketSellTestAsync(orders, 1.25, erc20Token); await forwarderTestFactory.marketSellTestAsync(orders, 1.25, [erc20Token]);
}); });
it('should refund remaining ETH if amount is greater than takerAssetAmount', async () => { it('should refund remaining ETH if amount is greater than takerAssetAmount', async () => {
const order = await orderFactory.newSignedOrderAsync(); const order = await orderFactory.newSignedOrderAsync();
@ -310,6 +310,21 @@ blockchainTests(ContractName.Forwarder, env => {
expect(takerEthBalanceAfter).to.be.bignumber.equal(takerEthBalanceBefore.minus(totalEthSpent)); expect(takerEthBalanceAfter).to.be.bignumber.equal(takerEthBalanceBefore.minus(totalEthSpent));
}); });
it('should fill orders with different makerAssetData', async () => {
const firstOrderMakerAssetData = assetDataUtils.encodeERC20AssetData(erc20Token.address);
const firstOrder = await orderFactory.newSignedOrderAsync({
makerAssetData: firstOrderMakerAssetData,
});
const secondOrderMakerAssetData = assetDataUtils.encodeERC20AssetData(secondErc20Token.address);
const secondOrder = await orderFactory.newSignedOrderAsync({
makerAssetData: secondOrderMakerAssetData,
});
await forwarderWrapper.approveMakerAssetProxyAsync(secondOrderMakerAssetData, { from: takerAddress });
const orders = [firstOrder, secondOrder];
await forwarderTestFactory.marketSellTestAsync(orders, 1.5, [erc20Token, secondErc20Token]);
});
it('should fail to fill an order with a fee denominated in an asset other than makerAsset or WETH', async () => { it('should fail to fill an order with a fee denominated in an asset other than makerAsset or WETH', async () => {
const makerAssetData = assetDataUtils.encodeERC20AssetData(erc20Token.address); const makerAssetData = assetDataUtils.encodeERC20AssetData(erc20Token.address);
const takerFeeAssetData = assetDataUtils.encodeERC20AssetData(secondErc20Token.address); const takerFeeAssetData = assetDataUtils.encodeERC20AssetData(secondErc20Token.address);
@ -321,14 +336,14 @@ blockchainTests(ContractName.Forwarder, env => {
}); });
const revertError = new ForwarderRevertErrors.UnsupportedFeeError(takerFeeAssetData); const revertError = new ForwarderRevertErrors.UnsupportedFeeError(takerFeeAssetData);
await forwarderTestFactory.marketSellTestAsync([order], 0.5, erc20Token, { await forwarderTestFactory.marketSellTestAsync([order], 0.5, [erc20Token], {
revertError, revertError,
}); });
}); });
it('should fill a partially-filled order without a taker fee', async () => { it('should fill a partially-filled order without a taker fee', async () => {
const order = await orderFactory.newSignedOrderAsync(); const order = await orderFactory.newSignedOrderAsync();
await forwarderTestFactory.marketSellTestAsync([order], 0.3, erc20Token); await forwarderTestFactory.marketSellTestAsync([order], 0.3, [erc20Token]);
await forwarderTestFactory.marketSellTestAsync([order], 0.8, erc20Token); await forwarderTestFactory.marketSellTestAsync([order], 0.8, [erc20Token]);
}); });
it('should skip over an order with an invalid maker asset amount', async () => { it('should skip over an order with an invalid maker asset amount', async () => {
const unfillableOrder = await orderFactory.newSignedOrderAsync({ const unfillableOrder = await orderFactory.newSignedOrderAsync({
@ -336,7 +351,7 @@ blockchainTests(ContractName.Forwarder, env => {
}); });
const fillableOrder = await orderFactory.newSignedOrderAsync(); const fillableOrder = await orderFactory.newSignedOrderAsync();
await forwarderTestFactory.marketSellTestAsync([unfillableOrder, fillableOrder], 1.5, erc20Token); await forwarderTestFactory.marketSellTestAsync([unfillableOrder, fillableOrder], 1.5, [erc20Token]);
}); });
it('should skip over an order with an invalid taker asset amount', async () => { it('should skip over an order with an invalid taker asset amount', async () => {
const unfillableOrder = await orderFactory.newSignedOrderAsync({ const unfillableOrder = await orderFactory.newSignedOrderAsync({
@ -344,7 +359,7 @@ blockchainTests(ContractName.Forwarder, env => {
}); });
const fillableOrder = await orderFactory.newSignedOrderAsync(); const fillableOrder = await orderFactory.newSignedOrderAsync();
await forwarderTestFactory.marketSellTestAsync([unfillableOrder, fillableOrder], 1.5, erc20Token); await forwarderTestFactory.marketSellTestAsync([unfillableOrder, fillableOrder], 1.5, [erc20Token]);
}); });
it('should skip over an expired order', async () => { it('should skip over an expired order', async () => {
const currentTimestamp = await getLatestBlockTimestampAsync(); const currentTimestamp = await getLatestBlockTimestampAsync();
@ -353,21 +368,21 @@ blockchainTests(ContractName.Forwarder, env => {
}); });
const fillableOrder = await orderFactory.newSignedOrderAsync(); const fillableOrder = await orderFactory.newSignedOrderAsync();
await forwarderTestFactory.marketSellTestAsync([expiredOrder, fillableOrder], 1.5, erc20Token); await forwarderTestFactory.marketSellTestAsync([expiredOrder, fillableOrder], 1.5, [erc20Token]);
}); });
it('should skip over a fully filled order', async () => { it('should skip over a fully filled order', async () => {
const fullyFilledOrder = await orderFactory.newSignedOrderAsync(); const fullyFilledOrder = await orderFactory.newSignedOrderAsync();
await forwarderTestFactory.marketSellTestAsync([fullyFilledOrder], 1, erc20Token); await forwarderTestFactory.marketSellTestAsync([fullyFilledOrder], 1, [erc20Token]);
const fillableOrder = await orderFactory.newSignedOrderAsync(); const fillableOrder = await orderFactory.newSignedOrderAsync();
await forwarderTestFactory.marketSellTestAsync([fullyFilledOrder, fillableOrder], 1.5, erc20Token); await forwarderTestFactory.marketSellTestAsync([fullyFilledOrder, fillableOrder], 1.5, [erc20Token]);
}); });
it('should skip over a cancelled order', async () => { it('should skip over a cancelled order', async () => {
const cancelledOrder = await orderFactory.newSignedOrderAsync(); const cancelledOrder = await orderFactory.newSignedOrderAsync();
await exchangeWrapper.cancelOrderAsync(cancelledOrder, makerAddress); await exchangeWrapper.cancelOrderAsync(cancelledOrder, makerAddress);
const fillableOrder = await orderFactory.newSignedOrderAsync(); const fillableOrder = await orderFactory.newSignedOrderAsync();
await forwarderTestFactory.marketSellTestAsync([cancelledOrder, fillableOrder], 1.5, erc20Token); await forwarderTestFactory.marketSellTestAsync([cancelledOrder, fillableOrder], 1.5, [erc20Token]);
}); });
}); });
blockchainTests.resets('marketSellOrdersWithEth with extra fees', () => { blockchainTests.resets('marketSellOrdersWithEth with extra fees', () => {
@ -376,7 +391,7 @@ blockchainTests(ContractName.Forwarder, env => {
makerAssetAmount: Web3Wrapper.toBaseUnitAmount(157, DECIMALS_DEFAULT), makerAssetAmount: Web3Wrapper.toBaseUnitAmount(157, DECIMALS_DEFAULT),
takerAssetAmount: Web3Wrapper.toBaseUnitAmount(36, DECIMALS_DEFAULT), takerAssetAmount: Web3Wrapper.toBaseUnitAmount(36, DECIMALS_DEFAULT),
}); });
await forwarderTestFactory.marketSellTestAsync([order], 0.67, erc20Token, { await forwarderTestFactory.marketSellTestAsync([order], 0.67, [erc20Token], {
forwarderFeePercentage: new BigNumber(2), forwarderFeePercentage: new BigNumber(2),
}); });
}); });
@ -387,7 +402,7 @@ blockchainTests(ContractName.Forwarder, env => {
ForwarderTestFactory.getPercentageOfValue(constants.PERCENTAGE_DENOMINATOR, forwarderFeePercentage), ForwarderTestFactory.getPercentageOfValue(constants.PERCENTAGE_DENOMINATOR, forwarderFeePercentage),
); );
await forwarderTestFactory.marketSellTestAsync([order], 0.5, erc20Token, { await forwarderTestFactory.marketSellTestAsync([order], 0.5, [erc20Token], {
forwarderFeePercentage, forwarderFeePercentage,
revertError, revertError,
}); });
@ -399,7 +414,7 @@ blockchainTests(ContractName.Forwarder, env => {
makerAssetAmount: Web3Wrapper.toBaseUnitAmount(131, DECIMALS_DEFAULT), makerAssetAmount: Web3Wrapper.toBaseUnitAmount(131, DECIMALS_DEFAULT),
takerAssetAmount: Web3Wrapper.toBaseUnitAmount(20, DECIMALS_DEFAULT), takerAssetAmount: Web3Wrapper.toBaseUnitAmount(20, DECIMALS_DEFAULT),
}); });
await forwarderTestFactory.marketBuyTestAsync([order], 0.62, erc20Token); await forwarderTestFactory.marketBuyTestAsync([order], 0.62, [erc20Token]);
}); });
it('should buy the exact amount of makerAsset in multiple orders', async () => { it('should buy the exact amount of makerAsset in multiple orders', async () => {
const firstOrder = await orderFactory.newSignedOrderAsync(); const firstOrder = await orderFactory.newSignedOrderAsync();
@ -408,14 +423,29 @@ blockchainTests(ContractName.Forwarder, env => {
takerAssetAmount: Web3Wrapper.toBaseUnitAmount(11, DECIMALS_DEFAULT), takerAssetAmount: Web3Wrapper.toBaseUnitAmount(11, DECIMALS_DEFAULT),
}); });
const orders = [firstOrder, secondOrder]; const orders = [firstOrder, secondOrder];
await forwarderTestFactory.marketBuyTestAsync(orders, 1.96, erc20Token); await forwarderTestFactory.marketBuyTestAsync(orders, 1.96, [erc20Token]);
});
it('should buy exactly makerAssetBuyAmount in orders with different makerAssetData', async () => {
const firstOrderMakerAssetData = assetDataUtils.encodeERC20AssetData(erc20Token.address);
const firstOrder = await orderFactory.newSignedOrderAsync({
makerAssetData: firstOrderMakerAssetData,
});
const secondOrderMakerAssetData = assetDataUtils.encodeERC20AssetData(secondErc20Token.address);
const secondOrder = await orderFactory.newSignedOrderAsync({
makerAssetData: secondOrderMakerAssetData,
});
await forwarderWrapper.approveMakerAssetProxyAsync(secondOrderMakerAssetData, { from: takerAddress });
const orders = [firstOrder, secondOrder];
await forwarderTestFactory.marketBuyTestAsync(orders, 1.5, [erc20Token, secondErc20Token]);
}); });
it('should buy the exact amount of makerAsset and return excess ETH', async () => { it('should buy the exact amount of makerAsset and return excess ETH', async () => {
const order = await orderFactory.newSignedOrderAsync({ const order = await orderFactory.newSignedOrderAsync({
makerAssetAmount: Web3Wrapper.toBaseUnitAmount(80, DECIMALS_DEFAULT), makerAssetAmount: Web3Wrapper.toBaseUnitAmount(80, DECIMALS_DEFAULT),
takerAssetAmount: Web3Wrapper.toBaseUnitAmount(17, DECIMALS_DEFAULT), takerAssetAmount: Web3Wrapper.toBaseUnitAmount(17, DECIMALS_DEFAULT),
}); });
await forwarderTestFactory.marketBuyTestAsync([order], 0.57, erc20Token, { await forwarderTestFactory.marketBuyTestAsync([order], 0.57, [erc20Token], {
ethValueAdjustment: 2, ethValueAdjustment: 2,
}); });
}); });
@ -426,7 +456,7 @@ blockchainTests(ContractName.Forwarder, env => {
takerFee: Web3Wrapper.toBaseUnitAmount(1, DECIMALS_DEFAULT), takerFee: Web3Wrapper.toBaseUnitAmount(1, DECIMALS_DEFAULT),
takerFeeAssetData: wethAssetData, takerFeeAssetData: wethAssetData,
}); });
await forwarderTestFactory.marketBuyTestAsync([order], 0.38, erc20Token); await forwarderTestFactory.marketBuyTestAsync([order], 0.38, [erc20Token]);
}); });
it('should buy the exact amount of makerAsset from a single order with a percentage fee', async () => { it('should buy the exact amount of makerAsset from a single order with a percentage fee', async () => {
const order = await orderFactory.newSignedOrderAsync({ const order = await orderFactory.newSignedOrderAsync({
@ -435,7 +465,7 @@ blockchainTests(ContractName.Forwarder, env => {
takerFee: Web3Wrapper.toBaseUnitAmount(1, DECIMALS_DEFAULT), takerFee: Web3Wrapper.toBaseUnitAmount(1, DECIMALS_DEFAULT),
takerFeeAssetData: assetDataUtils.encodeERC20AssetData(defaultMakerAssetAddress), takerFeeAssetData: assetDataUtils.encodeERC20AssetData(defaultMakerAssetAddress),
}); });
await forwarderTestFactory.marketBuyTestAsync([order], 0.52, erc20Token); await forwarderTestFactory.marketBuyTestAsync([order], 0.52, [erc20Token]);
}); });
it('should revert if the amount of ETH sent is too low to fill the makerAssetAmount', async () => { it('should revert if the amount of ETH sent is too low to fill the makerAssetAmount', async () => {
const order = await orderFactory.newSignedOrderAsync(); const order = await orderFactory.newSignedOrderAsync();
@ -444,7 +474,7 @@ blockchainTests(ContractName.Forwarder, env => {
constants.ZERO_AMOUNT, constants.ZERO_AMOUNT,
); );
await forwarderTestFactory.marketBuyTestAsync([order], 0.5, erc20Token, { await forwarderTestFactory.marketBuyTestAsync([order], 0.5, [erc20Token], {
ethValueAdjustment: -2, ethValueAdjustment: -2,
revertError, revertError,
}); });
@ -456,7 +486,7 @@ blockchainTests(ContractName.Forwarder, env => {
makerAssetData: assetDataUtils.encodeERC721AssetData(erc721Token.address, makerAssetId), makerAssetData: assetDataUtils.encodeERC721AssetData(erc721Token.address, makerAssetId),
takerFeeAssetData: wethAssetData, takerFeeAssetData: wethAssetData,
}); });
await forwarderTestFactory.marketBuyTestAsync([erc721Order], 1, erc721Token, { await forwarderTestFactory.marketBuyTestAsync([erc721Order], 1, [erc721Token], {
makerAssetId, makerAssetId,
}); });
}); });
@ -468,7 +498,7 @@ blockchainTests(ContractName.Forwarder, env => {
takerFee: Web3Wrapper.toBaseUnitAmount(1, DECIMALS_DEFAULT), takerFee: Web3Wrapper.toBaseUnitAmount(1, DECIMALS_DEFAULT),
takerFeeAssetData: wethAssetData, takerFeeAssetData: wethAssetData,
}); });
await forwarderTestFactory.marketBuyTestAsync([erc721orderWithWethFee], 1, erc721Token, { await forwarderTestFactory.marketBuyTestAsync([erc721orderWithWethFee], 1, [erc721Token], {
makerAssetId, makerAssetId,
}); });
}); });
@ -483,14 +513,14 @@ blockchainTests(ContractName.Forwarder, env => {
}); });
const revertError = new ForwarderRevertErrors.UnsupportedFeeError(takerFeeAssetData); const revertError = new ForwarderRevertErrors.UnsupportedFeeError(takerFeeAssetData);
await forwarderTestFactory.marketBuyTestAsync([order], 0.5, erc20Token, { await forwarderTestFactory.marketBuyTestAsync([order], 0.5, [erc20Token], {
revertError, revertError,
}); });
}); });
it('should fill a partially-filled order without a taker fee', async () => { it('should fill a partially-filled order without a taker fee', async () => {
const order = await orderFactory.newSignedOrderAsync(); const order = await orderFactory.newSignedOrderAsync();
await forwarderTestFactory.marketBuyTestAsync([order], 0.3, erc20Token); await forwarderTestFactory.marketBuyTestAsync([order], 0.3, [erc20Token]);
await forwarderTestFactory.marketBuyTestAsync([order], 0.8, erc20Token); await forwarderTestFactory.marketBuyTestAsync([order], 0.8, [erc20Token]);
}); });
it('should skip over an order with an invalid maker asset amount', async () => { it('should skip over an order with an invalid maker asset amount', async () => {
const unfillableOrder = await orderFactory.newSignedOrderAsync({ const unfillableOrder = await orderFactory.newSignedOrderAsync({
@ -498,7 +528,7 @@ blockchainTests(ContractName.Forwarder, env => {
}); });
const fillableOrder = await orderFactory.newSignedOrderAsync(); const fillableOrder = await orderFactory.newSignedOrderAsync();
await forwarderTestFactory.marketBuyTestAsync([unfillableOrder, fillableOrder], 1.5, erc20Token); await forwarderTestFactory.marketBuyTestAsync([unfillableOrder, fillableOrder], 1.5, [erc20Token]);
}); });
it('should skip over an order with an invalid taker asset amount', async () => { it('should skip over an order with an invalid taker asset amount', async () => {
const unfillableOrder = await orderFactory.newSignedOrderAsync({ const unfillableOrder = await orderFactory.newSignedOrderAsync({
@ -506,7 +536,7 @@ blockchainTests(ContractName.Forwarder, env => {
}); });
const fillableOrder = await orderFactory.newSignedOrderAsync(); const fillableOrder = await orderFactory.newSignedOrderAsync();
await forwarderTestFactory.marketBuyTestAsync([unfillableOrder, fillableOrder], 1.5, erc20Token); await forwarderTestFactory.marketBuyTestAsync([unfillableOrder, fillableOrder], 1.5, [erc20Token]);
}); });
it('should skip over an expired order', async () => { it('should skip over an expired order', async () => {
const currentTimestamp = await getLatestBlockTimestampAsync(); const currentTimestamp = await getLatestBlockTimestampAsync();
@ -515,21 +545,21 @@ blockchainTests(ContractName.Forwarder, env => {
}); });
const fillableOrder = await orderFactory.newSignedOrderAsync(); const fillableOrder = await orderFactory.newSignedOrderAsync();
await forwarderTestFactory.marketBuyTestAsync([expiredOrder, fillableOrder], 1.5, erc20Token); await forwarderTestFactory.marketBuyTestAsync([expiredOrder, fillableOrder], 1.5, [erc20Token]);
}); });
it('should skip over a fully filled order', async () => { it('should skip over a fully filled order', async () => {
const fullyFilledOrder = await orderFactory.newSignedOrderAsync(); const fullyFilledOrder = await orderFactory.newSignedOrderAsync();
await forwarderTestFactory.marketBuyTestAsync([fullyFilledOrder], 1, erc20Token); await forwarderTestFactory.marketBuyTestAsync([fullyFilledOrder], 1, [erc20Token]);
const fillableOrder = await orderFactory.newSignedOrderAsync(); const fillableOrder = await orderFactory.newSignedOrderAsync();
await forwarderTestFactory.marketBuyTestAsync([fullyFilledOrder, fillableOrder], 1.5, erc20Token); await forwarderTestFactory.marketBuyTestAsync([fullyFilledOrder, fillableOrder], 1.5, [erc20Token]);
}); });
it('should skip over a cancelled order', async () => { it('should skip over a cancelled order', async () => {
const cancelledOrder = await orderFactory.newSignedOrderAsync(); const cancelledOrder = await orderFactory.newSignedOrderAsync();
await exchangeWrapper.cancelOrderAsync(cancelledOrder, makerAddress); await exchangeWrapper.cancelOrderAsync(cancelledOrder, makerAddress);
const fillableOrder = await orderFactory.newSignedOrderAsync(); const fillableOrder = await orderFactory.newSignedOrderAsync();
await forwarderTestFactory.marketBuyTestAsync([cancelledOrder, fillableOrder], 1.5, erc20Token); await forwarderTestFactory.marketBuyTestAsync([cancelledOrder, fillableOrder], 1.5, [erc20Token]);
}); });
it('Should buy slightly greater makerAsset when exchange rate is rounded', async () => { it('Should buy slightly greater makerAsset when exchange rate is rounded', async () => {
// The 0x Protocol contracts round the exchange rate in favor of the Maker. // The 0x Protocol contracts round the exchange rate in favor of the Maker.
@ -649,7 +679,7 @@ blockchainTests(ContractName.Forwarder, env => {
makerAssetAmount: Web3Wrapper.toBaseUnitAmount(125, DECIMALS_DEFAULT), makerAssetAmount: Web3Wrapper.toBaseUnitAmount(125, DECIMALS_DEFAULT),
takerAssetAmount: Web3Wrapper.toBaseUnitAmount(11, DECIMALS_DEFAULT), takerAssetAmount: Web3Wrapper.toBaseUnitAmount(11, DECIMALS_DEFAULT),
}); });
await forwarderTestFactory.marketBuyTestAsync([order], 0.33, erc20Token, { await forwarderTestFactory.marketBuyTestAsync([order], 0.33, [erc20Token], {
forwarderFeePercentage: new BigNumber(2), forwarderFeePercentage: new BigNumber(2),
}); });
}); });
@ -658,7 +688,7 @@ blockchainTests(ContractName.Forwarder, env => {
const revertError = new ForwarderRevertErrors.FeePercentageTooLargeError( const revertError = new ForwarderRevertErrors.FeePercentageTooLargeError(
ForwarderTestFactory.getPercentageOfValue(constants.PERCENTAGE_DENOMINATOR, new BigNumber(6)), ForwarderTestFactory.getPercentageOfValue(constants.PERCENTAGE_DENOMINATOR, new BigNumber(6)),
); );
await forwarderTestFactory.marketBuyTestAsync([order], 0.5, erc20Token, { await forwarderTestFactory.marketBuyTestAsync([order], 0.5, [erc20Token], {
forwarderFeePercentage: new BigNumber(6), forwarderFeePercentage: new BigNumber(6),
revertError, revertError,
}); });
@ -674,7 +704,7 @@ blockchainTests(ContractName.Forwarder, env => {
const revertError = new ForwarderRevertErrors.InsufficientEthForFeeError(ethFee, ethFee.minus(1)); const revertError = new ForwarderRevertErrors.InsufficientEthForFeeError(ethFee, ethFee.minus(1));
// -2 to compensate for the extra 1 wei added in ForwarderTestFactory to account for rounding // -2 to compensate for the extra 1 wei added in ForwarderTestFactory to account for rounding
await forwarderTestFactory.marketBuyTestAsync([order], 0.5, erc20Token, { await forwarderTestFactory.marketBuyTestAsync([order], 0.5, [erc20Token], {
ethValueAdjustment: -2, ethValueAdjustment: -2,
forwarderFeePercentage, forwarderFeePercentage,
revertError, revertError,

View File

@ -3,6 +3,7 @@ import { DummyERC20TokenContract } from '@0x/contracts-erc20';
import { DummyERC721TokenContract } from '@0x/contracts-erc721'; import { DummyERC721TokenContract } from '@0x/contracts-erc721';
import { ExchangeWrapper } from '@0x/contracts-exchange'; import { ExchangeWrapper } from '@0x/contracts-exchange';
import { constants, ERC20BalancesByOwner, expect, OrderStatus, web3Wrapper } from '@0x/contracts-test-utils'; import { constants, ERC20BalancesByOwner, expect, OrderStatus, web3Wrapper } from '@0x/contracts-test-utils';
import { assetDataUtils } from '@0x/order-utils';
import { OrderInfo, SignedOrder } from '@0x/types'; import { OrderInfo, SignedOrder } from '@0x/types';
import { BigNumber, RevertError } from '@0x/utils'; import { BigNumber, RevertError } from '@0x/utils';
import * as _ from 'lodash'; import * as _ from 'lodash';
@ -12,10 +13,14 @@ import { ForwarderWrapper } from './forwarder_wrapper';
// Necessary bookkeeping to validate Forwarder results // Necessary bookkeeping to validate Forwarder results
interface ForwarderFillState { interface ForwarderFillState {
takerAssetFillAmount: BigNumber; takerAssetFillAmount: BigNumber;
makerAssetFillAmount: BigNumber; makerAssetFillAmount: {
[makerAssetData: string]: BigNumber;
};
protocolFees: BigNumber; protocolFees: BigNumber;
wethFees: BigNumber; wethFees: BigNumber;
percentageFees: BigNumber; percentageFees: {
[makerAssetData: string]: BigNumber;
};
maxOversoldWeth: BigNumber; maxOversoldWeth: BigNumber;
maxOverboughtMakerAsset: BigNumber; maxOverboughtMakerAsset: BigNumber;
} }
@ -51,7 +56,7 @@ export class ForwarderTestFactory {
public async marketBuyTestAsync( public async marketBuyTestAsync(
orders: SignedOrder[], orders: SignedOrder[],
fractionalNumberOfOrdersToFill: number, fractionalNumberOfOrdersToFill: number,
makerAssetContract: DummyERC20TokenContract | DummyERC721TokenContract, makerAssetContracts: Array<DummyERC20TokenContract | DummyERC721TokenContract>,
options: { options: {
ethValueAdjustment?: number; // Used to provided insufficient/excess ETH ethValueAdjustment?: number; // Used to provided insufficient/excess ETH
forwarderFeePercentage?: BigNumber; forwarderFeePercentage?: BigNumber;
@ -83,9 +88,16 @@ export class ForwarderTestFactory {
constants.PERCENTAGE_DENOMINATOR, constants.PERCENTAGE_DENOMINATOR,
forwarderFeePercentage, forwarderFeePercentage,
); );
const totalMakerAssetFillAmount = Object.values(expectedResults.makerAssetFillAmount).reduce((prev, current) =>
prev.plus(current),
);
const totalPercentageFees = Object.values(expectedResults.percentageFees).reduce((prev, current) =>
prev.plus(current),
);
const tx = this._forwarderWrapper.marketBuyOrdersWithEthAsync( const tx = this._forwarderWrapper.marketBuyOrdersWithEthAsync(
orders, orders,
expectedResults.makerAssetFillAmount.minus(expectedResults.percentageFees), totalMakerAssetFillAmount.minus(totalPercentageFees),
{ {
value: ethValue, value: ethValue,
from: this._takerAddress, from: this._takerAddress,
@ -110,7 +122,7 @@ export class ForwarderTestFactory {
expectedResults, expectedResults,
takerEthBalanceBefore, takerEthBalanceBefore,
erc20Balances, erc20Balances,
makerAssetContract, makerAssetContracts,
{ {
forwarderFeePercentage, forwarderFeePercentage,
forwarderFeeRecipientEthBalanceBefore, forwarderFeeRecipientEthBalanceBefore,
@ -123,7 +135,7 @@ export class ForwarderTestFactory {
public async marketSellTestAsync( public async marketSellTestAsync(
orders: SignedOrder[], orders: SignedOrder[],
fractionalNumberOfOrdersToFill: number, fractionalNumberOfOrdersToFill: number,
makerAssetContract: DummyERC20TokenContract, makerAssetContracts: DummyERC20TokenContract[],
options: { options: {
forwarderFeePercentage?: BigNumber; forwarderFeePercentage?: BigNumber;
revertError?: RevertError; revertError?: RevertError;
@ -179,7 +191,7 @@ export class ForwarderTestFactory {
expectedResults, expectedResults,
takerEthBalanceBefore, takerEthBalanceBefore,
erc20Balances, erc20Balances,
makerAssetContract, makerAssetContracts,
{ {
forwarderFeePercentage, forwarderFeePercentage,
forwarderFeeRecipientEthBalanceBefore, forwarderFeeRecipientEthBalanceBefore,
@ -195,31 +207,35 @@ export class ForwarderTestFactory {
makerAssetContract: DummyERC20TokenContract, makerAssetContract: DummyERC20TokenContract,
): void { ): void {
const makerAssetAddress = makerAssetContract.address; const makerAssetAddress = makerAssetContract.address;
const makerAssetData = assetDataUtils.encodeERC20AssetData(makerAssetAddress);
const {
maxOverboughtMakerAsset,
makerAssetFillAmount: { [makerAssetData]: makerAssetFillAmount },
percentageFees: { [makerAssetData]: percentageFees },
} = expectedResults;
expectBalanceWithin( expectBalanceWithin(
newBalances[this._makerAddress][makerAssetAddress], newBalances[this._makerAddress][makerAssetAddress],
oldBalances[this._makerAddress][makerAssetAddress] oldBalances[this._makerAddress][makerAssetAddress]
.minus(expectedResults.makerAssetFillAmount) .minus(makerAssetFillAmount)
.minus(expectedResults.maxOverboughtMakerAsset), .minus(maxOverboughtMakerAsset),
oldBalances[this._makerAddress][makerAssetAddress].minus(expectedResults.makerAssetFillAmount), oldBalances[this._makerAddress][makerAssetAddress].minus(makerAssetFillAmount),
'Maker makerAsset balance', 'Maker makerAsset balance',
); );
expectBalanceWithin( expectBalanceWithin(
newBalances[this._takerAddress][makerAssetAddress], newBalances[this._takerAddress][makerAssetAddress],
oldBalances[this._takerAddress][makerAssetAddress].plus(makerAssetFillAmount).minus(percentageFees),
oldBalances[this._takerAddress][makerAssetAddress] oldBalances[this._takerAddress][makerAssetAddress]
.plus(expectedResults.makerAssetFillAmount) .plus(makerAssetFillAmount)
.minus(expectedResults.percentageFees), .minus(percentageFees)
oldBalances[this._takerAddress][makerAssetAddress] .plus(maxOverboughtMakerAsset),
.plus(expectedResults.makerAssetFillAmount)
.minus(expectedResults.percentageFees)
.plus(expectedResults.maxOverboughtMakerAsset),
'Taker makerAsset balance', 'Taker makerAsset balance',
); );
expect( expect(
newBalances[this._orderFeeRecipientAddress][makerAssetAddress], newBalances[this._orderFeeRecipientAddress][makerAssetAddress],
'Order fee recipient makerAsset balance', 'Order fee recipient makerAsset balance',
).to.be.bignumber.equal( ).to.be.bignumber.equal(oldBalances[this._orderFeeRecipientAddress][makerAssetAddress].plus(percentageFees));
oldBalances[this._orderFeeRecipientAddress][makerAssetAddress].plus(expectedResults.percentageFees),
);
expect( expect(
newBalances[this._forwarderAddress][makerAssetAddress], newBalances[this._forwarderAddress][makerAssetAddress],
'Forwarder contract makerAsset balance', 'Forwarder contract makerAsset balance',
@ -234,7 +250,7 @@ export class ForwarderTestFactory {
expectedResults: ForwarderFillState, expectedResults: ForwarderFillState,
takerEthBalanceBefore: BigNumber, takerEthBalanceBefore: BigNumber,
erc20Balances: ERC20BalancesByOwner, erc20Balances: ERC20BalancesByOwner,
makerAssetContract: DummyERC20TokenContract | DummyERC721TokenContract, makerAssetContracts: Array<DummyERC20TokenContract | DummyERC721TokenContract>,
options: { options: {
forwarderFeePercentage?: BigNumber; forwarderFeePercentage?: BigNumber;
forwarderFeeRecipientEthBalanceBefore?: BigNumber; forwarderFeeRecipientEthBalanceBefore?: BigNumber;
@ -277,11 +293,13 @@ export class ForwarderTestFactory {
); );
} }
if (makerAssetContract instanceof DummyERC20TokenContract) { for (const makerAssetContract of makerAssetContracts) {
this._checkErc20Balances(erc20Balances, newBalances, expectedResults, makerAssetContract); if (makerAssetContract instanceof DummyERC20TokenContract) {
} else if (options.makerAssetId !== undefined) { this._checkErc20Balances(erc20Balances, newBalances, expectedResults, makerAssetContract);
const newOwner = await makerAssetContract.ownerOf.callAsync(options.makerAssetId); } else if (options.makerAssetId !== undefined) {
expect(newOwner, 'New ERC721 owner').to.be.bignumber.equal(this._takerAddress); const newOwner = await makerAssetContract.ownerOf.callAsync(options.makerAssetId);
expect(newOwner, 'New ERC721 owner').to.be.bignumber.equal(this._takerAddress);
}
} }
expectBalanceWithin( expectBalanceWithin(
@ -313,18 +331,25 @@ export class ForwarderTestFactory {
ordersInfoBefore: OrderInfo[], ordersInfoBefore: OrderInfo[],
fractionalNumberOfOrdersToFill: number, fractionalNumberOfOrdersToFill: number,
): ForwarderFillState { ): ForwarderFillState {
const currentState = { const currentState: ForwarderFillState = {
takerAssetFillAmount: constants.ZERO_AMOUNT, takerAssetFillAmount: constants.ZERO_AMOUNT,
makerAssetFillAmount: constants.ZERO_AMOUNT, makerAssetFillAmount: {},
protocolFees: constants.ZERO_AMOUNT, protocolFees: constants.ZERO_AMOUNT,
wethFees: constants.ZERO_AMOUNT, wethFees: constants.ZERO_AMOUNT,
percentageFees: constants.ZERO_AMOUNT, percentageFees: {},
maxOversoldWeth: constants.ZERO_AMOUNT, maxOversoldWeth: constants.ZERO_AMOUNT,
maxOverboughtMakerAsset: constants.ZERO_AMOUNT, maxOverboughtMakerAsset: constants.ZERO_AMOUNT,
}; };
let remainingOrdersToFill = fractionalNumberOfOrdersToFill; let remainingOrdersToFill = fractionalNumberOfOrdersToFill;
for (const [i, order] of orders.entries()) { for (const [i, order] of orders.entries()) {
if (currentState.makerAssetFillAmount[order.makerAssetData] === undefined) {
currentState.makerAssetFillAmount[order.makerAssetData] = new BigNumber(0);
}
if (currentState.percentageFees[order.makerAssetData] === undefined) {
currentState.percentageFees[order.makerAssetData] = new BigNumber(0);
}
if (remainingOrdersToFill === 0) { if (remainingOrdersToFill === 0) {
break; break;
} }
@ -365,7 +390,9 @@ export class ForwarderTestFactory {
makerAssetAmount = BigNumber.max(makerAssetAmount.minus(makerAssetFilled), constants.ZERO_AMOUNT); makerAssetAmount = BigNumber.max(makerAssetAmount.minus(makerAssetFilled), constants.ZERO_AMOUNT);
currentState.takerAssetFillAmount = currentState.takerAssetFillAmount.plus(takerAssetAmount); currentState.takerAssetFillAmount = currentState.takerAssetFillAmount.plus(takerAssetAmount);
currentState.makerAssetFillAmount = currentState.makerAssetFillAmount.plus(makerAssetAmount); currentState.makerAssetFillAmount[order.makerAssetData] = currentState.makerAssetFillAmount[
order.makerAssetData
].plus(makerAssetAmount);
if (this._protocolFeeCollectorAddress !== constants.NULL_ADDRESS) { if (this._protocolFeeCollectorAddress !== constants.NULL_ADDRESS) {
currentState.protocolFees = currentState.protocolFees.plus( currentState.protocolFees = currentState.protocolFees.plus(
@ -373,7 +400,9 @@ export class ForwarderTestFactory {
); );
} }
if (order.takerFeeAssetData === order.makerAssetData) { if (order.takerFeeAssetData === order.makerAssetData) {
currentState.percentageFees = currentState.percentageFees.plus(takerFee); currentState.percentageFees[order.makerAssetData] = currentState.percentageFees[
order.makerAssetData
].plus(takerFee);
} else if (order.takerFeeAssetData === order.takerAssetData) { } else if (order.takerFeeAssetData === order.takerAssetData) {
currentState.wethFees = currentState.wethFees.plus(takerFee); currentState.wethFees = currentState.wethFees.plus(takerFee);
} }