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