// SPDX-License-Identifier: MIT pragma solidity 0.8.24; /** * @title AereCancunCanary * @notice On-chain canary proving Cancun EVM support on AERE (chain 2800): * - TSTORE / TLOAD (EIP-1153), including auto-reset between * transactions and a transient reentrancy lock, * - MCOPY (EIP-5656) round-trip verified by keccak256, * - PUSH0 (EIP-3855), implicit in solc >=0.8.20 shanghai+ output, * - BLOBHASH (EIP-4844) and BLOBBASEFEE (EIP-7516) opcode execution. * * Compiled with solc 0.8.24, evmVersion = cancun, via a per-file * hardhat override (the repo global stays 0.8.23 / shanghai-default). * * The constructor self-tests every opcode, so a successful deploy, * or even a read-only eth_estimateGas / eth_call of the creation * bytecode, is itself proof that the live chain executes Cancun * opcodes: any TSTORE/TLOAD/MCOPY/BLOBHASH/BLOBBASEFEE failure * reverts deployment. * * PUSH0 note for verifiers: the deployed runtime bytecode contains * genuine PUSH0 (0x5f) opcodes; the canonical solc revert stub * `5f80fd` (PUSH0 DUP1 REVERT) appears in the runtime code. To check * rigorously, walk the bytecode from offset 0 skipping PUSH1..PUSH32 * immediates and confirm 0x5f at an opcode position. Every * successful call to this contract executes PUSH0 on-chain. * * Blob note: AERE is a QBFT L1 that does not accept blob (type-3) * transactions, so blobhash(0) == bytes32(0) in every execution * context. blobbasefee is whatever the node's Cancun fee market * reports (protocol minimum is 1 wei); it is recorded, not asserted. */ contract AereCancunCanary { /// @dev keccak256("aere.cancun.canary.tslot"); literal so assembly can use it. uint256 private constant T_SLOT = 0x600b3c753bdea90810db30bcdc9015ebc00429a1e131fa35c272b23785c573b1; /// @dev keccak256("aere.cancun.canary.lock"); literal so assembly can use it. uint256 private constant LOCK_SLOT = 0xe4d0d997dd2753c27700e5fe7c65208afc32dd89178d805290c63adf8ca6331e; /// @notice true iff the constructor's TSTORE/TLOAD + MCOPY + blob-opcode /// self-test passed on the deploying chain. bool public immutable deploySelfTest; event TransientWithinTx(uint256 written, uint256 readBack); event TransientCrossTx(uint256 observed); event ReentrancyLockProbe(bool attemptedReentry, bool innerCallReverted); event McopyChecked(uint256 dataLen, bytes32 inputHash, bytes32 copyHash, bool ok); event BlobProbed(bytes32 blobHash0, uint256 blobBaseFee); error TransientLockActive(); /// @dev Reentrancy guard using EIP-1153 transient storage. Costs no /// cold-SSTORE gas and needs no post-tx cleanup guarantees beyond /// the EVM's own end-of-transaction transient reset. modifier transientLock() { uint256 lockV; assembly { lockV := tload(LOCK_SLOT) } if (lockV != 0) revert TransientLockActive(); assembly { tstore(LOCK_SLOT, 1) } _; assembly { tstore(LOCK_SLOT, 0) } } constructor() { // EIP-1153 round-trip. uint256 v; assembly { tstore(T_SLOT, 0xCA9A17) v := tload(T_SLOT) } require(v == 0xCA9A17, "TSTORE/TLOAD failed"); assembly { tstore(T_SLOT, 0) } // EIP-5656 round-trip. bytes memory src = abi.encodePacked( keccak256("aere.cancun.canary.mcopy.src"), uint256(2800) ); bytes memory dst = new bytes(src.length); uint256 len = src.length; assembly { mcopy(add(dst, 0x20), add(src, 0x20), len) } require(keccak256(dst) == keccak256(src), "MCOPY failed"); // EIP-4844 / EIP-7516 opcodes must execute (no blob txs on QBFT, so // blobhash(0) is zero; blobbasefee is recorded, not asserted). bytes32 h; uint256 f; assembly { h := blobhash(0) f := blobbasefee() } require(h == bytes32(0), "unexpected blob hash"); emit BlobProbed(h, f); deploySelfTest = true; } // --------------------------------------------------------------------- // EIP-1153 transient storage // --------------------------------------------------------------------- /// @notice Method A: TSTOREs `value` and TLOADs it back within the same /// transaction; emits both as proof. function transientWriteAndRead(uint256 value) external returns (uint256 readBack) { assembly { tstore(T_SLOT, value) readBack := tload(T_SLOT) } emit TransientWithinTx(value, readBack); } /// @notice Method B: TLOADs the same slot in a LATER transaction. Must /// observe 0, proving the EVM auto-resets transient storage /// between transactions. function transientReadOnly() external returns (uint256 observed) { assembly { observed := tload(T_SLOT) } emit TransientCrossTx(observed); } /// @notice View twin of transientReadOnly for eth_call checks. function transientPeek() external view returns (uint256 observed) { assembly { observed := tload(T_SLOT) } } /// @notice Current value of the transient reentrancy lock slot (always 0 /// when read from a fresh transaction). function lockPeek() external view returns (uint256 v) { assembly { v := tload(LOCK_SLOT) } } /// @notice Transient reentrancy lock demonstration. With /// `attemptReentry = true` the function re-enters itself via an /// external self-call; the inner call MUST revert with /// TransientLockActive, and `innerCallReverted` is emitted true. /// A subsequent plain call (`attemptReentry = false`) succeeds, /// showing the lock cleared. function guardedEnter(bool attemptReentry) external transientLock returns (bool innerCallReverted) { if (attemptReentry) { (bool ok, ) = address(this).call( abi.encodeWithSelector(this.guardedEnter.selector, false) ); innerCallReverted = !ok; } emit ReentrancyLockProbe(attemptReentry, innerCallReverted); } // --------------------------------------------------------------------- // EIP-5656 MCOPY // --------------------------------------------------------------------- /// @notice Copies `data` with assembly `mcopy` and returns keccak256 of /// both the input and the copy; equal hashes prove a faithful /// MCOPY round-trip. function mcopyHash(bytes calldata data) public pure returns (bytes32 inputHash, bytes32 copyHash) { bytes memory input = data; inputHash = keccak256(input); uint256 len = input.length; bytes memory copyBuf = new bytes(len); assembly { mcopy(add(copyBuf, 0x20), add(input, 0x20), len) copyHash := keccak256(add(copyBuf, 0x20), len) } } /// @notice Transaction variant of mcopyHash; emits the comparison so the /// proof lives in a receipt. function mcopyCheck(bytes calldata data) external returns (bool ok) { (bytes32 inputHash, bytes32 copyHash) = mcopyHash(data); ok = inputHash == copyHash; emit McopyChecked(data.length, inputHash, copyHash, ok); } // --------------------------------------------------------------------- // EIP-4844 BLOBHASH / EIP-7516 BLOBBASEFEE // --------------------------------------------------------------------- /// @notice Returns blobhash(0) and blobbasefee via assembly. On AERE /// (QBFT, no blob transactions) blobhash(0) == bytes32(0); /// blobbasefee is the node-reported blob base fee (min 1 wei). function blobView() public view returns (bytes32 blobHash0, uint256 blobBaseFee) { assembly { blobHash0 := blobhash(0) blobBaseFee := blobbasefee() } } /// @notice Transaction variant of blobView; emits the values. function blobProbe() external returns (bytes32 blobHash0, uint256 blobBaseFee) { (blobHash0, blobBaseFee) = blobView(); emit BlobProbed(blobHash0, blobBaseFee); } }