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.
260 lines
11 KiB
Solidity
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;
|
|
}
|
|
}
|