Skip to content

Commit 83117fa

Browse files
committed
fix: improve permit flow
1 parent 4b03fb0 commit 83117fa

File tree

3 files changed

+235
-80
lines changed

3 files changed

+235
-80
lines changed

src/core/EVM/EVMStepExecutor.ts

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,8 @@ export class EVMStepExecutor extends BaseStepExecutor {
345345
return step
346346
}
347347

348+
let isTransactionRelayed = false
349+
348350
if (atomicBatchSupported) {
349351
const transferCall: Call = {
350352
chainId: fromChain.id,
@@ -387,18 +389,24 @@ export class EVMStepExecutor extends BaseStepExecutor {
387389
callData: transactionRequest.data!,
388390
})
389391
txHash = relayedTransaction.data.taskId
392+
isTransactionRelayed = true
390393
} else {
391394
if (permitSignature) {
392395
// If we have a permit signature, we need to use updated data
393396
transactionRequest.data = permitSignature.data
394397
try {
395398
// Try to re-estimate the gas due to additional Permit data
396-
transactionRequest.gas = await estimateGas(this.client, {
399+
const estimatedGas = await estimateGas(this.client, {
397400
account: this.client.account!,
398401
to: transactionRequest.to as Address,
399402
data: transactionRequest.data as Hex,
400403
value: transactionRequest.value,
401404
})
405+
transactionRequest.gas =
406+
transactionRequest.gas &&
407+
transactionRequest.gas > estimatedGas
408+
? transactionRequest.gas
409+
: estimatedGas
402410
} catch {
403411
// Let the wallet estimate the gas in case of failure
404412
transactionRequest.gas = undefined
@@ -425,7 +433,6 @@ export class EVMStepExecutor extends BaseStepExecutor {
425433
} as SendTransactionParameters)
426434
}
427435
}
428-
429436
process = this.statusManager.updateProcess(
430437
step,
431438
process.type,
@@ -435,10 +442,12 @@ export class EVMStepExecutor extends BaseStepExecutor {
435442
? {
436443
atomicBatchSupported,
437444
}
438-
: {
439-
txHash: txHash,
440-
txLink: `${fromChain.metamask.blockExplorerUrls[0]}tx/${txHash}`,
441-
}
445+
: isTransactionRelayed
446+
? undefined
447+
: {
448+
txHash: txHash,
449+
txLink: `${fromChain.metamask.blockExplorerUrls[0]}tx/${txHash}`,
450+
}
442451
)
443452
}
444453

src/core/EVM/getNativePermit.ts

Lines changed: 219 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
import type { ExtendedChain } from '@lifi/types'
2-
import type { Address, Client } from 'viem'
2+
import {
3+
encodeAbiParameters,
4+
keccak256,
5+
pad,
6+
parseAbiParameters,
7+
toBytes,
8+
toHex,
9+
} from 'viem'
10+
import type { Address, Client, Hex } from 'viem'
11+
import type { TypedDataDomain } from 'viem'
312
import { multicall, readContract } from 'viem/actions'
413
import { eip2612Abi } from './abi.js'
514
import { getMulticallAddress } from './utils.js'
@@ -9,6 +18,136 @@ export type NativePermitData = {
918
version: string
1019
nonce: bigint
1120
supported: boolean
21+
domain: TypedDataDomain
22+
}
23+
24+
/**
25+
* EIP-712 domain typehash with chainId
26+
* @link https://eips.ethereum.org/EIPS/eip-712#specification
27+
*
28+
* keccak256(toBytes(
29+
* 'EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)'
30+
* ))
31+
*/
32+
const EIP712_DOMAIN_TYPEHASH =
33+
'0x8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f' as Hex
34+
35+
/**
36+
* EIP-712 domain typehash with salt (e.g. USDC.e on Polygon)
37+
* @link https://eips.ethereum.org/EIPS/eip-712#specification
38+
*
39+
* keccak256(toBytes(
40+
* 'EIP712Domain(string name,string version,address verifyingContract,bytes32 salt)'
41+
* ))
42+
*/
43+
const EIP712_DOMAIN_TYPEHASH_WITH_SALT =
44+
'0x36c25de3e541d5d970f66e4210d728721220fff5c077cc6cd008b3a0c62adab7' as Hex
45+
46+
function makeDomainSeparator({
47+
name,
48+
version,
49+
chainId,
50+
verifyingContract,
51+
withSalt = false,
52+
}: {
53+
name: string
54+
version: string
55+
chainId: bigint
56+
verifyingContract: Address
57+
withSalt?: boolean
58+
}): Hex {
59+
const nameHash = keccak256(toBytes(name))
60+
const versionHash = keccak256(toBytes(version))
61+
62+
const encoded = withSalt
63+
? encodeAbiParameters(
64+
parseAbiParameters('bytes32, bytes32, bytes32, address, bytes32'),
65+
[
66+
EIP712_DOMAIN_TYPEHASH_WITH_SALT,
67+
nameHash,
68+
versionHash,
69+
verifyingContract,
70+
pad(toHex(chainId), { size: 32 }),
71+
]
72+
)
73+
: encodeAbiParameters(
74+
parseAbiParameters('bytes32, bytes32, bytes32, uint256, address'),
75+
[
76+
EIP712_DOMAIN_TYPEHASH,
77+
nameHash,
78+
versionHash,
79+
chainId,
80+
verifyingContract,
81+
]
82+
)
83+
84+
return keccak256(encoded)
85+
}
86+
87+
// TODO: Add support for EIP-5267 when adoption increases
88+
// This EIP provides a standard way to query domain separator and permit type hash
89+
// via eip712Domain() function, which would simplify permit validation
90+
// https://eips.ethereum.org/EIPS/eip-5267
91+
function validateDomainSeparator({
92+
name,
93+
version,
94+
chainId,
95+
verifyingContract,
96+
domainSeparator,
97+
}: {
98+
name: string
99+
version: string
100+
chainId: bigint
101+
verifyingContract: Address
102+
domainSeparator: Hex
103+
}): { isValid: boolean; domain: TypedDataDomain } {
104+
if (!name || !domainSeparator) {
105+
return {
106+
isValid: false,
107+
domain: {},
108+
}
109+
}
110+
111+
for (const withSalt of [false, true]) {
112+
const computedDS = makeDomainSeparator({
113+
name,
114+
version,
115+
chainId,
116+
verifyingContract,
117+
withSalt,
118+
})
119+
if (domainSeparator.toLowerCase() === computedDS.toLowerCase()) {
120+
return {
121+
isValid: true,
122+
domain: withSalt
123+
? {
124+
name,
125+
version,
126+
verifyingContract,
127+
salt: pad(toHex(chainId), { size: 32 }),
128+
}
129+
: {
130+
name,
131+
version,
132+
chainId,
133+
verifyingContract,
134+
},
135+
}
136+
}
137+
}
138+
139+
return {
140+
isValid: false,
141+
domain: {},
142+
}
143+
}
144+
145+
const defaultPermit: NativePermitData = {
146+
name: '',
147+
version: '1',
148+
nonce: 0n,
149+
supported: false,
150+
domain: {},
12151
}
13152

14153
/**
@@ -27,88 +166,102 @@ export const getNativePermit = async (
27166
try {
28167
const multicallAddress = await getMulticallAddress(chain.id)
29168

30-
if (multicallAddress) {
31-
const [nameResult, domainSeparatorResult, noncesResult, versionResult] =
32-
await multicall(client, {
33-
contracts: [
34-
{
35-
address: tokenAddress,
36-
abi: eip2612Abi,
37-
functionName: 'name',
38-
},
39-
{
40-
address: tokenAddress,
41-
abi: eip2612Abi,
42-
functionName: 'DOMAIN_SEPARATOR',
43-
},
44-
{
45-
address: tokenAddress,
46-
abi: eip2612Abi,
47-
functionName: 'nonces',
48-
args: [client.account!.address],
49-
},
50-
{
51-
address: tokenAddress,
52-
abi: eip2612Abi,
53-
functionName: 'version',
54-
},
55-
],
56-
multicallAddress,
57-
})
58-
59-
const supported =
60-
nameResult.status === 'success' &&
61-
domainSeparatorResult.status === 'success' &&
62-
noncesResult.status === 'success' &&
63-
!!nameResult.result &&
64-
!!domainSeparatorResult.result &&
65-
noncesResult.result !== undefined
66-
67-
return {
68-
name: nameResult.result!,
69-
version: versionResult.result ?? '1',
70-
nonce: noncesResult.result!,
71-
supported,
72-
}
73-
}
74-
75-
// Fallback to individual calls
76-
const [name, domainSeparator, nonce, version] = await Promise.all([
77-
readContract(client, {
169+
const contractCalls = [
170+
{
78171
address: tokenAddress,
79172
abi: eip2612Abi,
80173
functionName: 'name',
81-
}),
82-
readContract(client, {
174+
},
175+
{
83176
address: tokenAddress,
84177
abi: eip2612Abi,
85178
functionName: 'DOMAIN_SEPARATOR',
86-
}),
87-
readContract(client, {
179+
},
180+
{
88181
address: tokenAddress,
89182
abi: eip2612Abi,
90183
functionName: 'nonces',
91184
args: [client.account!.address],
92-
}),
93-
readContract(client, {
185+
},
186+
{
94187
address: tokenAddress,
95188
abi: eip2612Abi,
96189
functionName: 'version',
97-
}),
98-
])
190+
},
191+
] as const
192+
193+
if (multicallAddress) {
194+
const [nameResult, domainSeparatorResult, noncesResult, versionResult] =
195+
await multicall(client, {
196+
contracts: contractCalls,
197+
multicallAddress,
198+
})
199+
200+
if (
201+
nameResult.status !== 'success' ||
202+
domainSeparatorResult.status !== 'success' ||
203+
noncesResult.status !== 'success' ||
204+
!nameResult.result ||
205+
!domainSeparatorResult.result ||
206+
noncesResult.result === undefined
207+
) {
208+
return defaultPermit
209+
}
210+
211+
const { isValid, domain } = validateDomainSeparator({
212+
name: nameResult.result,
213+
version: versionResult.result ?? '1',
214+
chainId: BigInt(chain.id),
215+
verifyingContract: tokenAddress,
216+
domainSeparator: domainSeparatorResult.result,
217+
})
218+
219+
return {
220+
name: nameResult.result,
221+
version: versionResult.result ?? '1',
222+
nonce: noncesResult.result,
223+
supported: isValid,
224+
domain,
225+
}
226+
}
227+
228+
const [nameResult, domainSeparatorResult, noncesResult, versionResult] =
229+
(await Promise.allSettled(
230+
contractCalls.map((call) => readContract(client, call))
231+
)) as [
232+
PromiseSettledResult<string>,
233+
PromiseSettledResult<Hex>,
234+
PromiseSettledResult<bigint>,
235+
PromiseSettledResult<string>,
236+
]
237+
238+
if (
239+
nameResult.status !== 'fulfilled' ||
240+
domainSeparatorResult.status !== 'fulfilled' ||
241+
noncesResult.status !== 'fulfilled'
242+
) {
243+
return defaultPermit
244+
}
245+
246+
const name = nameResult.value
247+
const version =
248+
versionResult.status === 'fulfilled' ? versionResult.value : '1'
249+
const { isValid, domain } = validateDomainSeparator({
250+
name,
251+
version,
252+
chainId: BigInt(chain.id),
253+
verifyingContract: tokenAddress,
254+
domainSeparator: domainSeparatorResult.value,
255+
})
99256

100257
return {
101258
name,
102-
version: version ?? '1',
103-
nonce,
104-
supported: !!name && !!domainSeparator && nonce !== undefined,
259+
version,
260+
nonce: noncesResult.value,
261+
supported: isValid,
262+
domain,
105263
}
106264
} catch {
107-
return {
108-
name: '',
109-
version: '1',
110-
nonce: 0n,
111-
supported: false,
112-
}
265+
return defaultPermit
113266
}
114267
}

0 commit comments

Comments
 (0)