AereAgentWallet2of2 holds the agent's tokens and accepts only the agent's signature followed by the policy service's, over the same digest. In the new cosign mode the wallet service signs only its half, after every check it already made, and the agent adds its half only after it recomputes the payment itself (payer, recipient, amount, the nonce of its own ledger entry, validity, digest, declared policy signer). verifica-plati.mjs requires the wallet's code on chain to be exactly the compiled contract with the two signers; recompileaza-contract.mjs recompiles the published artifact byte for byte with solc 0.8.23. Tests: co-signing 16/16, payment verifier 14/14, negative control 30/30, wallet 25/25. On the public testnet 28001 on 2026-09-29: 13/13 with the 2-of-2 wallet (the agent alone and the policy signer alone refused by the facilitator and by the token asked on chain) and 9/9 with the wallet key. Evidence in dovezi-28001/. Nothing here has been run on the Aere Network mainnet.
54 lines
3.0 KiB
Solidity
54 lines
3.0 KiB
Solidity
// 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;
|
|
}
|
|
}
|