Skip to content

feat(evm): EIP-2612 permit signing helper (Node SDK + Rust CLI) - #187

Open
teyrebaz33 wants to merge 1 commit into
open-wallet-standard:mainfrom
teyrebaz33:feat/eip-2612-permit-helper
Open

feat(evm): EIP-2612 permit signing helper (Node SDK + Rust CLI)#187
teyrebaz33 wants to merge 1 commit into
open-wallet-standard:mainfrom
teyrebaz33:feat/eip-2612-permit-helper

Conversation

@teyrebaz33

Copy link
Copy Markdown

Summary

Adds a purpose-built signPermit helper that eliminates the boilerplate currently required to sign EIP-2612 permits via OWS. Closes #132

What's included

Node SDK — signPermit()

import { signPermit } from '@open-wallet-standard/core/evm/permit';

const sig = await signPermit(ownerAddress, 'eip155:8453', {
  token:    '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', // USDC on Base
  spender:  '0xYourProtocol',
  value:    '1000000',
  deadline: Math.floor(Date.now() / 1000) + 3600,
}, owsSignTypedData);
// → { v, r, s, signature, typedData }

CLI — ows sign permit

ows sign permit   --wallet my-wallet   --chain  eip155:8453   --token  0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913   --spender 0xYourProtocol   --value  1000000   --deadline 1800000000   --json

Domain resolution

The hard part of EIP-2612 is getting the domain separator right. signPermit resolves it in three stages:

  1. eip712Domain() (EIP-5267) — self-describing; works for USDC v2.2+, OZ ERC20Permit 5.x
  2. Well-known override table — USDC ("2"), DAI ("1"), Base USDT
  3. name() + default version "1" — generic fallback

Tokens that omit version from their domain are handled correctly — the EIP712Domain type array is built dynamically.

Tests

✔ returns correct v / r / s from mock signature
✔ builds correct EIP-712 typed data structure
✔ resolves domain via eip712Domain()
✔ falls back to name() + override when eip712Domain() reverts
✔ auto-fetches nonce when not supplied
✔ uses supplied nonce without RPC call
✔ omits version field in EIP712Domain when token has no version
✔ chainId matches CAIP-2 chain segment
✔ throws when token name is empty and no override exists
✔ throws when RPC is unreachable
✔ DAI fallback domain version is 1
✔ Ethereum USDC fallback domain version is 2

12 passed, 0 failed — fully offline, no network required

Files changed

File Description
bindings/node/src/evm/permit.mjs SDK function
bindings/node/__test__/permit.spec.mjs 12 offline tests
docs/examples/eip2612-permit.md Usage guide
ows/crates/ows-lib/src/ops.rs sign_permit() Rust implementation
ows/crates/ows-lib/src/types.rs PermitParams, PermitSignResult types
ows/crates/ows-lib/Cargo.toml Added ureq for sync HTTP
ows/crates/ows-cli/src/commands/sign_permit.rs CLI command handler
ows/crates/ows-cli/src/commands/mod.rs Module registration
ows/crates/ows-cli/src/main.rs Permit subcommand + handler

Checklist

  • Node SDK signPermit()
  • CLI ows sign permit
  • 12 offline tests, all passing
  • Rust workspace builds without errors
  • Documentation
  • Follows existing code patterns (sign_message, sign_transaction)

Adds signPermit() to the Node SDK and `ows sign permit` to the CLI.
Closes open-wallet-standard#132

Node SDK (bindings/node/src/evm/permit.mjs):
- signPermit(ownerAddress, chainId, params, signTypedData) function
- Auto-fetches permit nonce from chain when not supplied
- Domain resolution: eip712Domain() (EIP-5267) → well-known override
  table (USDC v2, DAI v1) → name() + version fallback
- Handles tokens that omit version from their EIP712Domain
- No new runtime dependencies (raw fetch + minimal ABI decode)

Rust CLI (ows sign permit):
- New sign_permit() in ows-lib with synchronous ureq HTTP calls
- PermitParams and PermitSignResult types in ows-lib
- ows sign permit --wallet --chain --token --spender --value --deadline
  [--nonce] [--rpc-url] [--json] [--index]
- Follows existing sign_message / sign_transaction command patterns

Tests (bindings/node/__test__/permit.spec.mjs):
- 12 offline tests, all passing
- Covers: v/r/s splitting, typed data structure, eip712Domain() path,
  fallback path, nonce auto-fetch, nonce skip, version omission,
  chainId, error cases, known token versions

Docs (docs/examples/eip2612-permit.md):
- SDK and CLI usage examples
- Domain resolution strategy explained
- Supported chains and tokens table
@teyrebaz33
teyrebaz33 requested a review from njdawn as a code owner April 4, 2026 13:24
@vercel

vercel Bot commented Apr 4, 2026

Copy link
Copy Markdown

@teyrebaz33 is attempting to deploy a commit to the MoonPay Team on Vercel.

A member of the Team first needs to authorize it.

@socket-security

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addedcargo/​ureq@​2.12.19710093100100

View full report

@socket-security

Copy link
Copy Markdown

Warning

Review the following alerts detected in dependencies.

According to your organization's Security Policy, it is recommended to resolve "Warn" alerts. Learn more about Socket for GitHub.

Action Severity Alert  (click "▶" to expand/collapse)
Warn High
License policy violation: cargo webpki-roots under CDLA-Permissive-2.0

License: CDLA-Permissive-2.0 - the applicable license policy does not allow this license (4) (webpki-roots-0.26.11/Cargo.toml)

License: CDLA-Permissive-2.0 - the applicable license policy does not allow this license (4) (webpki-roots-0.26.11/LICENSE)

From: ?cargo/ureq@2.12.1cargo/webpki-roots@0.26.11

ℹ Read more on: This package | This alert | What is a license policy violation?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Find a package that does not violate your license policy or adjust your policy to allow this package's license.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore cargo/webpki-roots@0.26.11. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

View full report

@0xultravioleta

Copy link
Copy Markdown
Collaborator

Thanks for this. The EIP-712 structure looks right, and reusing the existing typed-data signing path for the permit is the correct call. The concern is what it pulls in around the signing flow on the Rust side.

This adds a new HTTP client (ureq) into ows-lib, the FFI library crate that sits right next to key handling and today has no HTTP dependencies at all, and it calls hardcoded third-party RPC endpoints from the permit flow by default. That crate should not be opening network connections, and definitely not to endpoints baked into the source. Could you move the network calls out of ows-lib, to the CLI or binding layer, or make the RPC endpoint an explicit, required argument so nothing is contacted implicitly and nothing is hardcoded?

One more: the hand-rolled ABI decode silently returns 0 for a nonce at or above 2^64 (the parse overflows and falls back to 0), so an oversized nonce would produce a wrong value and an invalid signature with no error raised. Please use the existing ABI decoding rather than a hand-rolled one so large values are handled correctly.

Once the network surface is out of ows-lib and the decode is fixed, this is in good shape. Final sign-off is @njdawn's.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

RFC: EIP-2612 permit signing helper

2 participants