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.
197 lines
7.9 KiB
Solidity
197 lines
7.9 KiB
Solidity
// SPDX-License-Identifier: MIT
|
|
pragma solidity 0.8.23;
|
|
|
|
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
|
|
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
|
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
|
|
|
|
interface IAereAgent {
|
|
function debit(uint256 agentId, address token, address payee, uint256 amount) external;
|
|
function signerOf(uint256 agentId) external view returns (address);
|
|
}
|
|
|
|
interface IAereSinkFacilitator {
|
|
function flush(address token, uint256 amount) external;
|
|
}
|
|
|
|
/**
|
|
* @title AERE402Facilitator — HTTP 402 agentic settlement
|
|
* @notice Service providers (web APIs, MCP servers, agent-to-agent endpoints)
|
|
* exchange HTTP 402 (Payment Required) for an EIP-712 signed
|
|
* payment authorisation from an AereAgent. The provider then
|
|
* submits the authorisation to this contract; the facilitator
|
|
* debits the agent's prepaid balance (via the AereAgent registry),
|
|
* credits the provider, and routes a 25-bps protocol fee to
|
|
* AereSink (the flywheel).
|
|
*
|
|
* CORE FLOW per call:
|
|
* 1. Provider responds to a request with HTTP 402 +
|
|
* X-AERE402-Quote: { facilitator, token, amount, resourceId,
|
|
* providerAddress, nonce, deadline }.
|
|
* 2. Agent signs the authorisation (EIP-712 typed data) using
|
|
* its registered signer key.
|
|
* 3. Agent retries the request with X-AERE402-Sig: <hex>.
|
|
* 4. Provider POSTs the (auth + signature) to this contract via
|
|
* `settle(...)`. The facilitator:
|
|
* a. Validates signature against AereAgent.signerOf(agentId).
|
|
* b. Checks nonce hasn't been spent + deadline.
|
|
* c. Splits amount: (amount - fee) → provider, fee → sink.
|
|
* d. Calls AereAgent.debit(agentId, token, address(this), amount).
|
|
* e. Forwards the splits in one tx.
|
|
* 5. Provider serves the response now that settlement is final.
|
|
*
|
|
* IMMUTABILITY:
|
|
* - AereAgent address fixed in constructor.
|
|
* - SINK address fixed.
|
|
* - 25-bps fee constant.
|
|
* - DOMAIN_SEPARATOR pinned.
|
|
*
|
|
* REPLAY-PROTECTION:
|
|
* - Per-(agentId, nonce) bitmap of consumed nonces.
|
|
* - Provider chooses nonces; convention: monotonic per provider-
|
|
* agent pair.
|
|
*/
|
|
contract AERE402Facilitator is ReentrancyGuard {
|
|
using ECDSA for bytes32;
|
|
|
|
/* ------------------------------- immutable ------------------------------- */
|
|
|
|
address public immutable AGENT_REGISTRY;
|
|
address public immutable SINK;
|
|
uint16 public constant PROTOCOL_FEE_BPS = 25; // 0.25%
|
|
|
|
bytes32 public immutable DOMAIN_SEPARATOR;
|
|
bytes32 public constant AUTH_TYPEHASH = keccak256(
|
|
"PaymentAuth(uint256 agentId,address token,address payee,uint256 amount,bytes32 resourceId,uint256 nonce,uint256 deadline)"
|
|
);
|
|
|
|
/* --------------------------------- state -------------------------------- */
|
|
|
|
/// @notice agentId → nonce → consumed
|
|
mapping(uint256 => mapping(uint256 => bool)) public consumed;
|
|
|
|
/* --------------------------------- events ------------------------------- */
|
|
|
|
event Settled(
|
|
uint256 indexed agentId,
|
|
address indexed payee,
|
|
address indexed token,
|
|
uint256 amount,
|
|
uint256 protocolFee,
|
|
bytes32 resourceId,
|
|
uint256 nonce
|
|
);
|
|
|
|
/* --------------------------------- errors ------------------------------- */
|
|
|
|
error ZeroAddress();
|
|
error InvalidSignature();
|
|
error NonceAlreadyConsumed();
|
|
error DeadlinePassed();
|
|
error PayeeMismatch();
|
|
error ZeroAmount();
|
|
error TransferFailed();
|
|
error SelfDealForbidden(); // payee == agent's signer key
|
|
|
|
/* ------------------------------- constructor ---------------------------- */
|
|
|
|
constructor(address agentRegistry, address sink) {
|
|
if (agentRegistry == address(0) || sink == address(0)) revert ZeroAddress();
|
|
AGENT_REGISTRY = agentRegistry;
|
|
SINK = sink;
|
|
DOMAIN_SEPARATOR = keccak256(abi.encode(
|
|
keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"),
|
|
keccak256("AERE402Facilitator"),
|
|
keccak256("1"),
|
|
block.chainid,
|
|
address(this)
|
|
));
|
|
}
|
|
|
|
/* --------------------------------- settle ------------------------------- */
|
|
|
|
/// @notice Provider settles a payment auth from an agent.
|
|
/// @dev msg.sender MUST equal the `payee` in the auth; this prevents
|
|
/// a third party from replaying someone else's auth toward an
|
|
/// attacker-controlled payee even if the agent's signature leaks.
|
|
function settle(
|
|
uint256 agentId,
|
|
address token,
|
|
address payee,
|
|
uint256 amount,
|
|
bytes32 resourceId,
|
|
uint256 nonce,
|
|
uint256 deadline,
|
|
bytes calldata signature
|
|
) external nonReentrant {
|
|
if (block.timestamp > deadline) revert DeadlinePassed();
|
|
if (msg.sender != payee) revert PayeeMismatch();
|
|
if (amount == 0) revert ZeroAmount();
|
|
if (consumed[agentId][nonce]) revert NonceAlreadyConsumed();
|
|
|
|
bytes32 structHash = keccak256(abi.encode(
|
|
AUTH_TYPEHASH,
|
|
agentId,
|
|
token,
|
|
payee,
|
|
amount,
|
|
resourceId,
|
|
nonce,
|
|
deadline
|
|
));
|
|
bytes32 digest = keccak256(abi.encodePacked("\x19\x01", DOMAIN_SEPARATOR, structHash));
|
|
address signer = digest.recover(signature);
|
|
address expected = IAereAgent(AGENT_REGISTRY).signerOf(agentId);
|
|
if (signer == address(0) || signer != expected) revert InvalidSignature();
|
|
|
|
// Block self-deal: a compromised signer key cannot drain the agent
|
|
// balance by paying itself. The signer MUST sign a payment to a
|
|
// different address — typically an actual service provider.
|
|
if (payee == expected) revert SelfDealForbidden();
|
|
|
|
consumed[agentId][nonce] = true;
|
|
|
|
// Pull the full amount from the agent via the registry. The registry
|
|
// sends the AERE to address(this) (the facilitator); we then split
|
|
// into payee + sink.
|
|
IAereAgent(AGENT_REGISTRY).debit(agentId, token, address(this), amount);
|
|
|
|
uint256 fee = (amount * PROTOCOL_FEE_BPS) / 10_000;
|
|
uint256 toPayee = amount - fee;
|
|
|
|
if (toPayee > 0) {
|
|
bool ok = IERC20(token).transfer(payee, toPayee);
|
|
if (!ok) revert TransferFailed();
|
|
}
|
|
if (fee > 0) {
|
|
// Forward to sink for flywheel; if the sink doesn't accept,
|
|
// the fee stays in the facilitator as residual (sweepable).
|
|
// ROUND-3 FIX: zero-then-set approve (USDT-style tokens revert
|
|
// on non-zero→non-zero); also re-zero after the call so no
|
|
// lingering allowance survives a silent sink failure.
|
|
IERC20(token).approve(SINK, 0);
|
|
IERC20(token).approve(SINK, fee);
|
|
(bool sinkOk, ) = SINK.call(abi.encodeWithSignature("flush(address,uint256)", token, fee));
|
|
sinkOk; // intentional: don't gate user flow on sink success
|
|
IERC20(token).approve(SINK, 0);
|
|
}
|
|
|
|
emit Settled(agentId, payee, token, amount, fee, resourceId, nonce);
|
|
}
|
|
|
|
/* ------------------------------- residual flush -------------------------- */
|
|
|
|
/// @notice Anyone can re-attempt the sink flush for a residual balance.
|
|
function flushResidual(address token, uint256 amount) external nonReentrant {
|
|
if (amount == 0) revert ZeroAmount();
|
|
IERC20(token).approve(SINK, 0);
|
|
IERC20(token).approve(SINK, amount);
|
|
(bool ok, ) = SINK.call(abi.encodeWithSignature("flush(address,uint256)", token, amount));
|
|
if (!ok) {
|
|
IERC20(token).approve(SINK, 0);
|
|
revert TransferFailed();
|
|
}
|
|
IERC20(token).approve(SINK, 0);
|
|
}
|
|
}
|