From c8009ebb1fd9b8efb1d3a44c27eaa4d912a63cf5 Mon Sep 17 00:00:00 2001 From: Vladimir Borovik Date: Wed, 18 Feb 2026 12:21:38 +0300 Subject: [PATCH 01/15] wip --- src/api/quoter/quoter.api.spec.ts | 3 +- src/constants.ts | 17 +++++- src/fusion-order/fusion-order.ts | 54 ++++++++++++++++++- src/fusion-order/index.ts | 1 + src/fusion-order/permit/constants.ts | 40 ++++++++++++++ src/fusion-order/permit/index.ts | 2 + .../permit/permit-transfer-from.ts | 39 ++++++++++++++ src/fusion-order/permit/utils.ts | 9 ++++ 8 files changed, 159 insertions(+), 6 deletions(-) create mode 100644 src/fusion-order/permit/constants.ts create mode 100644 src/fusion-order/permit/index.ts create mode 100644 src/fusion-order/permit/permit-transfer-from.ts create mode 100644 src/fusion-order/permit/utils.ts diff --git a/src/api/quoter/quoter.api.spec.ts b/src/api/quoter/quoter.api.spec.ts index b0e349c5..f2c6b67d 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 c9a23d94..98ef8664 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -16,8 +16,21 @@ export enum NetworkEnum { UNICHAIN = 130 } -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 diff --git a/src/fusion-order/fusion-order.ts b/src/fusion-order/fusion-order.ts index 3c5481da..2a14990e 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,8 +19,13 @@ 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 {AuctionCalculator} from '../amount-calculator/auction-calculator/index.js' -import {NetworkEnum, ZX} from '../constants.js' +import { + NetworkEnum, + ONE_INCH_LIMIT_ORDER_V4_ADDRESSES, + ZX +} from '../constants.js' import {calcTakingAmount} from '../utils/amounts.js' import {now} from '../utils/time.js' import {AmountCalculator} from '../amount-calculator/amount-calculator.js' @@ -408,6 +415,15 @@ export class FusionOrder { return fusionOrder } + public withTransferPermit( + permit: PermitTransferFrom, + signature: string + ): this { + // todo: update all required fields + + return this + } + public build(): LimitOrderV4Struct { return this.inner.build() } @@ -693,4 +709,38 @@ 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 1inch Limit Order Protocol v4 contract + * (as spender) to transfer up to `makingAmount` of the `makerAsset` token, + * with a random 256-bit nonce and the order's deadline. + * + * @param chainId - The chain ID of the network (must be a supported {@link NetworkEnum} value) + * @returns A {@link PermitTransferFrom} instance that can be signed and attached to the order + * + * @throws If `multipleFillsAllowed` is `true` + * @throws If `chainId` is not a supported network + */ + public createTransferPermit(chainId: number): PermitTransferFrom { + assert( + !this.multipleFillsAllowed, + 'transfer permit can be used only for orders where multipleFillsAllowed=false' + ) + + assert(NetworkEnum[chainId], 'unsupported chain id') + + return new PermitTransferFrom( + this.makerAsset, + this.makingAmount, + new Address( + ONE_INCH_LIMIT_ORDER_V4_ADDRESSES[chainId as NetworkEnum] + ), + randBigInt(UINT_256_MAX), + this.deadline + ) + } } diff --git a/src/fusion-order/index.ts b/src/fusion-order/index.ts index c173d9ad..24870e51 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 00000000..bde498dc --- /dev/null +++ b/src/fusion-order/permit/constants.ts @@ -0,0 +1,40 @@ +import {EIP712Types} from '@1inch/limit-order-sdk' +import {NetworkEnum} from '../../constants.js' + +export const PERMIT2_ADDRESS = '0x000000000022D473030F116dDEE9F6B43aC78BA3' +export 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 +} + +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 00000000..35049c69 --- /dev/null +++ b/src/fusion-order/permit/index.ts @@ -0,0 +1,2 @@ +export * from './permit-transfer-from.js' +export {PERMIT2_ADDRESS} from './constants.js' 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 00000000..aec8edca --- /dev/null +++ b/src/fusion-order/permit/permit-transfer-from.ts @@ -0,0 +1,39 @@ +import {Address, EIP712TypedData} from '@1inch/limit-order-sdk' +import {PERMIT2_DOMAIN_NAME, PERMIT_TRANSFER_FROM_TYPES} from './constants.js' +import {getPermit2Address} from './utils.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 + } + } + } +} diff --git a/src/fusion-order/permit/utils.ts b/src/fusion-order/permit/utils.ts new file mode 100644 index 00000000..898c0dcb --- /dev/null +++ b/src/fusion-order/permit/utils.ts @@ -0,0 +1,9 @@ +import assert from 'assert' +import {PERMIT2_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] +} From 88473e8e46f79671296d9c1eec58ca1259f0db98 Mon Sep 17 00:00:00 2001 From: Vladimir Borovik Date: Wed, 18 Feb 2026 18:00:12 +0300 Subject: [PATCH 02/15] wip --- foundry.toml | 3 +- tests/addresses.ts | 3 + tests/fusion-order-from-native.spec.ts | 961 +------------------------ tests/fusion-order.spec.ts | 78 +- tests/setup-chain.ts | 35 +- tests/test-wallet.ts | 2 +- 6 files changed, 121 insertions(+), 961 deletions(-) diff --git a/foundry.toml b/foundry.toml index f2b67a8e..cd6f104e 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/tests/addresses.ts b/tests/addresses.ts index 2b23b200..400f4664 100644 --- a/tests/addresses.ts +++ b/tests/addresses.ts @@ -1,3 +1,6 @@ export const WETH = '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2' export const USDC = '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48' export const USDC_DONOR = '0x37305B1cD40574E4C5Ce33f8e8306Be057fD7341' + +export const ONE_INCH_LIMIT_ORDER_V4 = + '0x111111125421ca6dc452d289314280a0f8842a65' diff --git a/tests/fusion-order-from-native.spec.ts b/tests/fusion-order-from-native.spec.ts index bec1757d..e13028a5 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 f2574626..f49f4e04 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,6 @@ import { AuctionDetails, FusionOrder, LimitOrderContract, - ONE_INCH_LIMIT_ORDER_V4, SurplusParams, TakerTraits, Whitelist @@ -34,7 +33,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 +942,79 @@ describe('SettlementExtension', () => { }) }) + it('should execute order with PermitTransferFrom', async () => { + 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, + enablePermit2: true + } + ) + + const permit = order.createTransferPermit(1) + const permitSignature = await maker.signTypedData( + permit.getTypedData(1) + ) + + const orderWithPermit = order.withTransferPermit( + permit, + permitSignature + ) + + const signature = await maker.signTypedData( + orderWithPermit.getTypedData(1) + ) + + 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) + }, + weth: { + maker: await maker.tokenBalance(WETH) + } + } + + expect(finalBalances.usdc.maker).toBeGreaterThan(0n) + }) + 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 cb112bed..08a617e9 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,10 @@ 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} 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' export type EvmNodeConfig = { chainId?: number @@ -38,7 +37,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') @@ -145,20 +146,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 +159,19 @@ async function deployContracts(provider: JsonRpcProvider): Promise<{ deployer ) + const nativeOrderFactoryContract = new Contract( + nativeOrderFactory, + NativeOrderFactory.abi, + deployer + ) + + const nativeOrdersImpl: string = + await nativeOrderFactoryContract.IMPLEMENTATION() + return { settlement, nativeOrdersFactory: nativeOrderFactory, - nativeOrdersImpl: nativeOrderImpl + nativeOrdersImpl } } @@ -209,6 +205,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 30a3b040..4fc208bc 100644 --- a/tests/test-wallet.ts +++ b/tests/test-wallet.ts @@ -156,7 +156,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) From 30ed16bee982ba34bedc1eeb2b1d42d95e6ce02a Mon Sep 17 00:00:00 2001 From: Vladimir Borovik Date: Wed, 18 Feb 2026 18:22:14 +0300 Subject: [PATCH 03/15] wip --- .../permit/permit-transfer-from.spec.ts | 123 ++++++++++++++++++ tests/test-wallet.ts | 9 +- 2 files changed, 127 insertions(+), 5 deletions(-) create mode 100644 src/fusion-order/permit/permit-transfer-from.spec.ts 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 00000000..687bf58c --- /dev/null +++ b/src/fusion-order/permit/permit-transfer-from.spec.ts @@ -0,0 +1,123 @@ +import {Address} from '@1inch/limit-order-sdk' +import {verifyTypedData, Wallet} from 'ethers' +import {PermitTransferFrom} from './permit-transfer-from.js' +import { + PERMIT2_ADDRESS, + PERMIT2_ADDRESS_ZK, + PERMIT2_DOMAIN_NAME, + PERMIT_TRANSFER_FROM_TYPES +} from './constants.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: PERMIT2_ADDRESS + }, + 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(PERMIT2_ADDRESS_ZK) + }) + + 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/tests/test-wallet.ts b/tests/test-wallet.ts index 4fc208bc..cd1ecaaa 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( From 3f1eee2274d3009ee18ef98e6810837e32c1e476 Mon Sep 17 00:00:00 2001 From: Vladimir Borovik Date: Wed, 18 Feb 2026 18:22:26 +0300 Subject: [PATCH 04/15] temp copy contracts --- contracts/src/ImmutableOwner.sol | 19 ++++++ contracts/src/Permit2Proxy.sol | 66 +++++++++++++++++++ .../src/interfaces/IPermit2TransferFrom.sol | 42 ++++++++++++ 3 files changed, 127 insertions(+) create mode 100644 contracts/src/ImmutableOwner.sol create mode 100644 contracts/src/Permit2Proxy.sol create mode 100644 contracts/src/interfaces/IPermit2TransferFrom.sol diff --git a/contracts/src/ImmutableOwner.sol b/contracts/src/ImmutableOwner.sol new file mode 100644 index 00000000..87745990 --- /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 00000000..508fc4d8 --- /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 00000000..d3db517e --- /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; +} From a83358d5ab2bd777895bbbae1ca510548dd2ec93 Mon Sep 17 00:00:00 2001 From: Vladimir Borovik Date: Wed, 18 Feb 2026 19:09:05 +0300 Subject: [PATCH 05/15] wip --- src/fusion-order/fusion-order.ts | 66 ++++++++++++++----- .../permit/permit-transfer-from.ts | 35 ++++++++++ src/fusion-order/permit/utils.ts | 8 +++ tests/addresses.ts | 2 + tests/fusion-order.spec.ts | 11 +++- tests/setup-chain.ts | 21 +++++- 6 files changed, 122 insertions(+), 21 deletions(-) diff --git a/src/fusion-order/fusion-order.ts b/src/fusion-order/fusion-order.ts index 2a14990e..4dfdb3ab 100644 --- a/src/fusion-order/fusion-order.ts +++ b/src/fusion-order/fusion-order.ts @@ -21,11 +21,7 @@ import {SurplusParams} from './surplus-params.js' import type {Details, Extra} from './types.js' import {PermitTransferFrom} from './permit/permit-transfer-from.js' import {AuctionCalculator} from '../amount-calculator/auction-calculator/index.js' -import { - NetworkEnum, - ONE_INCH_LIMIT_ORDER_V4_ADDRESSES, - ZX -} from '../constants.js' +import {NetworkEnum, ZX} from '../constants.js' import {calcTakingAmount} from '../utils/amounts.js' import {now} from '../utils/time.js' import {AmountCalculator} from '../amount-calculator/amount-calculator.js' @@ -419,7 +415,45 @@ export class FusionOrder { permit: PermitTransferFrom, signature: string ): this { - // todo: update all required fields + assert( + this.inner.makerTraits.isPermit2(), + 'enablePermit2 must be set to use withTransferPermit' + ) + + const suffix = permit.getTransferFromSuffix(signature) + + const currentExtension = this.inner.extension + const newExtension = new Extension({ + makerAssetSuffix: suffix, + takerAssetSuffix: currentExtension.takerAssetSuffix, + makingAmountData: currentExtension.makingAmountData, + takingAmountData: currentExtension.takingAmountData, + predicate: currentExtension.predicate, + makerPermit: currentExtension.makerPermit, + preInteraction: currentExtension.preInteraction, + postInteraction: currentExtension.postInteraction, + customData: currentExtension.customData + }) + + 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 } @@ -715,30 +749,30 @@ export class FusionOrder { * * Can only be used for orders where `multipleFillsAllowed` is `false`. * - * The returned permit authorizes the 1inch Limit Order Protocol v4 contract - * (as spender) to transfer up to `makingAmount` of the `makerAsset` token, + * The returned permit authorizes the given `permit2Proxy` address (as spender) + * to transfer up to `makingAmount` of the `makerAsset` token, * with a random 256-bit nonce and the order's deadline. * - * @param chainId - The chain ID of the network (must be a supported {@link NetworkEnum} value) + * The resulting permit can be signed and then attached to the order + * via {@link FusionOrder.withTransferPermit}. + * + * @param permit2Proxy - The address of the Permit2Proxy contract that will act as spender * @returns A {@link PermitTransferFrom} instance that can be signed and attached to the order * * @throws If `multipleFillsAllowed` is `true` - * @throws If `chainId` is not a supported network + * + * @see FusionOrder.withTransferPermit */ - public createTransferPermit(chainId: number): PermitTransferFrom { + public createTransferPermit(permit2Proxy: Address): PermitTransferFrom { assert( !this.multipleFillsAllowed, 'transfer permit can be used only for orders where multipleFillsAllowed=false' ) - assert(NetworkEnum[chainId], 'unsupported chain id') - return new PermitTransferFrom( this.makerAsset, this.makingAmount, - new Address( - ONE_INCH_LIMIT_ORDER_V4_ADDRESSES[chainId as NetworkEnum] - ), + permit2Proxy, randBigInt(UINT_256_MAX), this.deadline ) diff --git a/src/fusion-order/permit/permit-transfer-from.ts b/src/fusion-order/permit/permit-transfer-from.ts index aec8edca..bce8cad5 100644 --- a/src/fusion-order/permit/permit-transfer-from.ts +++ b/src/fusion-order/permit/permit-transfer-from.ts @@ -1,4 +1,6 @@ import {Address, EIP712TypedData} from '@1inch/limit-order-sdk' +import {AbiCoder} from 'ethers' +import {trim0x} from '@1inch/byte-utils' import {PERMIT2_DOMAIN_NAME, PERMIT_TRANSFER_FROM_TYPES} from './constants.js' import {getPermit2Address} from './utils.js' @@ -36,4 +38,37 @@ export class PermitTransferFrom { } } } + + /** + * 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 { + const abiCoder = AbiCoder.defaultAbiCoder() + + const encoded = abiCoder.encode( + [ + 'tuple(tuple(address token, uint256 amount) permitted, uint256 nonce, uint256 deadline)', + 'bytes' + ], + [ + { + permitted: { + token: this.token.toString(), + amount: this.maxSpendAmount + }, + nonce: this.nonce, + deadline: this.deadline + }, + signature + ] + ) + + const STRIPPED_HEAD_BYTES = 3 * 32 + + return '0x' + trim0x(encoded).slice(STRIPPED_HEAD_BYTES * 2) + } } diff --git a/src/fusion-order/permit/utils.ts b/src/fusion-order/permit/utils.ts index 898c0dcb..3d6e1376 100644 --- a/src/fusion-order/permit/utils.ts +++ b/src/fusion-order/permit/utils.ts @@ -1,3 +1,4 @@ +import {Address} from '@1inch/limit-order-sdk' import assert from 'assert' import {PERMIT2_ADDRESSES} from './constants.js' import {NetworkEnum} from '../../constants.js' @@ -7,3 +8,10 @@ export function getPermit2Address(chainId: number): string { return PERMIT2_ADDRESSES[chainId as NetworkEnum] } + +export function getDefaultPermit2Proxy(): Address { + // todo: fix + throw new Error( + 'permit2Proxy address is required: no default Permit2Proxy addresses configured' + ) +} diff --git a/tests/addresses.ts b/tests/addresses.ts index 400f4664..f1d7012d 100644 --- a/tests/addresses.ts +++ b/tests/addresses.ts @@ -4,3 +4,5 @@ export const USDC_DONOR = '0x37305B1cD40574E4C5Ce33f8e8306Be057fD7341' export const ONE_INCH_LIMIT_ORDER_V4 = '0x111111125421ca6dc452d289314280a0f8842a65' + +export const PERMIT2 = '0x000000000022D473030F116dDEE9F6B43aC78BA3' diff --git a/tests/fusion-order.spec.ts b/tests/fusion-order.spec.ts index f49f4e04..05ac1b70 100644 --- a/tests/fusion-order.spec.ts +++ b/tests/fusion-order.spec.ts @@ -17,6 +17,7 @@ import { AuctionDetails, FusionOrder, LimitOrderContract, + NetworkEnum, SurplusParams, TakerTraits, Whitelist @@ -975,9 +976,13 @@ describe('SettlementExtension', () => { } ) - const permit = order.createTransferPermit(1) + const chainId = NetworkEnum.ETHEREUM + + const permit2Proxy = new Address(testNode.addresses.permit2Proxy) + + const permit = order.createTransferPermit(permit2Proxy) const permitSignature = await maker.signTypedData( - permit.getTypedData(1) + permit.getTypedData(chainId) ) const orderWithPermit = order.withTransferPermit( @@ -986,7 +991,7 @@ describe('SettlementExtension', () => { ) const signature = await maker.signTypedData( - orderWithPermit.getTypedData(1) + orderWithPermit.getTypedData(chainId) ) const data = LimitOrderContract.getFillOrderArgsCalldata( diff --git a/tests/setup-chain.ts b/tests/setup-chain.ts index 08a617e9..9103573d 100644 --- a/tests/setup-chain.ts +++ b/tests/setup-chain.ts @@ -11,10 +11,17 @@ import { } from 'ethers' import {randBigInt} from '@1inch/limit-order-sdk' -import {USDC, USDC_DONOR, WETH, ONE_INCH_LIMIT_ORDER_V4} 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 Permit2Proxy from '../dist/contracts/Permit2Proxy.sol/Permit2Proxy.json' export type EvmNodeConfig = { chainId?: number @@ -29,6 +36,7 @@ export type ReadyEvmFork = { settlement: string nativeOrdersFactory: string nativeOrdersImpl: string + permit2Proxy: string } maker: TestWallet taker: TestWallet @@ -128,6 +136,7 @@ async function deployContracts(provider: JsonRpcProvider): Promise<{ settlement: string nativeOrdersFactory: string nativeOrdersImpl: string + permit2Proxy: string }> { const deployer = new Wallet( '0x3667482b9520ea17999acd812ad3db1ff29c12c006e756cdcb5fd6cc5d5a9b01', @@ -168,10 +177,17 @@ async function deployContracts(provider: JsonRpcProvider): Promise<{ const nativeOrdersImpl: string = await nativeOrderFactoryContract.IMPLEMENTATION() + const permit2Proxy = await deploy( + Permit2Proxy, + [ONE_INCH_LIMIT_ORDER_V4, PERMIT2], + deployer + ) + return { settlement, nativeOrdersFactory: nativeOrderFactory, - nativeOrdersImpl + nativeOrdersImpl, + permit2Proxy } } @@ -183,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 ( From 30c9f9e3c65f779bacd953887a80107f5800efcf Mon Sep 17 00:00:00 2001 From: Vladimir Borovik Date: Wed, 18 Feb 2026 20:05:39 +0300 Subject: [PATCH 06/15] chore: add transfer permit2 support through permit2Proxy adapter --- README.md | 199 +++++++++++++++--- src/fusion-order/fusion-order.spec.ts | 141 +++++++++++++ src/fusion-order/fusion-order.ts | 51 +++-- src/fusion-order/permit/constants.ts | 4 +- src/fusion-order/permit/index.ts | 3 +- .../permit/permit-transfer-from.spec.ts | 14 +- .../permit/permit-transfer-from.ts | 31 +-- .../permit/transfer-from-suffix.ts | 81 +++++++ tests/fusion-order.spec.ts | 41 +++- 9 files changed, 486 insertions(+), 79 deletions(-) create mode 100644 src/fusion-order/permit/transfer-from-suffix.ts diff --git a/README.md b/README.md index 22ebb536..803ea83e 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, + Address, + 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 PERMIT2_PROXY_ADDRESS = 'PERMIT2_PROXY_CONTRACT_ADDRESS' + +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 permit2Proxy = new Address(PERMIT2_PROXY_ADDRESS) + const permit = order.createTransferPermit(permit2Proxy) + + // 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/src/fusion-order/fusion-order.spec.ts b/src/fusion-order/fusion-order.spec.ts index d8a14e13..64d25e8b 100644 --- a/src/fusion-order/fusion-order.spec.ts +++ b/src/fusion-order/fusion-order.spec.ts @@ -790,4 +790,145 @@ 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 return false for non-permit2 suffix data', () => { + 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 + }) + + const rebuilt = FusionOrder.fromDataAndExtension( + order.build(), + tampered + ) + + expect(rebuilt.isTransferPermit()).toBe(false) + }) + }) }) diff --git a/src/fusion-order/fusion-order.ts b/src/fusion-order/fusion-order.ts index 4dfdb3ab..990f27b0 100644 --- a/src/fusion-order/fusion-order.ts +++ b/src/fusion-order/fusion-order.ts @@ -20,6 +20,10 @@ 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 { + 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' @@ -168,6 +172,10 @@ export class FusionOrder { } get makerAsset(): Address { + if (this.isTransferPermit()) { + return this.decodeTransferPermitSuffix().token + } + return this.inner.makerAsset } @@ -411,28 +419,33 @@ 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 { - assert( - this.inner.makerTraits.isPermit2(), - 'enablePermit2 must be set to use withTransferPermit' - ) - const suffix = permit.getTransferFromSuffix(signature) const currentExtension = this.inner.extension const newExtension = new Extension({ - makerAssetSuffix: suffix, - takerAssetSuffix: currentExtension.takerAssetSuffix, - makingAmountData: currentExtension.makingAmountData, - takingAmountData: currentExtension.takingAmountData, - predicate: currentExtension.predicate, - makerPermit: currentExtension.makerPermit, - preInteraction: currentExtension.preInteraction, - postInteraction: currentExtension.postInteraction, - customData: currentExtension.customData + ...currentExtension, + makerAssetSuffix: suffix }) this.inner.makerTraits.disablePermit2() @@ -777,4 +790,14 @@ export class FusionOrder { this.deadline ) } + + 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/permit/constants.ts b/src/fusion-order/permit/constants.ts index bde498dc..ffea5312 100644 --- a/src/fusion-order/permit/constants.ts +++ b/src/fusion-order/permit/constants.ts @@ -1,8 +1,8 @@ import {EIP712Types} from '@1inch/limit-order-sdk' import {NetworkEnum} from '../../constants.js' -export const PERMIT2_ADDRESS = '0x000000000022D473030F116dDEE9F6B43aC78BA3' -export const PERMIT2_ADDRESS_ZK = '0x0000000000225e31d15943971f47ad3022f714fa' +const PERMIT2_ADDRESS = '0x000000000022D473030F116dDEE9F6B43aC78BA3' +const PERMIT2_ADDRESS_ZK = '0x0000000000225e31d15943971f47ad3022f714fa' export const PERMIT2_ADDRESSES: Record = { [NetworkEnum.ZKSYNC]: PERMIT2_ADDRESS_ZK, diff --git a/src/fusion-order/permit/index.ts b/src/fusion-order/permit/index.ts index 35049c69..d073665e 100644 --- a/src/fusion-order/permit/index.ts +++ b/src/fusion-order/permit/index.ts @@ -1,2 +1,3 @@ export * from './permit-transfer-from.js' -export {PERMIT2_ADDRESS} from './constants.js' +export * from './transfer-from-suffix.js' +export {getPermit2Address} 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 index 687bf58c..ab981927 100644 --- a/src/fusion-order/permit/permit-transfer-from.spec.ts +++ b/src/fusion-order/permit/permit-transfer-from.spec.ts @@ -1,12 +1,8 @@ import {Address} from '@1inch/limit-order-sdk' import {verifyTypedData, Wallet} from 'ethers' import {PermitTransferFrom} from './permit-transfer-from.js' -import { - PERMIT2_ADDRESS, - PERMIT2_ADDRESS_ZK, - PERMIT2_DOMAIN_NAME, - PERMIT_TRANSFER_FROM_TYPES -} from './constants.js' +import {PERMIT2_DOMAIN_NAME, PERMIT_TRANSFER_FROM_TYPES} from './constants.js' +import {getPermit2Address} from './utils.js' import {NetworkEnum} from '../../constants.js' describe('PermitTransferFrom', () => { @@ -33,7 +29,7 @@ describe('PermitTransferFrom', () => { domain: { name: PERMIT2_DOMAIN_NAME, chainId: NetworkEnum.ETHEREUM, - verifyingContract: PERMIT2_ADDRESS + verifyingContract: getPermit2Address(NetworkEnum.ETHEREUM) }, message: { permitted: { @@ -58,7 +54,9 @@ describe('PermitTransferFrom', () => { const typedData = permit.getTypedData(NetworkEnum.ZKSYNC) - expect(typedData.domain.verifyingContract).toBe(PERMIT2_ADDRESS_ZK) + expect(typedData.domain.verifyingContract).toBe( + getPermit2Address(NetworkEnum.ZKSYNC) + ) }) it('should use custom permit2 address when provided', () => { diff --git a/src/fusion-order/permit/permit-transfer-from.ts b/src/fusion-order/permit/permit-transfer-from.ts index bce8cad5..bcc37915 100644 --- a/src/fusion-order/permit/permit-transfer-from.ts +++ b/src/fusion-order/permit/permit-transfer-from.ts @@ -1,8 +1,7 @@ import {Address, EIP712TypedData} from '@1inch/limit-order-sdk' -import {AbiCoder} from 'ethers' -import {trim0x} from '@1inch/byte-utils' 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( @@ -47,28 +46,12 @@ export class PermitTransferFrom { * with selector 0x23b872dd (same as transferFrom). The suffix is everything after (from,to,amount). */ public getTransferFromSuffix(signature: string): string { - const abiCoder = AbiCoder.defaultAbiCoder() - - const encoded = abiCoder.encode( - [ - 'tuple(tuple(address token, uint256 amount) permitted, uint256 nonce, uint256 deadline)', - 'bytes' - ], - [ - { - permitted: { - token: this.token.toString(), - amount: this.maxSpendAmount - }, - nonce: this.nonce, - deadline: this.deadline - }, - signature - ] + return encodeTransferFromSuffix( + this.token, + this.maxSpendAmount, + this.nonce, + this.deadline, + signature ) - - const STRIPPED_HEAD_BYTES = 3 * 32 - - return '0x' + trim0x(encoded).slice(STRIPPED_HEAD_BYTES * 2) } } 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 00000000..af4f24a4 --- /dev/null +++ b/src/fusion-order/permit/transfer-from-suffix.ts @@ -0,0 +1,81 @@ +import {Address} from '@1inch/limit-order-sdk' +import {AbiCoder} from 'ethers' +import {trim0x} from '@1inch/byte-utils' +import assert from 'assert' + +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 +} + +/** + * 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). + */ +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/tests/fusion-order.spec.ts b/tests/fusion-order.spec.ts index 05ac1b70..59738fb1 100644 --- a/tests/fusion-order.spec.ts +++ b/tests/fusion-order.spec.ts @@ -944,6 +944,19 @@ 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()) @@ -971,8 +984,7 @@ describe('SettlementExtension', () => { { allowPartialFills: false, allowMultipleFills: false, - nonce: 1n, - enablePermit2: true + nonce: 1n } ) @@ -1010,14 +1022,33 @@ describe('SettlementExtension', () => { const finalBalances = { usdc: { - maker: await maker.tokenBalance(USDC) + maker: await maker.tokenBalance(USDC), + taker: await taker.tokenBalance(USDC), + protocol: await protocol.tokenBalance(USDC) }, weth: { - maker: await maker.tokenBalance(WETH) + maker: await maker.tokenBalance(WETH), + taker: await taker.tokenBalance(WETH), + protocol: await protocol.tokenBalance(WETH) } } - expect(finalBalances.usdc.maker).toBeGreaterThan(0n) + 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 () => { From cc2df80e6a632acb7b30eee1017a0befed639b10 Mon Sep 17 00:00:00 2001 From: Vladimir Borovik Date: Wed, 18 Feb 2026 20:22:34 +0300 Subject: [PATCH 07/15] chore: use default permit2Proxy addresses --- README.md | 10 +++++----- src/fusion-order/fusion-order.ts | 20 ++++++++++++++++---- src/fusion-order/permit/constants.ts | 20 ++++++++++++++++++++ src/fusion-order/permit/index.ts | 2 +- src/fusion-order/permit/utils.ts | 11 +++++------ 5 files changed, 47 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 803ea83e..6d16d910 100644 --- a/README.md +++ b/README.md @@ -263,7 +263,6 @@ import { OrderStatus, PrivateKeyProviderConnector, Web3Like, - Address, getPermit2Address } from '@1inch/fusion-sdk' import {computeAddress, JsonRpcProvider, Wallet} from 'ethers' @@ -271,7 +270,6 @@ 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 PERMIT2_PROXY_ADDRESS = 'PERMIT2_PROXY_CONTRACT_ADDRESS' const ethersRpcProvider = new JsonRpcProvider(NODE_URL) @@ -315,12 +313,14 @@ async function main() { const {order, quoteId} = await sdk.createOrder(params) // Step 3: Create a transfer permit for the order - const permit2Proxy = new Address(PERMIT2_PROXY_ADDRESS) - const permit = order.createTransferPermit(permit2Proxy) + 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) + const permitSignature = await connector.signTypedData( + params.walletAddress, + permitTypedData + ) // Step 5: Attach the signed permit to the order const orderWithPermit = order.withTransferPermit(permit, permitSignature) diff --git a/src/fusion-order/fusion-order.ts b/src/fusion-order/fusion-order.ts index 990f27b0..ae3168e1 100644 --- a/src/fusion-order/fusion-order.ts +++ b/src/fusion-order/fusion-order.ts @@ -20,6 +20,7 @@ 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 @@ -762,30 +763,41 @@ export class FusionOrder { * * Can only be used for orders where `multipleFillsAllowed` is `false`. * - * The returned permit authorizes the given `permit2Proxy` address (as spender) + * 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 permit2Proxy - The address of the Permit2Proxy contract that will act as spender + * @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(permit2Proxy: Address): PermitTransferFrom { + 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, - permit2Proxy, + spender, randBigInt(UINT_256_MAX), this.deadline ) diff --git a/src/fusion-order/permit/constants.ts b/src/fusion-order/permit/constants.ts index ffea5312..9d736aea 100644 --- a/src/fusion-order/permit/constants.ts +++ b/src/fusion-order/permit/constants.ts @@ -20,6 +20,26 @@ export const PERMIT2_ADDRESSES: Record = { [NetworkEnum.UNICHAIN]: PERMIT2_ADDRESS } +// todo: update +const PERMIT2_PROXY_ADDRESS = '0x0000000000000000000000000000000000000000' +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 = { diff --git a/src/fusion-order/permit/index.ts b/src/fusion-order/permit/index.ts index d073665e..7c2dce78 100644 --- a/src/fusion-order/permit/index.ts +++ b/src/fusion-order/permit/index.ts @@ -1,3 +1,3 @@ export * from './permit-transfer-from.js' export * from './transfer-from-suffix.js' -export {getPermit2Address} from './utils.js' +export {getPermit2Address, getPermit2ProxyAddress} from './utils.js' diff --git a/src/fusion-order/permit/utils.ts b/src/fusion-order/permit/utils.ts index 3d6e1376..68da6657 100644 --- a/src/fusion-order/permit/utils.ts +++ b/src/fusion-order/permit/utils.ts @@ -1,6 +1,6 @@ import {Address} from '@1inch/limit-order-sdk' import assert from 'assert' -import {PERMIT2_ADDRESSES} from './constants.js' +import {PERMIT2_ADDRESSES, PERMIT2_PROXY_ADDRESSES} from './constants.js' import {NetworkEnum} from '../../constants.js' export function getPermit2Address(chainId: number): string { @@ -9,9 +9,8 @@ export function getPermit2Address(chainId: number): string { return PERMIT2_ADDRESSES[chainId as NetworkEnum] } -export function getDefaultPermit2Proxy(): Address { - // todo: fix - throw new Error( - 'permit2Proxy address is required: no default Permit2Proxy addresses configured' - ) +export function getPermit2ProxyAddress(chainId: number): Address { + assert(NetworkEnum[chainId], 'unsupported chainId') + + return new Address(PERMIT2_PROXY_ADDRESSES[chainId as NetworkEnum]) } From e80022e43efda48820c7cbc9db556b246b5c9426 Mon Sep 17 00:00:00 2001 From: rharutyunyan Date: Wed, 25 Mar 2026 15:40:08 +0400 Subject: [PATCH 08/15] feat: update permit2 proxy address --- src/fusion-order/permit/constants.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/fusion-order/permit/constants.ts b/src/fusion-order/permit/constants.ts index 9d736aea..36c5dba1 100644 --- a/src/fusion-order/permit/constants.ts +++ b/src/fusion-order/permit/constants.ts @@ -21,7 +21,7 @@ export const PERMIT2_ADDRESSES: Record = { } // todo: update -const PERMIT2_PROXY_ADDRESS = '0x0000000000000000000000000000000000000000' +const PERMIT2_PROXY_ADDRESS = '0xcf56da25062c954b252515244dfefb739c254c23' const PERMIT2_PROXY_ADDRESS_ZK = '0x0000000000000000000000000000000000000000' export const PERMIT2_PROXY_ADDRESSES: Record = { From dde39df3f80c7b94aca451fd042df08f14039034 Mon Sep 17 00:00:00 2001 From: CI/CD Bot Date: Wed, 25 Mar 2026 11:45:55 +0000 Subject: [PATCH 09/15] version v2.4.7-rc.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 81a91272..d6900edd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@1inch/fusion-sdk", - "version": "2.4.6", + "version": "2.4.7-rc.0", "description": "1inch Fusion SDK", "author": "@1inch", "files": [ From a883a2dad46f88570ab76370f7b74ef0650067cf Mon Sep 17 00:00:00 2001 From: rharutyunyan Date: Thu, 26 Mar 2026 16:56:18 +0400 Subject: [PATCH 10/15] fix: makerAssetSuffix decode --- src/fusion-order/fusion-order.spec.ts | 33 +++++++++++++++++++++------ src/fusion-order/fusion-order.ts | 32 ++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 7 deletions(-) diff --git a/src/fusion-order/fusion-order.spec.ts b/src/fusion-order/fusion-order.spec.ts index 64d25e8b..b203c8f8 100644 --- a/src/fusion-order/fusion-order.spec.ts +++ b/src/fusion-order/fusion-order.spec.ts @@ -907,7 +907,29 @@ describe('FusionOrder Native', () => { expect(orderWithPermit.makerAsset).toEqual(weth) }) - it('should return false for non-permit2 suffix data', () => { + 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 @@ -923,12 +945,9 @@ describe('FusionOrder Native', () => { customData: ext.customData }) - const rebuilt = FusionOrder.fromDataAndExtension( - order.build(), - tampered - ) - - expect(rebuilt.isTransferPermit()).toBe(false) + 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 ae3168e1..564c6044 100644 --- a/src/fusion-order/fusion-order.ts +++ b/src/fusion-order/fusion-order.ts @@ -412,6 +412,10 @@ export class FusionOrder { } ) + if (extension.makerAssetSuffix !== ZX) { + fusionOrder.restoreMakerAssetSuffix(extension.makerAssetSuffix) + } + assert( providedSalt === fusionOrder.salt, 'invalid salt for passed extension' @@ -803,6 +807,34 @@ export class FusionOrder { ) } + /** + * 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 From 9cef761dcb5ccbbef90f823234b70fef62c23570 Mon Sep 17 00:00:00 2001 From: rharutyunyan Date: Thu, 26 Mar 2026 17:00:01 +0400 Subject: [PATCH 11/15] choreL version bump --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d6900edd..42366ebd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@1inch/fusion-sdk", - "version": "2.4.7-rc.0", + "version": "2.4.7-rc.1", "description": "1inch Fusion SDK", "author": "@1inch", "files": [ From 589c4351d36e5760296a2b1148f2dbf948588776 Mon Sep 17 00:00:00 2001 From: CI/CD Bot Date: Thu, 26 Mar 2026 13:00:52 +0000 Subject: [PATCH 12/15] version v2.4.7-rc.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 42366ebd..3103d69d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@1inch/fusion-sdk", - "version": "2.4.7-rc.1", + "version": "2.4.7-rc.2", "description": "1inch Fusion SDK", "author": "@1inch", "files": [ From a94e8c0b7e7a80b4c7fecf41fd23be93507da702 Mon Sep 17 00:00:00 2001 From: rharutyunyan Date: Fri, 27 Mar 2026 14:27:43 +0400 Subject: [PATCH 13/15] feat: adjusted permit2transferFrom encoding function --- src/constants.ts | 4 ++ .../permit/transfer-from-suffix.ts | 44 ++++++------------- 2 files changed, 18 insertions(+), 30 deletions(-) diff --git a/src/constants.ts b/src/constants.ts index 98ef8664..6d443caf 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -16,6 +16,10 @@ 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', diff --git a/src/fusion-order/permit/transfer-from-suffix.ts b/src/fusion-order/permit/transfer-from-suffix.ts index af4f24a4..d0293e66 100644 --- a/src/fusion-order/permit/transfer-from-suffix.ts +++ b/src/fusion-order/permit/transfer-from-suffix.ts @@ -1,18 +1,18 @@ import {Address} from '@1inch/limit-order-sdk' import {AbiCoder} from 'ethers' -import {trim0x} from '@1inch/byte-utils' import assert from 'assert' -const FUNC_N_ZH_TCH_ABI = [ - 'address', - 'address', - 'uint256', +/** + * Permit2Proxy exposes `func_nZHTch(address,address,uint256,((address,uint256),uint256,uint256),bytes)` + * whose selector collides with `transferFrom(address,address,uint256)` (0x23b872dd). + * The LOP calls `_callTransferFromWithSuffix`, appending these extra params as raw suffix bytes. + */ +const PERMIT2_TRANSFER_FROM_EXTRA_PARAMS_ABI = [ 'tuple(tuple(address token, uint256 amount) permitted, uint256 nonce, uint256 deadline)', 'bytes' ] -const ZERO_SLOT = '0'.repeat(64) -const STRIPPED_SLOTS = 3 +const abiCoder = AbiCoder.defaultAbiCoder() export type DecodedTransferPermitSuffix = { token: Address @@ -22,13 +22,6 @@ export type DecodedTransferPermitSuffix = { signature: string } -/** - * 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). - */ export function encodeTransferFromSuffix( token: Address, amount: bigint, @@ -36,12 +29,7 @@ export function encodeTransferFromSuffix( 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, + return abiCoder.encode(PERMIT2_TRANSFER_FROM_EXTRA_PARAMS_ABI, [ { permitted: { token: token.toString(), @@ -52,22 +40,18 @@ export function encodeTransferFromSuffix( }, 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 decoded = abiCoder.decode( + PERMIT2_TRANSFER_FROM_EXTRA_PARAMS_ABI, + suffix + ) - const permit = decoded[3] - const sig: string = decoded[4] + const permit = decoded[0] + const sig: string = decoded[1] assert(sig.length > 0, 'empty permit signature') From a616116d8cc3f6ce8490131caa01ff77d43eb178 Mon Sep 17 00:00:00 2001 From: CI/CD Bot Date: Fri, 27 Mar 2026 13:17:53 +0000 Subject: [PATCH 14/15] version v2.4.7-rc.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3103d69d..6e445550 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@1inch/fusion-sdk", - "version": "2.4.7-rc.2", + "version": "2.4.7-rc.3", "description": "1inch Fusion SDK", "author": "@1inch", "files": [ From 1c56753929546f6d6cea866dc2b6f261bb562db4 Mon Sep 17 00:00:00 2001 From: rharutyunyan Date: Mon, 30 Mar 2026 14:40:44 +0400 Subject: [PATCH 15/15] fix: encoding --- .../permit/transfer-from-suffix.ts | 42 +++++++++++++------ 1 file changed, 30 insertions(+), 12 deletions(-) diff --git a/src/fusion-order/permit/transfer-from-suffix.ts b/src/fusion-order/permit/transfer-from-suffix.ts index d0293e66..be0754b2 100644 --- a/src/fusion-order/permit/transfer-from-suffix.ts +++ b/src/fusion-order/permit/transfer-from-suffix.ts @@ -1,18 +1,27 @@ import {Address} from '@1inch/limit-order-sdk' import {AbiCoder} from 'ethers' +import {trim0x} from '@1inch/byte-utils' import assert from 'assert' /** - * Permit2Proxy exposes `func_nZHTch(address,address,uint256,((address,uint256),uint256,uint256),bytes)` - * whose selector collides with `transferFrom(address,address,uint256)` (0x23b872dd). - * The LOP calls `_callTransferFromWithSuffix`, appending these extra params as raw suffix bytes. + * 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 PERMIT2_TRANSFER_FROM_EXTRA_PARAMS_ABI = [ +const FUNC_N_ZH_TCH_ABI = [ + 'address', + 'address', + 'uint256', 'tuple(tuple(address token, uint256 amount) permitted, uint256 nonce, uint256 deadline)', 'bytes' ] -const abiCoder = AbiCoder.defaultAbiCoder() +const ZERO_SLOT = '0'.repeat(64) +const STRIPPED_SLOTS = 3 export type DecodedTransferPermitSuffix = { token: Address @@ -29,7 +38,12 @@ export function encodeTransferFromSuffix( deadline: bigint, signature: string ): string { - return abiCoder.encode(PERMIT2_TRANSFER_FROM_EXTRA_PARAMS_ABI, [ + 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(), @@ -40,18 +54,22 @@ export function encodeTransferFromSuffix( }, signature ]) + + const strippedHexChars = STRIPPED_SLOTS * 32 * 2 + + return '0x' + trim0x(encoded).slice(strippedHexChars) } export function decodeTransferFromSuffix( suffix: string ): DecodedTransferPermitSuffix { - const decoded = abiCoder.decode( - PERMIT2_TRANSFER_FROM_EXTRA_PARAMS_ABI, - suffix - ) + 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[0] - const sig: string = decoded[1] + const permit = decoded[3] + const sig: string = decoded[4] assert(sig.length > 0, 'empty permit signature')