Skip to main content

Dual-Signature Pattern

Every operation in the Stablecoin Stack requires exactly two independent ECDSA signatures. They serve different purposes, are verified by different components, and protect against different classes of attack. Neither is sufficient alone.


The Two Signatures

Permit Signature (permitSig / v1, r1, s1)

PropertyValue
SignsERC-2612 Permit typed-data: owner, spender, value, nonce, deadline
Produced byThe token holder (owner)
Verified byThe ERC-2612 token contract inside its permit() function
EffectGrants the Settlement Contract an allowance to pull tokens from the owner's wallet
Replay protectionERC-2612 nonce — consumed on first use
DomainToken contract's EIP-712 domain separator

This signature answers: "Is the token holder allowing their tokens to be moved?"

Binding Signature (payWithPermitSig / v2, r2, s2)

PropertyValue
SignsFull operation parameters: token, beneficiary/acquiring address, amount, ref, deadline
Produced byThe payer (same account as owner)
Verified byThe Payment Processor (off-chain) and the Settlement Contract (on-chain)
EffectAuthorises the processor to submit this specific operation
Replay protectionusedHashes registry in the Settlement Contract — digest consumed on first use
DomainSettlement Contract's EIP-712 domain separator

This signature answers: "Is the payer authorising this specific payment with these exact parameters?"


Why Both Are Required

Without the Binding Signature: the Permit Signature alone grants an allowance but does not determine where the funds go. The Payment Processor could alter the beneficiary, amount, or reference without detection.

Without the Permit Signature: the Binding Signature alone authorises the operation but provides no gasless mechanism to pull tokens. The payer would need an on-chain approval transaction, reintroducing the gas requirement.

Together: the Permit Signature enables gasless token movement; the Binding Signature binds the processor to the exact parameters the payer authorised.


Verification Flow

Client Wallet
├── signs ERC-2612 Permit typed-data → permitSig (v1, r1, s1)
└── signs operation parameters → payWithPermitSig (v2, r2, s2)


Payment Processor (off-chain)
├── recovers signer from payWithPermitSig
├── verifies signer == permitParams.owner
├── recomputes digest; verifies hash field matches
└── optionally pre-verifies permitSig using token's domain separator


Settlement Contract (on-chain)
├── checks digest(payWithPermitSig) not in usedHashes
├── records digest in usedHashes ← replay protection
├── calls token.permit(owner, spender, value, deadline, v1, r1, s1)
│ └── token contract verifies permitSig
└── calls token.transferFrom(owner, contract, amount)

The usedHashes Registry

The Settlement Contract maintains a usedHashes mapping. Every time transferWithPermit or buyAcquiringPack succeeds, the EIP-712 digest of the Binding Signature is recorded. Any future transaction presenting the same digest reverts.

This provides operation-level replay protection that is independent of and complementary to the ERC-2612 nonce:

MechanismProtects againstMaintained by
ERC-2612 noncePermit Signature replayERC-2612 token contract
usedHashesBinding Signature replay even if a new permit were issuedSettlement Contract