This scaffold implements the design discussed in the chat:
- 5 backend/off-chain signers sign the same EIP-712
AgentActiondigest in parallel. - Backend proceeds after collecting the first valid threshold, e.g. 3-of-5.
- Only one
UserOperation/ transaction goes on-chain. - The module decodes
userOp.signature, extractsagentId, loads the configured signer set, checks signer indexes, recovers signers, enforces threshold, then lets the smart account execute. - The same payload can be adapted to a GlobalSessionPolicyModule hook, ERC-7579 validator, ERC-1271 threshold signer, or Safe-style module.
- ERC-4337: account validates
UserOperationviavalidateUserOp; EntryPoint does not call arbitrary modules directly. - ERC-7579: modular smart accounts and validator modules.
- ERC-1271:
isValidSignature(bytes32,bytes)returns0x1626ba7ewhen contract-based signature validation succeeds.
contracts/
src/AgentThresholdValidator7579.sol
src/GlobalSessionPolicyModuleAdapter.sol
backend/
src/index.ts
src/agentDigest.ts
src/packSignature.ts
src/types.ts
AgentThresholdValidator7579.sol stores config per:
account => agentId => AgentConfig
account => agentId => signerIndex => signerAddressThe signature payload format is:
abi.encode(
SignatureMode.AGENT_THRESHOLD,
bytes32 agentId,
AgentAction action,
IndexedSignature[] sigs,
bytes executionContext
)Where:
struct IndexedSignature {
uint8 index;
bytes signature;
}The module does not trust the index by itself. It uses the index only to load the expected signer, then recovers the real signer from the ECDSA signature and compares:
expectedSigner = agentSignerAt[account][agentId][index];
recoveredSigner = ECDSA.recover(digest, signature);
require(recoveredSigner == expectedSigner);Replay protection is enforced by binding the EIP-712 digest to:
chainId
validator/module address
account
agentId
target
value
selector
calldataHash
nonce
deadline
policyHash
1. Backend receives action request.
2. Builds AgentAction.
3. Hashes calldata and extracts selector.
4. Sends the same EIP-712 typed data to 5 signers in parallel.
5. Collects the first 3 valid signatures.
6. Packs moduleSignature.
7. That moduleSignature becomes userOp.signature or the nested module signature, depending on your smart-account implementation.
Run locally:
cd backend
cp .env.example .env
npm install
npm run devExample request:
curl -X POST http://localhost:3000/agent/sign-action \
-H 'content-type: application/json' \
-d '{
"account":"0xYourSmartAccount",
"agentId":"0x1111111111111111111111111111111111111111111111111111111111111111",
"target":"0xYourPositionManager",
"value":"0",
"callData":"0xabcdef01...",
"nonce":"0",
"policyHash":"0x2222222222222222222222222222222222222222222222222222222222222222",
"threshold":3,
"deadlineSeconds":60
}'EntryPoint will not call this module automatically. Your smart account must route validation to it. The expected path is:
Bundler
-> EntryPoint.handleOps()
-> SmartAccount.validateUserOp()
-> ERC-7579 validator module / GlobalSessionPolicyModule hook
-> threshold verification
-> policy checks
-> account execute()
If your account expects a wrapper signature format, wrap moduleSignature in the account-specific format.
Do not keep all five private keys on one server in production. For a real deployment, use signer isolation:
Signer 1 -> AWS KMS
Signer 2 -> GCP KMS
Signer 3 -> Fireblocks/MPC
Signer 4 -> separate HSM/self-hosted signer
Signer 5 -> fallback signer in separate infra
This scaffold uses env private keys only to demonstrate the flow.
- Use off-chain parallel signatures, not five on-chain pre-transactions.
- Bind signatures to chainId, module address, account, agentId, nonce, deadline, policyHash, target, value, selector, and calldataHash.
- Use short deadlines for MEV-sensitive actions.
- Use private RPC / MEV-protected submission for swaps and rebalances.
- Keep policy checks on-chain: allowlists, slippage, oracle skew, recipient restrictions, spender restrictions, and post-execution accounting.
- Use Safe/timelock for signer rotation and config changes.