// SPDX-License-Identifier: MIT pragma solidity 0.8.23; import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /** * @title AereAgentV2 — machine-account registry (F13 companion redeploy) * @notice Logic-identical companion to AereAgent * (0xE96396B4b596B3A74e4195Be12aADd5257863536). The ONLY difference is * the constructor argument: this instance's immutable * AERE402_FACILITATOR points at AERE402FacilitatorV2 instead of the V1 * facilitator. * * WHY A COMPANION REDEPLOY IS REQUIRED: `debit` is access-gated to the * immutable AERE402_FACILITATOR. The F13 fix lives in the facilitator * (per-payee nonce namespace), which forces a new facilitator address; * a new facilitator cannot call `debit` on the V1 AereAgent (its * facilitator is pinned to the V1 address). So V2 facilitator + V2 agent * are cross-wired: AereAgentV2.AERE402_FACILITATOR == AERE402FacilitatorV2 * and AERE402FacilitatorV2.AGENT_REGISTRY == AereAgentV2. * * Registry semantics are byte-for-byte identical to V1: identity * (operator + signer + optional passkey), prepaid per-(agent,token) * balance, per-call / per-hour / per-day caps, pause, and the * facilitator-only debit path. No new token; no owner. See AereAgent for * the full model documentation. */ contract AereAgentV2 is ReentrancyGuard { /// @notice The only contract that may call debit(). address public immutable AERE402_FACILITATOR; struct Agent { bool registered; bool paused; address operator; // human / org running the agent (rotatable) address signer; // secp256k1 key that signs payment auths (rotatable) bytes32 passkeyPubX; // optional WebAuthn passkey for additional auth surface bytes32 passkeyPubY; uint128 perCallCap; uint128 perHourCap; uint128 perDayCap; string metadataUri; // ipfs / https with agent description } /// @notice agentId → Agent mapping(uint256 => Agent) public agents; uint256 public nextAgentId; /// @notice agentId → token → prepaid balance mapping(uint256 => mapping(address => uint256)) public balances; /// @notice agentId → token → (hour epoch, spent in that hour) mapping(uint256 => mapping(address => uint256)) public hourlySpentEpoch; mapping(uint256 => mapping(address => uint256)) public hourlySpent; /// @notice agentId → token → (day epoch, spent in that day) mapping(uint256 => mapping(address => uint256)) public dailySpentEpoch; mapping(uint256 => mapping(address => uint256)) public dailySpent; /* --------------------------------- events ------------------------------- */ event AgentRegistered(uint256 indexed agentId, address indexed operator, address indexed signer, string metadataUri); event OperatorTransferred(uint256 indexed agentId, address indexed oldOperator, address indexed newOperator); event SignerSet(uint256 indexed agentId, address indexed newSigner); event PasskeySet(uint256 indexed agentId, bytes32 pubX, bytes32 pubY); event RateLimitsSet(uint256 indexed agentId, uint128 perCallCap, uint128 perHourCap, uint128 perDayCap); event AgentPausedSet(uint256 indexed agentId, bool paused); event MetadataUpdated(uint256 indexed agentId, string metadataUri); event TopUp(uint256 indexed agentId, address indexed token, address indexed funder, uint256 amount); event Withdrawn(uint256 indexed agentId, address indexed token, address indexed recipient, uint256 amount); event Debited(uint256 indexed agentId, address indexed token, address indexed payee, uint256 amount); /* --------------------------------- errors ------------------------------- */ error NotOperator(); error NotFacilitator(); error UnknownAgent(); error AgentPausedErr(); error InsufficientBalance(uint256 have, uint256 want); error PerCallCapExceeded(uint256 want, uint256 cap); error PerHourCapExceeded(uint256 wouldBe, uint256 cap); error PerDayCapExceeded(uint256 wouldBe, uint256 cap); error ZeroAddress(); error ZeroAmount(); error TransferFailed(); /* ------------------------------- modifiers ------------------------------- */ modifier onlyOperator(uint256 agentId) { Agent storage a = agents[agentId]; if (!a.registered) revert UnknownAgent(); if (msg.sender != a.operator) revert NotOperator(); _; } /* ------------------------------- constructor ---------------------------- */ constructor(address facilitator) { if (facilitator == address(0)) revert ZeroAddress(); AERE402_FACILITATOR = facilitator; } /* ------------------------------- registration --------------------------- */ function registerAgent( address signer, bytes32 passkeyPubX, bytes32 passkeyPubY, uint128 perCallCap, uint128 perHourCap, uint128 perDayCap, string calldata metadataUri ) external returns (uint256 agentId) { if (signer == address(0)) revert ZeroAddress(); agentId = nextAgentId++; agents[agentId] = Agent({ registered: true, paused: false, operator: msg.sender, signer: signer, passkeyPubX: passkeyPubX, passkeyPubY: passkeyPubY, perCallCap: perCallCap, perHourCap: perHourCap, perDayCap: perDayCap, metadataUri: metadataUri }); emit AgentRegistered(agentId, msg.sender, signer, metadataUri); emit RateLimitsSet(agentId, perCallCap, perHourCap, perDayCap); if (passkeyPubX != bytes32(0) || passkeyPubY != bytes32(0)) { emit PasskeySet(agentId, passkeyPubX, passkeyPubY); } } function transferOperator(uint256 agentId, address newOperator) external onlyOperator(agentId) { if (newOperator == address(0)) revert ZeroAddress(); address old = agents[agentId].operator; agents[agentId].operator = newOperator; emit OperatorTransferred(agentId, old, newOperator); } function setSigner(uint256 agentId, address newSigner) external onlyOperator(agentId) { if (newSigner == address(0)) revert ZeroAddress(); agents[agentId].signer = newSigner; emit SignerSet(agentId, newSigner); } function setPasskey(uint256 agentId, bytes32 pubX, bytes32 pubY) external onlyOperator(agentId) { agents[agentId].passkeyPubX = pubX; agents[agentId].passkeyPubY = pubY; emit PasskeySet(agentId, pubX, pubY); } function setRateLimits(uint256 agentId, uint128 perCallCap, uint128 perHourCap, uint128 perDayCap) external onlyOperator(agentId) { agents[agentId].perCallCap = perCallCap; agents[agentId].perHourCap = perHourCap; agents[agentId].perDayCap = perDayCap; emit RateLimitsSet(agentId, perCallCap, perHourCap, perDayCap); } function setPaused(uint256 agentId, bool paused) external onlyOperator(agentId) { agents[agentId].paused = paused; emit AgentPausedSet(agentId, paused); } function updateMetadata(uint256 agentId, string calldata metadataUri) external onlyOperator(agentId) { agents[agentId].metadataUri = metadataUri; emit MetadataUpdated(agentId, metadataUri); } /* ------------------------------- balance --------------------------------- */ function topUp(uint256 agentId, address token, uint256 amount) external nonReentrant { if (!agents[agentId].registered) revert UnknownAgent(); if (amount == 0) revert ZeroAmount(); bool ok = IERC20(token).transferFrom(msg.sender, address(this), amount); if (!ok) revert TransferFailed(); balances[agentId][token] += amount; emit TopUp(agentId, token, msg.sender, amount); } function withdraw(uint256 agentId, address token, uint256 amount, address recipient) external onlyOperator(agentId) nonReentrant { if (recipient == address(0)) revert ZeroAddress(); if (amount == 0) revert ZeroAmount(); uint256 bal = balances[agentId][token]; if (bal < amount) revert InsufficientBalance(bal, amount); unchecked { balances[agentId][token] = bal - amount; } bool ok = IERC20(token).transfer(recipient, amount); if (!ok) revert TransferFailed(); emit Withdrawn(agentId, token, recipient, amount); } /* --------------------------------- debit -------------------------------- */ /// @notice Called by AERE402FacilitatorV2 to debit the agent's prepaid /// balance and transfer `amount` of `token` to `payee`. Enforces /// per-call / per-hour / per-day caps + paused. function debit( uint256 agentId, address token, address payee, uint256 amount ) external nonReentrant { if (msg.sender != AERE402_FACILITATOR) revert NotFacilitator(); Agent storage a = agents[agentId]; if (!a.registered) revert UnknownAgent(); if (a.paused) revert AgentPausedErr(); if (amount == 0) revert ZeroAmount(); if (payee == address(0)) revert ZeroAddress(); if (a.perCallCap != 0 && amount > a.perCallCap) revert PerCallCapExceeded(amount, a.perCallCap); uint256 bal = balances[agentId][token]; if (bal < amount) revert InsufficientBalance(bal, amount); uint256 thisHour = block.timestamp / 1 hours; uint256 thisDay = block.timestamp / 1 days; uint256 hourSpent = hourlySpentEpoch[agentId][token] == thisHour ? hourlySpent[agentId][token] : 0; uint256 daySpent = dailySpentEpoch[agentId][token] == thisDay ? dailySpent[agentId][token] : 0; uint256 newHourSpent = hourSpent + amount; uint256 newDaySpent = daySpent + amount; if (a.perHourCap != 0 && newHourSpent > a.perHourCap) revert PerHourCapExceeded(newHourSpent, a.perHourCap); if (a.perDayCap != 0 && newDaySpent > a.perDayCap) revert PerDayCapExceeded(newDaySpent, a.perDayCap); unchecked { balances[agentId][token] = bal - amount; hourlySpentEpoch[agentId][token] = thisHour; hourlySpent[agentId][token] = newHourSpent; dailySpentEpoch[agentId][token] = thisDay; dailySpent[agentId][token] = newDaySpent; } bool ok = IERC20(token).transfer(payee, amount); if (!ok) revert TransferFailed(); emit Debited(agentId, token, payee, amount); } /* --------------------------------- views -------------------------------- */ function signerOf(uint256 agentId) external view returns (address) { return agents[agentId].signer; } }