// SPDX-License-Identifier: MIT pragma solidity 0.8.23; import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; import {IERC1271} from "@openzeppelin/contracts/interfaces/IERC1271.sol"; /** * @title AereAgentWallet2of2, an AI agent's payment wallet that neither the agent nor its owner can spend from alone * * @notice The wallet holds tokens and has no function that moves them. Tokens leave it only through a token that asks it, * by ERC-1271, whether a signature over a digest is valid (EIP-3009 transferWithAuthorization in the testnet token * AereTestUSD3009 does that for a contract `from`). The answer is yes only for a 130-byte signature that is the agent's * secp256k1 signature followed by the policy signer's, both over that digest, both non-malleable (OpenZeppelin ECDSA). * * The agent holds the first key. The owner's policy service holds the second and co-signs a payment only when the * agent's post-quantum ledger records it and the owner's policy allows it (tools/agent-policy/x402/wallet.mjs, the * `cosign` mode). So a stolen agent key cannot spend past the policy, and the owner cannot spend without the agent. * * HONEST SCOPE. Both signers are fixed at deployment; there is no rotation, no recovery and no owner: if either key * is lost, the tokens stay in the wallet. The agent's key here is classical (secp256k1); the post-quantum binding is * the ledger the policy service checks before it co-signs, not this contract. Any digest both keys sign is valid, as * in any 2-of-2 multisig: the software signs only EIP-3009 digests. Written 2026-09-29 for the public testnet 28001; * not deployed on chain 2800 (that is the founder's decision). */ contract AereAgentWallet2of2 is IERC1271 { bytes4 private constant MAGIC = 0x1626ba7e; bytes4 private constant INVALID = 0xffffffff; /// @notice the agent's key (first 65 bytes of a valid signature) address public immutable agentSigner; /// @notice the owner's policy service key (last 65 bytes of a valid signature) address public immutable policySigner; error ZeroSigner(); error SameSigner(); constructor(address agent, address policy) { if (agent == address(0) || policy == address(0)) revert ZeroSigner(); if (agent == policy) revert SameSigner(); agentSigner = agent; policySigner = policy; } /// @inheritdoc IERC1271 function isValidSignature(bytes32 hash, bytes calldata signature) external view returns (bytes4) { if (signature.length != 130) return INVALID; (address a, ECDSA.RecoverError ea) = ECDSA.tryRecover(hash, signature[0:65]); if (ea != ECDSA.RecoverError.NoError || a != agentSigner) return INVALID; (address p, ECDSA.RecoverError ep) = ECDSA.tryRecover(hash, signature[65:130]); if (ep != ECDSA.RecoverError.NoError || p != policySigner) return INVALID; return MAGIC; } }