@integraledger/lcp-binding-evm-common 0.12.2 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +80 -0
- package/dist/eip3009.d.ts +37 -0
- package/dist/eip3009.d.ts.map +1 -1
- package/dist/eip3009.js +78 -1
- package/dist/eip3009.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
- package/src/eip3009.ts +90 -1
- package/src/index.ts +2 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,85 @@
|
|
|
1
1
|
# @integraledger/lcp-binding-evm-common
|
|
2
2
|
|
|
3
|
+
## 0.13.0
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- @integraledger/lcp-authority@0.13.0
|
|
8
|
+
- @integraledger/lcp-binding-core@0.13.0
|
|
9
|
+
- @integraledger/lcp-kernel@0.13.0
|
|
10
|
+
|
|
11
|
+
## 0.12.3
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- 82444ad: `verifyEip3009Signature` does the token's whole acceptance test, and says what it does not do.
|
|
16
|
+
|
|
17
|
+
⛔⛔ **IT ANSWERED `true` FOR SIGNATURES `FiatTokenV2` REVERTS ON.** For any `(r, s, v)` the pair
|
|
18
|
+
`(r, n − s, v ^ 1)` recovers the same address, so `recoverTypedDataAddress` alone accepts both encodings —
|
|
19
|
+
**measured: the malleated form of an honest payer's own signature verified.** Circle's token routes EOA
|
|
20
|
+
signatures through `ECRecover.sol`, which reverts before recovery on a high-s value and on any `v` outside
|
|
21
|
+
`{27, 28}`. A payer could present the malleated form, be served the resource, and the transfer would revert
|
|
22
|
+
— free goods, which is the exact failure the function exists to prevent. `isCanonicalSignature` is those two
|
|
23
|
+
gates, exported so its boundary is reachable: `s === n/2` is ACCEPTED, because the token's guard is
|
|
24
|
+
`s > n/2`, and no signing run will ever produce that value.
|
|
25
|
+
|
|
26
|
+
⛔ **The components are read out of the regex MATCH, not sliced at fixed offsets.** With `.slice(66, 130)`,
|
|
27
|
+
dropping the leading anchor shifted every offset and the malformed input failed the `v` gate by accident —
|
|
28
|
+
so the anchor's own mutant survived. The shape check and the component read are now one statement. (Fourth
|
|
29
|
+
instance of an anchor mutant surviving in one day; the other three were killed by adding a `junk0x…` case.)
|
|
30
|
+
|
|
31
|
+
⛔⛔ **AND THE ERC-1271 CLAIM WAS FALSE.** The first version of this docblock said a contract wallet _"cannot
|
|
32
|
+
sign an EIP-3009 authorization at all — the token has no `isValidSignature` call in that path"_. That is
|
|
33
|
+
wrong for **`FiatTokenV2_2`**, the 2023 implementation deployed as USDC on Base, Arbitrum and Polygon among
|
|
34
|
+
others, which routes `transferWithAuthorization` through `SignatureChecker.isValidSignatureNow` and
|
|
35
|
+
therefore **does** dispatch to ERC-1271 for a contract account. Deciding that needs a chain read and this
|
|
36
|
+
function takes no ports, so its contract is stated narrowly instead: **a `false` means "not signed by that
|
|
37
|
+
EOA", never "the chain will reject it".** A caller that must accept smart-account payers has to make the
|
|
38
|
+
ERC-1271 call itself; one that refuses on this answer alone is choosing to accept EOA payers only, and
|
|
39
|
+
should say so at the call site.
|
|
40
|
+
|
|
41
|
+
⚠️ **The `try` came off.** It caught every failure and answered `false`, including a `typedData` this
|
|
42
|
+
deployment could not encode — so an operator with a mis-copied `tokenName` would have been told that every
|
|
43
|
+
honest payer is a forger, which is the live mistake the docblock itself names. The two untrusted inputs
|
|
44
|
+
still answer `false`; a wiring error throws.
|
|
45
|
+
|
|
46
|
+
Mutation: `binding-evm-common` 98.56, floor 98. The two remaining `eip3009.ts` survivors are pre-existing,
|
|
47
|
+
in `eip155ChainId`.
|
|
48
|
+
|
|
49
|
+
- 48d1346: `verifyEip3009Signature` — the check the token makes, available before the money moves.
|
|
50
|
+
|
|
51
|
+
`buildEip3009TypedData` has been here since the canonical EVM binding shipped, and nothing beside it could
|
|
52
|
+
answer whether a given authorization was actually signed by the account it names. So a seller surface
|
|
53
|
+
holding a payer's credential had three bad options: take the signature on trust until settlement, grow a
|
|
54
|
+
viem dependency of its own, or reimplement recovery. `seller-mpp` needs exactly this for the 2026-08-24
|
|
55
|
+
audit's C-28, and `seller-x402` already reaches here for `makeEvmAcceptanceVerifier` — the commons owns EVM
|
|
56
|
+
crypto, and this is the piece that was missing.
|
|
57
|
+
|
|
58
|
+
⭐ **`ecrecover`, because that is what the TOKEN does.** `FiatTokenV2.transferWithAuthorization` recovers the
|
|
59
|
+
signer from the EIP-712 digest and compares it to `from`; this recovers and compares the same way.
|
|
60
|
+
|
|
61
|
+
⛔ **It deliberately does NOT fall back to ERC-1271.** A contract wallet cannot sign an EIP-3009
|
|
62
|
+
authorization at all — the token has no `isValidSignature` call on that path — so accepting one here would
|
|
63
|
+
accept a payment the chain rejects, which is the failure this check exists to prevent rather than to cause.
|
|
64
|
+
|
|
65
|
+
⛔ **Answers `false` rather than throwing,** for the reason `atrHashEquals` states about itself: a predicate
|
|
66
|
+
that throws is a worse contract than one that answers. A malformed signature, a signature of the wrong
|
|
67
|
+
length and an `expectedSigner` that is not an address are all _"no, this is not signed by them"_ — and the
|
|
68
|
+
caller is holding an untrusted credential and needs a value it can put on the wire. Addresses compare as
|
|
69
|
+
decoded 20-byte values, so a payer's own lowercase spelling is the same payer.
|
|
70
|
+
|
|
71
|
+
⚠️ The domain stays the caller's to get right, and it is where this goes wrong in practice: `tokenName` and
|
|
72
|
+
`tokenVersion` are the token's own EIP-712 domain and differ between USDC deployments, so a signature
|
|
73
|
+
verified against a domain copied from another chain fails here exactly as it would on-chain.
|
|
74
|
+
|
|
75
|
+
Mutation: `binding-evm-common` 98.40, floor 98 holds. ⛔ Both regex anchors needed their own case —
|
|
76
|
+
`junk0x…` and `0x…ff` — because every other malformed input was refused by both mutants and the anchors
|
|
77
|
+
could otherwise be deleted with the suite green.
|
|
78
|
+
|
|
79
|
+
- @integraledger/lcp-authority@0.12.3
|
|
80
|
+
- @integraledger/lcp-binding-core@0.12.3
|
|
81
|
+
- @integraledger/lcp-kernel@0.12.3
|
|
82
|
+
|
|
3
83
|
## 0.12.2
|
|
4
84
|
|
|
5
85
|
### Patch Changes
|
package/dist/eip3009.d.ts
CHANGED
|
@@ -101,4 +101,41 @@ export interface LcpEvmSigner {
|
|
|
101
101
|
message: Record<string, unknown>;
|
|
102
102
|
}): Promise<Hex>;
|
|
103
103
|
}
|
|
104
|
+
/**
|
|
105
|
+
* Whether the 65-byte signature is one `FiatTokenV2` will accept at all, before asking who signed it.
|
|
106
|
+
*
|
|
107
|
+
* Not a style check. Both rules come straight out of the token's `ECRecover.sol`, and skipping either one
|
|
108
|
+
* turns {@link verifyEip3009Signature} from *"the chain will accept this"* into *"some chain might"*.
|
|
109
|
+
*
|
|
110
|
+
* ⭐ Exported so the boundary is reachable by a test. `s === n/2` is ACCEPTED (the token's guard is
|
|
111
|
+
* `s > n/2`), and no signing run will ever produce that value — so an off-by-one there is invisible from
|
|
112
|
+
* the public predicate, where such a signature simply fails to recover and answers `false` either way.
|
|
113
|
+
*/
|
|
114
|
+
export declare function isCanonicalSignature(signature: string): boolean;
|
|
115
|
+
/**
|
|
116
|
+
* Whether an EIP-3009 authorization was signed, canonically, by the account it says it is from.
|
|
117
|
+
*
|
|
118
|
+
* ⭐ **The token's OWN acceptance test, not merely `ecrecover`.** `FiatTokenV2.transferWithAuthorization`
|
|
119
|
+
* routes an EOA signature through `ECRecover.sol`, which refuses a high-s signature and any `v` outside
|
|
120
|
+
* `{27, 28}` before recovering. A check that only recovered would answer `true` to the malleated form of an
|
|
121
|
+
* honest payer's own signature — measured — and the seller would serve the resource against a transfer that
|
|
122
|
+
* reverts. {@link isCanonicalSignature} is those two gates.
|
|
123
|
+
*
|
|
124
|
+
* ⛔⛔ **EOA ONLY, AND ON `FiatTokenV2_2` THAT IS NARROWER THAN THE TOKEN.** An earlier draft of this
|
|
125
|
+
* docblock said a contract wallet *"cannot sign an EIP-3009 authorization at all"*. That is false for
|
|
126
|
+
* `FiatTokenV2_2` — the 2023 implementation deployed as USDC on Base, Arbitrum and Polygon among others —
|
|
127
|
+
* which routes `transferWithAuthorization` through `SignatureChecker.isValidSignatureNow` and therefore
|
|
128
|
+
* DOES dispatch to ERC-1271 for a contract account. Deciding that needs a chain read, and this function
|
|
129
|
+
* takes no ports: **a `false` here means "not signed by that EOA", never "the chain will reject it"**. A
|
|
130
|
+
* caller that must accept smart-account payers has to make the ERC-1271 call itself, and one that refuses
|
|
131
|
+
* on this answer alone is choosing to accept EOA payers only. Say which, at the call site.
|
|
132
|
+
*
|
|
133
|
+
* ⛔ **Answers `false` rather than throwing for the two UNTRUSTED inputs,** for the reason `atrHashEquals`
|
|
134
|
+
* states about itself: a predicate that throws is a worse contract than one that answers. A malformed
|
|
135
|
+
* `signature` and an `expectedSigner` that is not an address are facts about the credential in hand.
|
|
136
|
+
* ⚠️ A `typedData` that cannot be encoded is NOT — that is this deployment's own domain being wrong, and
|
|
137
|
+
* returning `false` for it would tell an operator with a mis-copied `tokenName` that every honest payer is
|
|
138
|
+
* a forger. It throws.
|
|
139
|
+
*/
|
|
140
|
+
export declare function verifyEip3009Signature(typedData: Eip3009TypedData, signature: string, expectedSigner: string): Promise<boolean>;
|
|
104
141
|
//# sourceMappingURL=eip3009.d.ts.map
|
package/dist/eip3009.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"eip3009.d.ts","sourceRoot":"","sources":["../src/eip3009.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,
|
|
1
|
+
{"version":3,"file":"eip3009.d.ts","sourceRoot":"","sources":["../src/eip3009.ts"],"names":[],"mappings":"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"}
|
package/dist/eip3009.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* The x402-client scheme class is deliberately NOT here; it lives in binding-evm-x402. This module is
|
|
8
8
|
* protocol-agnostic typed-data construction, viem-only.
|
|
9
9
|
*/
|
|
10
|
-
import { getAddress } from "viem";
|
|
10
|
+
import { getAddress, recoverTypedDataAddress, } from "viem";
|
|
11
11
|
/** EIP-3009 TransferWithAuthorization typed-data field layout (USDC FiatTokenV2). */
|
|
12
12
|
export const TRANSFER_WITH_AUTHORIZATION_TYPE = {
|
|
13
13
|
TransferWithAuthorization: [
|
|
@@ -70,4 +70,81 @@ export function buildEip3009TypedData(i) {
|
|
|
70
70
|
};
|
|
71
71
|
return { authorization, typedData };
|
|
72
72
|
}
|
|
73
|
+
/**
|
|
74
|
+
* secp256k1's order halved — the low-s boundary `FiatTokenV2`'s own `ECRecover.sol` enforces.
|
|
75
|
+
*
|
|
76
|
+
* ⛔⛔ **A SIGNATURE HAS TWO VALID ENCODINGS AND THE TOKEN ACCEPTS ONE.** For any `(r, s, v)` the pair
|
|
77
|
+
* `(r, n − s, v ^ 1)` recovers the same address, so `ecrecover` alone says `true` to both. Circle's token
|
|
78
|
+
* reverts on the high-s form before `ecrecover` is reached — *"ECRecover: invalid signature 's' value"* —
|
|
79
|
+
* and on any `v` outside `{27, 28}`. A pre-flight check without these gates answers `true` for a
|
|
80
|
+
* credential the chain refuses, and the seller has already served the resource by then.
|
|
81
|
+
*/
|
|
82
|
+
const SECP256K1_HALF_N = 0x7fffffffffffffffffffffffffffffff5d576e7357a4501ddfe92f46681b20a0n;
|
|
83
|
+
/**
|
|
84
|
+
* Whether the 65-byte signature is one `FiatTokenV2` will accept at all, before asking who signed it.
|
|
85
|
+
*
|
|
86
|
+
* Not a style check. Both rules come straight out of the token's `ECRecover.sol`, and skipping either one
|
|
87
|
+
* turns {@link verifyEip3009Signature} from *"the chain will accept this"* into *"some chain might"*.
|
|
88
|
+
*
|
|
89
|
+
* ⭐ Exported so the boundary is reachable by a test. `s === n/2` is ACCEPTED (the token's guard is
|
|
90
|
+
* `s > n/2`), and no signing run will ever produce that value — so an off-by-one there is invisible from
|
|
91
|
+
* the public predicate, where such a signature simply fails to recover and answers `false` either way.
|
|
92
|
+
*/
|
|
93
|
+
export function isCanonicalSignature(signature) {
|
|
94
|
+
// ⛔ Captured, not sliced at fixed offsets. With `.slice(66, 130)` the components were read from
|
|
95
|
+
// positions that only line up when the anchors hold — so dropping `^` shifted every offset and the
|
|
96
|
+
// malformed input failed the `v` gate by accident, which left the anchor's mutant alive. Reading r/s/v
|
|
97
|
+
// out of the match makes the shape check and the component read the same statement.
|
|
98
|
+
const parts = /^0x([0-9a-fA-F]{64})([0-9a-fA-F]{64})([0-9a-fA-F]{2})$/.exec(signature);
|
|
99
|
+
if (parts === null)
|
|
100
|
+
return false;
|
|
101
|
+
if (BigInt(`0x${parts[2]}`) > SECP256K1_HALF_N)
|
|
102
|
+
return false;
|
|
103
|
+
const v = Number.parseInt(parts[3], 16);
|
|
104
|
+
return v === 27 || v === 28;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Whether an EIP-3009 authorization was signed, canonically, by the account it says it is from.
|
|
108
|
+
*
|
|
109
|
+
* ⭐ **The token's OWN acceptance test, not merely `ecrecover`.** `FiatTokenV2.transferWithAuthorization`
|
|
110
|
+
* routes an EOA signature through `ECRecover.sol`, which refuses a high-s signature and any `v` outside
|
|
111
|
+
* `{27, 28}` before recovering. A check that only recovered would answer `true` to the malleated form of an
|
|
112
|
+
* honest payer's own signature — measured — and the seller would serve the resource against a transfer that
|
|
113
|
+
* reverts. {@link isCanonicalSignature} is those two gates.
|
|
114
|
+
*
|
|
115
|
+
* ⛔⛔ **EOA ONLY, AND ON `FiatTokenV2_2` THAT IS NARROWER THAN THE TOKEN.** An earlier draft of this
|
|
116
|
+
* docblock said a contract wallet *"cannot sign an EIP-3009 authorization at all"*. That is false for
|
|
117
|
+
* `FiatTokenV2_2` — the 2023 implementation deployed as USDC on Base, Arbitrum and Polygon among others —
|
|
118
|
+
* which routes `transferWithAuthorization` through `SignatureChecker.isValidSignatureNow` and therefore
|
|
119
|
+
* DOES dispatch to ERC-1271 for a contract account. Deciding that needs a chain read, and this function
|
|
120
|
+
* takes no ports: **a `false` here means "not signed by that EOA", never "the chain will reject it"**. A
|
|
121
|
+
* caller that must accept smart-account payers has to make the ERC-1271 call itself, and one that refuses
|
|
122
|
+
* on this answer alone is choosing to accept EOA payers only. Say which, at the call site.
|
|
123
|
+
*
|
|
124
|
+
* ⛔ **Answers `false` rather than throwing for the two UNTRUSTED inputs,** for the reason `atrHashEquals`
|
|
125
|
+
* states about itself: a predicate that throws is a worse contract than one that answers. A malformed
|
|
126
|
+
* `signature` and an `expectedSigner` that is not an address are facts about the credential in hand.
|
|
127
|
+
* ⚠️ A `typedData` that cannot be encoded is NOT — that is this deployment's own domain being wrong, and
|
|
128
|
+
* returning `false` for it would tell an operator with a mis-copied `tokenName` that every honest payer is
|
|
129
|
+
* a forger. It throws.
|
|
130
|
+
*/
|
|
131
|
+
export async function verifyEip3009Signature(typedData, signature, expectedSigner) {
|
|
132
|
+
if (!/^0x[0-9a-fA-F]{40}$/.test(expectedSigner))
|
|
133
|
+
return false;
|
|
134
|
+
if (!isCanonicalSignature(signature))
|
|
135
|
+
return false;
|
|
136
|
+
// ⛔ NOT wrapped in a try. `isCanonicalSignature` has already established that the signature parses, so
|
|
137
|
+
// the only way this throws now is an unencodable `typedData` — a wiring error, and one a `false` would
|
|
138
|
+
// report as the counterparty's fault. See the head note.
|
|
139
|
+
const recovered = await recoverTypedDataAddress({
|
|
140
|
+
domain: typedData.domain,
|
|
141
|
+
types: typedData.types,
|
|
142
|
+
primaryType: typedData.primaryType,
|
|
143
|
+
message: typedData.message,
|
|
144
|
+
signature: signature,
|
|
145
|
+
});
|
|
146
|
+
// Compared as decoded 20-byte values, not as strings: `getAddress` checksums both sides, so a payer
|
|
147
|
+
// spelling their own address in lowercase is the same payer.
|
|
148
|
+
return getAddress(recovered) === getAddress(expectedSigner);
|
|
149
|
+
}
|
|
73
150
|
//# sourceMappingURL=eip3009.js.map
|
package/dist/eip3009.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"eip3009.js","sourceRoot":"","sources":["../src/eip3009.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,
|
|
1
|
+
{"version":3,"file":"eip3009.js","sourceRoot":"","sources":["../src/eip3009.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export { makeChainReader } from "./client.js";
|
|
2
2
|
export { decodeEasAttestation, EAS_GET_ATTESTATION_ABI, type EasAttestation, isEasValidAsOf, type RawEasAttestation, readEasAttestation, } from "./eas.js";
|
|
3
3
|
export { hashEip712 } from "./eip712.js";
|
|
4
|
-
export { assertBytes32, type BuildEip3009Input, buildEip3009TypedData, type Eip3009Authorization, type Eip3009TypedData, eip155ChainId, type LcpEvmSigner, TRANSFER_WITH_AUTHORIZATION_TYPE, } from "./eip3009.js";
|
|
4
|
+
export { assertBytes32, type BuildEip3009Input, buildEip3009TypedData, type Eip3009Authorization, type Eip3009TypedData, eip155ChainId, isCanonicalSignature, type LcpEvmSigner, TRANSFER_WITH_AUTHORIZATION_TYPE, verifyEip3009Signature, } from "./eip3009.js";
|
|
5
5
|
export { assetWasTransferred, ERC20_TRANSFER_TOPIC0 } from "./erc20.js";
|
|
6
6
|
export { ACCEPTANCE_DOMAIN_NAME, ACCEPTANCE_DOMAIN_VERSION, ACCEPTANCE_ENVELOPE_TYPE, type AcceptanceSignatureInput, type AcceptanceTypedData, type AcceptanceVerifyOpts, buildAcceptanceTypedData, EVM_ACCEPTANCE_SCHEMES, EVM_PAYLOAD_TYPES, type EvmAcceptanceScheme, type EvmPayloadType, isEvmAcceptanceScheme, isEvmPayloadType, makeEvmAcceptanceVerifier, makeLocalEvmSigner, rfc3339ToUnixSeconds, verifyAcceptanceSignature, } from "./erc1271.js";
|
|
7
7
|
export { AUTHORIZATION_USED_ABI, type AuthorizationUsedEvent, type OnChainAtrHashProof, readAuthorizationUsed, refOf, verifyAtrHashOnChain, } from "./events.js";
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,EACL,oBAAoB,EACpB,uBAAuB,EACvB,KAAK,cAAc,EACnB,cAAc,EACd,KAAK,iBAAiB,EACtB,kBAAkB,GACnB,MAAM,UAAU,CAAC;AAClB,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EACL,aAAa,EACb,KAAK,iBAAiB,EACtB,qBAAqB,EACrB,KAAK,oBAAoB,EACzB,KAAK,gBAAgB,EACrB,aAAa,EACb,KAAK,YAAY,EACjB,gCAAgC,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,EACL,oBAAoB,EACpB,uBAAuB,EACvB,KAAK,cAAc,EACnB,cAAc,EACd,KAAK,iBAAiB,EACtB,kBAAkB,GACnB,MAAM,UAAU,CAAC;AAClB,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EACL,aAAa,EACb,KAAK,iBAAiB,EACtB,qBAAqB,EACrB,KAAK,oBAAoB,EACzB,KAAK,gBAAgB,EACrB,aAAa,EACb,oBAAoB,EACpB,KAAK,YAAY,EACjB,gCAAgC,EAChC,sBAAsB,GACvB,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,mBAAmB,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AACxE,OAAO,EACL,sBAAsB,EACtB,yBAAyB,EACzB,wBAAwB,EACxB,KAAK,wBAAwB,EAC7B,KAAK,mBAAmB,EACxB,KAAK,oBAAoB,EACzB,wBAAwB,EACxB,sBAAsB,EACtB,iBAAiB,EACjB,KAAK,mBAAmB,EACxB,KAAK,cAAc,EACnB,qBAAqB,EACrB,gBAAgB,EAChB,yBAAyB,EACzB,kBAAkB,EAClB,oBAAoB,EACpB,yBAAyB,GAC1B,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,sBAAsB,EACtB,KAAK,sBAAsB,EAC3B,KAAK,mBAAmB,EACxB,qBAAqB,EACrB,KAAK,EACL,oBAAoB,GACrB,MAAM,aAAa,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export { makeChainReader } from "./client.js";
|
|
2
2
|
export { decodeEasAttestation, EAS_GET_ATTESTATION_ABI, isEasValidAsOf, readEasAttestation, } from "./eas.js";
|
|
3
3
|
export { hashEip712 } from "./eip712.js";
|
|
4
|
-
export { assertBytes32, buildEip3009TypedData, eip155ChainId, TRANSFER_WITH_AUTHORIZATION_TYPE, } from "./eip3009.js";
|
|
4
|
+
export { assertBytes32, buildEip3009TypedData, eip155ChainId, isCanonicalSignature, TRANSFER_WITH_AUTHORIZATION_TYPE, verifyEip3009Signature, } from "./eip3009.js";
|
|
5
5
|
export { assetWasTransferred, ERC20_TRANSFER_TOPIC0 } from "./erc20.js";
|
|
6
6
|
export { ACCEPTANCE_DOMAIN_NAME, ACCEPTANCE_DOMAIN_VERSION, ACCEPTANCE_ENVELOPE_TYPE, buildAcceptanceTypedData, EVM_ACCEPTANCE_SCHEMES, EVM_PAYLOAD_TYPES, isEvmAcceptanceScheme, isEvmPayloadType, makeEvmAcceptanceVerifier, makeLocalEvmSigner, rfc3339ToUnixSeconds, verifyAcceptanceSignature, } from "./erc1271.js";
|
|
7
7
|
export { AUTHORIZATION_USED_ABI, readAuthorizationUsed, refOf, verifyAtrHashOnChain, } from "./events.js";
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,EACL,oBAAoB,EACpB,uBAAuB,EAEvB,cAAc,EAEd,kBAAkB,GACnB,MAAM,UAAU,CAAC;AAClB,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EACL,aAAa,EAEb,qBAAqB,EAGrB,aAAa,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,EACL,oBAAoB,EACpB,uBAAuB,EAEvB,cAAc,EAEd,kBAAkB,GACnB,MAAM,UAAU,CAAC;AAClB,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EACL,aAAa,EAEb,qBAAqB,EAGrB,aAAa,EACb,oBAAoB,EAEpB,gCAAgC,EAChC,sBAAsB,GACvB,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,mBAAmB,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AACxE,OAAO,EACL,sBAAsB,EACtB,yBAAyB,EACzB,wBAAwB,EAIxB,wBAAwB,EACxB,sBAAsB,EACtB,iBAAiB,EAGjB,qBAAqB,EACrB,gBAAgB,EAChB,yBAAyB,EACzB,kBAAkB,EAClB,oBAAoB,EACpB,yBAAyB,GAC1B,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,sBAAsB,EAGtB,qBAAqB,EACrB,KAAK,EACL,oBAAoB,GACrB,MAAM,aAAa,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@integraledger/lcp-binding-evm-common",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"description": "Shared EVM machinery for LCP rail bindings: typed-data construction and signature verification.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"lcp",
|
|
@@ -44,9 +44,9 @@
|
|
|
44
44
|
"homepage": "https://github.com/IntegraLedger/integra-protocol/tree/main/packages/binding-evm-common#readme",
|
|
45
45
|
"dependencies": {
|
|
46
46
|
"viem": "2.55.11",
|
|
47
|
-
"@integraledger/lcp-
|
|
48
|
-
"@integraledger/lcp-
|
|
49
|
-
"@integraledger/lcp-kernel": "0.
|
|
47
|
+
"@integraledger/lcp-authority": "0.13.0",
|
|
48
|
+
"@integraledger/lcp-binding-core": "0.13.0",
|
|
49
|
+
"@integraledger/lcp-kernel": "0.13.0"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
52
|
"@types/node": "24.13.3",
|
package/src/eip3009.ts
CHANGED
|
@@ -7,7 +7,13 @@
|
|
|
7
7
|
* The x402-client scheme class is deliberately NOT here; it lives in binding-evm-x402. This module is
|
|
8
8
|
* protocol-agnostic typed-data construction, viem-only.
|
|
9
9
|
*/
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
type Address,
|
|
12
|
+
getAddress,
|
|
13
|
+
type Hex,
|
|
14
|
+
recoverTypedDataAddress,
|
|
15
|
+
type TypedDataDomain,
|
|
16
|
+
} from "viem";
|
|
11
17
|
|
|
12
18
|
/** EIP-3009 TransferWithAuthorization typed-data field layout (USDC FiatTokenV2). */
|
|
13
19
|
export const TRANSFER_WITH_AUTHORIZATION_TYPE = {
|
|
@@ -140,3 +146,86 @@ export interface LcpEvmSigner {
|
|
|
140
146
|
message: Record<string, unknown>;
|
|
141
147
|
}): Promise<Hex>;
|
|
142
148
|
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* secp256k1's order halved — the low-s boundary `FiatTokenV2`'s own `ECRecover.sol` enforces.
|
|
152
|
+
*
|
|
153
|
+
* ⛔⛔ **A SIGNATURE HAS TWO VALID ENCODINGS AND THE TOKEN ACCEPTS ONE.** For any `(r, s, v)` the pair
|
|
154
|
+
* `(r, n − s, v ^ 1)` recovers the same address, so `ecrecover` alone says `true` to both. Circle's token
|
|
155
|
+
* reverts on the high-s form before `ecrecover` is reached — *"ECRecover: invalid signature 's' value"* —
|
|
156
|
+
* and on any `v` outside `{27, 28}`. A pre-flight check without these gates answers `true` for a
|
|
157
|
+
* credential the chain refuses, and the seller has already served the resource by then.
|
|
158
|
+
*/
|
|
159
|
+
const SECP256K1_HALF_N =
|
|
160
|
+
0x7fffffffffffffffffffffffffffffff5d576e7357a4501ddfe92f46681b20a0n;
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Whether the 65-byte signature is one `FiatTokenV2` will accept at all, before asking who signed it.
|
|
164
|
+
*
|
|
165
|
+
* Not a style check. Both rules come straight out of the token's `ECRecover.sol`, and skipping either one
|
|
166
|
+
* turns {@link verifyEip3009Signature} from *"the chain will accept this"* into *"some chain might"*.
|
|
167
|
+
*
|
|
168
|
+
* ⭐ Exported so the boundary is reachable by a test. `s === n/2` is ACCEPTED (the token's guard is
|
|
169
|
+
* `s > n/2`), and no signing run will ever produce that value — so an off-by-one there is invisible from
|
|
170
|
+
* the public predicate, where such a signature simply fails to recover and answers `false` either way.
|
|
171
|
+
*/
|
|
172
|
+
export function isCanonicalSignature(signature: string): boolean {
|
|
173
|
+
// ⛔ Captured, not sliced at fixed offsets. With `.slice(66, 130)` the components were read from
|
|
174
|
+
// positions that only line up when the anchors hold — so dropping `^` shifted every offset and the
|
|
175
|
+
// malformed input failed the `v` gate by accident, which left the anchor's mutant alive. Reading r/s/v
|
|
176
|
+
// out of the match makes the shape check and the component read the same statement.
|
|
177
|
+
const parts = /^0x([0-9a-fA-F]{64})([0-9a-fA-F]{64})([0-9a-fA-F]{2})$/.exec(
|
|
178
|
+
signature,
|
|
179
|
+
);
|
|
180
|
+
if (parts === null) return false;
|
|
181
|
+
if (BigInt(`0x${parts[2] as string}`) > SECP256K1_HALF_N) return false;
|
|
182
|
+
const v = Number.parseInt(parts[3] as string, 16);
|
|
183
|
+
return v === 27 || v === 28;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Whether an EIP-3009 authorization was signed, canonically, by the account it says it is from.
|
|
188
|
+
*
|
|
189
|
+
* ⭐ **The token's OWN acceptance test, not merely `ecrecover`.** `FiatTokenV2.transferWithAuthorization`
|
|
190
|
+
* routes an EOA signature through `ECRecover.sol`, which refuses a high-s signature and any `v` outside
|
|
191
|
+
* `{27, 28}` before recovering. A check that only recovered would answer `true` to the malleated form of an
|
|
192
|
+
* honest payer's own signature — measured — and the seller would serve the resource against a transfer that
|
|
193
|
+
* reverts. {@link isCanonicalSignature} is those two gates.
|
|
194
|
+
*
|
|
195
|
+
* ⛔⛔ **EOA ONLY, AND ON `FiatTokenV2_2` THAT IS NARROWER THAN THE TOKEN.** An earlier draft of this
|
|
196
|
+
* docblock said a contract wallet *"cannot sign an EIP-3009 authorization at all"*. That is false for
|
|
197
|
+
* `FiatTokenV2_2` — the 2023 implementation deployed as USDC on Base, Arbitrum and Polygon among others —
|
|
198
|
+
* which routes `transferWithAuthorization` through `SignatureChecker.isValidSignatureNow` and therefore
|
|
199
|
+
* DOES dispatch to ERC-1271 for a contract account. Deciding that needs a chain read, and this function
|
|
200
|
+
* takes no ports: **a `false` here means "not signed by that EOA", never "the chain will reject it"**. A
|
|
201
|
+
* caller that must accept smart-account payers has to make the ERC-1271 call itself, and one that refuses
|
|
202
|
+
* on this answer alone is choosing to accept EOA payers only. Say which, at the call site.
|
|
203
|
+
*
|
|
204
|
+
* ⛔ **Answers `false` rather than throwing for the two UNTRUSTED inputs,** for the reason `atrHashEquals`
|
|
205
|
+
* states about itself: a predicate that throws is a worse contract than one that answers. A malformed
|
|
206
|
+
* `signature` and an `expectedSigner` that is not an address are facts about the credential in hand.
|
|
207
|
+
* ⚠️ A `typedData` that cannot be encoded is NOT — that is this deployment's own domain being wrong, and
|
|
208
|
+
* returning `false` for it would tell an operator with a mis-copied `tokenName` that every honest payer is
|
|
209
|
+
* a forger. It throws.
|
|
210
|
+
*/
|
|
211
|
+
export async function verifyEip3009Signature(
|
|
212
|
+
typedData: Eip3009TypedData,
|
|
213
|
+
signature: string,
|
|
214
|
+
expectedSigner: string,
|
|
215
|
+
): Promise<boolean> {
|
|
216
|
+
if (!/^0x[0-9a-fA-F]{40}$/.test(expectedSigner)) return false;
|
|
217
|
+
if (!isCanonicalSignature(signature)) return false;
|
|
218
|
+
// ⛔ NOT wrapped in a try. `isCanonicalSignature` has already established that the signature parses, so
|
|
219
|
+
// the only way this throws now is an unencodable `typedData` — a wiring error, and one a `false` would
|
|
220
|
+
// report as the counterparty's fault. See the head note.
|
|
221
|
+
const recovered: Address = await recoverTypedDataAddress({
|
|
222
|
+
domain: typedData.domain,
|
|
223
|
+
types: typedData.types,
|
|
224
|
+
primaryType: typedData.primaryType,
|
|
225
|
+
message: typedData.message,
|
|
226
|
+
signature: signature as Hex,
|
|
227
|
+
});
|
|
228
|
+
// Compared as decoded 20-byte values, not as strings: `getAddress` checksums both sides, so a payer
|
|
229
|
+
// spelling their own address in lowercase is the same payer.
|
|
230
|
+
return getAddress(recovered) === getAddress(expectedSigner);
|
|
231
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -15,8 +15,10 @@ export {
|
|
|
15
15
|
type Eip3009Authorization,
|
|
16
16
|
type Eip3009TypedData,
|
|
17
17
|
eip155ChainId,
|
|
18
|
+
isCanonicalSignature,
|
|
18
19
|
type LcpEvmSigner,
|
|
19
20
|
TRANSFER_WITH_AUTHORIZATION_TYPE,
|
|
21
|
+
verifyEip3009Signature,
|
|
20
22
|
} from "./eip3009.js";
|
|
21
23
|
export { assetWasTransferred, ERC20_TRANSFER_TOPIC0 } from "./erc20.js";
|
|
22
24
|
export {
|