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