diff --git a/Makefile b/Makefile index 28550f8..653835a 100644 --- a/Makefile +++ b/Makefile @@ -38,6 +38,11 @@ build: $(GEAS) mkdir -p bytecode/factory $(GEAS) -a -no-nl -o bytecode/factory/main.hex src/factory/main.eas + # 8141 + mkdir -p bytecode/frames_expiry + $(GEAS) -a -no-nl -o bytecode/frames_expiry/main.hex src/frames_expiry/main.eas + $(GEAS) -a -no-nl -o bytecode/frames_expiry/ctor.hex src/frames_expiry/ctor.eas + # test helper mkdir -p bytecode/fake_expo_test $(GEAS) -a -no-nl -o bytecode/fake_expo_test/main.hex src/common/fake_expo_test.eas diff --git a/bytecode/frames_expiry/ctor.hex b/bytecode/frames_expiry/ctor.hex new file mode 100644 index 0000000..ebf6e6e --- /dev/null +++ b/bytecode/frames_expiry/ctor.hex @@ -0,0 +1 @@ +601a8060095f395ff360083614600a575f5ffd5b5f3560c01c4211601657005b5f5ffd \ No newline at end of file diff --git a/bytecode/frames_expiry/main.hex b/bytecode/frames_expiry/main.hex new file mode 100644 index 0000000..2b4e264 --- /dev/null +++ b/bytecode/frames_expiry/main.hex @@ -0,0 +1 @@ +60083614600a575f5ffd5b5f3560c01c4211601657005b5f5ffd \ No newline at end of file diff --git a/checksums.txt b/checksums.txt index 37501b9..f219872 100644 --- a/checksums.txt +++ b/checksums.txt @@ -5,5 +5,6 @@ ea3eebcbb9549c4c158fbf92b77bccb250909095c824846ecbff6e4f4829249e bytecode/conso b7ff005f1ad4e7adc56859f2fc3876b42e9216def18000c5373b69629adca7a6 bytecode/execution_hash/ctor.hex 1676499334d750e5cc9e6bd9bbaf8b78a901b675b1ea1c8c8dbcde2fbc7751b4 bytecode/execution_hash/main.hex e48ada9a3056d5a2a52de5c078229849c3392e6a6c8e87ebb2c4b65bf1045e6d bytecode/factory/main.hex +fa7a30dec1a6fdf440b5a453f8c9f8d82a65f5a133b45b91477a464502b3f579 bytecode/frames_expiry/main.hex cc660e9bde40daf93521f879909ecf7b3b207524356f8e87b888e68a44181f65 bytecode/withdrawals/ctor.hex 39c5522e72643757cc35d4cffc36848b2ab860d5ad38a18590b93513d550ba9f bytecode/withdrawals/main.hex diff --git a/scripts/addr.sh b/scripts/addr.sh index 2d934cb..478c60f 100755 --- a/scripts/addr.sh +++ b/scripts/addr.sh @@ -45,7 +45,11 @@ case $1 in echo "searching for builder_exits deployment data " bin/nick search --score=$score --initcode="0x$(cat bytecode/builder_exits/ctor.hex)" --prefix=0x8282 --suffix=0xeeee --gaslimit=$gaslimit ;; + frames_expiry) + echo "searching for frames_expiry deployment data " + bin/nick search --score=$score --initcode="0x$(cat bytecode/frames_expiry/ctor.hex)" --prefix=0x8141 --suffix=0xffff + ;; *) - echo "Invalid option. Usage: $0 {withdrawals|consolidations|exechash|beaconroot}" + echo "Invalid option. Usage: $0 {withdrawals|consolidations|exechash|beaconroot|builder_deposits|builder_exits|frames_expiry}" ;; esac diff --git a/src/frames_expiry/ctor.eas b/src/frames_expiry/ctor.eas new file mode 100644 index 0000000..45af0f2 --- /dev/null +++ b/src/frames_expiry/ctor.eas @@ -0,0 +1,10 @@ + ;; Copy and return code. + push len(code) ; [len] + dup1 ; [len, len] + push @code ; [codeOffset, len, len] + push 0 ; [dest, codeOffset, len, len] + codecopy ; [len] + push 0 ; [ptr, len] + return ; [] + +#bytes code: assemble("main.eas") diff --git a/src/frames_expiry/main.eas b/src/frames_expiry/main.eas new file mode 100644 index 0000000..ac4eb7e --- /dev/null +++ b/src/frames_expiry/main.eas @@ -0,0 +1,55 @@ +;;; █████╗ ██╗██╗ ██╗ ██╗ █████╗ ███████╗███╗ ███╗ +;;; ██╔══██╗███║██║ ██║███║ ██╔══██╗██╔════╝████╗ ████║ +;;; ╚█████╔╝╚██║███████║╚██║ ███████║███████╗██╔████╔██║ +;;; ██╔══██╗ ██║╚════██║ ██║ ██╔══██║╚════██║██║╚██╔╝██║ +;;; ╚█████╔╝ ██║ ██║ ██║ ██║ ██║███████║██║ ╚═╝ ██║ +;;; ╚════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝ +;;; +;;; This is an implementation of EIP-8141's frames expiry contract. It +;;; is called by expiry verifier frames with an 8-byte big-endian expiry timestamp +;;; as calldata, and reverts unless block.timestamp <= expiry. + +#pragma target "prague" + +;;; ---------------------------------------------------------------------------- +;;; MACROS --------------------------------------------------------------------- +;;; ---------------------------------------------------------------------------- + +;;; EXPIRY_DATA_LENGTH is the size of the expiry timestamp input in bytes. +#define EXPIRY_DATA_LENGTH = 8 + +;;; ---------------------------------------------------------------------------- +;;; MACROS END ----------------------------------------------------------------- +;;; ---------------------------------------------------------------------------- + + ;; Verify input is exactly EXPIRY_DATA_LENGTH bytes long. + push EXPIRY_DATA_LENGTH ; [8] + calldatasize ; [calldatasize, 8] + eq ; [calldatasize == 8] + jumpi @check ; [] + + ;; Input length is invalid, revert. + push 0 ; [0] + push 0 ; [0, 0] + revert ; [] + +check: + ;; Load the expiry timestamp. The 8 input bytes occupy the high-order end + ;; of the loaded word, so shift them down to get the numeric value. + push 0 ; [0] + calldataload ; [word] + push 256 - 8*EXPIRY_DATA_LENGTH ; [192, word] + shr ; [expiry] + + ;; Revert if the deadline has passed. + timestamp ; [timestamp, expiry] + gt ; [timestamp > expiry] + jumpi @expired ; [] + + ;; Deadline has not passed, succeed with no return data. + stop + +expired: + push 0 ; [0] + push 0 ; [0, 0] + revert ; [] diff --git a/test/FramesExpiry.t.sol b/test/FramesExpiry.t.sol new file mode 100644 index 0000000..417570e --- /dev/null +++ b/test/FramesExpiry.t.sol @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import "forge-std/Test.sol"; + +address constant addr = 0x0000000000000000000000000000000000008141; + +contract FramesExpiryTest is Test { + function setUp() public { + vm.etch(addr, vm.parseBytes(vm.readFile("bytecode/frames_expiry/main.hex"))); + vm.warp(1_700_000_000); + } + + // verify calls the contract with an 8-byte big-endian expiry. + function verify(uint64 expiry) internal returns (bool) { + (bool ret, bytes memory data) = addr.call(abi.encodePacked(expiry)); + assertEq(data, hex"", "unexpected return data"); + return ret; + } + + // testDeadline checks the timestamp boundary: expiry >= timestamp passes, + // expiry < timestamp reverts. + function testDeadline() public { + uint64 ts = uint64(block.timestamp); + assertTrue(verify(ts), "expiry == timestamp should pass"); + assertTrue(verify(ts + 1), "future expiry should pass"); + assertTrue(verify(type(uint64).max), "max expiry should pass"); + assertFalse(verify(ts - 1), "past expiry should revert"); + assertFalse(verify(0), "zero expiry should revert"); + } + + // testBadCalldataSize checks inputs that aren't exactly 8 bytes revert, + // even when their leading bytes encode a valid future expiry. + function testBadCalldataSize() public { + bytes memory future = abi.encodePacked(type(uint64).max); + bytes[5] memory inputs = [ + bytes(hex""), + slice(future, 7), + bytes.concat(future, hex"00"), + bytes.concat(bytes32(type(uint256).max)), + bytes.concat(hex"deadbeef", future) + ]; + for (uint256 i = 0; i < inputs.length; i++) { + (bool ret, bytes memory data) = addr.call(inputs[i]); + assertFalse(ret); + assertEq(data, hex""); + } + } + + // testDeploy checks the constructor installs the runtime code. + function testDeploy() public { + bytes memory initcode = vm.parseBytes(vm.readFile("bytecode/frames_expiry/ctor.hex")); + address deployed; + assembly { + deployed := create(0, add(initcode, 0x20), mload(initcode)) + } + assertEq(deployed.code, addr.code); + } + + function slice(bytes memory data, uint256 len) internal pure returns (bytes memory out) { + out = new bytes(len); + for (uint256 i = 0; i < len; i++) { + out[i] = data[i]; + } + } +}