aere-contracts/contracts/AereCardEscrow.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

223 lines
11 KiB
Solidity

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
/**
* @title AereCardEscrow
* @notice Pre-auth + capture rail for Bank28 (and future card programs) on AERE.
* Settlement node calls preAuth() to lock user funds via EIP-2612 permit
* (~3s, 1 block on AERE QBFT). When the card auth resolves, the node
* calls capture() with the final amount, which transfers to the
* issuer settlement address minus protocol/program fees, and refunds
* the residual hold to the user. Expired auths are released by anyone.
*
* This is the "slower path" alternative to AereLightningChannels for
* programs that don't run a long-lived channel counterparty.
*/
interface IERC20 {
function transfer(address to, uint256 amount) external returns (bool);
function transferFrom(address from, address to, uint256 amount) external returns (bool);
function balanceOf(address account) external view returns (uint256);
}
interface IERC2612 {
function permit(
address owner, address spender, uint256 value,
uint256 deadline, uint8 v, bytes32 r, bytes32 s
) external;
}
interface IAereIdentity {
function hasClaim(address identity, bytes32 claimType) external view returns (bool);
}
contract AereCardEscrow {
// ── Auth state ─────────────────────────────────────────────────
enum Status { None, Held, Captured, Released }
struct Auth {
address user;
address asset;
uint128 amount; // amount on hold (residual after partial captures)
uint64 expiresAt;
uint64 programId; // 1 = Bank28, future programs allocated by owner
Status status;
}
mapping(bytes32 => Auth) public auths; // authId => Auth
// ── Programs ───────────────────────────────────────────────────
struct Program {
address settlementNode; // authorized to call preAuth/capture for this program
address feeRecipient; // program's own fee bucket (Bank28 treasury)
uint16 programFeeBps; // program-charged bps on capture (max 1000)
bool active;
}
mapping(uint64 => Program) public programs;
uint64 public nextProgramId = 1;
// ── Globals ────────────────────────────────────────────────────
address public owner;
address public protocolTreasury;
uint16 public protocolFeeBps = 30; // 30 bps = 0.30% to AereTreasury
uint16 public constant MAX_PROGRAM_FEE_BPS = 1000;
uint16 public constant MAX_PROTOCOL_FEE_BPS = 100;
IAereIdentity public identity;
bytes32 public requiredClaim; // e.g. keccak256("KYC_TIER_2")
// ── Events ─────────────────────────────────────────────────────
event ProgramRegistered(uint64 indexed programId, address settlementNode, address feeRecipient, uint16 feeBps);
event ProgramUpdated(uint64 indexed programId, address settlementNode, address feeRecipient, uint16 feeBps, bool active);
event PreAuth(bytes32 indexed authId, uint64 indexed programId, address indexed user, address asset, uint128 amount, uint64 expiresAt);
event Captured(bytes32 indexed authId, uint128 captured, address recipient, uint128 protocolFee, uint128 programFee, uint128 refunded);
event Released(bytes32 indexed authId, uint128 refunded);
event ProtocolFeeUpdated(uint16 bps);
event RequiredClaimUpdated(bytes32 claim);
modifier onlyOwner() { require(msg.sender == owner, "owner"); _; }
constructor(address _treasury, address _identity, bytes32 _requiredClaim) {
require(_treasury != address(0), "treasury=0");
owner = msg.sender;
protocolTreasury = _treasury;
identity = IAereIdentity(_identity);
requiredClaim = _requiredClaim;
}
// ── Admin ──────────────────────────────────────────────────────
function registerProgram(address settlementNode, address feeRecipient, uint16 feeBps) external onlyOwner returns (uint64 id) {
require(settlementNode != address(0) && feeRecipient != address(0), "zero");
require(feeBps <= MAX_PROGRAM_FEE_BPS, "feeBps");
id = nextProgramId++;
programs[id] = Program(settlementNode, feeRecipient, feeBps, true);
emit ProgramRegistered(id, settlementNode, feeRecipient, feeBps);
}
function updateProgram(uint64 id, address settlementNode, address feeRecipient, uint16 feeBps, bool active) external onlyOwner {
require(programs[id].settlementNode != address(0), "no program");
require(feeBps <= MAX_PROGRAM_FEE_BPS, "feeBps");
programs[id] = Program(settlementNode, feeRecipient, feeBps, active);
emit ProgramUpdated(id, settlementNode, feeRecipient, feeBps, active);
}
function setProtocolFeeBps(uint16 bps) external onlyOwner {
require(bps <= MAX_PROTOCOL_FEE_BPS, "max");
protocolFeeBps = bps;
emit ProtocolFeeUpdated(bps);
}
function setProtocolTreasury(address t) external onlyOwner { require(t != address(0)); protocolTreasury = t; }
function setIdentity(address a, bytes32 claim) external onlyOwner { identity = IAereIdentity(a); requiredClaim = claim; emit RequiredClaimUpdated(claim); }
function transferOwnership(address n) external onlyOwner { require(n != address(0)); owner = n; }
// ── Pre-auth (settlement node only) ────────────────────────────
/**
* @notice Lock user funds for an upcoming card auth.
* @dev Uses EIP-2612 permit so the user signs off-chain and the settlement
* node submits the on-chain tx (sponsored by the program's paymaster).
* authId is supplied by the caller — typically keccak256(programId, externalAuthRef)
* so the off-chain card system controls the namespace.
*/
function preAuthWithPermit(
bytes32 authId,
uint64 programId,
address user,
address asset,
uint128 amount,
uint64 expiresAt,
uint256 permitDeadline,
uint8 v, bytes32 r, bytes32 s
) external {
Program storage p = programs[programId];
require(p.active && msg.sender == p.settlementNode, "not node");
require(auths[authId].status == Status.None, "authId used");
require(amount > 0 && expiresAt > block.timestamp, "params");
if (address(identity) != address(0) && requiredClaim != bytes32(0)) {
require(identity.hasClaim(user, requiredClaim), "not eligible");
}
// Pull user funds via permit. Permit failure (e.g. already used) is OK
// if allowance is already sufficient.
try IERC2612(asset).permit(user, address(this), amount, permitDeadline, v, r, s) {} catch {}
require(IERC20(asset).transferFrom(user, address(this), amount), "transferFrom");
auths[authId] = Auth(user, asset, amount, expiresAt, programId, Status.Held);
emit PreAuth(authId, programId, user, asset, amount, expiresAt);
}
/// @notice Lock funds when the user has already approved this contract (no permit).
function preAuth(
bytes32 authId, uint64 programId, address user,
address asset, uint128 amount, uint64 expiresAt
) external {
Program storage p = programs[programId];
require(p.active && msg.sender == p.settlementNode, "not node");
require(auths[authId].status == Status.None, "authId used");
require(amount > 0 && expiresAt > block.timestamp, "params");
if (address(identity) != address(0) && requiredClaim != bytes32(0)) {
require(identity.hasClaim(user, requiredClaim), "not eligible");
}
require(IERC20(asset).transferFrom(user, address(this), amount), "transferFrom");
auths[authId] = Auth(user, asset, amount, expiresAt, programId, Status.Held);
emit PreAuth(authId, programId, user, asset, amount, expiresAt);
}
// ── Capture (settlement node only) ─────────────────────────────
/**
* @notice Settle a held auth. Splits funds: protocolFeeBps to AereTreasury,
* programFeeBps to program.feeRecipient, captureAmount-fees to
* issuerRecipient, residual to the user. Closes the auth.
*/
function capture(bytes32 authId, uint128 captureAmount, address issuerRecipient) external {
Auth storage a = auths[authId];
require(a.status == Status.Held, "not held");
Program storage p = programs[a.programId];
require(msg.sender == p.settlementNode, "not node");
require(captureAmount <= a.amount, "over");
require(issuerRecipient != address(0), "recipient=0");
uint128 protoFee = uint128(uint256(captureAmount) * protocolFeeBps / 10_000);
uint128 progFee = uint128(uint256(captureAmount) * p.programFeeBps / 10_000);
require(uint256(protoFee) + progFee < captureAmount, "fees>=capture");
uint128 toIssuer = captureAmount - protoFee - progFee;
uint128 toRefund = a.amount - captureAmount;
a.status = Status.Captured;
a.amount = 0;
if (protoFee > 0) require(IERC20(a.asset).transfer(protocolTreasury, protoFee), "t1");
if (progFee > 0) require(IERC20(a.asset).transfer(p.feeRecipient, progFee), "t2");
if (toIssuer > 0) require(IERC20(a.asset).transfer(issuerRecipient, toIssuer), "t3");
if (toRefund > 0) require(IERC20(a.asset).transfer(a.user, toRefund), "t4");
emit Captured(authId, captureAmount, issuerRecipient, protoFee, progFee, toRefund);
}
// ── Release (anyone, after expiry) ─────────────────────────────
/// @notice Refund a held auth that wasn't captured before expiry. Permissionless.
function release(bytes32 authId) external {
Auth storage a = auths[authId];
require(a.status == Status.Held, "not held");
require(block.timestamp >= a.expiresAt, "not expired");
uint128 amt = a.amount;
a.status = Status.Released;
a.amount = 0;
require(IERC20(a.asset).transfer(a.user, amt), "refund");
emit Released(authId, amt);
}
/// @notice Voluntary release by the program before expiry (e.g. cardholder voided).
function voidAuth(bytes32 authId) external {
Auth storage a = auths[authId];
require(a.status == Status.Held, "not held");
Program storage p = programs[a.programId];
require(msg.sender == p.settlementNode, "not node");
uint128 amt = a.amount;
a.status = Status.Released;
a.amount = 0;
require(IERC20(a.asset).transfer(a.user, amt), "refund");
emit Released(authId, amt);
}
}