// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import {AereUSDC} from "./AereUSDC.sol"; import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol"; /// @title AereBridge — Foundation-attested on-ramp from Ethereum USDC to AereUSDC /// @notice The off-chain bridge watcher (Foundation-operated, source under /// /opt/aere-bridge-watcher) monitors a Foundation receiver address on /// Ethereum mainnet for incoming USDC. When a deposit is detected and /// confirmed (≥12 block confirmations), the watcher calls /// `attestDeposit(ethTxHash, ethSender, aereDestination, amount)` on this /// contract, which mints AereUSDC to the destination. /// /// Replay protection: every (ethTxHash, logIndex) is recorded — re-attesting /// the same deposit is a no-op. /// /// Honest framing: this contract assumes a trusted ATTESTOR role. Phase 1 /// the attestor is a Foundation-controlled EOA. Phase 2 swaps to a multisig. /// Phase 3 replaces with trustless settlement via Across/Hyperlane. contract AereBridge is AccessControl { bytes32 public constant ATTESTOR_ROLE = keccak256("ATTESTOR_ROLE"); AereUSDC public immutable aereUSDC; /// @notice Foundation's USDC receiver address on Ethereum mainnet — informational /// only; the watcher actually decides which Ethereum address to monitor. string public ethReceiverHint; /// @notice (ethTxHash, logIndex) tuple → already-attested marker. Prevents double-mint. mapping(bytes32 => bool) public attestedDeposits; /// @notice Total bridged in (USDC, 6 decimals). Visible for transparency dashboard. uint256 public totalBridgedIn; struct Deposit { bytes32 ethTxHash; uint64 ethLogIndex; address ethSender; address aereDestination; uint256 amount; uint64 attestedAt; uint64 attestedBlock; } /// @notice Append-only deposit log. Phase 1 only; off-chain indexer reads events. Deposit[] public deposits; event Attested( uint256 indexed depositId, bytes32 indexed ethTxHash, address indexed aereDestination, address ethSender, uint256 amount ); event EthReceiverHintUpdated(string ethReceiver); error AlreadyAttested(); error InvalidAmount(); error InvalidDestination(); constructor(address foundation, address _aereUSDC, address initialAttestor, string memory _ethReceiverHint) { require(_aereUSDC != address(0), "bridge: aereUSDC=0"); aereUSDC = AereUSDC(_aereUSDC); ethReceiverHint = _ethReceiverHint; _grantRole(DEFAULT_ADMIN_ROLE, foundation); _grantRole(ATTESTOR_ROLE, initialAttestor); } /// @notice Off-chain watcher calls this when an Ethereum USDC deposit is confirmed. /// @param ethTxHash The Ethereum transaction hash containing the USDC Transfer. /// @param ethLogIndex The log index within that tx (multiple logs per tx possible). /// @param ethSender The Ethereum address that sent the USDC (used for refund routing). /// @param aereDestination The AERE address to receive AereUSDC. /// @param amount The amount in USDC units (6 decimals). function attestDeposit( bytes32 ethTxHash, uint64 ethLogIndex, address ethSender, address aereDestination, uint256 amount ) external onlyRole(ATTESTOR_ROLE) { if (amount == 0) revert InvalidAmount(); if (aereDestination == address(0)) revert InvalidDestination(); bytes32 key = keccak256(abi.encodePacked(ethTxHash, ethLogIndex)); if (attestedDeposits[key]) revert AlreadyAttested(); attestedDeposits[key] = true; uint256 id = deposits.length; deposits.push(Deposit({ ethTxHash: ethTxHash, ethLogIndex: ethLogIndex, ethSender: ethSender, aereDestination: aereDestination, amount: amount, attestedAt: uint64(block.timestamp), attestedBlock: uint64(block.number) })); totalBridgedIn += amount; aereUSDC.mint(aereDestination, amount); emit Attested(id, ethTxHash, aereDestination, ethSender, amount); } /// @notice Foundation can update the receiver-address hint shown to users. function setEthReceiverHint(string calldata _ethReceiverHint) external onlyRole(DEFAULT_ADMIN_ROLE) { ethReceiverHint = _ethReceiverHint; emit EthReceiverHintUpdated(_ethReceiverHint); } function depositCount() external view returns (uint256) { return deposits.length; } }