aere-contracts/contracts/agentic/AereAgent.sol
Aere Network a13a649b77
Some checks are pending
contracts-ci / Install (lockfile) → compile → full test suite (push) Waiting to run
contracts-ci / PQC known-answer tests (NIST vectors) (push) Waiting to run
contracts-ci / Coverage (scoped, with artifacts) (push) Waiting to run
Initial public release
Aere Network public source. Everything here can be checked against the live
chain (chain id 2800, https://rpc.aere.network).

Scope note, stated up front rather than buried: consensus on chain 2800 is
classical secp256k1 ECDSA QBFT. The post-quantum work in this repository is at
the signature, precompile, account and transport layers. Nothing here makes the
consensus post-quantum, and no document in it should be read as claiming so.
2026-07-20 01:02:37 +03:00

260 lines
11 KiB
Solidity

// SPDX-License-Identifier: MIT
pragma solidity 0.8.23;
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
/**
* @title AereAgent — machine-account registry for the agentic economy
* @notice Autonomous workers (Claude, GPT, Gemini, bespoke agents) register
* an on-chain identity, fund a prepaid balance, and then settle
* per-call payments to API providers via the AERE402 facilitator.
*
* Identity model:
* - Each agent has a stable agentId (monotonic uint256).
* - Each agent has an OPERATOR address (the human / org running
* the agent) and a SIGNING address (the secp256k1 key the
* agent uses for EIP-712 payment authorisations). Both are
* rotatable by the operator via setSigner / transferOperator.
* - Prepaid balance per (agent, settlementToken). Settlement
* token is whatever ERC-20 the operator funds (typically
* WAERE or USDC.e). Operators top up with `topUp(agentId,
* token, amount)` and withdraw idle balance with
* `withdraw(agentId, token, amount)`.
*
* Rate limits:
* - per-call cap (uint128, in token units)
* - per-hour cap (uint128, in token units)
* - per-day cap (uint128, in token units)
* Enforced at the AERE402Facilitator (which calls debit() here).
*
* Pause:
* - Operator can pause the agent instantly. Facilitator checks
* paused flag before debiting; reverts on a paused agent.
*
* @dev Only the AERE402_FACILITATOR (immutable, set in constructor)
* can call debit(). This makes the facilitator the SOLE
* spend-side authority, and lets the operator change keys
* without juggling token-level approvals.
*/
contract AereAgent 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 AERE402Facilitator 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;
}
}