diff --git a/README.md b/README.md index 22ebb53..6d16d91 100644 --- a/README.md +++ b/README.md @@ -16,16 +16,22 @@ yarn add @1inch/fusion-sdk@2 ## Modules docs -- [auction-details](src/fusion-order/auction-details/README.md) -- [fusion-order](src/fusion-order/README.md) -- [sdk](src/sdk/README.md) -- [ws-api](src/ws-api/README.md) +- [auction-details](src/fusion-order/auction-details/README.md) +- [fusion-order](src/fusion-order/README.md) +- [sdk](src/sdk/README.md) +- [ws-api](src/ws-api/README.md) ## How to swap with Fusion Mode ```typescript -import {FusionSDK, NetworkEnum, OrderStatus, PrivateKeyProviderConnector, Web3Like,} from "@1inch/fusion-sdk"; -import {computeAddress, formatUnits, JsonRpcProvider} from "ethers"; +import { + FusionSDK, + NetworkEnum, + OrderStatus, + PrivateKeyProviderConnector, + Web3Like +} from '@1inch/fusion-sdk' +import {computeAddress, formatUnits, JsonRpcProvider} from 'ethers' const PRIVATE_KEY = 'YOUR_PRIVATE_KEY' const NODE_URL = 'YOUR_WEB3_NODE_URL' @@ -48,7 +54,7 @@ const connector = new PrivateKeyProviderConnector( ) const sdk = new FusionSDK({ - url: 'https://api.1inch.dev/fusion', + url: 'https://api.1inch.com/fusion', network: NetworkEnum.BINANCE, blockchainProvider: connector, authKey: DEV_PORTAL_API_TOKEN @@ -57,7 +63,7 @@ const sdk = new FusionSDK({ async function main() { const params = { fromTokenAddress: '0x8ac76a51cc950d9822d68b83fe1ad97b32cd580d', // USDC - toTokenAddress: '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', // BNB + toTokenAddress: '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', // BNB amount: '10000000000000000000', // 10 USDC walletAddress: computeAddress(PRIVATE_KEY), source: 'sdk-test' @@ -66,12 +72,25 @@ async function main() { const quote = await sdk.getQuote(params) const dstTokenDecimals = 18 - console.log('Auction start amount', formatUnits(quote.presets[quote.recommendedPreset].auctionStartAmount, dstTokenDecimals)) - console.log('Auction end amount', formatUnits(quote.presets[quote.recommendedPreset].auctionEndAmount), dstTokenDecimals) + console.log( + 'Auction start amount', + formatUnits( + quote.presets[quote.recommendedPreset].auctionStartAmount, + dstTokenDecimals + ) + ) + console.log( + 'Auction end amount', + formatUnits(quote.presets[quote.recommendedPreset].auctionEndAmount), + dstTokenDecimals + ) const preparedOrder = await sdk.createOrder(params) - const info = await sdk.submitOrder(preparedOrder.order, preparedOrder.quoteId) + const info = await sdk.submitOrder( + preparedOrder.order, + preparedOrder.quoteId + ) console.log('OrderHash', info.orderHash) @@ -90,7 +109,7 @@ async function main() { console.log('Order Expired') break } - + if (data.status === OrderStatus.Cancelled) { console.log('Order Cancelled') break @@ -98,7 +117,6 @@ async function main() { } catch (e) { console.log(e) } - } console.log('Order executed for', (Date.now() - start) / 1000, 'sec') @@ -108,9 +126,18 @@ main() ``` ## How to swap with Fusion mode from Native asset + ```typescript -import {FusionSDK, NetworkEnum, OrderStatus, PrivateKeyProviderConnector, Web3Like, Address, NativeOrdersFactory} from "@1inch/fusion-sdk"; -import {computeAddress, formatUnits, JsonRpcProvider, Wallet} from "ethers"; +import { + FusionSDK, + NetworkEnum, + OrderStatus, + PrivateKeyProviderConnector, + Web3Like, + Address, + NativeOrdersFactory +} from '@1inch/fusion-sdk' +import {computeAddress, formatUnits, JsonRpcProvider, Wallet} from 'ethers' const PRIVATE_KEY = 'YOUR_PRIVATE_KEY' const NODE_URL = 'YOUR_WEB3_NODE_URL' @@ -133,7 +160,7 @@ const connector = new PrivateKeyProviderConnector( ) const sdk = new FusionSDK({ - url: 'https://api.1inch.dev/fusion', + url: 'https://api.1inch.com/fusion', network: NetworkEnum.BINANCE, blockchainProvider: connector, authKey: DEV_PORTAL_API_TOKEN @@ -144,26 +171,43 @@ const wallet = new Wallet(PRIVATE_KEY, ethersRpcProvider) async function main() { const params = { fromTokenAddress: '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', // ETH - toTokenAddress: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', // USDC + toTokenAddress: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', // USDC amount: '2000000000000000', // 0.002 ETH walletAddress: computeAddress(PRIVATE_KEY), source: 'sdk-test' } - + const quote = await sdk.getQuote(params) const dstTokenDecimals = 6 - console.log('Auction start amount', formatUnits(quote.presets[quote.recommendedPreset].auctionStartAmount, dstTokenDecimals)) - console.log('Auction end amount', formatUnits(quote.presets[quote.recommendedPreset].auctionEndAmount), dstTokenDecimals) + console.log( + 'Auction start amount', + formatUnits( + quote.presets[quote.recommendedPreset].auctionStartAmount, + dstTokenDecimals + ) + ) + console.log( + 'Auction end amount', + formatUnits(quote.presets[quote.recommendedPreset].auctionEndAmount), + dstTokenDecimals + ) const preparedOrder = await sdk.createOrder(params) - const info = await sdk.submitNativeOrder(preparedOrder.order, new Address(params.walletAddress), preparedOrder.quoteId) + const info = await sdk.submitNativeOrder( + preparedOrder.order, + new Address(params.walletAddress), + preparedOrder.quoteId + ) console.log('OrderHash', info.orderHash) const factory = NativeOrdersFactory.default(NetworkEnum.BINANCE) - const call = factory.create(new Address(wallet.address), preparedOrder.order.build()) + const call = factory.create( + new Address(wallet.address), + preparedOrder.order.build() + ) const txRes = await wallet.sendTransaction({ to: call.to.toString(), @@ -175,7 +219,6 @@ async function main() { await wallet.provider.waitForTransaction(txRes.hash) - const start = Date.now() while (true) { @@ -191,7 +234,7 @@ async function main() { console.log('Order Expired') break } - + if (data.status === OrderStatus.Cancelled) { console.log('Order Cancelled') break @@ -199,7 +242,6 @@ async function main() { } catch (e) { console.log(e) } - } console.log('Order executed for', (Date.now() - start) / 1000, 'sec') @@ -208,6 +250,113 @@ async function main() { main() ``` +## How to swap with Fusion Mode using TransferPermit + +Instead of granting a token approval to the 1inch Limit Order Protocol, you can use a `TransferPermit` for signature-based transfers via a Permit2Proxy contract. + +The maker only needs to approve tokens to the Permit2 contract once. Each order then carries a single-use `PermitTransferFrom` signature instead of an on-chain allowance to the protocol. + +```typescript +import { + FusionSDK, + NetworkEnum, + OrderStatus, + PrivateKeyProviderConnector, + Web3Like, + getPermit2Address +} from '@1inch/fusion-sdk' +import {computeAddress, JsonRpcProvider, Wallet} from 'ethers' + +const PRIVATE_KEY = 'YOUR_PRIVATE_KEY' +const NODE_URL = 'YOUR_WEB3_NODE_URL' +const DEV_PORTAL_API_TOKEN = 'YOUR_DEV_PORTAL_API_TOKEN' + +const ethersRpcProvider = new JsonRpcProvider(NODE_URL) + +const ethersProviderConnector: Web3Like = { + eth: { + call(transactionConfig): Promise { + return ethersRpcProvider.call(transactionConfig) + } + }, + extend(): void {} +} + +const connector = new PrivateKeyProviderConnector( + PRIVATE_KEY, + ethersProviderConnector +) + +const sdk = new FusionSDK({ + url: 'https://api.1inch.com/fusion', + network: NetworkEnum.ETHEREUM, + blockchainProvider: connector, + authKey: DEV_PORTAL_API_TOKEN +}) + +const wallet = new Wallet(PRIVATE_KEY, ethersRpcProvider) + +async function main() { + // Step 1: Approve token to the Permit2 contract (one-time, can be unlimited) + // This replaces the usual approval to the 1inch Limit Order Protocol + const permit2Address = getPermit2Address(NetworkEnum.ETHEREUM) + // await approveToken(fromTokenAddress, permit2Address, MAX_UINT256) + + // Step 2: Get quote and create order + const params = { + fromTokenAddress: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', // WETH + toTokenAddress: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', // USDC + amount: '50000000000000000', // 0.05 WETH + walletAddress: computeAddress(PRIVATE_KEY) + } + + const {order, quoteId} = await sdk.createOrder(params) + + // Step 3: Create a transfer permit for the order + const permit = order.createTransferPermit(NetworkEnum.ETHEREUM) + + // Step 4: Sign the transfer permit + const permitTypedData = permit.getTypedData(NetworkEnum.ETHEREUM) + const permitSignature = await connector.signTypedData( + params.walletAddress, + permitTypedData + ) + + // Step 5: Attach the signed permit to the order + const orderWithPermit = order.withTransferPermit(permit, permitSignature) + + // Step 6: Submit the order (the SDK signs the order and sends it to the relayer) + const info = await sdk.submitOrder(orderWithPermit, quoteId) + + console.log('OrderHash', info.orderHash) + + while (true) { + const data = await sdk.getOrderStatus(info.orderHash) + + if (data.status === OrderStatus.Filled) { + console.log('fills', data.fills) + break + } + + if ( + data.status === OrderStatus.Expired || + data.status === OrderStatus.Cancelled + ) { + console.log('Order', data.status) + break + } + } +} + +main() +``` + +**Key differences from a standard swap:** + +- Token approval goes to `Permit2` instead of the 1inch protocol +- Create and sign a `PermitTransferFrom` using the Permit2Proxy address as spender +- Call `withTransferPermit` before submitting — this modifies the order to route through the Permit2Proxy + ## Resolvers `settleOrders` function usage and Resolver contract examples you can find [here](https://github.com/1inch/fusion-resolver-example) diff --git a/contracts/src/ImmutableOwner.sol b/contracts/src/ImmutableOwner.sol new file mode 100644 index 0000000..8774599 --- /dev/null +++ b/contracts/src/ImmutableOwner.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.23; + +/// @title A helper contract with helper modifiers to allow access to original contract creator only +contract ImmutableOwner { + error IOAccessDenied(); + + address public immutable IMMUTABLE_OWNER; + + modifier onlyImmutableOwner() { + if (msg.sender != IMMUTABLE_OWNER) revert IOAccessDenied(); + _; + } + + constructor(address _immutableOwner) { + IMMUTABLE_OWNER = _immutableOwner; + } +} diff --git a/contracts/src/Permit2Proxy.sol b/contracts/src/Permit2Proxy.sol new file mode 100644 index 0000000..508fc4d --- /dev/null +++ b/contracts/src/Permit2Proxy.sol @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.23; + +import '@openzeppelin/contracts/token/ERC20/IERC20.sol'; + +import './interfaces/IPermit2TransferFrom.sol'; +import './ImmutableOwner.sol'; + +/* solhint-disable func-name-mixedcase */ + +/// @title Permit2Proxy +/// @notice A proxy contract that enables using Uniswap's Permit2 `permitTransferFrom` within the limit order protocol. +/// @dev Permit2 nonces are single-use + +contract Permit2Proxy is ImmutableOwner { + /// @notice The Permit2 contract address. + /// @dev Use `0x000000000022D473030F116dDEE9F6B43aC78BA3` for EVM chains + /// or `0x0000000000225e31d15943971f47ad3022f714fa` for zkSync Era. + /// See https://docs.uniswap.org/contracts/v3/reference/deployments + IPermit2TransferFrom private immutable _PERMIT2; + + /// @notice Thrown when `func_nZHTch` selector does not match `IERC20.transferFrom` selector. + error Permit2ProxyBadSelector(); + + /// @notice Initializes the proxy with the immutable owner and the Permit2 contract address. + /// @param _immutableOwner The address of the limit order protocol contract. + /// @param _permit2 The Permit2 contract address for the target chain. + constructor( + address _immutableOwner, + address _permit2 + ) ImmutableOwner(_immutableOwner) { + if (Permit2Proxy.func_nZHTch.selector != IERC20.transferFrom.selector) + revert Permit2ProxyBadSelector(); + _PERMIT2 = IPermit2TransferFrom(_permit2); + } + + /// @notice Proxy transfer method for `Permit2.permitTransferFrom`. Selector must match `IERC20.transferFrom`. + /// @dev The function name `func_nZHTch` is chosen so that its selector equals `0x23b872dd` + /// (same as `IERC20.transferFrom`), allowing it to be used as a maker asset in limit orders. + /// keccak256("func_nZHTch(address,address,uint256,((address,uint256),uint256,uint256),bytes)") == 0x23b872dd + /// @param from The token owner whose tokens are being transferred. + /// @param to The recipient of the tokens. + /// @param amount The amount of tokens to transfer. + /// @param permit The Permit2 permit data containing token permissions, nonce, and deadline. + /// @param sig The signature authorizing the transfer, signed by `from`. + function func_nZHTch( + address from, + address to, + uint256 amount, + IPermit2TransferFrom.PermitTransferFrom calldata permit, + bytes calldata sig + ) external onlyImmutableOwner { + _PERMIT2.permitTransferFrom( + permit, + IPermit2TransferFrom.SignatureTransferDetails({ + to: to, + requestedAmount: amount + }), + from, + sig + ); + } +} + +/* solhint-enable func-name-mixedcase */ diff --git a/contracts/src/interfaces/IPermit2TransferFrom.sol b/contracts/src/interfaces/IPermit2TransferFrom.sol new file mode 100644 index 0000000..d3db517 --- /dev/null +++ b/contracts/src/interfaces/IPermit2TransferFrom.sol @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +/// @title IPermit2TransferFrom +/// @notice Interface for Uniswap's Permit2 SignatureTransfer `permitTransferFrom` functionality. +/// @custom:security-contact security@1inch.io +interface IPermit2TransferFrom { + struct TokenPermissions { + // ERC20 token address + address token; + // the maximum amount that can be spent + uint256 amount; + } + + struct PermitTransferFrom { + TokenPermissions permitted; + // a unique value for every token owner's signature to prevent signature replays + uint256 nonce; + // deadline on the permit signature + uint256 deadline; + } + + struct SignatureTransferDetails { + // recipient address + address to; + // spender requested amount + uint256 requestedAmount; + } + + /// @notice Transfers tokens using a signed permit. + /// @param permit The permit data containing token permissions, nonce, and deadline. + /// @param transferDetails The transfer recipient and requested amount. + /// @param owner The token owner who signed the permit. + /// @param signature The signature authorizing the transfer. + function permitTransferFrom( + PermitTransferFrom calldata permit, + SignatureTransferDetails calldata transferDetails, + address owner, + bytes calldata signature + ) external; +} diff --git a/foundry.toml b/foundry.toml index f2b67a8..cd6f104 100644 --- a/foundry.toml +++ b/foundry.toml @@ -1,5 +1,6 @@ [profile.default] -solc = "0.8.23" +# solc = "0.8.23" +auto_detect_solc = true src = 'contracts/src' out = 'dist/contracts' libs = ['contracts/lib'] diff --git a/package.json b/package.json index 81a9127..6e44555 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@1inch/fusion-sdk", - "version": "2.4.6", + "version": "2.4.7-rc.3", "description": "1inch Fusion SDK", "author": "@1inch", "files": [ diff --git a/src/api/quoter/quoter.api.spec.ts b/src/api/quoter/quoter.api.spec.ts index b0e349c..f2c6b67 100644 --- a/src/api/quoter/quoter.api.spec.ts +++ b/src/api/quoter/quoter.api.spec.ts @@ -5,7 +5,6 @@ import {Quote} from './quote/index.js' import {PresetEnum, QuoterResponse} from './types.js' import {QuoterCustomPresetRequest} from './quoter-custom-preset.request.js' import {HttpProviderConnector} from '../../connector/index.js' -import {ONE_INCH_LIMIT_ORDER_V4} from '../../constants.js' describe('Quoter API', () => { let httpProvider: HttpProviderConnector @@ -121,7 +120,7 @@ describe('Quoter API', () => { ], fee: { whitelistDiscountPercent: 1, - receiver: ONE_INCH_LIMIT_ORDER_V4, + receiver: '0x02f92800F57BCD74066F5709F1Daa1A4302Df875', bps: 10 }, marketAmount: '626772029219852913' diff --git a/src/constants.ts b/src/constants.ts index c9a23d9..6d443ca 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -16,9 +16,26 @@ export enum NetworkEnum { UNICHAIN = 130 } +/** @deprecated Use ONE_INCH_LIMIT_ORDER_V4_ADDRESSES instead */ export const ONE_INCH_LIMIT_ORDER_V4 = '0x111111125421ca6dc452d289314280a0f8842a65' +export const ONE_INCH_LIMIT_ORDER_V4_ADDRESSES: Record = { + [NetworkEnum.ZKSYNC]: '0x6fd4383cb451173d5f9304f041c7bcbf27d561ff', + [NetworkEnum.ETHEREUM]: '0x111111125421ca6dc452d289314280a0f8842a65', + [NetworkEnum.POLYGON]: '0x111111125421ca6dc452d289314280a0f8842a65', + [NetworkEnum.BINANCE]: '0x111111125421ca6dc452d289314280a0f8842a65', + [NetworkEnum.ARBITRUM]: '0x111111125421ca6dc452d289314280a0f8842a65', + [NetworkEnum.AVALANCHE]: '0x111111125421ca6dc452d289314280a0f8842a65', + [NetworkEnum.OPTIMISM]: '0x111111125421ca6dc452d289314280a0f8842a65', + [NetworkEnum.FANTOM]: '0x111111125421ca6dc452d289314280a0f8842a65', + [NetworkEnum.GNOSIS]: '0x111111125421ca6dc452d289314280a0f8842a65', + [NetworkEnum.COINBASE]: '0x111111125421ca6dc452d289314280a0f8842a65', + [NetworkEnum.LINEA]: '0x111111125421ca6dc452d289314280a0f8842a65', + [NetworkEnum.SONIC]: '0x111111125421ca6dc452d289314280a0f8842a65', + [NetworkEnum.UNICHAIN]: '0x111111125421ca6dc452d289314280a0f8842a65' +} + export const UINT_160_MAX = (1n << 160n) - 1n export const UINT_16_MAX = (1n << 16n) - 1n export const UINT_80_MAX = (1n << 80n) - 1n diff --git a/src/fusion-order/fusion-order.spec.ts b/src/fusion-order/fusion-order.spec.ts index d8a14e1..b203c8f 100644 --- a/src/fusion-order/fusion-order.spec.ts +++ b/src/fusion-order/fusion-order.spec.ts @@ -790,4 +790,164 @@ describe('FusionOrder Native', () => { expect(nativeOrder.build().receiver).toEqual(settlementExt.toString()) }) + + describe('isTransferPermit', () => { + const extensionContract = new Address( + '0x8273f37417da37c4a6c3995e82cf442f87a25d9c' + ) + + const permit2Proxy = new Address( + '0x1234567890abcdef1234567890abcdef12345678' + ) + + const baseOrder = (): FusionOrder => + FusionOrder.new( + extensionContract, + { + makerAsset: new Address( + '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2' + ), + takerAsset: new Address( + '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48' + ), + makingAmount: parseEther('1'), + takingAmount: parseUnits('1000', 6), + maker: new Address( + '0x00000000219ab540356cbb839cbe05303d7705fa' + ) + }, + { + auction: new AuctionDetails({ + duration: 180n, + startTime: 1673548149n, + initialRateBump: 0, + points: [] + }), + whitelist: Whitelist.new(1673548139n, [ + { + address: new Address( + '0x00000000219ab540356cbb839cbe05303d7705fa' + ), + allowFrom: 0n + } + ]), + surplus: SurplusParams.NO_FEE + }, + { + allowPartialFills: false, + allowMultipleFills: false, + nonce: 1n + } + ) + + it('should return false for regular order', () => { + const order = FusionOrder.new( + extensionContract, + { + makerAsset: new Address( + '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2' + ), + takerAsset: new Address( + '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48' + ), + makingAmount: parseEther('1'), + takingAmount: parseUnits('1000', 6), + maker: new Address( + '0x00000000219ab540356cbb839cbe05303d7705fa' + ) + }, + { + auction: new AuctionDetails({ + duration: 180n, + startTime: 1673548149n, + initialRateBump: 0, + points: [] + }), + whitelist: Whitelist.new(1673548139n, [ + { + address: new Address( + '0x00000000219ab540356cbb839cbe05303d7705fa' + ), + allowFrom: 0n + } + ]), + surplus: SurplusParams.NO_FEE + } + ) + + expect(order.isTransferPermit()).toBe(false) + }) + + it('should return true after withTransferPermit', () => { + const order = baseOrder() + const permit = order.createTransferPermit(permit2Proxy) + const fakeSignature = '0x' + 'ab'.repeat(65) + + const orderWithPermit = order.withTransferPermit( + permit, + fakeSignature + ) + + expect(orderWithPermit.isTransferPermit()).toBe(true) + }) + + it('should return real token as makerAsset after withTransferPermit', () => { + const weth = new Address( + '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2' + ) + const order = baseOrder() + const permit = order.createTransferPermit(permit2Proxy) + const fakeSignature = '0x' + 'ab'.repeat(65) + + const orderWithPermit = order.withTransferPermit( + permit, + fakeSignature + ) + + expect(orderWithPermit.makerAsset).toEqual(weth) + }) + + it('should round-trip Permit2 order through fromDataAndExtension', () => { + const order = baseOrder() + const permit = order.createTransferPermit(permit2Proxy) + const fakeSignature = '0x' + 'ab'.repeat(65) + + const orderWithPermit = order.withTransferPermit( + permit, + fakeSignature + ) + + const built = orderWithPermit.build() + const rebuilt = FusionOrder.fromDataAndExtension( + built, + orderWithPermit.extension + ) + + expect(rebuilt.isTransferPermit()).toBe(true) + expect(rebuilt.makerAsset).toEqual(orderWithPermit.makerAsset) + expect(rebuilt.salt).toEqual(orderWithPermit.salt) + expect(rebuilt.build()).toEqual(built) + }) + + it('should reject tampered makerAssetSuffix via salt check', () => { + const order = baseOrder() + + const ext = order.extension + const tampered = new Extension({ + makerAssetSuffix: '0xdeadbeef', + takerAssetSuffix: ext.takerAssetSuffix, + makingAmountData: ext.makingAmountData, + takingAmountData: ext.takingAmountData, + predicate: ext.predicate, + makerPermit: ext.makerPermit, + preInteraction: ext.preInteraction, + postInteraction: ext.postInteraction, + customData: ext.customData + }) + + expect(() => + FusionOrder.fromDataAndExtension(order.build(), tampered) + ).toThrow('invalid salt for passed extension') + }) + }) }) diff --git a/src/fusion-order/fusion-order.ts b/src/fusion-order/fusion-order.ts index 3c5481d..564c604 100644 --- a/src/fusion-order/fusion-order.ts +++ b/src/fusion-order/fusion-order.ts @@ -7,8 +7,10 @@ import { LimitOrderV4Struct, MakerTraits, OrderInfoData, - ProxyFactory + ProxyFactory, + randBigInt } from '@1inch/limit-order-sdk' +import {UINT_256_MAX} from '@1inch/byte-utils' import assert from 'assert' import {FusionExtension} from './fusion-extension.js' import {AuctionDetails} from './auction-details/index.js' @@ -17,6 +19,12 @@ import {injectTrackCode} from './source-track.js' import {Whitelist} from './whitelist/whitelist.js' import {SurplusParams} from './surplus-params.js' import type {Details, Extra} from './types.js' +import {PermitTransferFrom} from './permit/permit-transfer-from.js' +import {getPermit2ProxyAddress} from './permit/utils.js' +import { + DecodedTransferPermitSuffix, + decodeTransferFromSuffix +} from './permit/transfer-from-suffix.js' import {AuctionCalculator} from '../amount-calculator/auction-calculator/index.js' import {NetworkEnum, ZX} from '../constants.js' import {calcTakingAmount} from '../utils/amounts.js' @@ -165,6 +173,10 @@ export class FusionOrder { } get makerAsset(): Address { + if (this.isTransferPermit()) { + return this.decodeTransferPermitSuffix().token + } + return this.inner.makerAsset } @@ -400,6 +412,10 @@ export class FusionOrder { } ) + if (extension.makerAssetSuffix !== ZX) { + fusionOrder.restoreMakerAssetSuffix(extension.makerAssetSuffix) + } + assert( providedSalt === fusionOrder.salt, 'invalid salt for passed extension' @@ -408,6 +424,58 @@ export class FusionOrder { return fusionOrder } + /** + * Returns true if the order uses a Permit2 transfer permit via Permit2Proxy. + * Decodes `makerAssetSuffix` and validates the Permit2 ABI structure. + * + * @see FusionOrder.withTransferPermit + * @see FusionOrder.createTransferPermit + */ + public isTransferPermit(): boolean { + try { + this.decodeTransferPermitSuffix() + + return true + } catch { + return false + } + } + + public withTransferPermit( + permit: PermitTransferFrom, + signature: string + ): this { + const suffix = permit.getTransferFromSuffix(signature) + + const currentExtension = this.inner.extension + const newExtension = new Extension({ + ...currentExtension, + makerAssetSuffix: suffix + }) + + this.inner.makerTraits.disablePermit2() + + const baseSalt = this.inner.salt >> 160n + const newSalt = LimitOrder.buildSalt(newExtension, baseSalt) + + this.inner = new LimitOrder( + { + maker: this.inner.maker, + makerAsset: permit.spender, + takerAsset: this.inner.takerAsset, + makingAmount: this.inner.makingAmount, + takingAmount: this.inner.takingAmount, + receiver: this.inner.receiver, + salt: newSalt + }, + this.inner.makerTraits, + newExtension, + {optimizeReceiverAddress: false} + ) + + return this + } + public build(): LimitOrderV4Struct { return this.inner.build() } @@ -693,4 +761,87 @@ export class FusionOrder { public nativeSignature(maker: Address): string { return this.inner.nativeSignature(maker) } + + /** + * Creates a Permit2 `PermitTransferFrom` object for the order's maker asset. + * + * Can only be used for orders where `multipleFillsAllowed` is `false`. + * + * The returned permit authorizes the `permit2Proxy` address (as spender) + * to transfer up to `makingAmount` of the `makerAsset` token, + * with a random 256-bit nonce and the order's deadline. + * + * The resulting permit can be signed and then attached to the order + * via {@link FusionOrder.withTransferPermit}. + * + * @param chainId - The chain ID used to resolve the default Permit2Proxy address + * @param permit2Proxy - Optional address of the Permit2Proxy contract that will act as spender. + * Defaults to the built-in address for the given `chainId`. + * @returns A {@link PermitTransferFrom} instance that can be signed and attached to the order + * + * @throws If `multipleFillsAllowed` is `true` + * + * @see FusionOrder.withTransferPermit + */ + public createTransferPermit( + chainIdOrPermit2Proxy: number | Address, + permit2Proxy?: Address + ): PermitTransferFrom { + assert( + !this.multipleFillsAllowed, + 'transfer permit can be used only for orders where multipleFillsAllowed=false' + ) + + const spender = + chainIdOrPermit2Proxy instanceof Address + ? chainIdOrPermit2Proxy + : (permit2Proxy ?? + getPermit2ProxyAddress(chainIdOrPermit2Proxy)) + + return new PermitTransferFrom( + this.makerAsset, + this.makingAmount, + spender, + randBigInt(UINT_256_MAX), + this.deadline + ) + } + + /** + * Restores the original `makerAssetSuffix` that `FusionExtension.build()` does not preserve. + * Recomputes the salt to match the patched extension hash. + */ + private restoreMakerAssetSuffix(makerAssetSuffix: string): void { + const patchedExtension = new Extension({ + ...this.inner.extension, + makerAssetSuffix + }) + + const baseSalt = this.inner.salt >> 160n + + this.inner = new LimitOrder( + { + maker: this.inner.maker, + makerAsset: this.inner.makerAsset, + takerAsset: this.inner.takerAsset, + makingAmount: this.inner.makingAmount, + takingAmount: this.inner.takingAmount, + receiver: this.inner.receiver, + salt: LimitOrder.buildSalt(patchedExtension, baseSalt) + }, + this.inner.makerTraits, + patchedExtension, + {optimizeReceiverAddress: false} + ) + } + + private decodeTransferPermitSuffix(): DecodedTransferPermitSuffix { + const suffix = this.inner.extension.makerAssetSuffix + + if (suffix === ZX) { + throw new Error('no makerAssetSuffix') + } + + return decodeTransferFromSuffix(suffix) + } } diff --git a/src/fusion-order/index.ts b/src/fusion-order/index.ts index c173d9a..24870e5 100644 --- a/src/fusion-order/index.ts +++ b/src/fusion-order/index.ts @@ -6,3 +6,4 @@ export * from './fees/index.js' export {CHAIN_TO_WRAPPER} from './constants.js' export * from './surplus-params.js' export * from './cancellation-auction.js' +export * from './permit/index.js' diff --git a/src/fusion-order/permit/constants.ts b/src/fusion-order/permit/constants.ts new file mode 100644 index 0000000..36c5dba --- /dev/null +++ b/src/fusion-order/permit/constants.ts @@ -0,0 +1,60 @@ +import {EIP712Types} from '@1inch/limit-order-sdk' +import {NetworkEnum} from '../../constants.js' + +const PERMIT2_ADDRESS = '0x000000000022D473030F116dDEE9F6B43aC78BA3' +const PERMIT2_ADDRESS_ZK = '0x0000000000225e31d15943971f47ad3022f714fa' + +export const PERMIT2_ADDRESSES: Record = { + [NetworkEnum.ZKSYNC]: PERMIT2_ADDRESS_ZK, + [NetworkEnum.ARBITRUM]: PERMIT2_ADDRESS, + [NetworkEnum.ETHEREUM]: PERMIT2_ADDRESS, + [NetworkEnum.POLYGON]: PERMIT2_ADDRESS, + [NetworkEnum.BINANCE]: PERMIT2_ADDRESS, + [NetworkEnum.AVALANCHE]: PERMIT2_ADDRESS, + [NetworkEnum.OPTIMISM]: PERMIT2_ADDRESS, + [NetworkEnum.FANTOM]: PERMIT2_ADDRESS, + [NetworkEnum.GNOSIS]: PERMIT2_ADDRESS, + [NetworkEnum.COINBASE]: PERMIT2_ADDRESS, + [NetworkEnum.LINEA]: PERMIT2_ADDRESS, + [NetworkEnum.SONIC]: PERMIT2_ADDRESS, + [NetworkEnum.UNICHAIN]: PERMIT2_ADDRESS +} + +// todo: update +const PERMIT2_PROXY_ADDRESS = '0xcf56da25062c954b252515244dfefb739c254c23' +const PERMIT2_PROXY_ADDRESS_ZK = '0x0000000000000000000000000000000000000000' + +export const PERMIT2_PROXY_ADDRESSES: Record = { + [NetworkEnum.ZKSYNC]: PERMIT2_PROXY_ADDRESS_ZK, + [NetworkEnum.ARBITRUM]: PERMIT2_PROXY_ADDRESS, + [NetworkEnum.ETHEREUM]: PERMIT2_PROXY_ADDRESS, + [NetworkEnum.POLYGON]: PERMIT2_PROXY_ADDRESS, + [NetworkEnum.BINANCE]: PERMIT2_PROXY_ADDRESS, + [NetworkEnum.AVALANCHE]: PERMIT2_PROXY_ADDRESS, + [NetworkEnum.OPTIMISM]: PERMIT2_PROXY_ADDRESS, + [NetworkEnum.FANTOM]: PERMIT2_PROXY_ADDRESS, + [NetworkEnum.GNOSIS]: PERMIT2_PROXY_ADDRESS, + [NetworkEnum.COINBASE]: PERMIT2_PROXY_ADDRESS, + [NetworkEnum.LINEA]: PERMIT2_PROXY_ADDRESS, + [NetworkEnum.SONIC]: PERMIT2_PROXY_ADDRESS, + [NetworkEnum.UNICHAIN]: PERMIT2_PROXY_ADDRESS +} + +export const PERMIT2_DOMAIN_NAME = 'Permit2' + +export const TOKEN_PERMISSIONS: EIP712Types = { + TokenPermissions: [ + {name: 'token', type: 'address'}, + {name: 'amount', type: 'uint256'} + ] +} + +export const PERMIT_TRANSFER_FROM_TYPES: EIP712Types = { + PermitTransferFrom: [ + {name: 'permitted', type: 'TokenPermissions'}, + {name: 'spender', type: 'address'}, + {name: 'nonce', type: 'uint256'}, + {name: 'deadline', type: 'uint256'} + ], + ...TOKEN_PERMISSIONS +} diff --git a/src/fusion-order/permit/index.ts b/src/fusion-order/permit/index.ts new file mode 100644 index 0000000..7c2dce7 --- /dev/null +++ b/src/fusion-order/permit/index.ts @@ -0,0 +1,3 @@ +export * from './permit-transfer-from.js' +export * from './transfer-from-suffix.js' +export {getPermit2Address, getPermit2ProxyAddress} from './utils.js' diff --git a/src/fusion-order/permit/permit-transfer-from.spec.ts b/src/fusion-order/permit/permit-transfer-from.spec.ts new file mode 100644 index 0000000..ab98192 --- /dev/null +++ b/src/fusion-order/permit/permit-transfer-from.spec.ts @@ -0,0 +1,121 @@ +import {Address} from '@1inch/limit-order-sdk' +import {verifyTypedData, Wallet} from 'ethers' +import {PermitTransferFrom} from './permit-transfer-from.js' +import {PERMIT2_DOMAIN_NAME, PERMIT_TRANSFER_FROM_TYPES} from './constants.js' +import {getPermit2Address} from './utils.js' +import {NetworkEnum} from '../../constants.js' + +describe('PermitTransferFrom', () => { + const token = new Address('0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2') + const spender = new Address('0x1111111254eeb25477b68fb85ed929f73a960582') + const maxSpendAmount = 1000000000000000000n + const nonce = 42n + const deadline = 1700000000n + + it('should return correct typed data for ethereum', () => { + const permit = new PermitTransferFrom( + token, + maxSpendAmount, + spender, + nonce, + deadline + ) + + const typedData = permit.getTypedData(NetworkEnum.ETHEREUM) + + expect(typedData).toStrictEqual({ + primaryType: 'PermitTransferFrom', + types: PERMIT_TRANSFER_FROM_TYPES, + domain: { + name: PERMIT2_DOMAIN_NAME, + chainId: NetworkEnum.ETHEREUM, + verifyingContract: getPermit2Address(NetworkEnum.ETHEREUM) + }, + message: { + permitted: { + token: token.toString(), + amount: maxSpendAmount + }, + spender: spender.toString(), + nonce, + deadline + } + }) + }) + + it('should use zksync permit2 address for zksync chain', () => { + const permit = new PermitTransferFrom( + token, + maxSpendAmount, + spender, + nonce, + deadline + ) + + const typedData = permit.getTypedData(NetworkEnum.ZKSYNC) + + expect(typedData.domain.verifyingContract).toBe( + getPermit2Address(NetworkEnum.ZKSYNC) + ) + }) + + it('should use custom permit2 address when provided', () => { + const customPermit2 = '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' + const permit = new PermitTransferFrom( + token, + maxSpendAmount, + spender, + nonce, + deadline + ) + + const typedData = permit.getTypedData( + NetworkEnum.ETHEREUM, + customPermit2 + ) + + expect(typedData.domain.verifyingContract).toBe(customPermit2) + }) + + it('should throw for unsupported chain id without custom address', () => { + const permit = new PermitTransferFrom( + token, + maxSpendAmount, + spender, + nonce, + deadline + ) + + expect(() => permit.getTypedData(999)).toThrow('unsupported chainId') + }) + + it('should produce signable typed data that recovers to the signer', async () => { + const wallet = Wallet.createRandom() + const permit = new PermitTransferFrom( + token, + maxSpendAmount, + spender, + nonce, + deadline + ) + + const typedData = permit.getTypedData(NetworkEnum.ETHEREUM) + const types = {...typedData.types} + delete types['EIP712Domain'] + + const signature = await wallet.signTypedData( + typedData.domain, + types, + typedData.message + ) + + const recovered = verifyTypedData( + typedData.domain, + types, + typedData.message, + signature + ) + + expect(recovered).toBe(wallet.address) + }) +}) diff --git a/src/fusion-order/permit/permit-transfer-from.ts b/src/fusion-order/permit/permit-transfer-from.ts new file mode 100644 index 0000000..bcc3791 --- /dev/null +++ b/src/fusion-order/permit/permit-transfer-from.ts @@ -0,0 +1,57 @@ +import {Address, EIP712TypedData} from '@1inch/limit-order-sdk' +import {PERMIT2_DOMAIN_NAME, PERMIT_TRANSFER_FROM_TYPES} from './constants.js' +import {getPermit2Address} from './utils.js' +import {encodeTransferFromSuffix} from './transfer-from-suffix.js' + +export class PermitTransferFrom { + constructor( + public readonly token: Address, + public readonly maxSpendAmount: bigint, + public readonly spender: Address, + public readonly nonce: bigint, + public readonly deadline: bigint + ) {} + + getTypedData( + chainId: number, + permit2Address: string = getPermit2Address(chainId) + ): EIP712TypedData { + return { + primaryType: 'PermitTransferFrom', + types: { + ...PERMIT_TRANSFER_FROM_TYPES + }, + domain: { + name: PERMIT2_DOMAIN_NAME, + chainId, + verifyingContract: permit2Address + }, + message: { + permitted: { + token: this.token.toString(), + amount: this.maxSpendAmount + }, + spender: this.spender.toString(), + nonce: this.nonce, + deadline: this.deadline + } + } + } + + /** + * ABI-encodes the Permit2 suffix appended to `transferFrom(from,to,amount)` calldata. + * + * The limit order protocol calls `_callTransferFromWithSuffix` on the Permit2Proxy, + * which has `func_nZHTch(address,address,uint256,((address,uint256),uint256,uint256),bytes)` + * with selector 0x23b872dd (same as transferFrom). The suffix is everything after (from,to,amount). + */ + public getTransferFromSuffix(signature: string): string { + return encodeTransferFromSuffix( + this.token, + this.maxSpendAmount, + this.nonce, + this.deadline, + signature + ) + } +} diff --git a/src/fusion-order/permit/transfer-from-suffix.ts b/src/fusion-order/permit/transfer-from-suffix.ts new file mode 100644 index 0000000..be0754b --- /dev/null +++ b/src/fusion-order/permit/transfer-from-suffix.ts @@ -0,0 +1,83 @@ +import {Address} from '@1inch/limit-order-sdk' +import {AbiCoder} from 'ethers' +import {trim0x} from '@1inch/byte-utils' +import assert from 'assert' + +/** + * The full ABI of `func_nZHTch(address,address,uint256,((address,uint256),uint256,uint256),bytes)` + * on Permit2Proxy. Its selector (0x23b872dd) collides with `transferFrom(address,address,uint256)`. + * + * The LOP calls `_callTransferFromWithSuffix`, appending the suffix after (from, to, amount). + * We must encode all 5 params and strip the first 3 slots so that the dynamic `bytes` offset + * remains correct relative to the start of the full parameter block once the contract + * prepends (from, to, amount). + */ +const FUNC_N_ZH_TCH_ABI = [ + 'address', + 'address', + 'uint256', + 'tuple(tuple(address token, uint256 amount) permitted, uint256 nonce, uint256 deadline)', + 'bytes' +] + +const ZERO_SLOT = '0'.repeat(64) +const STRIPPED_SLOTS = 3 + +export type DecodedTransferPermitSuffix = { + token: Address + amount: bigint + nonce: bigint + deadline: bigint + signature: string +} + +export function encodeTransferFromSuffix( + token: Address, + amount: bigint, + nonce: bigint, + deadline: bigint, + signature: string +): string { + const abiCoder = AbiCoder.defaultAbiCoder() + + const encoded = abiCoder.encode(FUNC_N_ZH_TCH_ABI, [ + Address.ZERO_ADDRESS.toString(), + Address.ZERO_ADDRESS.toString(), + 0n, + { + permitted: { + token: token.toString(), + amount + }, + nonce, + deadline + }, + signature + ]) + + const strippedHexChars = STRIPPED_SLOTS * 32 * 2 + + return '0x' + trim0x(encoded).slice(strippedHexChars) +} + +export function decodeTransferFromSuffix( + suffix: string +): DecodedTransferPermitSuffix { + const restored = '0x' + ZERO_SLOT.repeat(STRIPPED_SLOTS) + suffix.slice(2) + + const abiCoder = AbiCoder.defaultAbiCoder() + const decoded = abiCoder.decode(FUNC_N_ZH_TCH_ABI, restored) + + const permit = decoded[3] + const sig: string = decoded[4] + + assert(sig.length > 0, 'empty permit signature') + + return { + token: new Address(permit.permitted.token), + amount: BigInt(permit.permitted.amount), + nonce: BigInt(permit.nonce), + deadline: BigInt(permit.deadline), + signature: sig + } +} diff --git a/src/fusion-order/permit/utils.ts b/src/fusion-order/permit/utils.ts new file mode 100644 index 0000000..68da665 --- /dev/null +++ b/src/fusion-order/permit/utils.ts @@ -0,0 +1,16 @@ +import {Address} from '@1inch/limit-order-sdk' +import assert from 'assert' +import {PERMIT2_ADDRESSES, PERMIT2_PROXY_ADDRESSES} from './constants.js' +import {NetworkEnum} from '../../constants.js' + +export function getPermit2Address(chainId: number): string { + assert(NetworkEnum[chainId], 'unsupported chainId') + + return PERMIT2_ADDRESSES[chainId as NetworkEnum] +} + +export function getPermit2ProxyAddress(chainId: number): Address { + assert(NetworkEnum[chainId], 'unsupported chainId') + + return new Address(PERMIT2_PROXY_ADDRESSES[chainId as NetworkEnum]) +} diff --git a/tests/addresses.ts b/tests/addresses.ts index 2b23b20..f1d7012 100644 --- a/tests/addresses.ts +++ b/tests/addresses.ts @@ -1,3 +1,8 @@ export const WETH = '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2' export const USDC = '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48' export const USDC_DONOR = '0x37305B1cD40574E4C5Ce33f8e8306Be057fD7341' + +export const ONE_INCH_LIMIT_ORDER_V4 = + '0x111111125421ca6dc452d289314280a0f8842a65' + +export const PERMIT2 = '0x000000000022D473030F116dDEE9F6B43aC78BA3' diff --git a/tests/fusion-order-from-native.spec.ts b/tests/fusion-order-from-native.spec.ts index bec1757..e13028a 100644 --- a/tests/fusion-order-from-native.spec.ts +++ b/tests/fusion-order-from-native.spec.ts @@ -1,33 +1,25 @@ -import {parseEther, parseUnits, Contract} from 'ethers' -import {Bps, ProxyFactory} from '@1inch/limit-order-sdk' -import assert from 'assert' +import {parseEther, parseUnits} from 'ethers' +import {ProxyFactory, NativeOrdersFactory} from '@1inch/limit-order-sdk' import {ReadyEvmFork, setupEvm} from './setup-chain.js' -import {USDC, WETH} from './addresses.js' +import {USDC, WETH, ONE_INCH_LIMIT_ORDER_V4} from './addresses.js' import {TestWallet} from './test-wallet.js' import {now} from './utils.js' -import {Fees} from '../src/fusion-order/fees/fees.js' -import {IntegratorFee} from '../src/fusion-order/fees/integrator-fee.js' -import {ResolverFee} from '../src/fusion-order/fees/resolver-fee.js' import { Address, AmountMode, - AuctionCalculator, AuctionDetails, FusionOrder, LimitOrderContract, NetworkEnum, - ONE_INCH_LIMIT_ORDER_V4, SurplusParams, TakerTraits, Whitelist } from '../src/index.js' -import NativeOrderFactory from '../dist/contracts/NativeOrderFactory.sol/NativeOrderFactory.json' jest.setTimeout(100_000) -// eslint-disable-next-line max-lines-per-function describe('NativeOrders', () => { let maker: TestWallet let taker: TestWallet @@ -38,7 +30,7 @@ describe('NativeOrders', () => { let protocol: TestWallet beforeAll(async () => { - testNode = await setupEvm({}) + testNode = await setupEvm() maker = testNode.maker taker = testNode.taker EXT_ADDRESS = testNode.addresses.settlement @@ -56,7 +48,7 @@ describe('NativeOrders', () => { await testNode.localNode.stop() }) - it.only('should execute order without fees and auction', async () => { + it('should execute order without fees and auction', async () => { const initBalances = { usdc: { maker: await maker.tokenBalance(USDC), @@ -77,6 +69,7 @@ describe('NativeOrders', () => { const takerAddress = new Address(await taker.getAddress()) + const makerAddr = new Address(await maker.getAddress()) const order = FusionOrder.fromNative( NetworkEnum.ETHEREUM, new ProxyFactory( @@ -85,7 +78,7 @@ describe('NativeOrders', () => { ), new Address(EXT_ADDRESS), { - maker: new Address(await maker.getAddress()), + maker: makerAddr, takerAsset: new Address(USDC), makingAmount: parseEther('0.1'), takingAmount: parseUnits('100', 6) @@ -107,24 +100,25 @@ describe('NativeOrders', () => { const nativeOrderFactory = new Address(NATIVE_ORDERS_FACTORY) const orderData = order.build() - const signature = order.nativeSignature( - new Address(await maker.getAddress()) - ) + const signature = order.nativeSignature(makerAddr) - const factoryContract = new Contract( - nativeOrderFactory.toString(), - NativeOrderFactory.abi, - maker.provider - ) + const factory = new NativeOrdersFactory(nativeOrderFactory) + + const createTx = factory.create(makerAddr, orderData) - const createTx = - await factoryContract.create.populateTransaction(orderData) - await maker.send({ - ...createTx, - value: order.makingAmount + const createTxResult = await maker.send({ + to: createTx.to.toString(), + data: createTx.data.toString(), + value: createTx.value }) - const data = LimitOrderContract.getFillOrderArgsCalldata( + const createTxReceipt = await testNode.provider.getTransactionReceipt( + createTxResult.txHash + ) + const createTxGasCost = + createTxReceipt!.gasUsed * createTxReceipt!.gasPrice + + const data = LimitOrderContract.getFillContractOrderArgsCalldata( orderData, signature, TakerTraits.default() @@ -156,8 +150,8 @@ describe('NativeOrders', () => { } } - expect(initBalances.weth.maker - finalBalances.weth.maker).toBe( - order.makingAmount + expect(initBalances.eth.maker - finalBalances.eth.maker).toBe( + order.makingAmount + createTxGasCost ) expect(finalBalances.usdc.maker - initBalances.usdc.maker).toBe( order.takingAmount @@ -170,911 +164,4 @@ describe('NativeOrders', () => { order.calcTakingAmount(takerAddress, order.makingAmount, now()) ) }) - - // eslint-disable-next-line max-lines-per-function - describe('Fees', () => { - it('only integrator fee', async () => { - const integratorAddress = Address.fromBigInt(1337n) - const integrator = await TestWallet.fromAddress( - integratorAddress, - testNode.provider - ) - const initBalances = { - usdc: { - maker: await maker.tokenBalance(USDC), - taker: await taker.tokenBalance(USDC), - protocol: await protocol.tokenBalance(USDC), - integrator: await integrator.tokenBalance(USDC) - }, - weth: { - maker: await maker.tokenBalance(WETH), - taker: await taker.tokenBalance(WETH), - protocol: await protocol.tokenBalance(WETH), - integrator: await integrator.tokenBalance(WETH) - } - } - - const takerAddress = new Address(await taker.getAddress()) - - const order = FusionOrder.new( - new Address(EXT_ADDRESS), - { - maker: new Address(await maker.getAddress()), - makerAsset: new Address(WETH), - takerAsset: new Address(USDC), - makingAmount: parseEther('0.1'), - takingAmount: parseUnits('100', 6) - }, - { - auction: new AuctionDetails({ - duration: 120n, - startTime: now(), - points: [], - initialRateBump: 0 - }), - whitelist: Whitelist.new(0n, [ - {address: takerAddress, allowFrom: 0n} - ]), - surplus: SurplusParams.NO_FEE - }, - { - fees: Fees.integratorFee( - new IntegratorFee( - integratorAddress, - new Address(await protocol.getAddress()), - Bps.fromPercent(1), - Bps.fromPercent(50) - ) - ) - } - ) - - const signature = await maker.signTypedData(order.getTypedData(1)) - - const data = LimitOrderContract.getFillOrderArgsCalldata( - order.build(), - signature, - TakerTraits.default() - .setExtension(order.extension) - .setAmountMode(AmountMode.maker), - order.makingAmount - ) - - await taker.send({ - data, - to: ONE_INCH_LIMIT_ORDER_V4 - }) - - const finalBalances = { - usdc: { - maker: await maker.tokenBalance(USDC), - taker: await taker.tokenBalance(USDC), - protocol: await protocol.tokenBalance(USDC), - integrator: await integrator.tokenBalance(USDC) - }, - weth: { - maker: await maker.tokenBalance(WETH), - taker: await taker.tokenBalance(WETH), - protocol: await protocol.tokenBalance(WETH), - integrator: await integrator.tokenBalance(WETH) - } - } - - expect(initBalances.weth.maker - finalBalances.weth.maker).toBe( - order.makingAmount - ) - expect(finalBalances.usdc.maker - initBalances.usdc.maker).toBe( - order.takingAmount - ) - - expect(finalBalances.weth.taker - initBalances.weth.taker).toBe( - order.makingAmount - ) - expect(initBalances.usdc.taker - finalBalances.usdc.taker).toBe( - order.calcTakingAmount( - takerAddress, - order.makingAmount, - now(), - 0n - ) - ) - - expect( - finalBalances.usdc.protocol - initBalances.usdc.protocol - ).toBe(order.getProtocolFee(takerAddress, now(), 0n)) - expect( - finalBalances.weth.protocol - initBalances.weth.protocol - ).toBe(0n) - - expect( - finalBalances.usdc.integrator - initBalances.usdc.integrator - ).toBe(order.getIntegratorFee(takerAddress, now(), 0n)) - expect( - finalBalances.weth.integrator - initBalances.weth.integrator - ).toBe(0n) - }) - it('only resolver fee', async () => { - const initBalances = { - usdc: { - maker: await maker.tokenBalance(USDC), - taker: await taker.tokenBalance(USDC), - protocol: await protocol.tokenBalance(USDC) - }, - weth: { - maker: await maker.tokenBalance(WETH), - taker: await taker.tokenBalance(WETH), - protocol: await protocol.tokenBalance(WETH) - } - } - - const takerAddress = new Address(await taker.getAddress()) - - const order = FusionOrder.new( - new Address(EXT_ADDRESS), - { - maker: new Address(await maker.getAddress()), - makerAsset: new Address(WETH), - takerAsset: new Address(USDC), - makingAmount: parseEther('0.1'), - takingAmount: parseUnits('100', 6) - }, - { - auction: new AuctionDetails({ - duration: 120n, - startTime: now(), - points: [], - initialRateBump: 0 - }), - whitelist: Whitelist.new(0n, [ - {address: takerAddress, allowFrom: 0n} - ]), - surplus: SurplusParams.NO_FEE - }, - { - fees: Fees.resolverFee( - new ResolverFee( - new Address(await protocol.getAddress()), - Bps.fromPercent(1) - ) - ) - } - ) - - const signature = await maker.signTypedData(order.getTypedData(1)) - - const data = LimitOrderContract.getFillOrderArgsCalldata( - order.build(), - signature, - TakerTraits.default() - .setExtension(order.extension) - .setAmountMode(AmountMode.maker), - order.makingAmount - ) - - await taker.send({ - data, - to: ONE_INCH_LIMIT_ORDER_V4 - }) - - const finalBalances = { - usdc: { - maker: await maker.tokenBalance(USDC), - taker: await taker.tokenBalance(USDC), - protocol: await protocol.tokenBalance(USDC) - }, - weth: { - maker: await maker.tokenBalance(WETH), - taker: await taker.tokenBalance(WETH), - protocol: await protocol.tokenBalance(WETH) - } - } - - expect(initBalances.weth.maker - finalBalances.weth.maker).toBe( - order.makingAmount - ) - expect(finalBalances.usdc.maker - initBalances.usdc.maker).toBe( - order.takingAmount - ) - - expect(finalBalances.weth.taker - initBalances.weth.taker).toBe( - order.makingAmount - ) - expect(initBalances.usdc.taker - finalBalances.usdc.taker).toBe( - order.calcTakingAmount( - takerAddress, - order.makingAmount, - now(), - 0n - ) - ) - - expect( - finalBalances.usdc.protocol - initBalances.usdc.protocol - ).toBe(order.getProtocolFee(takerAddress, now(), 0n)) - expect( - finalBalances.weth.protocol - initBalances.weth.protocol - ).toBe(0n) - }) - it('resolver and integrator fees', async () => { - const integratorAddress = Address.fromBigInt(1337n) - const protocolAddress = new Address(await protocol.getAddress()) - const integrator = await TestWallet.fromAddress( - integratorAddress, - testNode.provider - ) - const initBalances = { - usdc: { - maker: await maker.tokenBalance(USDC), - taker: await taker.tokenBalance(USDC), - protocol: await protocol.tokenBalance(USDC), - integrator: await integrator.tokenBalance(USDC) - }, - weth: { - maker: await maker.tokenBalance(WETH), - taker: await taker.tokenBalance(WETH), - protocol: await protocol.tokenBalance(WETH), - integrator: await integrator.tokenBalance(WETH) - } - } - - const takerAddress = new Address(await taker.getAddress()) - - const order = FusionOrder.new( - new Address(EXT_ADDRESS), - { - maker: new Address(await maker.getAddress()), - makerAsset: new Address(WETH), - takerAsset: new Address(USDC), - makingAmount: parseEther('0.1'), - takingAmount: parseUnits('100', 6) - }, - { - auction: new AuctionDetails({ - duration: 120n, - startTime: now(), - points: [], - initialRateBump: 0 - }), - whitelist: Whitelist.new(0n, [ - {address: takerAddress, allowFrom: 0n} - ]), - surplus: SurplusParams.NO_FEE - }, - { - fees: new Fees( - new ResolverFee( - new Address(await protocol.getAddress()), - Bps.fromPercent(1) - ), - new IntegratorFee( - integratorAddress, - protocolAddress, - Bps.fromPercent(0.1), - Bps.fromPercent(10) - ) - ) - } - ) - - const signature = await maker.signTypedData(order.getTypedData(1)) - - const data = LimitOrderContract.getFillOrderArgsCalldata( - order.build(), - signature, - TakerTraits.default() - .setExtension(order.extension) - .setAmountMode(AmountMode.maker), - order.makingAmount - ) - - await taker.send({ - data, - to: ONE_INCH_LIMIT_ORDER_V4 - }) - - const finalBalances = { - usdc: { - maker: await maker.tokenBalance(USDC), - taker: await taker.tokenBalance(USDC), - protocol: await protocol.tokenBalance(USDC), - integrator: await integrator.tokenBalance(USDC) - }, - weth: { - maker: await maker.tokenBalance(WETH), - taker: await taker.tokenBalance(WETH), - protocol: await protocol.tokenBalance(WETH), - integrator: await integrator.tokenBalance(WETH) - } - } - - expect(initBalances.weth.maker - finalBalances.weth.maker).toBe( - order.makingAmount - ) - expect(finalBalances.usdc.maker - initBalances.usdc.maker).toBe( - order.takingAmount - ) - - expect(finalBalances.weth.taker - initBalances.weth.taker).toBe( - order.makingAmount - ) - expect(initBalances.usdc.taker - finalBalances.usdc.taker).toBe( - order.calcTakingAmount( - takerAddress, - order.makingAmount, - now(), - 0n - ) - ) - - expect( - finalBalances.usdc.protocol - initBalances.usdc.protocol - ).toBe(order.getProtocolFee(takerAddress, now(), 0n)) - expect( - finalBalances.weth.protocol - initBalances.weth.protocol - ).toBe(0n) - - expect( - finalBalances.usdc.integrator - initBalances.usdc.integrator - ).toBe(order.getIntegratorFee(takerAddress, now(), 0n)) - expect( - finalBalances.weth.integrator - initBalances.weth.integrator - ).toBe(0n) - }) - - it('resolver and integrator fees with custom receiver', async () => { - const integratorAddress = Address.fromBigInt(1337n) - const protocolAddress = new Address(await protocol.getAddress()) - const customReceiver = await TestWallet.fromAddress( - Address.fromBigInt(1312n), - testNode.provider - ) - - const integrator = await TestWallet.fromAddress( - integratorAddress, - testNode.provider - ) - const initBalances = { - usdc: { - maker: await maker.tokenBalance(USDC), - taker: await taker.tokenBalance(USDC), - protocol: await protocol.tokenBalance(USDC), - integrator: await integrator.tokenBalance(USDC), - receiver: await customReceiver.tokenBalance(USDC) - }, - weth: { - maker: await maker.tokenBalance(WETH), - taker: await taker.tokenBalance(WETH), - protocol: await protocol.tokenBalance(WETH), - integrator: await integrator.tokenBalance(WETH), - receiver: await customReceiver.tokenBalance(WETH) - } - } - - const takerAddress = new Address(await taker.getAddress()) - - const order = FusionOrder.new( - new Address(EXT_ADDRESS), - { - maker: new Address(await maker.getAddress()), - makerAsset: new Address(WETH), - takerAsset: new Address(USDC), - makingAmount: parseEther('0.1'), - takingAmount: parseUnits('100', 6), - receiver: new Address(await customReceiver.getAddress()) - }, - { - auction: new AuctionDetails({ - duration: 120n, - startTime: now(), - points: [], - initialRateBump: 0 - }), - whitelist: Whitelist.new(0n, [ - {address: takerAddress, allowFrom: 0n} - ]), - surplus: SurplusParams.NO_FEE - }, - { - fees: new Fees( - new ResolverFee( - new Address(await protocol.getAddress()), - Bps.fromPercent(1) - ), - new IntegratorFee( - integratorAddress, - protocolAddress, - Bps.fromPercent(0.1), - Bps.fromPercent(10) - ) - ) - } - ) - - const signature = await maker.signTypedData(order.getTypedData(1)) - - const data = LimitOrderContract.getFillOrderArgsCalldata( - order.build(), - signature, - TakerTraits.default() - .setExtension(order.extension) - .setAmountMode(AmountMode.maker), - order.makingAmount - ) - - await taker.send({ - data, - to: ONE_INCH_LIMIT_ORDER_V4 - }) - - const finalBalances = { - usdc: { - maker: await maker.tokenBalance(USDC), - taker: await taker.tokenBalance(USDC), - protocol: await protocol.tokenBalance(USDC), - integrator: await integrator.tokenBalance(USDC), - receiver: await customReceiver.tokenBalance(USDC) - }, - weth: { - maker: await maker.tokenBalance(WETH), - taker: await taker.tokenBalance(WETH), - protocol: await protocol.tokenBalance(WETH), - integrator: await integrator.tokenBalance(WETH), - receiver: await customReceiver.tokenBalance(WETH) - } - } - - expect(initBalances.weth.maker - finalBalances.weth.maker).toBe( - order.makingAmount - ) - expect(finalBalances.usdc.maker - initBalances.usdc.maker).toBe(0n) - - expect( - initBalances.weth.receiver - finalBalances.weth.receiver - ).toBe(0n) - expect( - finalBalances.usdc.receiver - initBalances.usdc.receiver - ).toBe(order.takingAmount) - - expect(finalBalances.weth.taker - initBalances.weth.taker).toBe( - order.makingAmount - ) - expect(initBalances.usdc.taker - finalBalances.usdc.taker).toBe( - order.calcTakingAmount( - takerAddress, - order.makingAmount, - now(), - 0n - ) - ) - - expect( - finalBalances.usdc.protocol - initBalances.usdc.protocol - ).toBe(order.getProtocolFee(takerAddress, now(), 0n)) - expect( - finalBalances.weth.protocol - initBalances.weth.protocol - ).toBe(0n) - - expect( - finalBalances.usdc.integrator - initBalances.usdc.integrator - ).toBe(order.getIntegratorFee(takerAddress, now(), 0n)) - expect( - finalBalances.weth.integrator - initBalances.weth.integrator - ).toBe(0n) - }) - - it('resolver and integrator fees with auction', async () => { - const integratorAddress = Address.fromBigInt(1337n) - const protocolAddress = new Address(await protocol.getAddress()) - const integrator = await TestWallet.fromAddress( - integratorAddress, - testNode.provider - ) - const initBalances = { - usdc: { - maker: await maker.tokenBalance(USDC), - taker: await taker.tokenBalance(USDC), - protocol: await protocol.tokenBalance(USDC), - integrator: await integrator.tokenBalance(USDC) - }, - weth: { - maker: await maker.tokenBalance(WETH), - taker: await taker.tokenBalance(WETH), - protocol: await protocol.tokenBalance(WETH), - integrator: await integrator.tokenBalance(WETH) - } - } - - const takerAddress = new Address(await taker.getAddress()) - - const currentTime = now() - - const order = FusionOrder.new( - new Address(EXT_ADDRESS), - { - maker: new Address(await maker.getAddress()), - makerAsset: new Address(WETH), - takerAsset: new Address(USDC), - makingAmount: parseEther('0.1'), - takingAmount: parseUnits('100', 6) - }, - { - auction: new AuctionDetails({ - duration: 120n, - startTime: currentTime, - points: [], - initialRateBump: Number( - AuctionCalculator.RATE_BUMP_DENOMINATOR - ) - }), - whitelist: Whitelist.new(0n, [ - {address: takerAddress, allowFrom: 0n} - ]), - surplus: SurplusParams.NO_FEE - }, - { - fees: new Fees( - new ResolverFee( - new Address(await protocol.getAddress()), - Bps.fromPercent(1) - ), - new IntegratorFee( - integratorAddress, - protocolAddress, - Bps.fromPercent(0.1), - Bps.fromPercent(10) - ) - ) - } - ) - - const fillAmount = order.makingAmount / 2n - const signature = await maker.signTypedData(order.getTypedData(1)) - - const data = LimitOrderContract.getFillOrderArgsCalldata( - order.build(), - signature, - TakerTraits.default() - .setExtension(order.extension) - .setAmountMode(AmountMode.maker), - fillAmount - ) - - const {blockTimestamp, blockHash} = await taker.send({ - data, - to: ONE_INCH_LIMIT_ORDER_V4 - }) - - const baseFee = (await testNode.provider.getBlock(blockHash)) - ?.baseFeePerGas - assert(baseFee) - - const finalBalances = { - usdc: { - maker: await maker.tokenBalance(USDC), - taker: await taker.tokenBalance(USDC), - protocol: await protocol.tokenBalance(USDC), - integrator: await integrator.tokenBalance(USDC) - }, - weth: { - maker: await maker.tokenBalance(WETH), - taker: await taker.tokenBalance(WETH), - protocol: await protocol.tokenBalance(WETH), - integrator: await integrator.tokenBalance(WETH) - } - } - - expect(initBalances.weth.maker - finalBalances.weth.maker).toBe( - fillAmount - ) - expect(finalBalances.usdc.maker - initBalances.usdc.maker).toBe( - order.getUserReceiveAmount( - takerAddress, - fillAmount, - blockTimestamp, - baseFee - ) - ) - - expect(finalBalances.weth.taker - initBalances.weth.taker).toBe( - fillAmount - ) - expect(initBalances.usdc.taker - finalBalances.usdc.taker).toBe( - order.calcTakingAmount( - takerAddress, - fillAmount, - blockTimestamp, - baseFee - ) - ) - - expect( - finalBalances.usdc.protocol - initBalances.usdc.protocol - ).toBe( - order.getProtocolFee( - takerAddress, - blockTimestamp, - baseFee, - fillAmount - ) - ) - expect( - finalBalances.weth.protocol - initBalances.weth.protocol - ).toBe(0n) - - expect( - finalBalances.usdc.integrator - initBalances.usdc.integrator - ).toBe( - order.getIntegratorFee( - takerAddress, - blockTimestamp, - baseFee, - fillAmount - ) - ) - expect( - finalBalances.weth.integrator - initBalances.weth.integrator - ).toBe(0n) - }) - - it('resolver and integrator fees with auction and surplus fee', async () => { - const integratorAddress = Address.fromBigInt(1337n) - const protocolAddress = new Address(await protocol.getAddress()) - const integrator = await TestWallet.fromAddress( - integratorAddress, - testNode.provider - ) - const initBalances = { - usdc: { - maker: await maker.tokenBalance(USDC), - taker: await taker.tokenBalance(USDC), - protocol: await protocol.tokenBalance(USDC), - integrator: await integrator.tokenBalance(USDC) - }, - weth: { - maker: await maker.tokenBalance(WETH), - taker: await taker.tokenBalance(WETH), - protocol: await protocol.tokenBalance(WETH), - integrator: await integrator.tokenBalance(WETH) - } - } - - const takerAddress = new Address(await taker.getAddress()) - - const currentTime = now() - - const order = FusionOrder.new( - new Address(EXT_ADDRESS), - { - maker: new Address(await maker.getAddress()), - makerAsset: new Address(WETH), - takerAsset: new Address(USDC), - makingAmount: parseEther('0.1'), - takingAmount: parseUnits('100', 6) // will be 200 at time of fill because of rate bump - }, - { - auction: new AuctionDetails({ - duration: 120n, - startTime: currentTime, - points: [], - initialRateBump: Number( - AuctionCalculator.RATE_BUMP_DENOMINATOR - ) - }), - whitelist: Whitelist.new(0n, [ - {address: takerAddress, allowFrom: 0n} - ]), - surplus: new SurplusParams( - parseUnits('100', 6), - Bps.fromPercent(50) - ) - }, - { - fees: new Fees( - new ResolverFee( - new Address(await protocol.getAddress()), - Bps.fromPercent(1) - ), - new IntegratorFee( - integratorAddress, - protocolAddress, - Bps.fromPercent(0.1), - Bps.fromPercent(10) - ) - ) - } - ) - - const fillAmount = order.makingAmount / 2n - const signature = await maker.signTypedData(order.getTypedData(1)) - - const data = LimitOrderContract.getFillOrderArgsCalldata( - order.build(), - signature, - TakerTraits.default() - .setExtension(order.extension) - .setAmountMode(AmountMode.maker), - fillAmount - ) - - const {blockTimestamp, blockHash} = await taker.send({ - data, - to: ONE_INCH_LIMIT_ORDER_V4 - }) - - const baseFee = (await testNode.provider.getBlock(blockHash)) - ?.baseFeePerGas - assert(baseFee) - - const finalBalances = { - usdc: { - maker: await maker.tokenBalance(USDC), - taker: await taker.tokenBalance(USDC), - protocol: await protocol.tokenBalance(USDC), - integrator: await integrator.tokenBalance(USDC) - }, - weth: { - maker: await maker.tokenBalance(WETH), - taker: await taker.tokenBalance(WETH), - protocol: await protocol.tokenBalance(WETH), - integrator: await integrator.tokenBalance(WETH) - } - } - - expect(initBalances.weth.maker - finalBalances.weth.maker).toBe( - fillAmount - ) - - expect(finalBalances.usdc.maker - initBalances.usdc.maker).toBe( - order.getUserReceiveAmount( - takerAddress, - fillAmount, - blockTimestamp, - baseFee - ) - ) - - expect(finalBalances.weth.taker - initBalances.weth.taker).toBe( - fillAmount - ) - expect(initBalances.usdc.taker - finalBalances.usdc.taker).toBe( - order.calcTakingAmount( - takerAddress, - fillAmount, - blockTimestamp, - baseFee - ) - ) - - expect( - finalBalances.usdc.protocol - initBalances.usdc.protocol - ).toBe( - order.getProtocolFee( - takerAddress, - blockTimestamp, - baseFee, - fillAmount - ) + - order.getSurplusFee( - takerAddress, - fillAmount, - blockTimestamp, - baseFee - ) - ) - expect( - finalBalances.weth.protocol - initBalances.weth.protocol - ).toBe(0n) - - expect( - finalBalances.usdc.integrator - initBalances.usdc.integrator - ).toBe( - order.getIntegratorFee( - takerAddress, - blockTimestamp, - baseFee, - fillAmount - ) - ) - expect( - finalBalances.weth.integrator - initBalances.weth.integrator - ).toBe(0n) - }) - }) - - it('should execute with custom receiver no fee', async () => { - const customReceiver = await TestWallet.fromAddress( - Address.fromBigInt(1337n), - testNode.provider - ) - - const initBalances = { - usdc: { - maker: await maker.tokenBalance(USDC), - taker: await taker.tokenBalance(USDC), - protocol: await protocol.tokenBalance(USDC), - receiver: await customReceiver.tokenBalance(USDC) - }, - weth: { - maker: await maker.tokenBalance(WETH), - taker: await taker.tokenBalance(WETH), - protocol: await protocol.tokenBalance(WETH), - receiver: await customReceiver.tokenBalance(WETH) - } - } - - const takerAddress = new Address(await taker.getAddress()) - - const order = FusionOrder.new( - new Address(EXT_ADDRESS), - { - maker: new Address(await maker.getAddress()), - makerAsset: new Address(WETH), - takerAsset: new Address(USDC), - makingAmount: parseEther('0.1'), - takingAmount: parseUnits('100', 6), - receiver: new Address(await customReceiver.getAddress()) - }, - { - auction: new AuctionDetails({ - duration: 120n, - startTime: now(), - points: [], - initialRateBump: 0 - }), - whitelist: Whitelist.new(0n, [ - {address: takerAddress, allowFrom: 0n} - ]), - surplus: SurplusParams.NO_FEE - } - ) - - const signature = await maker.signTypedData(order.getTypedData(1)) - - const data = LimitOrderContract.getFillOrderArgsCalldata( - order.build(), - signature, - TakerTraits.default() - .setExtension(order.extension) - .setAmountMode(AmountMode.maker), - order.makingAmount - ) - - await taker.send({ - data, - to: ONE_INCH_LIMIT_ORDER_V4 - }) - - const finalBalances = { - usdc: { - maker: await maker.tokenBalance(USDC), - taker: await taker.tokenBalance(USDC), - protocol: await protocol.tokenBalance(USDC), - receiver: await customReceiver.tokenBalance(USDC) - }, - weth: { - maker: await maker.tokenBalance(WETH), - taker: await taker.tokenBalance(WETH), - protocol: await protocol.tokenBalance(WETH), - receiver: await customReceiver.tokenBalance(WETH) - } - } - - expect(initBalances.weth.maker - finalBalances.weth.maker).toBe( - order.makingAmount - ) - expect(finalBalances.usdc.maker - initBalances.usdc.maker).toBe(0n) - expect(finalBalances.usdc.receiver - initBalances.usdc.receiver).toBe( - order.takingAmount - ) - expect(finalBalances.weth.receiver - initBalances.weth.receiver).toBe( - 0n - ) - - expect(finalBalances.weth.taker - initBalances.weth.taker).toBe( - order.makingAmount - ) - expect(initBalances.usdc.taker - finalBalances.usdc.taker).toBe( - order.takingAmount - ) - }) }) diff --git a/tests/fusion-order.spec.ts b/tests/fusion-order.spec.ts index f257462..59738fb 100644 --- a/tests/fusion-order.spec.ts +++ b/tests/fusion-order.spec.ts @@ -3,7 +3,7 @@ import {Bps} from '@1inch/limit-order-sdk' import assert from 'assert' import {ReadyEvmFork, setupEvm} from './setup-chain.js' -import {USDC, WETH} from './addresses.js' +import {USDC, WETH, ONE_INCH_LIMIT_ORDER_V4} from './addresses.js' import {TestWallet} from './test-wallet.js' import {now} from './utils.js' @@ -17,7 +17,7 @@ import { AuctionDetails, FusionOrder, LimitOrderContract, - ONE_INCH_LIMIT_ORDER_V4, + NetworkEnum, SurplusParams, TakerTraits, Whitelist @@ -34,7 +34,7 @@ describe('SettlementExtension', () => { let protocol: TestWallet beforeAll(async () => { - testNode = await setupEvm({}) + testNode = await setupEvm() maker = testNode.maker taker = testNode.taker EXT_ADDRESS = testNode.addresses.settlement @@ -943,6 +943,114 @@ describe('SettlementExtension', () => { }) }) + it('should execute order with PermitTransferFrom', async () => { + const initBalances = { + usdc: { + maker: await maker.tokenBalance(USDC), + taker: await taker.tokenBalance(USDC), + protocol: await protocol.tokenBalance(USDC) + }, + weth: { + maker: await maker.tokenBalance(WETH), + taker: await taker.tokenBalance(WETH), + protocol: await protocol.tokenBalance(WETH) + } + } + + const takerAddress = new Address(await taker.getAddress()) + const makerAddress = new Address(await maker.getAddress()) + + const order = FusionOrder.new( + new Address(EXT_ADDRESS), + { + maker: makerAddress, + makerAsset: new Address(WETH), + takerAsset: new Address(USDC), + makingAmount: parseEther('0.1'), + takingAmount: parseUnits('100', 6) + }, + { + auction: new AuctionDetails({ + duration: 120n, + startTime: now(), + points: [], + initialRateBump: 0 + }), + whitelist: Whitelist.new(0n, [ + {address: takerAddress, allowFrom: 0n} + ]), + surplus: SurplusParams.NO_FEE + }, + { + allowPartialFills: false, + allowMultipleFills: false, + nonce: 1n + } + ) + + const chainId = NetworkEnum.ETHEREUM + + const permit2Proxy = new Address(testNode.addresses.permit2Proxy) + + const permit = order.createTransferPermit(permit2Proxy) + const permitSignature = await maker.signTypedData( + permit.getTypedData(chainId) + ) + + const orderWithPermit = order.withTransferPermit( + permit, + permitSignature + ) + + const signature = await maker.signTypedData( + orderWithPermit.getTypedData(chainId) + ) + + const data = LimitOrderContract.getFillOrderArgsCalldata( + orderWithPermit.build(), + signature, + TakerTraits.default() + .setExtension(orderWithPermit.extension) + .setAmountMode(AmountMode.maker), + orderWithPermit.makingAmount + ) + + await taker.send({ + data, + to: ONE_INCH_LIMIT_ORDER_V4 + }) + + const finalBalances = { + usdc: { + maker: await maker.tokenBalance(USDC), + taker: await taker.tokenBalance(USDC), + protocol: await protocol.tokenBalance(USDC) + }, + weth: { + maker: await maker.tokenBalance(WETH), + taker: await taker.tokenBalance(WETH), + protocol: await protocol.tokenBalance(WETH) + } + } + + expect(initBalances.weth.maker - finalBalances.weth.maker).toBe( + order.makingAmount + ) + expect(finalBalances.usdc.maker - initBalances.usdc.maker).toBe( + order.takingAmount + ) + + expect(finalBalances.weth.taker - initBalances.weth.taker).toBe( + order.makingAmount + ) + expect(initBalances.usdc.taker - finalBalances.usdc.taker).toBe( + order.calcTakingAmount(takerAddress, order.makingAmount, now()) + ) + + expect(finalBalances.weth.protocol).toBe(initBalances.weth.protocol) + expect(finalBalances.usdc.protocol).toBe(initBalances.usdc.protocol) + }) + it('should execute with custom receiver no fee', async () => { const customReceiver = await TestWallet.fromAddress( Address.fromBigInt(1337n), diff --git a/tests/setup-chain.ts b/tests/setup-chain.ts index cb112be..9103573 100644 --- a/tests/setup-chain.ts +++ b/tests/setup-chain.ts @@ -1,6 +1,7 @@ import {GenericContainer, StartedTestContainer} from 'testcontainers' import {LogWaitStrategy} from 'testcontainers/build/wait-strategies/log-wait-strategy' import { + Contract, ContractFactory, InterfaceAbi, JsonRpcProvider, @@ -10,12 +11,17 @@ import { } from 'ethers' import {randBigInt} from '@1inch/limit-order-sdk' -import {USDC, USDC_DONOR, WETH} from './addresses.js' +import { + USDC, + USDC_DONOR, + WETH, + ONE_INCH_LIMIT_ORDER_V4, + PERMIT2 +} from './addresses.js' import {TestWallet} from './test-wallet.js' import SimpleSettlement from '../dist/contracts/SimpleSettlement.sol/SimpleSettlement.json' import NativeOrderFactory from '../dist/contracts/NativeOrderFactory.sol/NativeOrderFactory.json' -import NativeOrderImpl from '../dist/contracts/NativeOrderImpl.sol/NativeOrderImpl.json' -import {ONE_INCH_LIMIT_ORDER_V4} from '../src/constants.js' +import Permit2Proxy from '../dist/contracts/Permit2Proxy.sol/Permit2Proxy.json' export type EvmNodeConfig = { chainId?: number @@ -30,6 +36,7 @@ export type ReadyEvmFork = { settlement: string nativeOrdersFactory: string nativeOrdersImpl: string + permit2Proxy: string } maker: TestWallet taker: TestWallet @@ -38,7 +45,9 @@ export type ReadyEvmFork = { // Setup evm fork with escrow factory contract and users with funds // maker have WETH // taker have USDC on resolver contract -export async function setupEvm(config: EvmNodeConfig): Promise { +export async function setupEvm( + config: EvmNodeConfig = {} +): Promise { const chainId = config.chainId || 1 const forkUrl = config.forkUrl ?? (process.env.FORK_URL || 'https://eth.llamarpc.com') @@ -127,6 +136,7 @@ async function deployContracts(provider: JsonRpcProvider): Promise<{ settlement: string nativeOrdersFactory: string nativeOrdersImpl: string + permit2Proxy: string }> { const deployer = new Wallet( '0x3667482b9520ea17999acd812ad3db1ff29c12c006e756cdcb5fd6cc5d5a9b01', @@ -145,20 +155,6 @@ async function deployContracts(provider: JsonRpcProvider): Promise<{ deployer ) - const nativeOrderImpl = await deploy( - NativeOrderImpl, - [ - WETH, - deployer.address, - ONE_INCH_LIMIT_ORDER_V4, - accessToken, - 60, - '1inch Aggregation Router', - '6' // version - ], - deployer - ) - const nativeOrderFactory = await deploy( NativeOrderFactory, [ @@ -172,10 +168,26 @@ async function deployContracts(provider: JsonRpcProvider): Promise<{ deployer ) + const nativeOrderFactoryContract = new Contract( + nativeOrderFactory, + NativeOrderFactory.abi, + deployer + ) + + const nativeOrdersImpl: string = + await nativeOrderFactoryContract.IMPLEMENTATION() + + const permit2Proxy = await deploy( + Permit2Proxy, + [ONE_INCH_LIMIT_ORDER_V4, PERMIT2], + deployer + ) + return { settlement, nativeOrdersFactory: nativeOrderFactory, - nativeOrdersImpl: nativeOrderImpl + nativeOrdersImpl, + permit2Proxy } } @@ -187,6 +199,7 @@ async function setupBalances( // maker have WETH await maker.transfer(WETH, parseEther('5')) await maker.unlimitedApprove(WETH, ONE_INCH_LIMIT_ORDER_V4) + await maker.unlimitedApprove(WETH, PERMIT2) // taker have USDC await ( @@ -209,6 +222,7 @@ async function deploy( json.bytecode, deployer ).deploy(...params) + await deployed.waitForDeployment() return deployed.getAddress() diff --git a/tests/test-wallet.ts b/tests/test-wallet.ts index 30a3b04..cd1ecaa 100644 --- a/tests/test-wallet.ts +++ b/tests/test-wallet.ts @@ -32,11 +32,10 @@ export class TestWallet { signer: Signer, typedData: EIP712TypedData ): Promise { - return signer.signTypedData( - typedData.domain, - {Order: typedData.types[typedData.primaryType]}, - typedData.message - ) + const types = {...typedData.types} + delete types['EIP712Domain'] + + return signer.signTypedData(typedData.domain, types, typedData.message) } public static async fromAddress( @@ -156,7 +155,7 @@ export class TestWallet { const res = await this.signer.sendTransaction({ ...param, gasLimit: 10_000_000, - from: this.getAddress() + from: await this.getAddress() }) const receipt = await res.wait(1)