diff --git a/contracts/integrations/CHANGELOG.json b/contracts/integrations/CHANGELOG.json index ea50ea783e..5538ad897a 100644 --- a/contracts/integrations/CHANGELOG.json +++ b/contracts/integrations/CHANGELOG.json @@ -1,4 +1,13 @@ [ + { + "version": "2.5.0", + "changes": [ + { + "note": "Add `ChaiBridge` and `DydxBridge` gas benchmark tests.", + "pr": 2478 + } + ] + }, { "timestamp": 1582677073, "version": "2.4.2", diff --git a/contracts/integrations/test/benchmarks/chai_bridge_test.ts b/contracts/integrations/test/benchmarks/chai_bridge_test.ts new file mode 100644 index 0000000000..5c9ea2440b --- /dev/null +++ b/contracts/integrations/test/benchmarks/chai_bridge_test.ts @@ -0,0 +1,199 @@ +import { encodeERC20AssetData, encodeERC20BridgeAssetData } from '@0x/contracts-asset-proxy'; +import { ERC20TokenContract } from '@0x/contracts-erc20'; +import { ExchangeContract } from '@0x/contracts-exchange'; +import { blockchainTests, constants, expect, FillEventArgs, getRandomInteger } from '@0x/contracts-test-utils'; +import { orderHashUtils } from '@0x/order-utils'; +import { Order } from '@0x/types'; +import { BigNumber, logUtils } from '@0x/utils'; +import { DecodedLogEntry } from 'ethereum-types'; + +import { contractAddresses } from '../mainnet_fork_utils'; + +const CHONKY_DAI_WALLET = '0xe235AAa27428E32cA14089b03F532c571C7ab3c8'; +const CHONKY_CHAI_WALLET = '0xfc64382c9ce89ba1c21692a68000366a35ff0336'; +const CHONKY_WETH_WALLET = '0x4abB24590606f5bf4645185e20C4E7B97596cA3B'; +blockchainTests.configure({ + fork: { + unlockedAccounts: [CHONKY_CHAI_WALLET, CHONKY_WETH_WALLET, CHONKY_DAI_WALLET], + }, +}); + +blockchainTests.fork.skip('ChaiBridge fill benchmarks', env => { + let exchange: ExchangeContract; + + before(async () => { + exchange = new ExchangeContract(contractAddresses.exchange, env.provider, env.txDefaults); + }); + + const DAI_ADDRESS = '0x6B175474E89094C44Da98b954EedeAC495271d0F'; + const CHAI_ADDRESS = '0x06AF07097C9Eeb7fD685c692751D5C66dB49c215'; + const CHAI_BRIDGE_ASSET_DATA = encodeERC20BridgeAssetData( + DAI_ADDRESS, + contractAddresses.chaiBridge, + constants.NULL_BYTES, + ); + const DAI_ASSET_DATA = encodeERC20AssetData(DAI_ADDRESS); + const WETH_ASSET_DATA = encodeERC20AssetData(contractAddresses.etherToken); + const SIGNATURE_PRESIGN = '0x06'; + const PROTOCOL_FEE = 150e3; + const ONE_DAY = 60 * 60 * 24; + const ORDER_DEFAULTS: Order = { + chainId: 1, + exchangeAddress: contractAddresses.exchange, + expirationTimeSeconds: new BigNumber(Math.floor(Date.now() / 1e3) + ONE_DAY), + salt: getRandomInteger(0, constants.MAX_UINT256), + makerAddress: CHONKY_CHAI_WALLET, + feeRecipientAddress: constants.NULL_ADDRESS, + senderAddress: constants.NULL_ADDRESS, + takerAddress: constants.NULL_ADDRESS, + makerAssetAmount: new BigNumber(1e18), + takerAssetAmount: new BigNumber(1e18), + makerFee: constants.ZERO_AMOUNT, + takerFee: constants.ZERO_AMOUNT, + makerAssetData: CHAI_BRIDGE_ASSET_DATA, + takerAssetData: WETH_ASSET_DATA, + makerFeeAssetData: constants.NULL_BYTES, + takerFeeAssetData: constants.NULL_BYTES, + }; + + async function approveSpenderAsync( + ownerAddress: string, + spenderAddress: string, + tokenAddress: string, + ): Promise { + const token = new ERC20TokenContract(tokenAddress, env.provider, env.txDefaults); + await token.approve(spenderAddress, constants.MAX_UINT256).awaitTransactionSuccessAsync( + { + from: ownerAddress, + }, + { shouldValidate: false }, + ); + } + + describe('chai gas usage', () => { + before(async () => { + await approveSpenderAsync(CHONKY_CHAI_WALLET, contractAddresses.chaiBridge, CHAI_ADDRESS); + await approveSpenderAsync(CHONKY_WETH_WALLET, contractAddresses.erc20Proxy, contractAddresses.etherToken); + }); + + async function prepareOrderAsync(fields: Partial = {}): Promise { + const order = { + ...ORDER_DEFAULTS, + ...fields, + }; + const orderHash = orderHashUtils.getOrderHash(order); + await exchange.preSign(orderHash).awaitTransactionSuccessAsync( + { + from: order.makerAddress, + }, + { shouldValidate: false }, + ); + return order; + } + + // Last run: 282194 + it('filling one chai maker asset', async () => { + const order = await prepareOrderAsync(); + const receipt = await exchange + .fillOrder(order, order.takerAssetAmount, SIGNATURE_PRESIGN) + .awaitTransactionSuccessAsync( + { + from: CHONKY_WETH_WALLET, + value: PROTOCOL_FEE, + gasPrice: 1, + }, + { shouldValidate: false }, + ); + const fillEvent = (receipt.logs as Array>).find(log => log.event === 'Fill'); + expect(fillEvent).to.exist(''); + logUtils.log(`gas used: ${receipt.gasUsed}`); + }); + + // Last run: 292707 + it('filling one chai taker asset', async () => { + const order = await prepareOrderAsync({ + makerAddress: CHONKY_WETH_WALLET, + takerAssetData: CHAI_BRIDGE_ASSET_DATA, + makerAssetData: WETH_ASSET_DATA, + }); + const receipt = await exchange + .fillOrder(order, order.takerAssetAmount, SIGNATURE_PRESIGN) + .awaitTransactionSuccessAsync( + { + from: CHONKY_CHAI_WALLET, + value: PROTOCOL_FEE, + gasPrice: 1, + }, + { shouldValidate: false }, + ); + const fillEvent = (receipt.logs as Array>).find(log => log.event === 'Fill'); + expect(fillEvent).to.exist(''); + logUtils.log(`gas used: ${receipt.gasUsed}`); + }); + }); + + describe('dai gas usage', () => { + before(async () => { + await approveSpenderAsync(CHONKY_DAI_WALLET, contractAddresses.erc20Proxy, DAI_ADDRESS); + await approveSpenderAsync(CHONKY_WETH_WALLET, contractAddresses.erc20Proxy, contractAddresses.etherToken); + }); + + async function prepareOrderAsync(fields: Partial = {}): Promise { + const order = { + ...ORDER_DEFAULTS, + ...fields, + }; + const orderHash = orderHashUtils.getOrderHash(order); + await exchange.preSign(orderHash).awaitTransactionSuccessAsync( + { + from: order.makerAddress, + }, + { shouldValidate: false }, + ); + return order; + } + + // Last run: 124665 + it('filling one dai maker asset', async () => { + const order = await prepareOrderAsync({ + makerAddress: CHONKY_DAI_WALLET, + makerAssetData: DAI_ASSET_DATA, + }); + const receipt = await exchange + .fillOrder(order, order.takerAssetAmount, SIGNATURE_PRESIGN) + .awaitTransactionSuccessAsync( + { + from: CHONKY_WETH_WALLET, + value: PROTOCOL_FEE, + gasPrice: 1, + }, + { shouldValidate: false }, + ); + const fillEvent = (receipt.logs as Array>).find(log => log.event === 'Fill'); + expect(fillEvent).to.exist(''); + logUtils.log(`gas used: ${receipt.gasUsed}`); + }); + + // Last run: 124665 + it('filling one dai taker asset', async () => { + const order = await prepareOrderAsync({ + makerAddress: CHONKY_WETH_WALLET, + takerAssetData: DAI_ASSET_DATA, + makerAssetData: WETH_ASSET_DATA, + }); + const receipt = await exchange + .fillOrder(order, order.takerAssetAmount, SIGNATURE_PRESIGN) + .awaitTransactionSuccessAsync( + { + from: CHONKY_DAI_WALLET, + value: PROTOCOL_FEE, + gasPrice: 1, + }, + { shouldValidate: false }, + ); + const fillEvent = (receipt.logs as Array>).find(log => log.event === 'Fill'); + expect(fillEvent).to.exist(''); + logUtils.log(`gas used: ${receipt.gasUsed}`); + }); + }); +}); diff --git a/contracts/integrations/test/benchmarks/dydx_bridge_test.ts b/contracts/integrations/test/benchmarks/dydx_bridge_test.ts new file mode 100644 index 0000000000..474b1de0ad --- /dev/null +++ b/contracts/integrations/test/benchmarks/dydx_bridge_test.ts @@ -0,0 +1,267 @@ +import { + DydxBridgeActionType, + DydxBridgeData, + dydxBridgeDataEncoder, + encodeERC20AssetData, + encodeERC20BridgeAssetData, + IDydxContract, +} from '@0x/contracts-asset-proxy'; +import { ERC20TokenContract } from '@0x/contracts-erc20'; +import { ExchangeContract } from '@0x/contracts-exchange'; +import { + blockchainTests, + constants, + expect, + FillEventArgs, + getRandomInteger, + Numberish, +} from '@0x/contracts-test-utils'; +import { orderHashUtils } from '@0x/order-utils'; +import { Order } from '@0x/types'; +import { BigNumber, fromTokenUnitAmount, logUtils } from '@0x/utils'; +import { DecodedLogEntry } from 'ethereum-types'; + +import { contractAddresses } from '../mainnet_fork_utils'; + +// A chonky dai wallet. +const MAKER_ADDRESS = '0xe235AAa27428E32cA14089b03F532c571C7ab3c8'; +// Also a chonky dai wallet. +const TAKER_ADDRESS = '0x66c57bf505a85a74609d2c83e94aabb26d691e1f'; +blockchainTests.configure({ + fork: { + unlockedAccounts: [TAKER_ADDRESS, MAKER_ADDRESS], + }, +}); + +blockchainTests.fork.skip('DydxBridge fill benchmarks', env => { + let exchange: ExchangeContract; + let dydx: IDydxContract; + + before(async () => { + exchange = new ExchangeContract(contractAddresses.exchange, env.provider, env.txDefaults); + dydx = new IDydxContract(DYDX_ADDRESS, env.provider, env.txDefaults); + // Initialize a dydx account with some Dai collateral and USDC borrowed. + await approveSpenderAsync(MAKER_ADDRESS, BRIDGE_ADDRESS, DAI_ADDRESS); + await approveSpenderAsync(MAKER_ADDRESS, DYDX_ADDRESS, DAI_ADDRESS); + await dydx + .setOperators([{ operator: BRIDGE_ADDRESS, trusted: true }]) + .awaitTransactionSuccessAsync({ from: MAKER_ADDRESS }, { shouldValidate: false }); + await depositAndWithdrawAsync(100, 1); + }); + + async function approveSpenderAsync( + ownerAddress: string, + spenderAddress: string, + tokenAddress: string, + ): Promise { + const token = new ERC20TokenContract(tokenAddress, env.provider, env.txDefaults); + await token.approve(spenderAddress, constants.MAX_UINT256).awaitTransactionSuccessAsync( + { + from: ownerAddress, + }, + { shouldValidate: false }, + ); + } + + const ZERO = constants.ZERO_AMOUNT; + const BRIDGE_ADDRESS = contractAddresses.dydxBridge; + const DYDX_ACCOUNT_ID = getRandomInteger(0, constants.MAX_UINT256); + const DYDX_ADDRESS = '0x1E0447b19BB6EcFdAe1e4AE1694b0C3659614e4e'; + const DAI_ADDRESS = '0x6B175474E89094C44Da98b954EedeAC495271d0F'; + const USDC_ADDRESS = '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48'; + const TOKEN_INFO: { [addr: string]: { decimals: number; marketId: number } } = { + [DAI_ADDRESS]: { + decimals: 18, + marketId: 3, + }, + [USDC_ADDRESS]: { + decimals: 6, + marketId: 2, + }, + }; + + function encodeDydxBridgeAssetData(fromToken: string, toToken: string, depositRate: number = 1): string { + const fromTokenMarketId = new BigNumber(TOKEN_INFO[fromToken].marketId); + const toTokenMarketId = new BigNumber(TOKEN_INFO[toToken].marketId); + const bridgeData: DydxBridgeData = { + accountNumbers: [DYDX_ACCOUNT_ID], + actions: [ + ...(depositRate > 0 + ? [ + { + actionType: DydxBridgeActionType.Deposit, + accountIdx: ZERO, + marketId: fromTokenMarketId, + ...createConversionFraction(toToken, fromToken, depositRate), + }, + ] + : []), + { + actionType: DydxBridgeActionType.Withdraw, + accountIdx: ZERO, + marketId: toTokenMarketId, + ...createConversionFraction(toToken, toToken, 1), + }, + ], + }; + return encodeERC20BridgeAssetData( + toToken, + contractAddresses.dydxBridge, + dydxBridgeDataEncoder.encode({ bridgeData }), + ); + } + + // Create fraction with default 18 decimal precision. + function createConversionFraction( + fromToken: string, + toToken: string, + rate: number, + ): { + conversionRateNumerator: BigNumber; + conversionRateDenominator: BigNumber; + } { + const fromDecimals = TOKEN_INFO[fromToken].decimals; + const toDecimals = TOKEN_INFO[toToken].decimals; + return { + conversionRateNumerator: fromTokenUnitAmount(rate, toDecimals), + conversionRateDenominator: fromTokenUnitAmount(1, fromDecimals), + }; + } + + enum DydxActionType { + Deposit = 0, + Withdraw = 1, + } + + enum DydxAssetDenomination { + Wei = 0, + Par = 1, + } + + enum DydxAssetReference { + Delta = 0, + Target = 1, + } + + async function depositAndWithdrawAsync(depositSize: Numberish, withdrawSize: Numberish): Promise { + const dai = TOKEN_INFO[DAI_ADDRESS]; + const usdc = TOKEN_INFO[USDC_ADDRESS]; + await dydx + .operate( + [{ owner: MAKER_ADDRESS, number: DYDX_ACCOUNT_ID }], + [ + { + actionType: DydxActionType.Deposit, + accountIdx: ZERO, + amount: { + sign: true, + denomination: DydxAssetDenomination.Wei, + ref: DydxAssetReference.Delta, + value: fromTokenUnitAmount(depositSize, dai.decimals), + }, + primaryMarketId: new BigNumber(dai.marketId), + secondaryMarketId: new BigNumber(constants.NULL_ADDRESS), + otherAddress: MAKER_ADDRESS, + otherAccountIdx: ZERO, + data: constants.NULL_BYTES, + }, + { + actionType: DydxActionType.Withdraw, + accountIdx: ZERO, + amount: { + sign: false, + denomination: DydxAssetDenomination.Wei, + ref: DydxAssetReference.Delta, + value: fromTokenUnitAmount(withdrawSize, usdc.decimals), + }, + primaryMarketId: new BigNumber(usdc.marketId), + secondaryMarketId: new BigNumber(constants.NULL_ADDRESS), + otherAddress: MAKER_ADDRESS, + otherAccountIdx: ZERO, + data: constants.NULL_BYTES, + }, + ], + ) + .awaitTransactionSuccessAsync({ from: MAKER_ADDRESS }, { shouldValidate: false }); + } + + const DYDX_ASSET_DATA = encodeDydxBridgeAssetData(DAI_ADDRESS, USDC_ADDRESS); + const DAI_ASSET_DATA = encodeERC20AssetData(DAI_ADDRESS); + const SIGNATURE_PRESIGN = '0x06'; + const PROTOCOL_FEE = 150e3; + const ONE_DAY = 60 * 60 * 24; + const ORDER_DEFAULTS: Order = { + chainId: 1, + exchangeAddress: contractAddresses.exchange, + expirationTimeSeconds: new BigNumber(Math.floor(Date.now() / 1e3) + ONE_DAY), + salt: getRandomInteger(0, constants.MAX_UINT256), + makerAddress: MAKER_ADDRESS, + feeRecipientAddress: constants.NULL_ADDRESS, + senderAddress: constants.NULL_ADDRESS, + takerAddress: constants.NULL_ADDRESS, + makerAssetAmount: fromTokenUnitAmount(50, TOKEN_INFO[USDC_ADDRESS].decimals), + takerAssetAmount: fromTokenUnitAmount(100, TOKEN_INFO[USDC_ADDRESS].decimals), + makerFee: constants.ZERO_AMOUNT, + takerFee: constants.ZERO_AMOUNT, + makerAssetData: DYDX_ASSET_DATA, + takerAssetData: DAI_ASSET_DATA, + makerFeeAssetData: constants.NULL_BYTES, + takerFeeAssetData: constants.NULL_BYTES, + }; + + describe('gas usage', () => { + async function prepareOrderAsync(fields: Partial = {}): Promise { + const order = { + ...ORDER_DEFAULTS, + ...fields, + }; + const orderHash = orderHashUtils.getOrderHash(order); + await exchange.preSign(orderHash).awaitTransactionSuccessAsync( + { + from: order.makerAddress, + }, + { shouldValidate: false }, + ); + await approveSpenderAsync(TAKER_ADDRESS, contractAddresses.erc20Proxy, DAI_ADDRESS); + return order; + } + + // Last run: 375066 + it('filling a DAI->USDC dydx order with a deposit action', async () => { + const order = await prepareOrderAsync(); + const receipt = await exchange + .fillOrder(order, order.takerAssetAmount, SIGNATURE_PRESIGN) + .awaitTransactionSuccessAsync( + { + from: TAKER_ADDRESS, + value: PROTOCOL_FEE, + gasPrice: 1, + }, + { shouldValidate: false }, + ); + const fillEvent = (receipt.logs as Array>).find(log => log.event === 'Fill'); + expect(fillEvent).to.exist(''); + logUtils.log(`gas used: ${receipt.gasUsed}`); + }); + + // Last run: 315896 + it('filling a DAI->USDC dydx order with no deposit action', async () => { + const order = await prepareOrderAsync({ + makerAssetData: encodeDydxBridgeAssetData(DAI_ADDRESS, USDC_ADDRESS, 0), + }); + const receipt = await exchange + .fillOrder(order, order.takerAssetAmount, SIGNATURE_PRESIGN) + .awaitTransactionSuccessAsync( + { + from: TAKER_ADDRESS, + value: PROTOCOL_FEE, + gasPrice: 1, + }, + { shouldValidate: false }, + ); + const fillEvent = (receipt.logs as Array>).find(log => log.event === 'Fill'); + expect(fillEvent).to.exist(''); + logUtils.log(`gas used: ${receipt.gasUsed}`); + }); + }); +});