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.
331 lines
17 KiB
Solidity
331 lines
17 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/token/ERC20/utils/SafeERC20.sol";
|
|
import {IDestinationSettler} from "./IERC7683.sol";
|
|
|
|
/// @notice The finality prover this settler's repayment path composes with. It is the
|
|
/// deployed AERE zk QBFT light client (contracts/interop/AereZkQbftLightClient.sol,
|
|
/// live on consuming chains at 0xCaDA54FA...6488), which verifies an SP1 Groth16
|
|
/// proof that ONE AERE block header is FINAL under Hyperledger Besu QBFT and, on
|
|
/// success, records the finalized (blockNumber, blockHash) for THIS pinned validator
|
|
/// set. This interface is declared here as the documented reference type so an
|
|
/// integrator reads the exact surface the origin-side verifier relies on. The
|
|
/// light client is deployed on the ORIGIN/consuming chain, not on AERE (AERE is
|
|
/// native there), so this settler does NOT call it on-chain; see the repayment
|
|
/// section of the NatSpec below and docs/AERE-ERC7683-DESTINATION.md.
|
|
interface IAereQbftFinalityLightClient {
|
|
/// @notice Highest QBFT-final AERE block number advanced so far.
|
|
function finalizedNumber() external view returns (uint64);
|
|
|
|
/// @notice On-chain block hash of the block at finalizedNumber().
|
|
function finalizedBlockHash() external view returns (bytes32);
|
|
|
|
/// @notice keccak256 over the trusted AERE validator set, in stored order. Every
|
|
/// accepted finality proof must attest THIS set. A consumer MUST check this
|
|
/// still equals keccak256 over the live qbft_getValidatorsByBlockNumber set.
|
|
function trustedValidatorSetRoot() external view returns (bytes32);
|
|
|
|
/// @notice Stateless finality check: reverts on an invalid proof or a mismatched
|
|
/// anchor, otherwise returns the proven block fields. Records nothing.
|
|
function verify(bytes calldata publicValues, bytes calldata proof)
|
|
external
|
|
view
|
|
returns (
|
|
bytes32 blockHash,
|
|
uint64 blockNumber,
|
|
bytes32 committedSealHash,
|
|
bytes32 validatorSetRoot,
|
|
bool finalized,
|
|
bool falconVerified
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @title AereDestinationSettler. ERC-7683 IDestinationSettler (the FILLING side).
|
|
*
|
|
* @notice AERE Network already ships the ORIGIN side of the ERC-7683 cross-chain intent
|
|
* standard (contracts/intents/AereSpokePool.sol, an IOriginSettler): a user locks
|
|
* input on the origin chain and emits an Open event that declares the OUTPUT leg
|
|
* (token, amount, recipient) they want on the destination chain. This contract is
|
|
* the missing DESTINATION half. A solver calls {fill} to deliver those promised
|
|
* output tokens to the recipient on AERE, and this contract records the fill so the
|
|
* ORIGIN chain can later repay the solver against a proof of AERE's QBFT finality
|
|
* (see "Repayment proof path" below) instead of trusting a relayer.
|
|
*
|
|
* WHY THIS REMOVES THE BRIDGE-LIQUIDITY BOOTSTRAP. A classic lock-and-mint bridge
|
|
* must pre-fund destination-side liquidity before it can serve a single transfer.
|
|
* An intent design does not: the SOLVER fronts their own liquidity to fill the
|
|
* order here, and is repaid the locked input on the origin chain after the fill is
|
|
* proven final. AERE therefore never has to seed or custody bridge liquidity; the
|
|
* market of solvers supplies it per intent.
|
|
*
|
|
* @dev SCOPE. This is a settlement / interop-layer contract. It does not touch, gate, or
|
|
* change AERE consensus, which remains classical secp256k1 ECDSA QBFT. The finality
|
|
* proof referenced below is a proof ABOUT that QBFT finality, verified classically
|
|
* today; that is the honest and intended posture, not a post-quantum consensus claim.
|
|
*
|
|
* @dev ON-CHAIN GUARANTEES (fail-closed). {fill}:
|
|
* 1. binds the standard's orderId argument to the orderId carried inside originData
|
|
* (OrderIdMismatch otherwise);
|
|
* 2. rejects a zero declared recipient (ZeroRecipient);
|
|
* 3. rejects a second fill of an order already filled (AlreadyFilled), so an order
|
|
* can be filled at most once, so a solver cannot be repaid twice for one intent;
|
|
* 4. rejects a delivered output that does not satisfy the order's declared output
|
|
* (OutputMismatch): the delivered token and recipient must match exactly and the
|
|
* delivered amount must be at least the declared amount. A fill that delivers the
|
|
* wrong token, to the wrong recipient, or short can never be recorded;
|
|
* 5. delivers the output atomically (SafeERC20 pull from the solver straight to the
|
|
* recipient, so this contract never custodies user output), then records the fill
|
|
* and emits {Filled}.
|
|
*
|
|
* @dev REPAYMENT PROOF PATH (how the origin chain repays the solver).
|
|
* After {fill}, the fill lives in this contract's storage and in the {Filled} log on
|
|
* AERE (chain 2800). To repay the solver, the origin chain must learn "this exact
|
|
* fill was included in a block that is FINAL on AERE" WITHOUT trusting a relayer. AERE
|
|
* already has the machinery for this, and this settler is built to plug into it:
|
|
*
|
|
* * FINALITY: contracts/interop/AereZkQbftLightClient.sol proves, in the SP1 zkVM
|
|
* (guest `qbft-lightclient-guest`), that an AERE header is QBFT-final: it decodes
|
|
* the Besu QBFT extraData, recomputes both Besu hash pre-images, ecrecovers each
|
|
* committed seal, and asserts the distinct-validator count reaches the QBFT quorum
|
|
* ceil(2N/3) (5-of-7 for AERE's live N=7 set), binding the proof to an IMMUTABLE
|
|
* validator-set anchor. See {IAereQbftFinalityLightClient} above.
|
|
*
|
|
* * INCLUSION + DELIVERY: contracts/interop/AereOutboundVerifierV2.sol is the
|
|
* origin-side pattern that CONSUMES such a proof. It accepts an SP1 proof that a
|
|
* specific AERE log was included in a QBFT-final AERE block (finality + receipt
|
|
* inclusion, one guest), binds the proof's committed validatorSetRoot to its own
|
|
* immutable anchor, enforces exactly-once replay, and only then acts. The origin
|
|
* chain runs an AereOutboundVerifierV2-style verifier whose trusted "source"
|
|
* address is THIS settler and whose event signature is {FILLED_EVENT_SIG}; it
|
|
* re-derives {fillCommitment} from the proven log and repays the solver the input
|
|
* AereSpokePool locked, against `repaymentAddress` from the fill record.
|
|
*
|
|
* So the proof shape is exactly the one AereOutboundVerifierV2 already documents (an
|
|
* SP1 Groth16 proof over AERE QBFT finality + log inclusion, validator-set-anchored).
|
|
* The on-chain surface THIS contract must provide for that path is: the {Filled}
|
|
* event, the persisted {FillRecord}, and the {fillCommitment} binder. It provides all
|
|
* three. The end-to-end origin-side repayment (off-chain SP1 prover run over the
|
|
* `qbft-lightclient-guest` ELF, plus the origin-chain verifier deployment) is NOT
|
|
* exercised by this contract or its unit test and is marked [MEASURE]: it needs the
|
|
* separate off-chain prover and a second chain. The exact receipt-trie inclusion
|
|
* encoding is [VERIFY: confirm against the final AereOutboundVerifierV2 guest].
|
|
*
|
|
* @dev TRUST MODEL. Read before quoting.
|
|
* * Recording a fill here is permissionless and does not, by itself, move the
|
|
* origin's locked input. Repayment happens ONLY on the origin chain and ONLY
|
|
* behind the finality+inclusion proof above. A forged or unproven fill record
|
|
* therefore cannot extract origin funds; the origin verifier's validator-set
|
|
* anchor is the gate.
|
|
* * Delivery correctness on AERE is enforced on-chain here (the four fail-closed
|
|
* checks). Repayment correctness on the origin chain rests on the honesty of
|
|
* >= 2f+1 of AERE's QBFT validators (5-of-7 today, all Foundation-operated: an
|
|
* operator assumption, not an economic one) plus a sound SP1/Groth16 verifier.
|
|
*/
|
|
contract AereDestinationSettler is ReentrancyGuard, IDestinationSettler {
|
|
using SafeERC20 for IERC20;
|
|
|
|
/* ------------------------------- constants ------------------------------ */
|
|
|
|
/// @notice AERE mainnet chain id. The repayment commitment binds to this constant
|
|
/// (matching AereOutboundVerifierV2.AERE_CHAIN_ID) rather than block.chainid, so
|
|
/// the value the origin-side verifier re-derives is fixed to AERE mainnet.
|
|
uint256 public constant AERE_CHAIN_ID = 2800;
|
|
|
|
/// @notice topic0 preimage of the {Filled} event. An origin-side
|
|
/// AereOutboundVerifierV2-style verifier uses this to select this settler's
|
|
/// fill log out of a proven-final AERE block.
|
|
bytes32 public constant FILLED_EVENT_SIG =
|
|
keccak256("Filled(bytes32,address,address,uint256,address,bytes32,uint64,uint64)");
|
|
|
|
/* --------------------------------- state -------------------------------- */
|
|
|
|
struct FillRecord {
|
|
address filler; // solver who delivered the output (repaid on origin)
|
|
address outputToken; // token actually delivered on AERE
|
|
uint256 outputAmount; // amount actually delivered
|
|
address recipient; // who received the output on AERE
|
|
uint64 filledAt; // block.timestamp of the fill
|
|
uint64 filledBlock; // block.number of the fill (the inclusion height)
|
|
bytes32 repaymentAddress; // solver's repayment address on the ORIGIN chain (bytes32)
|
|
}
|
|
|
|
/// @notice orderId => recorded fill. A record with filler == address(0) means "unfilled".
|
|
mapping(bytes32 => FillRecord) public fills;
|
|
|
|
/// @notice Number of orders filled through this settler.
|
|
uint64 public fillCount;
|
|
|
|
/* --------------------------------- events ------------------------------- */
|
|
|
|
/// @notice Emitted once per successful fill. This is the log an origin-side finality +
|
|
/// inclusion proof carries; its signature is {FILLED_EVENT_SIG}.
|
|
event Filled(
|
|
bytes32 indexed orderId,
|
|
address indexed filler,
|
|
address outputToken,
|
|
uint256 outputAmount,
|
|
address recipient,
|
|
bytes32 repaymentAddress,
|
|
uint64 filledAt,
|
|
uint64 filledBlock
|
|
);
|
|
|
|
/* --------------------------------- errors ------------------------------- */
|
|
|
|
error OrderIdMismatch(bytes32 fillArg, bytes32 inOriginData);
|
|
error ZeroRecipient();
|
|
error ZeroAmount();
|
|
error ZeroAddress();
|
|
error AlreadyFilled(bytes32 orderId);
|
|
error OutputMismatch();
|
|
error NotFilled(bytes32 orderId);
|
|
|
|
/* ---------------------------------- fill -------------------------------- */
|
|
|
|
/// @notice ERC-7683 IDestinationSettler. A solver (msg.sender, the filler) delivers the
|
|
/// order's promised output to the recipient on AERE and records the fill.
|
|
/// @param orderId the canonical order id (must equal the id inside originData).
|
|
/// @param originData the origin settler's opaque output leg, produced by AereSpokePool
|
|
/// as abi.encode(bytes32 orderId, address outputToken,
|
|
/// uint256 outputAmount, bytes32 recipient), the DECLARED output.
|
|
/// @param fillerData the solver's supplied fill data, abi.encode(address deliveredToken,
|
|
/// uint256 deliveredAmount, address deliveredRecipient,
|
|
/// bytes32 repaymentAddress). deliveredToken / deliveredRecipient must
|
|
/// match the declared output exactly and deliveredAmount must be >= the
|
|
/// declared amount; repaymentAddress is where the solver is repaid on
|
|
/// the origin chain.
|
|
function fill(bytes32 orderId, bytes calldata originData, bytes calldata fillerData)
|
|
external
|
|
override
|
|
nonReentrant
|
|
{
|
|
// ---- Decode the origin's declared output leg. ----
|
|
(
|
|
bytes32 declOrderId,
|
|
address declOutputToken,
|
|
uint256 declOutputAmount,
|
|
bytes32 declRecipient32
|
|
) = abi.decode(originData, (bytes32, address, uint256, bytes32));
|
|
|
|
// Bind the standard's orderId argument to the id the origin actually signed.
|
|
if (declOrderId != orderId) revert OrderIdMismatch(orderId, declOrderId);
|
|
// Fail-closed on a zero recipient (the order could never be honestly delivered).
|
|
if (declRecipient32 == bytes32(0)) revert ZeroRecipient();
|
|
if (declOutputToken == address(0)) revert ZeroAddress();
|
|
if (declOutputAmount == 0) revert ZeroAmount();
|
|
// Fail-closed on a duplicate: an order can be filled at most once.
|
|
if (fills[orderId].filler != address(0)) revert AlreadyFilled(orderId);
|
|
|
|
address declRecipient = address(uint160(uint256(declRecipient32)));
|
|
|
|
// ---- Decode the solver's actual delivery + repayment address. ----
|
|
(
|
|
address deliveredToken,
|
|
uint256 deliveredAmount,
|
|
address deliveredRecipient,
|
|
bytes32 repaymentAddress
|
|
) = abi.decode(fillerData, (address, uint256, address, bytes32));
|
|
|
|
// Fail-closed on a delivered output that does not satisfy the declared output:
|
|
// exact token, exact recipient, at-least the declared amount.
|
|
if (
|
|
deliveredToken != declOutputToken ||
|
|
deliveredRecipient != declRecipient ||
|
|
deliveredAmount < declOutputAmount
|
|
) revert OutputMismatch();
|
|
|
|
// ---- Effects before interaction (record the fill, then deliver). ----
|
|
fills[orderId] = FillRecord({
|
|
filler: msg.sender,
|
|
outputToken: deliveredToken,
|
|
outputAmount: deliveredAmount,
|
|
recipient: deliveredRecipient,
|
|
filledAt: uint64(block.timestamp),
|
|
filledBlock: uint64(block.number),
|
|
repaymentAddress: repaymentAddress
|
|
});
|
|
unchecked {
|
|
fillCount += 1;
|
|
}
|
|
|
|
emit Filled(
|
|
orderId,
|
|
msg.sender,
|
|
deliveredToken,
|
|
deliveredAmount,
|
|
deliveredRecipient,
|
|
repaymentAddress,
|
|
uint64(block.timestamp),
|
|
uint64(block.number)
|
|
);
|
|
|
|
// ---- Deliver the output atomically: solver -> recipient, no custody here. ----
|
|
IERC20(deliveredToken).safeTransferFrom(msg.sender, deliveredRecipient, deliveredAmount);
|
|
}
|
|
|
|
/* ------------------------------- fill views ----------------------------- */
|
|
|
|
/// @notice True once `orderId` has been filled through this settler.
|
|
function isFilled(bytes32 orderId) external view returns (bool) {
|
|
return fills[orderId].filler != address(0);
|
|
}
|
|
|
|
/// @notice The full recorded fill for `orderId`. Reverts if unfilled.
|
|
function getFill(bytes32 orderId)
|
|
external
|
|
view
|
|
returns (
|
|
address filler,
|
|
address outputToken,
|
|
uint256 outputAmount,
|
|
address recipient,
|
|
uint64 filledAt,
|
|
uint64 filledBlock,
|
|
bytes32 repaymentAddress
|
|
)
|
|
{
|
|
FillRecord storage r = fills[orderId];
|
|
if (r.filler == address(0)) revert NotFilled(orderId);
|
|
return (
|
|
r.filler,
|
|
r.outputToken,
|
|
r.outputAmount,
|
|
r.recipient,
|
|
r.filledAt,
|
|
r.filledBlock,
|
|
r.repaymentAddress
|
|
);
|
|
}
|
|
|
|
/// @notice The canonical repayment commitment an origin-side verifier re-derives from a
|
|
/// proven-final {Filled} log to repay the solver. It binds AERE's chain id, this
|
|
/// settler, and every economically-relevant fill field (orderId, filler, output
|
|
/// token / amount / recipient, and the solver's origin-chain repaymentAddress).
|
|
/// An AereOutboundVerifierV2-style verifier on the origin chain, gated by AERE's
|
|
/// validator-set anchor, checks a finality+inclusion proof for this fill and then
|
|
/// repays `filler`/`repaymentAddress` the locked input. Reverts if unfilled.
|
|
/// @dev The exact receipt-trie inclusion encoding the origin guest proves is
|
|
/// [VERIFY: confirm against the final AereOutboundVerifierV2 guest]; this
|
|
/// commitment fixes the fields that must be bound, independent of that encoding.
|
|
function fillCommitment(bytes32 orderId) external view returns (bytes32) {
|
|
FillRecord storage r = fills[orderId];
|
|
if (r.filler == address(0)) revert NotFilled(orderId);
|
|
return keccak256(
|
|
abi.encode(
|
|
AERE_CHAIN_ID,
|
|
address(this),
|
|
orderId,
|
|
r.filler,
|
|
r.outputToken,
|
|
r.outputAmount,
|
|
r.recipient,
|
|
r.repaymentAddress
|
|
)
|
|
);
|
|
}
|
|
}
|