Skip to content

Commit b450a34

Browse files
authored
Merge pull request #57 from trader-xyz/fix/v4-fees-eth-calc
Fixes 0x v4 orders that have ETH with fees
2 parents 2d5ca93 + 49ff496 commit b450a34

File tree

4 files changed

+141
-75
lines changed

4 files changed

+141
-75
lines changed

.npmignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,6 @@
22
.DS_Store
33
node_modules
44
local-scripts
5+
nft-swap-sdk-banner.jpg
6+
example.ts
7+
example-v4.ts

src/sdk/v4/NftSwapV4.ts

Lines changed: 36 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,6 @@ import type {
4242
UserFacingERC1155AssetDataSerializedV4,
4343
UserFacingERC20AssetDataSerializedV4,
4444
UserFacingERC721AssetDataSerializedV4,
45-
VerifyOrderOptionsOverrides,
4645
} from './types';
4746
import {
4847
ERC1155_TRANSFER_FROM_DATA,
@@ -57,9 +56,9 @@ import {
5756
ORDERBOOK_API_ROOT_URL_PRODUCTION,
5857
SearchOrdersResponsePayload,
5958
} from './orderbook';
59+
import { getWrappedNativeToken } from '../../utils/addresses';
6060
import { DIRECTION_MAPPING, OrderStatusV4, TradeDirection } from './enums';
6161
import { CONTRACT_ORDER_VALIDATOR } from './properties';
62-
import { getWrappedNativeToken } from '../../utils/addresses';
6362
import { ETH_ADDRESS_AS_ERC20 } from './constants';
6463
import { ZERO_AMOUNT } from '../../utils/eth';
6564

@@ -159,8 +158,6 @@ export interface AdditionalSdkConfig {
159158
orderbookRootUrl: string;
160159
}
161160

162-
export const FAKE_ETH_ADDRESS = '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee';
163-
164161
class NftSwapV4 implements INftSwapV4 {
165162
public provider: BaseProvider;
166163
public signer: Signer | undefined;
@@ -443,7 +440,7 @@ class NftSwapV4 implements INftSwapV4 {
443440

444441
// Validate that a bid does not use ETH.
445442
if (direction === TradeDirection.BuyNFT) {
446-
if (erc20.tokenAddress.toLowerCase() === FAKE_ETH_ADDRESS) {
443+
if (erc20.tokenAddress.toLowerCase() === ETH_ADDRESS_AS_ERC20) {
447444
throw new Error(
448445
'NFT Bids cannot use the native token (e.g. ETH). Please use the wrapped native token (e.g. WETH)'
449446
);
@@ -620,25 +617,36 @@ class NftSwapV4 implements INftSwapV4 {
620617
);
621618
};
622619

620+
isErc20NativeToken = (order: NftOrderV4): boolean => {
621+
return order.erc20Token.toLowerCase() === ETH_ADDRESS_AS_ERC20;
622+
};
623+
623624
fillSignedOrder = async (
624625
signedOrder: SignedNftOrderV4,
625626
fillOrderOverrides?: Partial<FillOrderOverrides>,
626627
transactionOverrides?: Partial<PayableOverrides>
627628
) => {
629+
// Only Sell orders can be filled with ETH
630+
const canOrderTypeBeFilledWithNativeToken =
631+
signedOrder.direction === TradeDirection.SellNFT;
632+
// Is ERC20 being traded the native token
633+
const isNativeToken = this.isErc20NativeToken(signedOrder);
634+
const needsEthAttached =
635+
isNativeToken && canOrderTypeBeFilledWithNativeToken;
636+
const erc20TotalAmount = this.getErc20TotalIncludingFees(signedOrder);
637+
628638
// do fill
629639
if ('erc1155Token' in signedOrder) {
630640
// If maker is selling an NFT, taker wants to 'buy' nft
631641
if (signedOrder.direction === TradeDirection.SellNFT) {
632-
const needsEthAttached =
633-
signedOrder.erc20Token.toLowerCase() === ETH_ADDRESS_AS_ERC20;
634-
635642
return this.exchangeProxy.buyERC1155(
636643
signedOrder,
637644
signedOrder.signature,
638645
signedOrder.erc1155TokenAmount,
639646
'0x',
640647
{
641-
value: needsEthAttached ? signedOrder.erc20TokenAmount : undefined,
648+
// If we're filling an order with ETH, be sure to include the value with fees added
649+
value: needsEthAttached ? erc20TotalAmount : undefined,
642650
...transactionOverrides,
643651
}
644652
);
@@ -675,17 +683,14 @@ class NftSwapV4 implements INftSwapV4 {
675683
}
676684
} else if ('erc721Token' in signedOrder) {
677685
// If maker is selling an NFT, taker wants to 'buy' nft
678-
679686
if (signedOrder.direction === TradeDirection.SellNFT) {
680-
const needsEthAttached =
681-
signedOrder.erc20Token.toLowerCase() === ETH_ADDRESS_AS_ERC20;
682-
683687
return this.exchangeProxy.buyERC721(
684688
signedOrder,
685689
signedOrder.signature,
686690
'0x',
687691
{
688-
value: needsEthAttached ? signedOrder.erc20TokenAmount : undefined,
692+
// If we're filling an order with ETH, be sure to include the value with fees added
693+
value: needsEthAttached ? erc20TotalAmount : undefined,
689694
...transactionOverrides,
690695
}
691696
);
@@ -981,19 +986,31 @@ class NftSwapV4 implements INftSwapV4 {
981986
};
982987
};
983988

989+
/**
990+
* Convenience function to sum all fees. Total fees denominated in erc20 token amount.
991+
* @param order A 0x v4 order (signed or un-signed);
992+
* @returns Total summed fees for a 0x v4 order. Amount is represented in Erc20 token units.
993+
*/
994+
getTotalFees = (order: NftOrderV4): BigNumber => {
995+
const fees = order.fees;
996+
// In 0x v4, fees are additive (not included in the original erc20 amount)
997+
let feesTotal = ZERO_AMOUNT;
998+
fees.forEach((fee) => {
999+
feesTotal = feesTotal.add(BigNumber.from(fee.amount));
1000+
});
1001+
return feesTotal;
1002+
};
1003+
9841004
/**
9851005
* Calculates total order cost.
9861006
* In 0x v4, fees are additive (i.e. they are not deducted from the order amount, but added on top of)
9871007
* @param order A 0x v4 order;
988-
* @returns BigNumber of order total cost in erc20 unit amount
1008+
* @returns Total cost of an order (base amount + fees). Amount is represented in Erc20 token units. Does not include gas costs.
9891009
*/
9901010
getErc20TotalIncludingFees = (order: NftOrderV4): BigNumber => {
9911011
const fees = order.fees;
9921012
// In 0x v4, fees are additive (not included in the original erc20 amount)
993-
let feesTotal = ZERO_AMOUNT;
994-
fees.forEach((fee) => {
995-
feesTotal = ZERO_AMOUNT.add(BigNumber.from(fee.amount));
996-
});
1013+
let feesTotal = this.getTotalFees(order);
9971014
const orderTotalCost = BigNumber.from(order.erc20TokenAmount).add(
9981015
feesTotal
9991016
);

src/sdk/v4/constants.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,3 +57,6 @@ export const PROPERTY_ABI = [
5757

5858
export const ETH_ADDRESS_AS_ERC20 =
5959
'0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee';
60+
61+
export const NATIVE_TOKEN_ADDRESS_AS_ERC20 =
62+
'0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee';

test/v4/fees.test.ts

Lines changed: 99 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,27 @@
11
import { BigNumber, ethers } from 'ethers';
2+
import { ETH_ADDRESS_AS_ERC20 } from '../../src/sdk';
23
import { NftSwapV4 } from '../../src/sdk/v4/NftSwapV4';
34

45
import { SwappableAssetV4 } from '../../src/sdk/v4/types';
56
import { SignedERC721OrderStruct } from '../../src/sdk/v4/types';
6-
import { NULL_ADDRESS } from '../../src/utils/eth';
77

88
jest.setTimeout(90 * 1000);
99

1010
const MAKER_WALLET_ADDRESS = '0xabc23F70Df4F45dD3Df4EC6DA6827CB05853eC9b';
1111
const MAKER_PRIVATE_KEY =
1212
'fc5db508b0a52da8fbcac3ab698088715595f8de9cccf2467d51952eec564ec9';
13-
// NOTE(johnrjj) - NEVER use these private keys for anything of value, testnets only!
1413

15-
const WETH_TOKEN_ADDRESS_TESTNET = '0xc778417e063141139fce010982780140aa0cd5ab';
1614
const DAI_TOKEN_ADDRESS_TESTNET = '0x31f42841c2db5173425b5223809cf3a38fede360';
1715
const TEST_NFT_CONTRACT_ADDRESS = '0xf5de760f2e916647fd766b4ad9e85ff943ce3a2b'; // https://ropsten.etherscan.io/token/0xf5de760f2e916647fd766b4ad9e85ff943ce3a2b?a=0xabc23F70Df4F45dD3Df4EC6DA6827CB05853eC9b
1816

1917
const RPC_TESTNET =
2018
'https://eth-ropsten.alchemyapi.io/v2/is1WqyAFM1nNFFx2aCozhTep7IxHVNGo';
2119

2220
const MAKER_WALLET = new ethers.Wallet(MAKER_PRIVATE_KEY);
23-
// const TAKER_WALLET = new ethers.Wallet(TAKER_PRIVATE_KEY);
2421

2522
const PROVIDER = new ethers.providers.StaticJsonRpcProvider(RPC_TESTNET);
2623

2724
const MAKER_SIGNER = MAKER_WALLET.connect(PROVIDER);
28-
// const TAKER_PROVIDER = TAKER_WALLET.connect(PROVIDER);
2925

3026
const ROPSTEN_CHAIN_ID = 3;
3127

@@ -34,26 +30,31 @@ const nftSwapperMaker = new NftSwapV4(
3430
MAKER_SIGNER,
3531
ROPSTEN_CHAIN_ID
3632
);
37-
// const nftSwapperTaker = new NftSwap(TAKER_PROVIDER as any, 4);
3833

39-
const MAKER_ASSET: SwappableAssetV4 = {
34+
const DAI_ASSET: SwappableAssetV4 = {
4035
type: 'ERC20',
4136
tokenAddress: DAI_TOKEN_ADDRESS_TESTNET,
4237
amount: '420000000000000', // 1 USDC
4338
};
4439

45-
const TAKER_ASSET: SwappableAssetV4 = {
40+
const ETH_ASSET: SwappableAssetV4 = {
41+
type: 'ERC20',
42+
tokenAddress: ETH_ADDRESS_AS_ERC20,
43+
amount: '420000000000000', // 1 USDC
44+
};
45+
46+
const NFT_ASSET: SwappableAssetV4 = {
4647
type: 'ERC721',
4748
tokenAddress: TEST_NFT_CONTRACT_ADDRESS,
4849
tokenId: '11045',
4950
};
5051

5152
describe('NFTSwapV4', () => {
52-
it('fees', async () => {
53+
it('erc20 fee', async () => {
5354
// NOTE(johnrjj) - Assumes USDC and DAI are already approved w/ the ExchangeProxy
5455
const v4Erc721Order = nftSwapperMaker.buildOrder(
55-
TAKER_ASSET,
56-
MAKER_ASSET,
56+
NFT_ASSET,
57+
DAI_ASSET,
5758
MAKER_WALLET_ADDRESS,
5859
{
5960
fees: [
@@ -63,30 +64,47 @@ describe('NFTSwapV4', () => {
6364
},
6465
],
6566
}
66-
// {
67-
// // Fix dates and salt so we have reproducible tests
68-
// expiration: new Date(3000, 10, 1),
69-
// }
7067
);
7168

72-
// console.log('v4Erc721Order.nonce', v4Erc721Order.nonce.toString());
69+
const signedOrder = (await nftSwapperMaker.signOrder(
70+
v4Erc721Order
71+
)) as SignedERC721OrderStruct;
7372

74-
// expect(v4Erc721Order.nonce.toString()./includes('-')).toBeFalsy();
73+
expect(signedOrder.fees[0].recipient).toEqual(
74+
'0xaaa1388cD71e88Ae3D8432f16bed3c603a58aD34'.toLowerCase()
75+
);
7576

76-
// const makerapprovalTx = await nftSwapperMaker.approveTokenOrNftByAsset(
77-
// MAKER_ASSET,
78-
// MAKER_WALLET_ADDRESS,
79-
// )
80-
// const makerApprovalTxHash = await (await makerapprovalTx.wait()).transactionHash
81-
// console.log('maker approval tx hash', makerApprovalTxHash)
77+
// Ensure getErc20TotalIncludingFees helper function works properly w/ fees.
78+
const total = nftSwapperMaker
79+
.getErc20TotalIncludingFees(signedOrder)
80+
.toString();
81+
const handCountedTotal = BigNumber.from(signedOrder.erc20TokenAmount).add(
82+
BigNumber.from(signedOrder.fees[0].amount)
83+
);
84+
expect(total).toBe(handCountedTotal.toString());
8285

83-
// const takerApprovalTx = await nftSwapperMaker.approveTokenOrNftByAsset(
84-
// TAKER_ASSET,
85-
// MAKER_WALLET_ADDRESS,
86-
// )
86+
// // Uncomment to actually fill order
87+
// const tx = await nftSwapperMaker.fillSignedOrder(signedOrder);
88+
// const txReceipt = await tx.wait();
89+
// expect(txReceipt.transactionHash).toBeTruthy();
90+
// console.log(`Swapped tx with fees on Ropsten (txHAsh: ${txReceipt.transactionHash})`);
91+
});
8792

88-
// const takerApprovalTxHash = await (await takerApprovalTx.wait()).transactionHash
89-
// console.log('taker approval tx hash', takerApprovalTxHash)
93+
it('eth w/ fee', async () => {
94+
// NOTE(johnrjj) - Assumes USDC and DAI are already approved w/ the ExchangeProxy
95+
const v4Erc721Order = nftSwapperMaker.buildOrder(
96+
NFT_ASSET,
97+
ETH_ASSET,
98+
MAKER_WALLET_ADDRESS,
99+
{
100+
fees: [
101+
{
102+
amount: '6900000000000',
103+
recipient: '0xaaa1388cD71e88Ae3D8432f16bed3c603a58aD34',
104+
},
105+
],
106+
}
107+
);
90108

91109
const signedOrder = (await nftSwapperMaker.signOrder(
92110
v4Erc721Order
@@ -105,41 +123,66 @@ describe('NFTSwapV4', () => {
105123
);
106124
expect(total).toBe(handCountedTotal.toString());
107125

108-
// console.log('erc721 signatuee', signedOrder.signature);
109-
// expect(signedOrder.signature.signatureType.toString()).toEqual('2');
110-
111-
// const fillTx = await nftSwapperMaker.fillSignedCollectionOrder(
112-
// signedOrder,
113-
// '11045',
114-
// {
115-
// fillOrderWithNativeTokenInsteadOfWrappedToken: false,
116-
// tokenIdToSellForCollectionOrder: '11045',
117-
// },
118-
// { gasLimit: '500000' }
126+
// Uncomment to actually fill order
127+
// const tx = await nftSwapperMaker.fillSignedOrder(signedOrder);
128+
// const txReceipt = await tx.wait();
129+
// expect(txReceipt.transactionHash).toBeTruthy();
130+
// console.log(
131+
// `Swapped tx eth with fees on Ropsten (txHAsh: ${txReceipt.transactionHash})`
119132
// );
120-
// const txReceipt = await fillTx.wait();
121-
// console.log('erc721 fill tx', txReceipt.transactionHash);
133+
});
122134

123-
// expect(txReceipt.transactionHash).toBeTruthy();
135+
it('eth w/ multiple fees', async () => {
136+
// NOTE(johnrjj) - Assumes USDC and DAI are already approved w/ the ExchangeProxy
137+
const v4Erc721Order = nftSwapperMaker.buildOrder(
138+
NFT_ASSET,
139+
ETH_ASSET,
140+
MAKER_WALLET_ADDRESS,
141+
{
142+
fees: [
143+
{
144+
amount: '6900000000000',
145+
recipient: '0xaaa1388cD71e88Ae3D8432f16bed3c603a58aD34',
146+
},
147+
{
148+
amount: '7000000000000',
149+
recipient: '0xbbb5A0ceB2344B6566a8e945872D2Ba8Fb04E58E',
150+
},
151+
],
152+
}
153+
);
124154

125-
// const normalizedOrder = normalizeOrder(order);
126-
// const signedOrder = await nftSwapperMaker.signOrder(
127-
// normalizedOrder,
128-
// );
155+
const signedOrder = (await nftSwapperMaker.signOrder(
156+
v4Erc721Order
157+
)) as SignedERC721OrderStruct;
129158

130-
// const normalizedSignedOrder = normalizeOrder(signedOrder);
159+
expect(signedOrder.fees[0].recipient).toEqual(
160+
'0xaaa1388cD71e88Ae3D8432f16bed3c603a58aD34'.toLowerCase()
161+
);
162+
expect(signedOrder.fees[1].recipient).toEqual(
163+
'0xbbb5A0ceB2344B6566a8e945872D2Ba8Fb04E58E'.toLowerCase()
164+
);
131165

132-
// expect(normalizedSignedOrder.makerAddress.toLowerCase()).toBe(
133-
// MAKER_WALLET_ADDRESS.toLowerCase()
134-
// );
166+
// Ensure getErc20TotalIncludingFees helper function works properly w/ fees.
167+
const computedTotal = nftSwapperMaker
168+
.getErc20TotalIncludingFees(signedOrder)
169+
.toString();
135170

136-
// Uncomment to actually fill order
137-
// const tx = await nftSwapperMaker.fillSignedOrder(signedOrder); //, undefined, { gasLimit: '500000'})
171+
expect(computedTotal).toBe('433900000000000');
172+
173+
const handCountedTotal = BigNumber.from(signedOrder.erc20TokenAmount)
174+
.add(BigNumber.from(signedOrder.fees[0].amount))
175+
.add(BigNumber.from(signedOrder.fees[1].amount))
176+
.toString();
138177

178+
expect(handCountedTotal).toBe('433900000000000');
179+
180+
// Uncomment to actually fill order
181+
// const tx = await nftSwapperMaker.fillSignedOrder(signedOrder);
139182
// const txReceipt = await tx.wait();
140183
// expect(txReceipt.transactionHash).toBeTruthy();
141-
// console.log(`Swapped on Rinkeby (txHAsh: ${txReceipt.transactionHash})`);
184+
// console.log(
185+
// `Swapped tx eth with multiple fees on Ropsten (txHAsh: ${txReceipt.transactionHash})`
186+
// );
142187
});
143188
});
144-
145-
// https://polygon-mumbai.g.alchemy.com/v2/VMBpFqjMYv2w-MWnc9df92w3R2TpMvSG

0 commit comments

Comments
 (0)