This repository contains a complete EIP-7702 sponsored-account system and a self-hosted relayer flow. A user signs an authorization and a batch of calls off chain. An allowlisted sponsor submits the EIP-7702 type-4 transaction and pays the native gas.
The contracts support two settlement modes:
- Platform subsidy: the sponsor pays all native gas and the user pays only the business transfer amount. This is the policy currently configured on the verified five-chain deployments.
- ERC-20 repayment: the user atomically repays gas and an optional service fee in an allowlisted ERC-20 token in the same transaction.
The repository uses a sponsor private key directly. It does not depend on a third-party relayer API.
Warning
This software has undergone an internal security review but has not yet been independently audited by an external security firm. Before using or >deploying the code, you should carefully review all components and assess any potential security, operational, and financial risks.
You are solely responsible for your deployment, configuration, security practices, and use of this software, including any transactions or losses >resulting from its use.
- The user EOA signs an EIP-7702 authorization for
Sponsored7702Account. - The user signs an EIP-712
SponsoredCallcontaining the account, nonce, deadline, sponsor, fee terms, and hash of the business calls. - The allowlisted sponsor sends a type-4 transaction to
SponsorRouter.executeSponsoredand pays the native gas. - The router verifies sponsor policy and forwards the request to the delegated user account.
- The account verifies the signature and nonce, executes the calls atomically,
and, when configured, transfers the ERC-20 repayment to
feeReceiver.
If any business call or fee transfer fails, the entire transaction reverts.
contracts/account/Sponsored7702Account.solimplements the delegated EOA logic, EIP-712 validation, replay protection, deadlines, batched calls, and optional ERC-20 fee settlement.contracts/router/SponsorRouter.solis the allowlisted sponsor entry point. It rejects native value and forwards sponsored requests.contracts/policy/SponsorPolicyRegistry.solmanages sponsors, fee tokens, fee limits,feeReceiver, router binding, maximum call count, and emergency pause state.contracts/libraries/contains call hashing and signature validation.contracts/mocks/contains test-only targets and ERC-20 variants.
The account also exposes executeFromSelf for user-funded transactions. A
registry pause blocks sponsored execution without preventing a user from
controlling their own assets through that path.
| Network | Chain ID | Sponsored payment | Deployment/configuration |
|---|---|---|---|
| Ethereum Mainnet | 1 | Yes | Yes |
| BNB Smart Chain | 56 | Yes | Yes |
| BSC Testnet | 97 | Yes | Yes |
| Arbitrum One | 42161 | Yes | Yes |
| Polygon PoS | 137 | Yes | Yes |
| Base | 8453 | No dedicated payment wrapper | Yes |
Canonical mainnet token restrictions are enforced by thin network wrapper scripts where applicable. Polygon uses native USDC, not USDC.e.
- Node.js and npm
- An RPC endpoint for each target network
- A deployment/configuration owner account funded with native gas
- A sponsor account funded with native gas
- A user account holding the business ERC-20 token
This project uses Hardhat 3, Solidity 0.8.28, viem, and npm. The production compiler profile enables the optimizer with 200 runs.
npm install
npm test
npm run compile:productionThe default build remains available as npm run compile.
Copy the environment template and replace every value required for the target network:
cp .env.example .envNever commit .env. It contains real private keys and has no remote backup.
Configuration uses a network-prefix-first fallback rule. For example,
ARBITRUM_ONE_SPONSOR_ADDRESS overrides the shared SPONSOR_ADDRESS; if the
prefixed value is absent, the shared value is used. One EOA private key derives
the same address on every EVM chain, so a single sponsor can be reused across
networks.
Private keys must be 0x followed by exactly 64 hexadecimal characters. The
scripts verify that each key derives the configured address before signing.
Important shared values include:
USER_PRIVATE_KEY=0xYOUR_USER_PRIVATE_KEY
SPONSOR_PRIVATE_KEY=0xYOUR_SPONSOR_PRIVATE_KEY
USER_ADDRESS=0xYOUR_USER_ADDRESS
SPONSOR_ADDRESS=0xYOUR_SPONSOR_ADDRESS
MERCHANT_ADDRESS=0xYOUR_MERCHANT_OR_RECIPIENT_ADDRESS
INITIAL_OWNER=0xYOUR_REGISTRY_OWNER_ADDRESS
INITIAL_FEE_RECEIVER=0xYOUR_FEE_RECEIVER_ADDRESS
SPONSORED_CALL_DEADLINE_SECONDS=3600
SERVICE_FEE_AMOUNT=0
MAX_SERVICE_FEE_AMOUNT=0
MAX_CALLS=10
SPONSORED_ALLOW_ZERO_FEE=trueSee .env.example for all network-specific contract, token,
amount, balance, and RPC settings.
When gasFeeAmount + serviceFeeAmount is zero,
Sponsored7702Account intentionally skips the fee token transfer. No fee token
or feeReceiver is required for execution, and no FeePaid event is emitted.
This is a valid platform-subsidy mode, but it means the sponsor receives no
on-chain repayment.
The sending scripts require SPONSORED_ALLOW_ZERO_FEE=true to acknowledge this
behavior explicitly. In zero-fee mode, on-chain fee caps do not provide
off-chain credit or settlement risk controls; those controls belong in the
relayer service.
Always check the RPC and chain ID first, then deploy with the production build:
npm run rpc:check:<network>
npm run compile:production
npm run deploy:contracts:<network>Supported command suffixes are ethereum, bsc, bsc-testnet,
arbitrum-one, polygon, and base where a matching script exists.
Copy the three deployed addresses into .env:
<PREFIX>_POLICY_REGISTRY=0x...
<PREFIX>_ACCOUNT_IMPLEMENTATION=0x...
<PREFIX>_SPONSOR_ROUTER=0x...Then preview and apply the registry configuration:
npm run configure:<network> -- --dry-run
npm run configure:<network>configure reads current on-chain state and writes only differences. It also
applies the fee policy from .env; use the sponsor-specific command when only
the sponsor allowlist should change.
npm run sponsor:add:<network> -- --dry-run
npm run sponsor:add:<network>
npm run sponsor:remove:<network> -- --dry-runAll write scripts enforce the expected chain ID. Owner-only scripts also verify that the configured signer is the current registry owner before sending a transaction.
Preview a single sponsored token payment:
npm run sponsored:payment:<network> -- --dry-runSend it only after reviewing the dry-run output:
npm run sponsored:payment:<network>Dedicated payment wrappers are available for Ethereum, BSC, BSC Testnet, Arbitrum One, and Polygon. Additional Arbitrum examples cover a sponsored batch and the user-funded path:
npm run sponsored:batch:arbitrum-one -- --dry-run
npm run self:payment:arbitrum-one -- --dry-runThe relayer performs preflight validation, reads the sponsored nonce from the fixed account storage slot, validates any existing EIP-7702 delegation, simulates the fully signed call with a state override, checks that the sponsor can cover a conservative gas estimate, broadcasts with a controlled gas limit, and verifies receipt events, balances, transaction type, addresses, and nonce.
Run the full production-path readiness scan without broadcasting:
npm run canary:self-relayer -- --dry-runRun a subset with --only=key1,key2. A misspelled target is rejected instead
of silently producing an empty run.
Live mainnet targets are blocked unless --allow-mainnet is explicitly
provided. Every passing live target sends a real transaction and spends real
funds:
npm run canary:self-relayer -- --allow-mainnetGenerated reports are written under evm-7702-sponsored/reports/. Dry-run and
live reports use different filenames so a preview cannot overwrite live
transaction hashes.
If a run reports SENT? or unconfirmed, do not immediately retry. The
transaction was broadcast but receipt polling did not finish, often because of
RPC rate limits. Check the transaction hash in an explorer or run the read-only
preflight before deciding whether another transaction is safe. Only failed
means the transaction was not sent.
Read the current five-chain deployment and registry state without sending a transaction:
npm run preflight:five-chainEmergency pause and recovery:
npm run registry:pause:<network> -- --dry-run
npm run registry:pause:<network>
npm run registry:unpause:<network> -- --dry-run
npm run registry:unpause:<network>Registry ownership uses Ownable2Step:
npm run owner:transfer:<network> -- --dry-run
npm run owner:transfer:<network>
# Change <PREFIX>_PRIVATE_KEY to the nominated owner.
npm run owner:accept:<network> -- --dry-run
npm run owner:accept:<network>The first step only nominates pendingOwner; ownership moves in the second
step. An incorrect nomination can be replaced by the current owner.
- The EIP-712 signature binds the delegated account, chain, sponsored nonce, deadline, sponsor, calls hash, fee token, fee amounts, and fee receiver.
- Sponsored nonces prevent replay and are separate from both the EOA authorization nonce and the sponsor transaction nonce.
- The router accepts only allowlisted sponsors and rejects
msg.value. - Fee tokens and all fee ceilings are registry-controlled.
- A maximum business-call count limits sponsored batches.
- Failed business calls and failed ERC-20 repayment revert atomically.
- ERC-20 transfers support standard, no-return, and false-return behavior.
- Reentrancy and signature edge cases are covered by the test suite.
- Sponsor solvency, quote generation, idempotency, transaction nonce allocation, reconciliation, monitoring, rate limits, and key custody remain production service responsibilities.
contracts/ Solidity contracts, interfaces, libraries, and mocks
deploy/ Deployment and registry administration scripts
ignition/ Hardhat Ignition deployment modules
scripts/ Self-relayer flows and shared safety helpers
test/ Hardhat tests
evm-7702-sponsored/ SDK, ABI, integration docs, canary tools, and reports
The thin network wrappers call the shared implementation in
scripts/sponsored-payment.ts. Input checks, gas coverage, receipt assertions,
and failure guidance live in scripts/sponsored-payment-checks.ts.
Sanitized deployment and acceptance summaries are retained in
evm-7702-sponsored/reports/. Operator addresses, user addresses, transaction
hashes, provider identifiers, and deployment journals are intentionally not
published.