diff --git a/Makefile b/Makefile index be6c8aa..0c066c5 100644 --- a/Makefile +++ b/Makefile @@ -1,22 +1,62 @@ -.PHONY: build-network test clean stop status lint +.PHONY: build-network test test-all suites clean stop status lint + +# Which suite(s) under tests/ to run. Empty = every suite. +# make test SUITE=eip1153 +# make test SUITE="eip1153 eip2935" +# make test-eip1153 (shorthand) +SUITE ?= +# Optional -run regexp: make test SUITE=eip1153 RUN=TestTransientStorage +RUN ?= +# -p 1: every suite shares one sender account, parallel packages fight over nonces. +TEST_FLAGS ?= -v -count=1 -p 1 -timeout 20m + +ifeq ($(strip $(SUITE)),) +TEST_PKGS := ./... +else +TEST_PKGS := $(patsubst %,./%/,$(strip $(SUITE))) +endif + +ifneq ($(strip $(RUN)),) +RUN_FLAG := -run '$(RUN)' +endif + +# $(call run_tests,) — boot network, run tests, always tear down. +define run_tests +@/tmp/interstellar-network start & \ +START_PID=$$! ; \ +trap '/tmp/interstellar-network stop 2>/dev/null || true; kill $$START_PID 2>/dev/null || true' EXIT ; \ +if ! NODE_URL=$$(/tmp/interstellar-network node-url); then \ + echo "ERROR: network never became ready (node-url timed out or failed) — aborting instead of reporting a false pass"; exit 1; \ +fi ; \ +if ! NODE_P2P_PORT=$$(/tmp/interstellar-network node-p2p-port); then \ + echo "ERROR: network never became ready (node-p2p-port timed out or failed) — aborting instead of reporting a false pass"; exit 1; \ +fi ; \ +if [ -z "$$NODE_URL" ] || [ -z "$$NODE_P2P_PORT" ]; then \ + echo "ERROR: empty node connection details — refusing to run e2e tests against no network"; exit 1; \ +fi ; \ +cd tests && NODE_URL=$$NODE_URL NODE_P2P_PORT=$$NODE_P2P_PORT go test $(TEST_FLAGS) $(RUN_FLAG) $(1) +endef build-network: cd network && go build -o /tmp/interstellar-network github.com/vechain/interstellar-e2e/network && cd .. test: build-network - @/tmp/interstellar-network start & \ - START_PID=$$! ; \ - trap '/tmp/interstellar-network stop 2>/dev/null || true; kill $$START_PID 2>/dev/null || true' EXIT ; \ - if ! NODE_URL=$$(/tmp/interstellar-network node-url); then \ - echo "ERROR: network never became ready (node-url timed out or failed) — aborting instead of reporting a false pass"; exit 1; \ - fi ; \ - if ! NODE_P2P_PORT=$$(/tmp/interstellar-network node-p2p-port); then \ - echo "ERROR: network never became ready (node-p2p-port timed out or failed) — aborting instead of reporting a false pass"; exit 1; \ - fi ; \ - if [ -z "$$NODE_URL" ] || [ -z "$$NODE_P2P_PORT" ]; then \ - echo "ERROR: empty node connection details — refusing to run e2e tests against no network"; exit 1; \ - fi ; \ - cd tests && NODE_URL=$$NODE_URL NODE_P2P_PORT=$$NODE_P2P_PORT go test -v -count=1 -timeout 20m ./... + @for s in $(strip $(SUITE)); do \ + if [ ! -d "tests/$$s" ]; then \ + echo "ERROR: no such suite 'tests/$$s'. Available:"; $(MAKE) -s suites; exit 1; \ + fi ; \ + done + $(call run_tests,$(TEST_PKGS)) + +# make test-eip1153 == make test SUITE=eip1153 +test-%: + @$(MAKE) test SUITE=$* + +test-all: + @$(MAKE) test SUITE= + +suites: + @find tests -mindepth 1 -maxdepth 1 -type d ! -name helper -exec basename {} \; | sort | sed 's/^/ /' stop: /tmp/interstellar-network stop 2>/dev/null || true diff --git a/network/setup/network.go b/network/setup/network.go index f1194f2..ac6ecc8 100644 --- a/network/setup/network.go +++ b/network/setup/network.go @@ -11,7 +11,7 @@ import ( const ( defaultThorRepo = "https://github.com/vechain/thor" - defaultThorBranch = "evm-upgrades" + defaultThorBranch = "release/interstellar" ) // BuildNetwork constructs the 3-node network configuration for interstellar testing. diff --git a/tests/eip1153/contracts/TransientProbe.sol b/tests/eip1153/contracts/TransientProbe.sol new file mode 100644 index 0000000..02b20e3 --- /dev/null +++ b/tests/eip1153/contracts/TransientProbe.sol @@ -0,0 +1,167 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +contract TransientProbe { + error Boom(); + + event Loaded(uint256 indexed key, uint256 value); + + + function write(uint256 k, uint256 v) external { + assembly { + tstore(k, v) + } + } + + + function read(uint256 k) external view returns (uint256 r) { + assembly { + r := tload(k) + } + } + + function readToEvent(uint256 k) external { + uint256 r; + assembly { + r := tload(k) + } + emit Loaded(k, r); + } + + + function writeThenReadToEvent(uint256 k, uint256 v) external { + uint256 r; + assembly { + tstore(k, v) + r := tload(k) + } + emit Loaded(k, r); + } + + function callThenStaticWrite(address target, uint256 k, uint256 v) + external + returns (bool callOk, bool staticOk, uint256 staticRetLen) + { + bytes memory payload = abi.encodeWithSelector(this.write.selector, k, v); + + (callOk, ) = target.call(payload); + + (staticOk, ) = target.staticcall(payload); + assembly { + staticRetLen := returndatasize() + } + } + + function writeThenStaticRead(address target, uint256 k, uint256 v) + external + returns (bool ok, uint256 value) + { + TransientProbe(target).write(k, v); + + bytes memory ret; + (ok, ret) = target.staticcall(abi.encodeWithSelector(this.read.selector, k)); + if (ok && ret.length == 32) { + value = abi.decode(ret, (uint256)); + } + } + + + function writeThenRevert(uint256 k, uint256 v) external { + assembly { + tstore(k, v) + } + revert Boom(); + } + + function rollbackAfterInnerRevert(uint256 k, uint256 pre, uint256 inner) + external + returns (uint256 r) + { + assembly { + tstore(k, pre) + } + (bool ok, ) = address(this).call( + abi.encodeWithSelector(this.writeThenRevert.selector, k, inner) + ); + require(!ok, "inner call was expected to revert"); + assembly { + r := tload(k) + } + } + + function writeCallWriteThenRevert(uint256 k, uint256 a, uint256 b) external { + assembly { + tstore(k, a) + } + TransientProbe(address(this)).write(k, b); + revert Boom(); + } + + + function rollbackIncludesInnerCallWrites(uint256 k, uint256 pre, uint256 a, uint256 b) + external + returns (uint256 r) + { + assembly { + tstore(k, pre) + } + (bool ok, ) = address(this).call( + abi.encodeWithSelector(this.writeCallWriteThenRevert.selector, k, a, b) + ); + require(!ok, "inner call was expected to revert"); + assembly { + r := tload(k) + } + } + + + function writeThenCallOtherWrite(address other, uint256 k, uint256 mine, uint256 theirs) + external + returns (uint256 ourValue, uint256 theirValue) + { + assembly { + tstore(k, mine) + } + TransientProbe(other).write(k, theirs); + assembly { + ourValue := tload(k) + } + theirValue = TransientProbe(other).read(k); + } + + + function delegateWrite(address impl, uint256 k, uint256 v) + external + returns (uint256 ourValue, uint256 implValue) + { + (bool ok, ) = impl.delegatecall(abi.encodeWithSelector(this.write.selector, k, v)); + require(ok, "delegatecall failed"); + assembly { + ourValue := tload(k) + } + implValue = TransientProbe(impl).read(k); + } + + + function readFrom(address origin, uint256 k) external view returns (uint256) { + return TransientProbe(origin).read(k); + } + + + function reentrantRead(address other, uint256 k, uint256 v) + external + returns (uint256 seen) + { + assembly { + tstore(k, v) + } + seen = TransientProbe(other).readFrom(address(this), k); + } + + function innerWritePersists(uint256 k, uint256 v) external returns (uint256 r) { + TransientProbe(address(this)).write(k, v); + assembly { + r := tload(k) + } + } +} diff --git a/tests/eip1153/contracts/gen.go b/tests/eip1153/contracts/gen.go new file mode 100644 index 0000000..e848cb2 --- /dev/null +++ b/tests/eip1153/contracts/gen.go @@ -0,0 +1,14 @@ +// Copyright (c) 2018 The VeChainThor developers +// Distributed under the GNU Lesser General Public License v3.0 software license, see the accompanying +// file LICENSE or + +package contracts + +// --evm-version cancun is pinned explicitly: TSTORE/TLOAD first exist at +// Cancun, and letting solc pick its own default risks emitting opcodes from a +// later fork that the INTERSTELLAR EVM does not implement. + +// --platform linux/amd64 is required because ghcr.io/argotorg/solc publishes no +// arm64 manifest; without it the pull fails outright on Apple Silicon. + +//go:generate sh -c "docker run --rm --platform linux/amd64 -v $(pwd):/src ghcr.io/argotorg/solc:stable --evm-version cancun --optimize --optimize-runs 200 --combined-json abi,bin,bin-runtime,hashes /src/TransientProbe.sol | docker run --rm -i -v $(pwd):/src otherview/solgen:latest --out /src/generated" diff --git a/tests/eip1153/contracts/generated/transientprobe/transientprobe.go b/tests/eip1153/contracts/generated/transientprobe/transientprobe.go new file mode 100644 index 0000000..954c813 --- /dev/null +++ b/tests/eip1153/contracts/generated/transientprobe/transientprobe.go @@ -0,0 +1,1787 @@ +// Code generated by github.com/otherview/solgen. DO NOT EDIT. +// SPDX-License-Identifier: MIT +// Contract: TransientProbe (solc 0.8.36+commit.8a079791.Linux.clang) + +package transientprobe + +import ( + "encoding/hex" + "errors" + "fmt" + "math/big" + "reflect" + "strings" +) + +// Contract metadata +var _abiJSON = "[{\"inputs\":[],\"name\":\"Boom\",\"type\":\"error\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"uint256\",\"name\":\"key\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"value\",\"type\":\"uint256\"}],\"name\":\"Loaded\",\"type\":\"event\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"target\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"k\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"v\",\"type\":\"uint256\"}],\"name\":\"callThenStaticWrite\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"callOk\",\"type\":\"bool\"},{\"internalType\":\"bool\",\"name\":\"staticOk\",\"type\":\"bool\"},{\"internalType\":\"uint256\",\"name\":\"staticRetLen\",\"type\":\"uint256\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"impl\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"k\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"v\",\"type\":\"uint256\"}],\"name\":\"delegateWrite\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"ourValue\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"implValue\",\"type\":\"uint256\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"k\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"v\",\"type\":\"uint256\"}],\"name\":\"innerWritePersists\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"r\",\"type\":\"uint256\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"k\",\"type\":\"uint256\"}],\"name\":\"read\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"r\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"origin\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"k\",\"type\":\"uint256\"}],\"name\":\"readFrom\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"k\",\"type\":\"uint256\"}],\"name\":\"readToEvent\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"other\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"k\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"v\",\"type\":\"uint256\"}],\"name\":\"reentrantRead\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"seen\",\"type\":\"uint256\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"k\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"pre\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"inner\",\"type\":\"uint256\"}],\"name\":\"rollbackAfterInnerRevert\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"r\",\"type\":\"uint256\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"k\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"pre\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"a\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"b\",\"type\":\"uint256\"}],\"name\":\"rollbackIncludesInnerCallWrites\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"r\",\"type\":\"uint256\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"k\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"v\",\"type\":\"uint256\"}],\"name\":\"write\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"k\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"a\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"b\",\"type\":\"uint256\"}],\"name\":\"writeCallWriteThenRevert\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"other\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"k\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"mine\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"theirs\",\"type\":\"uint256\"}],\"name\":\"writeThenCallOtherWrite\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"ourValue\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"theirValue\",\"type\":\"uint256\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"k\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"v\",\"type\":\"uint256\"}],\"name\":\"writeThenReadToEvent\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"k\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"v\",\"type\":\"uint256\"}],\"name\":\"writeThenRevert\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"target\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"k\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"v\",\"type\":\"uint256\"}],\"name\":\"writeThenStaticRead\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"ok\",\"type\":\"bool\"},{\"internalType\":\"uint256\",\"name\":\"value\",\"type\":\"uint256\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"}]" + +// ABI returns the contract ABI as a JSON string +func ABI() string { + return _abiJSON +} + +// Bytecode contains the contract creation bytecode +var Bytecode = HexData("0x6080604052348015600e575f5ffd5b50610cc58061001c5f395ff3fe608060405234801561000f575f5ffd5b50600436106100f0575f3560e01c80639e4d922a11610093578063b3f7a2fa11610063578063b3f7a2fa14610223578063e07cc98314610236578063e3d96a5814610249578063ed2e5a971461025c575f5ffd5b80639e4d922a146101c05780639e5bc2e3146101d35780639ede2c77146101e6578063aaab8bc614610210575f5ffd5b80637a533d18116100ce5780637a533d181461015f57806381d61dab1461017257806396b219de1461019a5780639c0e3f7a146101ad575f5ffd5b806322691119146100f457806367567e1114610129578063714f81dc1461014a575b5f5ffd5b610107610102366004610b04565b61026e565b6040805193151584529115156020840152908201526060015b60405180910390f35b61013c610137366004610b34565b61038d565b604051908152602001610120565b61015d610158366004610b63565b61045a565b005b61015d61016d366004610b8c565b6104c9565b610185610180366004610b04565b610510565b60408051928352602083019190915201610120565b61015d6101a8366004610bac565b610679565b61015d6101bb366004610b8c565b6106b5565b6101856101ce366004610bc3565b6106bc565b61013c6101e1366004610b63565b610793565b6101f96101f4366004610b04565b61084f565b604080519215158352602083019190915201610120565b61013c61021e366004610b8c565b610987565b61013c610231366004610b04565b6109e4565b61015d610244366004610b8c565b610a5d565b61013c610257366004610bf9565b610a79565b61013c61026a366004610bac565b5c90565b5f5f5f5f639c0e3f7a60e01b8686604051602401610296929190918252602082015260400190565b604051602081830303815290604052906001600160e01b0319166020820180516001600160e01b0383818316178352505050509050866001600160a01b0316816040516102e39190610c21565b5f604051808303815f865af19150503d805f811461031c576040519150601f19603f3d011682016040523d82523d5f602084013e610321565b606091505b505080945050866001600160a01b03168160405161033f9190610c21565b5f60405180830381855afa9150503d805f8114610377576040519150601f19603f3d011682016040523d82523d5f602084013e61037c565b606091505b5094989097503d9650945050505050565b5f83855d604080516024810187905260448101859052606480820185905282518083039091018152608490910182526020810180516001600160e01b0316631c53e07760e21b17905290515f9130916103e69190610c21565b5f604051808303815f865af19150503d805f811461041f576040519150601f19603f3d011682016040523d82523d5f602084013e610424565b606091505b50509050801561044f5760405162461bcd60e51b815260040161044690610c37565b60405180910390fd5b5050925c9392505050565b81835d604051634e071fbd60e11b815260048101849052602481018290523090639c0e3f7a906044015f604051808303815f87803b15801561049a575f5ffd5b505af11580156104ac573d5f5f3e3d5ffd5b50505050604051631f09feb960e21b815260040160405180910390fd5b5f81835d825c9050827fbe0010b84cfca7c0c197a0c450be660a13985cad1cec23df4f6936ff88354ec38260405161050391815260200190565b60405180910390a2505050565b5f5f5f856001600160a01b0316639c0e3f7a60e01b8686604051602401610541929190918252602082015260400190565b60408051601f198184030181529181526020820180516001600160e01b03166001600160e01b031990941693909317909252905161057f9190610c21565b5f60405180830381855af49150503d805f81146105b7576040519150601f19603f3d011682016040523d82523d5f602084013e6105bc565b606091505b50509050806106035760405162461bcd60e51b815260206004820152601360248201527219195b1959d85d1958d85b1b0819985a5b1959606a1b6044820152606401610446565b60405163ed2e5a9760e01b815260048101869052855c93506001600160a01b0387169063ed2e5a9790602401602060405180830381865afa15801561064a573d5f5f3e3d5ffd5b505050506040513d601f19601f8201168201806040525081019061066e9190610c78565b915050935093915050565b604051815c8082529082907fbe0010b84cfca7c0c197a0c450be660a13985cad1cec23df4f6936ff88354ec39060200160405180910390a25050565b80825d5050565b5f5f83855d604051634e071fbd60e11b815260048101869052602481018490526001600160a01b03871690639c0e3f7a906044015f604051808303815f87803b158015610707575f5ffd5b505af1158015610719573d5f5f3e3d5ffd5b505060405163ed2e5a9760e01b815260048101889052875c94506001600160a01b038916925063ed2e5a979150602401602060405180830381865afa158015610764573d5f5f3e3d5ffd5b505050506040513d601f19601f820116820180604052508101906107889190610c78565b905094509492505050565b5f82845d6040805160248101869052604480820185905282518083039091018152606490910182526020810180516001600160e01b031663e07cc98360e01b17905290515f9130916107e59190610c21565b5f604051808303815f865af19150503d805f811461081e576040519150601f19603f3d011682016040523d82523d5f602084013e610823565b606091505b5050905080156108455760405162461bcd60e51b815260040161044690610c37565b5050915c92915050565b604051634e071fbd60e11b815260048101839052602481018290525f9081906001600160a01b03861690639c0e3f7a906044015f604051808303815f87803b158015610899575f5ffd5b505af11580156108ab573d5f5f3e3d5ffd5b505050506060856001600160a01b031663ed2e5a9760e01b866040516024016108d691815260200190565b60408051601f198184030181529181526020820180516001600160e01b03166001600160e01b03199094169390931790925290516109149190610c21565b5f60405180830381855afa9150503d805f811461094c576040519150601f19603f3d011682016040523d82523d5f602084013e610951565b606091505b509093509050828015610965575080516020145b1561097e578080602001905181019061066e9190610c78565b50935093915050565b604051634e071fbd60e11b815260048101839052602481018290525f903090639c0e3f7a906044015f604051808303815f87803b1580156109c6575f5ffd5b505af11580156109d8573d5f5f3e3d5ffd5b5050935c949350505050565b5f81835d604051631c7b2d4b60e31b8152306004820152602481018490526001600160a01b0385169063e3d96a5890604401602060405180830381865afa158015610a31573d5f5f3e3d5ffd5b505050506040513d601f19601f82011682018060405250810190610a559190610c78565b949350505050565b80825d604051631f09feb960e21b815260040160405180910390fd5b60405163ed2e5a9760e01b8152600481018290525f906001600160a01b0384169063ed2e5a9790602401602060405180830381865afa158015610abe573d5f5f3e3d5ffd5b505050506040513d601f19601f82011682018060405250810190610ae29190610c78565b9392505050565b80356001600160a01b0381168114610aff575f5ffd5b919050565b5f5f5f60608486031215610b16575f5ffd5b610b1f84610ae9565b95602085013595506040909401359392505050565b5f5f5f5f60808587031215610b47575f5ffd5b5050823594602084013594506040840135936060013592509050565b5f5f5f60608486031215610b75575f5ffd5b505081359360208301359350604090920135919050565b5f5f60408385031215610b9d575f5ffd5b50508035926020909101359150565b5f60208284031215610bbc575f5ffd5b5035919050565b5f5f5f5f60808587031215610bd6575f5ffd5b610bdf85610ae9565b966020860135965060408601359560600135945092505050565b5f5f60408385031215610c0a575f5ffd5b610c1383610ae9565b946020939093013593505050565b5f82518060208501845e5f920191825250919050565b60208082526021908201527f696e6e65722063616c6c2077617320657870656374656420746f2072657665726040820152601d60fa1b606082015260800190565b5f60208284031215610c88575f5ffd5b505191905056fea26469706673582212202bfbc600932b6f94ef7072ce69812ea53d27adf735ef4cd080c92584fe7dcb1264736f6c63430008240033") + +// DeployedBytecode contains the contract runtime bytecode +var DeployedBytecode = HexData("0x608060405234801561000f575f5ffd5b50600436106100f0575f3560e01c80639e4d922a11610093578063b3f7a2fa11610063578063b3f7a2fa14610223578063e07cc98314610236578063e3d96a5814610249578063ed2e5a971461025c575f5ffd5b80639e4d922a146101c05780639e5bc2e3146101d35780639ede2c77146101e6578063aaab8bc614610210575f5ffd5b80637a533d18116100ce5780637a533d181461015f57806381d61dab1461017257806396b219de1461019a5780639c0e3f7a146101ad575f5ffd5b806322691119146100f457806367567e1114610129578063714f81dc1461014a575b5f5ffd5b610107610102366004610b04565b61026e565b6040805193151584529115156020840152908201526060015b60405180910390f35b61013c610137366004610b34565b61038d565b604051908152602001610120565b61015d610158366004610b63565b61045a565b005b61015d61016d366004610b8c565b6104c9565b610185610180366004610b04565b610510565b60408051928352602083019190915201610120565b61015d6101a8366004610bac565b610679565b61015d6101bb366004610b8c565b6106b5565b6101856101ce366004610bc3565b6106bc565b61013c6101e1366004610b63565b610793565b6101f96101f4366004610b04565b61084f565b604080519215158352602083019190915201610120565b61013c61021e366004610b8c565b610987565b61013c610231366004610b04565b6109e4565b61015d610244366004610b8c565b610a5d565b61013c610257366004610bf9565b610a79565b61013c61026a366004610bac565b5c90565b5f5f5f5f639c0e3f7a60e01b8686604051602401610296929190918252602082015260400190565b604051602081830303815290604052906001600160e01b0319166020820180516001600160e01b0383818316178352505050509050866001600160a01b0316816040516102e39190610c21565b5f604051808303815f865af19150503d805f811461031c576040519150601f19603f3d011682016040523d82523d5f602084013e610321565b606091505b505080945050866001600160a01b03168160405161033f9190610c21565b5f60405180830381855afa9150503d805f8114610377576040519150601f19603f3d011682016040523d82523d5f602084013e61037c565b606091505b5094989097503d9650945050505050565b5f83855d604080516024810187905260448101859052606480820185905282518083039091018152608490910182526020810180516001600160e01b0316631c53e07760e21b17905290515f9130916103e69190610c21565b5f604051808303815f865af19150503d805f811461041f576040519150601f19603f3d011682016040523d82523d5f602084013e610424565b606091505b50509050801561044f5760405162461bcd60e51b815260040161044690610c37565b60405180910390fd5b5050925c9392505050565b81835d604051634e071fbd60e11b815260048101849052602481018290523090639c0e3f7a906044015f604051808303815f87803b15801561049a575f5ffd5b505af11580156104ac573d5f5f3e3d5ffd5b50505050604051631f09feb960e21b815260040160405180910390fd5b5f81835d825c9050827fbe0010b84cfca7c0c197a0c450be660a13985cad1cec23df4f6936ff88354ec38260405161050391815260200190565b60405180910390a2505050565b5f5f5f856001600160a01b0316639c0e3f7a60e01b8686604051602401610541929190918252602082015260400190565b60408051601f198184030181529181526020820180516001600160e01b03166001600160e01b031990941693909317909252905161057f9190610c21565b5f60405180830381855af49150503d805f81146105b7576040519150601f19603f3d011682016040523d82523d5f602084013e6105bc565b606091505b50509050806106035760405162461bcd60e51b815260206004820152601360248201527219195b1959d85d1958d85b1b0819985a5b1959606a1b6044820152606401610446565b60405163ed2e5a9760e01b815260048101869052855c93506001600160a01b0387169063ed2e5a9790602401602060405180830381865afa15801561064a573d5f5f3e3d5ffd5b505050506040513d601f19601f8201168201806040525081019061066e9190610c78565b915050935093915050565b604051815c8082529082907fbe0010b84cfca7c0c197a0c450be660a13985cad1cec23df4f6936ff88354ec39060200160405180910390a25050565b80825d5050565b5f5f83855d604051634e071fbd60e11b815260048101869052602481018490526001600160a01b03871690639c0e3f7a906044015f604051808303815f87803b158015610707575f5ffd5b505af1158015610719573d5f5f3e3d5ffd5b505060405163ed2e5a9760e01b815260048101889052875c94506001600160a01b038916925063ed2e5a979150602401602060405180830381865afa158015610764573d5f5f3e3d5ffd5b505050506040513d601f19601f820116820180604052508101906107889190610c78565b905094509492505050565b5f82845d6040805160248101869052604480820185905282518083039091018152606490910182526020810180516001600160e01b031663e07cc98360e01b17905290515f9130916107e59190610c21565b5f604051808303815f865af19150503d805f811461081e576040519150601f19603f3d011682016040523d82523d5f602084013e610823565b606091505b5050905080156108455760405162461bcd60e51b815260040161044690610c37565b5050915c92915050565b604051634e071fbd60e11b815260048101839052602481018290525f9081906001600160a01b03861690639c0e3f7a906044015f604051808303815f87803b158015610899575f5ffd5b505af11580156108ab573d5f5f3e3d5ffd5b505050506060856001600160a01b031663ed2e5a9760e01b866040516024016108d691815260200190565b60408051601f198184030181529181526020820180516001600160e01b03166001600160e01b03199094169390931790925290516109149190610c21565b5f60405180830381855afa9150503d805f811461094c576040519150601f19603f3d011682016040523d82523d5f602084013e610951565b606091505b509093509050828015610965575080516020145b1561097e578080602001905181019061066e9190610c78565b50935093915050565b604051634e071fbd60e11b815260048101839052602481018290525f903090639c0e3f7a906044015f604051808303815f87803b1580156109c6575f5ffd5b505af11580156109d8573d5f5f3e3d5ffd5b5050935c949350505050565b5f81835d604051631c7b2d4b60e31b8152306004820152602481018490526001600160a01b0385169063e3d96a5890604401602060405180830381865afa158015610a31573d5f5f3e3d5ffd5b505050506040513d601f19601f82011682018060405250810190610a559190610c78565b949350505050565b80825d604051631f09feb960e21b815260040160405180910390fd5b60405163ed2e5a9760e01b8152600481018290525f906001600160a01b0384169063ed2e5a9790602401602060405180830381865afa158015610abe573d5f5f3e3d5ffd5b505050506040513d601f19601f82011682018060405250810190610ae29190610c78565b9392505050565b80356001600160a01b0381168114610aff575f5ffd5b919050565b5f5f5f60608486031215610b16575f5ffd5b610b1f84610ae9565b95602085013595506040909401359392505050565b5f5f5f5f60808587031215610b47575f5ffd5b5050823594602084013594506040840135936060013592509050565b5f5f5f60608486031215610b75575f5ffd5b505081359360208301359350604090920135919050565b5f5f60408385031215610b9d575f5ffd5b50508035926020909101359150565b5f60208284031215610bbc575f5ffd5b5035919050565b5f5f5f5f60808587031215610bd6575f5ffd5b610bdf85610ae9565b966020860135965060408601359560600135945092505050565b5f5f60408385031215610c0a575f5ffd5b610c1383610ae9565b946020939093013593505050565b5f82518060208501845e5f920191825250919050565b60208082526021908201527f696e6e65722063616c6c2077617320657870656374656420746f2072657665726040820152601d60fa1b606082015260800190565b5f60208284031215610c88575f5ffd5b505191905056fea26469706673582212202bfbc600932b6f94ef7072ce69812ea53d27adf735ef4cd080c92584fe7dcb1264736f6c63430008240033") + +// Address represents a 20-byte Ethereum address +type Address [20]byte + +// String returns the hex string representation of the address +func (a Address) String() string { + return "0x" + hex.EncodeToString(a[:]) +} + +// Hash represents a 32-byte hash +type Hash [32]byte + +// String returns the hex string representation of the hash +func (h Hash) String() string { + return "0x" + hex.EncodeToString(h[:]) +} + +// Bytes returns the hash as a byte slice +func (h Hash) Bytes() []byte { + return h[:] +} + +// AddressFromHex creates an Address from a hex string +func AddressFromHex(s string) Address { + var addr Address + if strings.HasPrefix(s, "0x") { + s = s[2:] + } + if len(s) != 40 { + panic("invalid address hex string length") + } + decoded, err := hex.DecodeString(s) + if err != nil { + panic("invalid address hex string: " + err.Error()) + } + copy(addr[:], decoded) + return addr +} + +// HashFromHex creates a Hash from a hex string +func HashFromHex(s string) Hash { + var hash Hash + if strings.HasPrefix(s, "0x") { + s = s[2:] + } + if len(s) != 64 { + panic("invalid hash hex string length") + } + decoded, err := hex.DecodeString(s) + if err != nil { + panic("invalid hash hex string: " + err.Error()) + } + copy(hash[:], decoded) + return hash +} + +// HexData provides convenient access to hex-encoded byte data +type HexData string + +// Hex returns the hex string representation +func (h HexData) Hex() string { + return string(h) +} + +// Bytes returns the decoded bytes from the hex string +func (h HexData) Bytes() []byte { + hexStr := string(h) + if hexStr == "" { + return nil + } + if strings.HasPrefix(hexStr, "0x") { + hexStr = hexStr[2:] + } + decoded, err := hex.DecodeString(hexStr) + if err != nil { + panic("invalid hex data: " + err.Error()) + } + return decoded +} + +// ABI Encoding Implementation + +// encodeUint256 encodes a uint256 value to 32 bytes (big-endian) +func encodeUint256(val interface{}) ([]byte, error) { + result := make([]byte, 32) + switch v := val.(type) { + case *big.Int: + if v.Sign() < 0 { + return nil, errors.New("negative values not supported for uint256") + } + if v.BitLen() > 256 { + return nil, errors.New("value too large for uint256") + } + v.FillBytes(result) + return result, nil + case uint64: + big.NewInt(0).SetUint64(v).FillBytes(result) + return result, nil + case int64: + if v < 0 { + return nil, errors.New("negative values not supported for uint256") + } + big.NewInt(v).FillBytes(result) + return result, nil + case int: + if v < 0 { + return nil, errors.New("negative values not supported for uint256") + } + big.NewInt(int64(v)).FillBytes(result) + return result, nil + default: + return nil, fmt.Errorf("unsupported type for uint256: %T", v) + } +} + +// encodeInt256 encodes a signed 256-bit integer to 32 bytes using two's complement. +// Valid range: [-2^255, 2^255-1]. +func encodeInt256(val interface{}) ([]byte, error) { + result := make([]byte, 32) + switch v := val.(type) { + case *big.Int: + if v.Sign() >= 0 { + // Positive: valid range [0, 2^255-1] → BitLen must be ≤ 255. + if v.BitLen() > 255 { + return nil, errors.New("value too large for int256") + } + v.FillBytes(result) + } else { + // Negative: valid range [-2^255, -1]. + // abs(-2^255) has BitLen == 256, which is the boundary. + abs := new(big.Int).Neg(v) + minNeg := new(big.Int).Lsh(big.NewInt(1), 255) // 2^255 + if abs.Cmp(minNeg) > 0 { + return nil, errors.New("value too small for int256") + } + // Two's-complement: compute 2^256 + v = 2^256 - abs(v). + mask := new(big.Int).Lsh(big.NewInt(1), 256) + new(big.Int).Add(mask, v).FillBytes(result) + } + return result, nil + case int64: + return encodeInt256(big.NewInt(v)) + case int: + return encodeInt256(big.NewInt(int64(v))) + default: + return nil, fmt.Errorf("unsupported type for int256: %T", v) + } +} + +// encodeAddress encodes an address to 32 bytes (zero-padded) +func encodeAddress(addr Address) ([]byte, error) { + result := make([]byte, 32) + copy(result[12:32], addr[:]) + return result, nil +} + +// encodeBool encodes a boolean to 32 bytes +func encodeBool(val bool) ([]byte, error) { + result := make([]byte, 32) + if val { + result[31] = 1 + } + return result, nil +} + +// encodeBytes encodes dynamic bytes +func encodeBytes(data []byte) ([]byte, error) { + // Length (32 bytes) + data (padded to multiple of 32 bytes) + length := len(data) + lengthBytes, err := encodeUint256(uint64(length)) + if err != nil { + return nil, err + } + + // Pad data to multiple of 32 bytes + paddedLength := ((length + 31) / 32) * 32 + paddedData := make([]byte, paddedLength) + copy(paddedData, data) + + return append(lengthBytes, paddedData...), nil +} + +// encodeString encodes a string as dynamic bytes +func encodeString(str string) ([]byte, error) { + return encodeBytes([]byte(str)) +} + +// encodeFixedBytes encodes fixed-size bytes (e.g., bytes32) as a single static +// 32-byte word: the value is left-aligned and right-padded with zeros. +func encodeFixedBytes(val []byte, size int) ([]byte, error) { + if size < 1 || size > 32 { + return nil, fmt.Errorf("invalid fixed bytes size: %d", size) + } + if len(val) != size { + return nil, fmt.Errorf("fixed bytes length mismatch: got %d, want %d", len(val), size) + } + result := make([]byte, 32) + copy(result, val) + return result, nil +} + +// encodeArg encodes a single ABI argument, returning its encoded bytes and +// whether it is a dynamic type (which gets a 32-byte offset pointer in the head +// and its data in the tail). It is the per-argument core shared by Pack. +func encodeArg(arg any) ([]byte, bool, error) { + switch v := arg.(type) { + case *big.Int: + if v.Sign() < 0 { + d, err := encodeInt256(v) + return d, false, err + } + d, err := encodeUint256(v) + return d, false, err + case uint8: + d, err := encodeUint256(uint64(v)) + return d, false, err + case uint16: + d, err := encodeUint256(uint64(v)) + return d, false, err + case uint32: + d, err := encodeUint256(uint64(v)) + return d, false, err + case uint64: + d, err := encodeUint256(v) + return d, false, err + case int8: + d, err := encodeInt256(big.NewInt(int64(v))) + return d, false, err + case int16: + d, err := encodeInt256(big.NewInt(int64(v))) + return d, false, err + case int32: + d, err := encodeInt256(big.NewInt(int64(v))) + return d, false, err + case int64: + d, err := encodeInt256(big.NewInt(v)) + return d, false, err + case Address: + d, err := encodeAddress(v) + return d, false, err + case bool: + d, err := encodeBool(v) + return d, false, err + case string: + d, err := encodeString(v) + return d, true, err + case []byte: + d, err := encodeBytes(v) + return d, true, err + case Hash: + d, err := encodeFixedBytes(v[:], 32) + return d, false, err + case [32]byte: + d, err := encodeFixedBytes(v[:], 32) + return d, false, err + default: + rt := reflect.TypeOf(arg) + if rt != nil && rt.Kind() == reflect.Array { + // Fixed-size byte array bytesN (1 <= N <= 32): one static word. + if rt.Elem().Kind() == reflect.Uint8 && rt.Len() <= 32 { + rv := reflect.ValueOf(arg) + b := make([]byte, rv.Len()) + reflect.Copy(reflect.ValueOf(b), rv) + d, err := encodeFixedBytes(b, len(b)) + return d, false, err + } + // Fixed-size array [N]T of static elements: N inline static words. + rv := reflect.ValueOf(arg) + var out []byte + for i := 0; i < rv.Len(); i++ { + elemData, elemDynamic, err := encodeArg(rv.Index(i).Interface()) + if err != nil { + return nil, false, fmt.Errorf("encoding array element %d: %w", i, err) + } + if elemDynamic { + return nil, false, fmt.Errorf("unsupported dynamic element in fixed-size array: %T", arg) + } + out = append(out, elemData...) + } + return out, false, nil + } + return nil, false, fmt.Errorf("unsupported argument type: %T", arg) + } +} + +// ABI Decoding Implementation + +// decodeUint256 decodes a uint256 from 32 bytes to *big.Int +func decodeUint256(data []byte) (*big.Int, error) { + if len(data) < 32 { + return nil, errors.New("insufficient data for uint256") + } + return new(big.Int).SetBytes(data[:32]), nil +} + +// decodeInt256 decodes a signed 256-bit integer from 32 bytes +func decodeInt256(data []byte) (*big.Int, error) { + if len(data) < 32 { + return nil, errors.New("insufficient data for int256") + } + + result := new(big.Int).SetBytes(data[:32]) + + // Check if negative (MSB is set) + if data[0]&0x80 != 0 { + // Convert from two's complement + // Create mask with all bits set for 256-bit number + mask := new(big.Int).Lsh(big.NewInt(1), 256) + mask.Sub(mask, big.NewInt(1)) + + // XOR with mask and add 1 to get absolute value + result.Xor(result, mask) + result.Add(result, big.NewInt(1)) + result.Neg(result) + } + + return result, nil +} + +// decodeAddress decodes an address from 32 bytes +func decodeAddress(data []byte) (Address, error) { + if len(data) < 32 { + return Address{}, errors.New("insufficient data for address") + } + var addr Address + copy(addr[:], data[12:32]) + return addr, nil +} + +// decodeBool decodes a boolean from 32 bytes +func decodeBool(data []byte) (bool, error) { + if len(data) < 32 { + return false, errors.New("insufficient data for bool") + } + return data[31] != 0, nil +} + +// decodeBytes decodes dynamic bytes +func decodeBytes(data []byte, offset int) ([]byte, int, error) { + if len(data) < offset+32 { + return nil, 0, errors.New("insufficient data for bytes length") + } + lengthBig, err := decodeUint256(data[offset : offset+32]) + if err != nil { + return nil, 0, fmt.Errorf("decoding bytes length: %w", err) + } + if !lengthBig.IsUint64() { + return nil, 0, errors.New("bytes length too large") + } + length := int(lengthBig.Uint64()) + if len(data) < offset+32+length { + return nil, 0, errors.New("insufficient data for bytes content") + } + result := make([]byte, length) + copy(result, data[offset+32:offset+32+length]) + // Calculate next offset (padded to 32 bytes) + paddedLength := ((length + 31) / 32) * 32 + return result, offset + 32 + paddedLength, nil +} + +// decodeFixedBytes decodes fixed-size bytes (e.g., bytes32) +func decodeFixedBytes(data []byte, size int) ([]byte, error) { + if len(data) < 32 { + return nil, errors.New("insufficient data for fixed bytes") + } + if size > 32 { + return nil, errors.New("fixed bytes size too large") + } + result := make([]byte, size) + copy(result, data[:size]) + return result, nil +} + +// decode various fixed-size byte arrays +func decodeBytes1(data []byte) ([1]byte, error) { + bytes, err := decodeFixedBytes(data, 1) + if err != nil { + return [1]byte{}, err + } + var result [1]byte + copy(result[:], bytes) + return result, nil +} + +func decodeBytes32(data []byte) ([32]byte, error) { + bytes, err := decodeFixedBytes(data, 32) + if err != nil { + return [32]byte{}, err + } + var result [32]byte + copy(result[:], bytes) + return result, nil +} + +// decodeArray decodes dynamic arrays +func decodeArray(data []byte, offset int, elemDecoder func([]byte) (interface{}, error)) ([]interface{}, int, error) { + if len(data) < offset+32 { + return nil, 0, errors.New("insufficient data for array length") + } + + lengthBig, err := decodeUint256(data[offset : offset+32]) + if err != nil { + return nil, 0, fmt.Errorf("decoding array length: %w", err) + } + if !lengthBig.IsUint64() { + return nil, 0, errors.New("array length too large") + } + length := int(lengthBig.Uint64()) + + currentOffset := offset + 32 + result := make([]interface{}, length) + + for i := 0; i < length; i++ { + if len(data) < currentOffset+32 { + return nil, 0, fmt.Errorf("insufficient data for array element %d", i) + } + elem, err := elemDecoder(data[currentOffset : currentOffset+32]) + if err != nil { + return nil, 0, fmt.Errorf("decoding array element %d: %w", i, err) + } + result[i] = elem + currentOffset += 32 + } + + return result, currentOffset, nil +} + +// Array element decoders (internal use) +func decodeUint256ArrayElement(data []byte) (interface{}, error) { + return decodeUint256(data) +} + +func decodeInt256ArrayElement(data []byte) (interface{}, error) { + return decodeInt256(data) +} + +func decodeAddressArrayElement(data []byte) (interface{}, error) { + return decodeAddress(data) +} + +func decodeBoolArrayElement(data []byte) (interface{}, error) { + return decodeBool(data) +} + +// readOffset reads a 32-byte ABI offset/length word at head position pos and +// returns it as an int. +func readOffset(data []byte, pos int) (int, error) { + if len(data) < pos+32 { + return 0, errors.New("insufficient data for offset pointer") + } + p, err := decodeUint256(data[pos : pos+32]) + if err != nil { + return 0, fmt.Errorf("decoding offset pointer: %w", err) + } + if !p.IsUint64() { + return 0, errors.New("offset pointer too large") + } + return int(p.Uint64()), nil +} + +// decodeStaticSliceAt decodes a dynamic array of static (32-byte) elements. pos +// is the head position holding the offset pointer to the array data; dec decodes +// a single element from its 32-byte word. +func decodeStaticSliceAt[T any](data []byte, pos int, dec func([]byte) (T, error)) ([]T, error) { + arrPos, err := readOffset(data, pos) + if err != nil { + return nil, err + } + length, err := readOffset(data, arrPos) + if err != nil { + return nil, fmt.Errorf("decoding array length: %w", err) + } + out := make([]T, length) + cur := arrPos + 32 + for i := 0; i < length; i++ { + if len(data) < cur+32 { + return nil, fmt.Errorf("insufficient data for array element %d", i) + } + v, err := dec(data[cur : cur+32]) + if err != nil { + return nil, fmt.Errorf("decoding array element %d: %w", i, err) + } + out[i] = v + cur += 32 + } + return out, nil +} + +// decodeStaticFixedArray decodes size consecutive static (32-byte) elements +// starting at pos into a slice; the caller copies it into the fixed-size [N]T +// value. dec decodes a single element from its 32-byte word. +func decodeStaticFixedArray[T any](data []byte, pos, size int, dec func([]byte) (T, error)) ([]T, error) { + out := make([]T, size) + for i := 0; i < size; i++ { + if len(data) < pos+(i+1)*32 { + return nil, fmt.Errorf("insufficient data for fixed array element %d", i) + } + v, err := dec(data[pos+i*32 : pos+(i+1)*32]) + if err != nil { + return nil, fmt.Errorf("decoding fixed array element %d: %w", i, err) + } + out[i] = v + } + return out, nil +} + +// decodeUint8 decodes a uint8 from 32 bytes +func decodeUint8(data []byte) (uint8, error) { + if len(data) < 32 { + return 0, errors.New("insufficient data for uint8") + } + // Verify upper bytes are zero + for i := 0; i < 31; i++ { + if data[i] != 0 { + return 0, errors.New("invalid uint8 encoding") + } + } + return data[31], nil +} + +// decodeUint16 decodes a uint16 from 32 bytes +func decodeUint16(data []byte) (uint16, error) { + if len(data) < 32 { + return 0, errors.New("insufficient data for uint16") + } + // Verify upper bytes are zero + for i := 0; i < 30; i++ { + if data[i] != 0 { + return 0, errors.New("invalid uint16 encoding") + } + } + return uint16(data[30])<<8 | uint16(data[31]), nil +} + +// decodeUint32 decodes a uint32 from 32 bytes +func decodeUint32(data []byte) (uint32, error) { + if len(data) < 32 { + return 0, errors.New("insufficient data for uint32") + } + // Verify upper bytes are zero + for i := 0; i < 28; i++ { + if data[i] != 0 { + return 0, errors.New("invalid uint32 encoding") + } + } + var result uint32 + for i := 28; i < 32; i++ { + result = (result << 8) | uint32(data[i]) + } + return result, nil +} + +// decodeUint64 decodes a uint64 from 32 bytes +func decodeUint64(data []byte) (uint64, error) { + if len(data) < 32 { + return 0, errors.New("insufficient data for uint64") + } + // Check if value exceeds uint64 range + for i := 0; i < 24; i++ { + if data[i] != 0 { + return 0, errors.New("value exceeds uint64 range") + } + } + var result uint64 + for i := 24; i < 32; i++ { + result = (result << 8) | uint64(data[i]) + } + return result, nil +} + +// decodeInt64 decodes an int64 from 32 bytes (ABI sign-extended big-endian). +func decodeInt64(data []byte) (int64, error) { + if len(data) < 32 { + return 0, errors.New("insufficient data for int64") + } + + // ABI sign-extension: bytes 0-23 must all match the sign byte + // (0x00 for non-negative, 0xFF for negative). + isNegative := data[0]&0x80 != 0 + expectedByte := byte(0) + if isNegative { + expectedByte = 0xFF + } + for i := 0; i < 24; i++ { + if data[i] != expectedByte { + return 0, errors.New("value exceeds int64 range") + } + } + + // Assemble the int64 from the last 8 bytes. + // Because data[24..31] already hold the correct two's-complement + // representation, no further sign extension is needed. + var result int64 + for i := 24; i < 32; i++ { + result = (result << 8) | int64(data[i]) + } + return result, nil +} + +// decodeHash decodes a 32-byte hash +func decodeHash(data []byte) (Hash, error) { + if len(data) < 32 { + return Hash{}, errors.New("insufficient data for hash") + } + var hash Hash + copy(hash[:], data[:32]) + return hash, nil +} + +// decodeString decodes a string from dynamic bytes +func decodeString(data []byte, offset int) (string, int, error) { + bytes, nextOffset, err := decodeBytes(data, offset) + if err != nil { + return "", 0, err + } + return string(bytes), nextOffset, nil +} // Method information +func GetCallThenStaticWriteMethod() MethodInfo { + return MethodInfo{ + Name: "callThenStaticWrite", + Signature: "callThenStaticWrite(address,uint256,uint256)", + Selector: HexData("0x22691119"), + } +} +func GetDelegateWriteMethod() MethodInfo { + return MethodInfo{ + Name: "delegateWrite", + Signature: "delegateWrite(address,uint256,uint256)", + Selector: HexData("0x81d61dab"), + } +} +func GetInnerWritePersistsMethod() MethodInfo { + return MethodInfo{ + Name: "innerWritePersists", + Signature: "innerWritePersists(uint256,uint256)", + Selector: HexData("0xaaab8bc6"), + } +} +func GetReadMethod() MethodInfo { + return MethodInfo{ + Name: "read", + Signature: "read(uint256)", + Selector: HexData("0xed2e5a97"), + } +} +func GetReadFromMethod() MethodInfo { + return MethodInfo{ + Name: "readFrom", + Signature: "readFrom(address,uint256)", + Selector: HexData("0xe3d96a58"), + } +} +func GetReadToEventMethod() MethodInfo { + return MethodInfo{ + Name: "readToEvent", + Signature: "readToEvent(uint256)", + Selector: HexData("0x96b219de"), + } +} +func GetReentrantReadMethod() MethodInfo { + return MethodInfo{ + Name: "reentrantRead", + Signature: "reentrantRead(address,uint256,uint256)", + Selector: HexData("0xb3f7a2fa"), + } +} +func GetRollbackAfterInnerRevertMethod() MethodInfo { + return MethodInfo{ + Name: "rollbackAfterInnerRevert", + Signature: "rollbackAfterInnerRevert(uint256,uint256,uint256)", + Selector: HexData("0x9e5bc2e3"), + } +} +func GetRollbackIncludesInnerCallWritesMethod() MethodInfo { + return MethodInfo{ + Name: "rollbackIncludesInnerCallWrites", + Signature: "rollbackIncludesInnerCallWrites(uint256,uint256,uint256,uint256)", + Selector: HexData("0x67567e11"), + } +} +func GetWriteMethod() MethodInfo { + return MethodInfo{ + Name: "write", + Signature: "write(uint256,uint256)", + Selector: HexData("0x9c0e3f7a"), + } +} +func GetWriteCallWriteThenRevertMethod() MethodInfo { + return MethodInfo{ + Name: "writeCallWriteThenRevert", + Signature: "writeCallWriteThenRevert(uint256,uint256,uint256)", + Selector: HexData("0x714f81dc"), + } +} +func GetWriteThenCallOtherWriteMethod() MethodInfo { + return MethodInfo{ + Name: "writeThenCallOtherWrite", + Signature: "writeThenCallOtherWrite(address,uint256,uint256,uint256)", + Selector: HexData("0x9e4d922a"), + } +} +func GetWriteThenReadToEventMethod() MethodInfo { + return MethodInfo{ + Name: "writeThenReadToEvent", + Signature: "writeThenReadToEvent(uint256,uint256)", + Selector: HexData("0x7a533d18"), + } +} +func GetWriteThenRevertMethod() MethodInfo { + return MethodInfo{ + Name: "writeThenRevert", + Signature: "writeThenRevert(uint256,uint256)", + Selector: HexData("0xe07cc983"), + } +} +func GetWriteThenStaticReadMethod() MethodInfo { + return MethodInfo{ + Name: "writeThenStaticRead", + Signature: "writeThenStaticRead(address,uint256,uint256)", + Selector: HexData("0x9ede2c77"), + } +} + +// Event information +func GetLoadedEvent() EventInfo { + return EventInfo{ + Name: "Loaded", + Topic: HashFromHex("0xbe0010b84cfca7c0c197a0c450be660a13985cad1cec23df4f6936ff88354ec3"), + } +} + +// Error information +func GetBoomError() ErrorInfo { + return ErrorInfo{ + Name: "Boom", + Signature: "Boom()", + Selector: HexData("0x7c27fae4"), + } +} + +// Method registry provides access to packable contract methods +type MethodRegistry struct{} + +// Event registry provides access to packable contract events +type EventRegistry struct{} + +// Error registry provides access to packable contract errors +type ErrorRegistry struct{} + +// PackableMethod represents a method with packing capabilities +type PackableMethod struct { + Name string + Signature string + Selector HexData +} + +// PackableEvent represents an event with unpacking capabilities +type PackableEvent struct { + Name string + Topic Hash +} + +// EventDecoder represents an event with decode functionality +type EventDecoder struct { + Name string + Topic Hash +} + +// PackableError represents an error with unpacking capabilities +type PackableError struct { + Name string + Signature string + Selector HexData +} + +// MethodInfo represents method metadata +type MethodInfo struct { + Name string + Signature string + Selector HexData +} + +// EventInfo represents event metadata +type EventInfo struct { + Name string + Topic Hash +} + +// ErrorInfo represents error metadata +type ErrorInfo struct { + Name string + Signature string + Selector HexData +} + +// Pack encodes method arguments and returns the method selector + encoded arguments. +// Uses ABI head-tail encoding: static args are inlined in the head (32 bytes each); +// dynamic args (string, []byte) get a 32-byte offset pointer in the head, with +// their data appended in the tail section. +func (pm *PackableMethod) Pack(args ...any) (HexData, error) { + // Start with the 4-byte method selector + selectorBytes := pm.Selector.Bytes() + if len(selectorBytes) == 0 { + return "", fmt.Errorf("invalid method selector") + } + + // If no arguments, return just the selector + if len(args) == 0 { + return pm.Selector, nil + } + + type argEncoding struct { + data []byte + isDynamic bool + } + + encoded := make([]argEncoding, len(args)) + for i, arg := range args { + data, dynamic, err := encodeArg(arg) + if err != nil { + return "", fmt.Errorf("encoding arg %d: %w", i, err) + } + encoded[i] = argEncoding{data: data, isDynamic: dynamic} + } + + // Build ABI head-tail encoding: + // Head: static args inlined (32 bytes); dynamic args get a 32-byte offset pointer. + // Tail: dynamic args' encoded data appended in order. + headSize := len(args) * 32 + tailOffset := headSize + + var head []byte + var tail []byte + for _, enc := range encoded { + if enc.isDynamic { + offsetBytes, err := encodeUint256(uint64(tailOffset)) + if err != nil { + return "", fmt.Errorf("encoding offset pointer: %w", err) + } + head = append(head, offsetBytes...) + tail = append(tail, enc.data...) + tailOffset += len(enc.data) + } else { + head = append(head, enc.data...) + } + } + + payload := append(selectorBytes, append(head, tail...)...) + return HexData("0x" + hex.EncodeToString(payload)), nil +} + +// MustPack encodes method arguments and panics on error +func (pm *PackableMethod) MustPack(args ...any) HexData { + result, err := pm.Pack(args...) + if err != nil { + panic(err) + } + return result +} + +// CallThenStaticWriteMethod returns a packable method for callThenStaticWrite +func (mr MethodRegistry) CallThenStaticWriteMethod() *CallThenStaticWriteMethod { + return &CallThenStaticWriteMethod{ + PackableMethod: PackableMethod{ + Name: "callThenStaticWrite", + Signature: "callThenStaticWrite(address,uint256,uint256)", + Selector: HexData("0x22691119"), + }, + } +} + +// DelegateWriteMethod returns a packable method for delegateWrite +func (mr MethodRegistry) DelegateWriteMethod() *DelegateWriteMethod { + return &DelegateWriteMethod{ + PackableMethod: PackableMethod{ + Name: "delegateWrite", + Signature: "delegateWrite(address,uint256,uint256)", + Selector: HexData("0x81d61dab"), + }, + } +} + +// InnerWritePersistsMethod returns a packable method for innerWritePersists +func (mr MethodRegistry) InnerWritePersistsMethod() *InnerWritePersistsMethod { + return &InnerWritePersistsMethod{ + PackableMethod: PackableMethod{ + Name: "innerWritePersists", + Signature: "innerWritePersists(uint256,uint256)", + Selector: HexData("0xaaab8bc6"), + }, + } +} + +// ReadMethod returns a packable method for read +func (mr MethodRegistry) ReadMethod() *ReadMethod { + return &ReadMethod{ + PackableMethod: PackableMethod{ + Name: "read", + Signature: "read(uint256)", + Selector: HexData("0xed2e5a97"), + }, + } +} + +// ReadFromMethod returns a packable method for readFrom +func (mr MethodRegistry) ReadFromMethod() *ReadFromMethod { + return &ReadFromMethod{ + PackableMethod: PackableMethod{ + Name: "readFrom", + Signature: "readFrom(address,uint256)", + Selector: HexData("0xe3d96a58"), + }, + } +} + +// ReadToEventMethod returns a packable method for readToEvent +func (mr MethodRegistry) ReadToEventMethod() *ReadToEventMethod { + return &ReadToEventMethod{ + PackableMethod: PackableMethod{ + Name: "readToEvent", + Signature: "readToEvent(uint256)", + Selector: HexData("0x96b219de"), + }, + } +} + +// ReentrantReadMethod returns a packable method for reentrantRead +func (mr MethodRegistry) ReentrantReadMethod() *ReentrantReadMethod { + return &ReentrantReadMethod{ + PackableMethod: PackableMethod{ + Name: "reentrantRead", + Signature: "reentrantRead(address,uint256,uint256)", + Selector: HexData("0xb3f7a2fa"), + }, + } +} + +// RollbackAfterInnerRevertMethod returns a packable method for rollbackAfterInnerRevert +func (mr MethodRegistry) RollbackAfterInnerRevertMethod() *RollbackAfterInnerRevertMethod { + return &RollbackAfterInnerRevertMethod{ + PackableMethod: PackableMethod{ + Name: "rollbackAfterInnerRevert", + Signature: "rollbackAfterInnerRevert(uint256,uint256,uint256)", + Selector: HexData("0x9e5bc2e3"), + }, + } +} + +// RollbackIncludesInnerCallWritesMethod returns a packable method for rollbackIncludesInnerCallWrites +func (mr MethodRegistry) RollbackIncludesInnerCallWritesMethod() *RollbackIncludesInnerCallWritesMethod { + return &RollbackIncludesInnerCallWritesMethod{ + PackableMethod: PackableMethod{ + Name: "rollbackIncludesInnerCallWrites", + Signature: "rollbackIncludesInnerCallWrites(uint256,uint256,uint256,uint256)", + Selector: HexData("0x67567e11"), + }, + } +} + +// WriteMethod returns a packable method for write +func (mr MethodRegistry) WriteMethod() *WriteMethod { + return &WriteMethod{ + PackableMethod: PackableMethod{ + Name: "write", + Signature: "write(uint256,uint256)", + Selector: HexData("0x9c0e3f7a"), + }, + } +} + +// WriteCallWriteThenRevertMethod returns a packable method for writeCallWriteThenRevert +func (mr MethodRegistry) WriteCallWriteThenRevertMethod() *WriteCallWriteThenRevertMethod { + return &WriteCallWriteThenRevertMethod{ + PackableMethod: PackableMethod{ + Name: "writeCallWriteThenRevert", + Signature: "writeCallWriteThenRevert(uint256,uint256,uint256)", + Selector: HexData("0x714f81dc"), + }, + } +} + +// WriteThenCallOtherWriteMethod returns a packable method for writeThenCallOtherWrite +func (mr MethodRegistry) WriteThenCallOtherWriteMethod() *WriteThenCallOtherWriteMethod { + return &WriteThenCallOtherWriteMethod{ + PackableMethod: PackableMethod{ + Name: "writeThenCallOtherWrite", + Signature: "writeThenCallOtherWrite(address,uint256,uint256,uint256)", + Selector: HexData("0x9e4d922a"), + }, + } +} + +// WriteThenReadToEventMethod returns a packable method for writeThenReadToEvent +func (mr MethodRegistry) WriteThenReadToEventMethod() *WriteThenReadToEventMethod { + return &WriteThenReadToEventMethod{ + PackableMethod: PackableMethod{ + Name: "writeThenReadToEvent", + Signature: "writeThenReadToEvent(uint256,uint256)", + Selector: HexData("0x7a533d18"), + }, + } +} + +// WriteThenRevertMethod returns a packable method for writeThenRevert +func (mr MethodRegistry) WriteThenRevertMethod() *WriteThenRevertMethod { + return &WriteThenRevertMethod{ + PackableMethod: PackableMethod{ + Name: "writeThenRevert", + Signature: "writeThenRevert(uint256,uint256)", + Selector: HexData("0xe07cc983"), + }, + } +} + +// WriteThenStaticReadMethod returns a packable method for writeThenStaticRead +func (mr MethodRegistry) WriteThenStaticReadMethod() *WriteThenStaticReadMethod { + return &WriteThenStaticReadMethod{ + PackableMethod: PackableMethod{ + Name: "writeThenStaticRead", + Signature: "writeThenStaticRead(address,uint256,uint256)", + Selector: HexData("0x9ede2c77"), + }, + } +} + +// Methods returns the method registry +func Methods() MethodRegistry { + return MethodRegistry{} +} + +// CallThenStaticWriteMethod represents the callThenStaticWrite method with type-safe decode functionality +type CallThenStaticWriteMethod struct { + PackableMethod +} + +// DelegateWriteMethod represents the delegateWrite method with type-safe decode functionality +type DelegateWriteMethod struct { + PackableMethod +} + +// InnerWritePersistsMethod represents the innerWritePersists method with type-safe decode functionality +type InnerWritePersistsMethod struct { + PackableMethod +} + +// ReadMethod represents the read method with type-safe decode functionality +type ReadMethod struct { + PackableMethod +} + +// ReadFromMethod represents the readFrom method with type-safe decode functionality +type ReadFromMethod struct { + PackableMethod +} + +// ReadToEventMethod represents the readToEvent method with type-safe decode functionality +type ReadToEventMethod struct { + PackableMethod +} + +// ReentrantReadMethod represents the reentrantRead method with type-safe decode functionality +type ReentrantReadMethod struct { + PackableMethod +} + +// RollbackAfterInnerRevertMethod represents the rollbackAfterInnerRevert method with type-safe decode functionality +type RollbackAfterInnerRevertMethod struct { + PackableMethod +} + +// RollbackIncludesInnerCallWritesMethod represents the rollbackIncludesInnerCallWrites method with type-safe decode functionality +type RollbackIncludesInnerCallWritesMethod struct { + PackableMethod +} + +// WriteMethod represents the write method with type-safe decode functionality +type WriteMethod struct { + PackableMethod +} + +// WriteCallWriteThenRevertMethod represents the writeCallWriteThenRevert method with type-safe decode functionality +type WriteCallWriteThenRevertMethod struct { + PackableMethod +} + +// WriteThenCallOtherWriteMethod represents the writeThenCallOtherWrite method with type-safe decode functionality +type WriteThenCallOtherWriteMethod struct { + PackableMethod +} + +// WriteThenReadToEventMethod represents the writeThenReadToEvent method with type-safe decode functionality +type WriteThenReadToEventMethod struct { + PackableMethod +} + +// WriteThenRevertMethod represents the writeThenRevert method with type-safe decode functionality +type WriteThenRevertMethod struct { + PackableMethod +} + +// WriteThenStaticReadMethod represents the writeThenStaticRead method with type-safe decode functionality +type WriteThenStaticReadMethod struct { + PackableMethod +} + +// LoadedEventDecoder returns a decoder for Loaded events +func (er EventRegistry) LoadedEventDecoder() *LoadedEventDecoder { + return &LoadedEventDecoder{ + PackableEvent: PackableEvent{ + Name: "Loaded", + Topic: HashFromHex("0xbe0010b84cfca7c0c197a0c450be660a13985cad1cec23df4f6936ff88354ec3"), + }, + } +} + +// Events returns the event registry +func Events() EventRegistry { + return EventRegistry{} +} + +// LoadedEventDecoder represents the Loaded event with type-safe decode functionality +type LoadedEventDecoder struct { + PackableEvent +} + +// BoomError returns a packable error for Boom +func (er ErrorRegistry) BoomError() *BoomErrorDecoder { + return &BoomErrorDecoder{ + PackableError: PackableError{ + Name: "Boom", + Signature: "Boom()", + Selector: HexData("0x7c27fae4"), + }, + } +} + +// Errors returns the error registry +func Errors() ErrorRegistry { + return ErrorRegistry{} +} + +// BoomErrorDecoder represents the Boom error with type-safe decode functionality +type BoomErrorDecoder struct { + PackableError +} + +// LoadedEvent represents the Loaded event +type LoadedEvent struct { + Key *big.Int `json:"key"` + Value *big.Int `json:"value"` +} + +// BoomError represents the Boom custom error +type BoomError struct { +} + +// CallThenStaticWriteInput represents inputs for method callThenStaticWrite +type CallThenStaticWriteInput struct { + Target Address `json:"target"` + K *big.Int `json:"k"` + V *big.Int `json:"v"` +} + +// CallThenStaticWriteOutput represents outputs for method callThenStaticWrite +type CallThenStaticWriteOutput struct { + CallOk bool `json:"callok"` + StaticOk bool `json:"staticok"` + StaticRetLen *big.Int `json:"staticretlen"` +} + +// DelegateWriteInput represents inputs for method delegateWrite +type DelegateWriteInput struct { + Impl Address `json:"impl"` + K *big.Int `json:"k"` + V *big.Int `json:"v"` +} + +// DelegateWriteOutput represents outputs for method delegateWrite +type DelegateWriteOutput struct { + OurValue *big.Int `json:"ourvalue"` + ImplValue *big.Int `json:"implvalue"` +} + +// InnerWritePersistsInput represents inputs for method innerWritePersists +type InnerWritePersistsInput struct { + K *big.Int `json:"k"` + V *big.Int `json:"v"` +} + +// ReadFromInput represents inputs for method readFrom +type ReadFromInput struct { + Origin Address `json:"origin"` + K *big.Int `json:"k"` +} + +// ReentrantReadInput represents inputs for method reentrantRead +type ReentrantReadInput struct { + Other Address `json:"other"` + K *big.Int `json:"k"` + V *big.Int `json:"v"` +} + +// RollbackAfterInnerRevertInput represents inputs for method rollbackAfterInnerRevert +type RollbackAfterInnerRevertInput struct { + K *big.Int `json:"k"` + Pre *big.Int `json:"pre"` + Inner *big.Int `json:"inner"` +} + +// RollbackIncludesInnerCallWritesInput represents inputs for method rollbackIncludesInnerCallWrites +type RollbackIncludesInnerCallWritesInput struct { + K *big.Int `json:"k"` + Pre *big.Int `json:"pre"` + A *big.Int `json:"a"` + B *big.Int `json:"b"` +} + +// WriteInput represents inputs for method write +type WriteInput struct { + K *big.Int `json:"k"` + V *big.Int `json:"v"` +} + +// WriteCallWriteThenRevertInput represents inputs for method writeCallWriteThenRevert +type WriteCallWriteThenRevertInput struct { + K *big.Int `json:"k"` + A *big.Int `json:"a"` + B *big.Int `json:"b"` +} + +// WriteThenCallOtherWriteInput represents inputs for method writeThenCallOtherWrite +type WriteThenCallOtherWriteInput struct { + Other Address `json:"other"` + K *big.Int `json:"k"` + Mine *big.Int `json:"mine"` + Theirs *big.Int `json:"theirs"` +} + +// WriteThenCallOtherWriteOutput represents outputs for method writeThenCallOtherWrite +type WriteThenCallOtherWriteOutput struct { + OurValue *big.Int `json:"ourvalue"` + TheirValue *big.Int `json:"theirvalue"` +} + +// WriteThenReadToEventInput represents inputs for method writeThenReadToEvent +type WriteThenReadToEventInput struct { + K *big.Int `json:"k"` + V *big.Int `json:"v"` +} + +// WriteThenRevertInput represents inputs for method writeThenRevert +type WriteThenRevertInput struct { + K *big.Int `json:"k"` + V *big.Int `json:"v"` +} + +// WriteThenStaticReadInput represents inputs for method writeThenStaticRead +type WriteThenStaticReadInput struct { + Target Address `json:"target"` + K *big.Int `json:"k"` + V *big.Int `json:"v"` +} + +// WriteThenStaticReadOutput represents outputs for method writeThenStaticRead +type WriteThenStaticReadOutput struct { + Ok bool `json:"ok"` + Value *big.Int `json:"value"` +} + +// CallThenStaticWriteResult represents the return values for callThenStaticWrite method +type CallThenStaticWriteResult struct { + CallOk bool `json:"callok"` + StaticOk bool `json:"staticok"` + StaticRetLen *big.Int `json:"staticretlen"` +} + +// DelegateWriteResult represents the return values for delegateWrite method +type DelegateWriteResult struct { + OurValue *big.Int `json:"ourvalue"` + ImplValue *big.Int `json:"implvalue"` +} + +// WriteThenCallOtherWriteResult represents the return values for writeThenCallOtherWrite method +type WriteThenCallOtherWriteResult struct { + OurValue *big.Int `json:"ourvalue"` + TheirValue *big.Int `json:"theirvalue"` +} + +// WriteThenStaticReadResult represents the return values for writeThenStaticRead method +type WriteThenStaticReadResult struct { + Ok bool `json:"ok"` + Value *big.Int `json:"value"` +} + +// Decode decodes return values for callThenStaticWrite method +func (m *CallThenStaticWriteMethod) Decode(data []byte) (CallThenStaticWriteResult, error) { + return m.decodeImpl(data) +} + +// MustDecode decodes return values for callThenStaticWrite method +func (m *CallThenStaticWriteMethod) MustDecode(data []byte) CallThenStaticWriteResult { + result, err := m.decodeImpl(data) + if err != nil { + panic(err) + } + return result +} + +// decodeImpl contains the actual decode logic +func (m *CallThenStaticWriteMethod) decodeImpl(data []byte) (CallThenStaticWriteResult, error) { + // Multiple return values - return as struct + var result CallThenStaticWriteResult + offset := 0 + if len(data) < offset+32 { + return result, errors.New("insufficient data for return value 0") + } + { + v, e := decodeBool(data[offset : offset+32]) + if e != nil { + return result, fmt.Errorf("decoding return value 0: %w", e) + } + result.CallOk = v + } + offset += 32 + if len(data) < offset+32 { + return result, errors.New("insufficient data for return value 1") + } + { + v, e := decodeBool(data[offset : offset+32]) + if e != nil { + return result, fmt.Errorf("decoding return value 1: %w", e) + } + result.StaticOk = v + } + offset += 32 + if len(data) < offset+32 { + return result, errors.New("insufficient data for return value 2") + } + { + v, e := decodeUint256(data[offset : offset+32]) + if e != nil { + return result, fmt.Errorf("decoding return value 2: %w", e) + } + result.StaticRetLen = v + } + offset += 32 + return result, nil +} + +// Decode decodes return values for delegateWrite method +func (m *DelegateWriteMethod) Decode(data []byte) (DelegateWriteResult, error) { + return m.decodeImpl(data) +} + +// MustDecode decodes return values for delegateWrite method +func (m *DelegateWriteMethod) MustDecode(data []byte) DelegateWriteResult { + result, err := m.decodeImpl(data) + if err != nil { + panic(err) + } + return result +} + +// decodeImpl contains the actual decode logic +func (m *DelegateWriteMethod) decodeImpl(data []byte) (DelegateWriteResult, error) { + // Multiple return values - return as struct + var result DelegateWriteResult + offset := 0 + if len(data) < offset+32 { + return result, errors.New("insufficient data for return value 0") + } + { + v, e := decodeUint256(data[offset : offset+32]) + if e != nil { + return result, fmt.Errorf("decoding return value 0: %w", e) + } + result.OurValue = v + } + offset += 32 + if len(data) < offset+32 { + return result, errors.New("insufficient data for return value 1") + } + { + v, e := decodeUint256(data[offset : offset+32]) + if e != nil { + return result, fmt.Errorf("decoding return value 1: %w", e) + } + result.ImplValue = v + } + offset += 32 + return result, nil +} + +// Decode decodes return values for innerWritePersists method +func (m *InnerWritePersistsMethod) Decode(data []byte) (*big.Int, error) { + return m.decodeImpl(data) +} + +// MustDecode decodes return values for innerWritePersists method +func (m *InnerWritePersistsMethod) MustDecode(data []byte) *big.Int { + result, err := m.decodeImpl(data) + if err != nil { + panic(err) + } + return result +} + +// decodeImpl contains the actual decode logic +func (m *InnerWritePersistsMethod) decodeImpl(data []byte) (*big.Int, error) { + var result *big.Int + offset := 0 + if len(data) < offset+32 { + return result, errors.New("insufficient data for return value") + } + { + v, e := decodeUint256(data[offset : offset+32]) + if e != nil { + return result, fmt.Errorf("decoding return value: %w", e) + } + result = v + } + offset += 32 + return result, nil +} + +// Decode decodes return values for read method +func (m *ReadMethod) Decode(data []byte) (*big.Int, error) { + return m.decodeImpl(data) +} + +// MustDecode decodes return values for read method +func (m *ReadMethod) MustDecode(data []byte) *big.Int { + result, err := m.decodeImpl(data) + if err != nil { + panic(err) + } + return result +} + +// decodeImpl contains the actual decode logic +func (m *ReadMethod) decodeImpl(data []byte) (*big.Int, error) { + var result *big.Int + offset := 0 + if len(data) < offset+32 { + return result, errors.New("insufficient data for return value") + } + { + v, e := decodeUint256(data[offset : offset+32]) + if e != nil { + return result, fmt.Errorf("decoding return value: %w", e) + } + result = v + } + offset += 32 + return result, nil +} + +// Decode decodes return values for readFrom method +func (m *ReadFromMethod) Decode(data []byte) (*big.Int, error) { + return m.decodeImpl(data) +} + +// MustDecode decodes return values for readFrom method +func (m *ReadFromMethod) MustDecode(data []byte) *big.Int { + result, err := m.decodeImpl(data) + if err != nil { + panic(err) + } + return result +} + +// decodeImpl contains the actual decode logic +func (m *ReadFromMethod) decodeImpl(data []byte) (*big.Int, error) { + var result *big.Int + offset := 0 + if len(data) < offset+32 { + return result, errors.New("insufficient data for return value") + } + { + v, e := decodeUint256(data[offset : offset+32]) + if e != nil { + return result, fmt.Errorf("decoding return value: %w", e) + } + result = v + } + offset += 32 + return result, nil +} + +// Decode decodes return values for reentrantRead method +func (m *ReentrantReadMethod) Decode(data []byte) (*big.Int, error) { + return m.decodeImpl(data) +} + +// MustDecode decodes return values for reentrantRead method +func (m *ReentrantReadMethod) MustDecode(data []byte) *big.Int { + result, err := m.decodeImpl(data) + if err != nil { + panic(err) + } + return result +} + +// decodeImpl contains the actual decode logic +func (m *ReentrantReadMethod) decodeImpl(data []byte) (*big.Int, error) { + var result *big.Int + offset := 0 + if len(data) < offset+32 { + return result, errors.New("insufficient data for return value") + } + { + v, e := decodeUint256(data[offset : offset+32]) + if e != nil { + return result, fmt.Errorf("decoding return value: %w", e) + } + result = v + } + offset += 32 + return result, nil +} + +// Decode decodes return values for rollbackAfterInnerRevert method +func (m *RollbackAfterInnerRevertMethod) Decode(data []byte) (*big.Int, error) { + return m.decodeImpl(data) +} + +// MustDecode decodes return values for rollbackAfterInnerRevert method +func (m *RollbackAfterInnerRevertMethod) MustDecode(data []byte) *big.Int { + result, err := m.decodeImpl(data) + if err != nil { + panic(err) + } + return result +} + +// decodeImpl contains the actual decode logic +func (m *RollbackAfterInnerRevertMethod) decodeImpl(data []byte) (*big.Int, error) { + var result *big.Int + offset := 0 + if len(data) < offset+32 { + return result, errors.New("insufficient data for return value") + } + { + v, e := decodeUint256(data[offset : offset+32]) + if e != nil { + return result, fmt.Errorf("decoding return value: %w", e) + } + result = v + } + offset += 32 + return result, nil +} + +// Decode decodes return values for rollbackIncludesInnerCallWrites method +func (m *RollbackIncludesInnerCallWritesMethod) Decode(data []byte) (*big.Int, error) { + return m.decodeImpl(data) +} + +// MustDecode decodes return values for rollbackIncludesInnerCallWrites method +func (m *RollbackIncludesInnerCallWritesMethod) MustDecode(data []byte) *big.Int { + result, err := m.decodeImpl(data) + if err != nil { + panic(err) + } + return result +} + +// decodeImpl contains the actual decode logic +func (m *RollbackIncludesInnerCallWritesMethod) decodeImpl(data []byte) (*big.Int, error) { + var result *big.Int + offset := 0 + if len(data) < offset+32 { + return result, errors.New("insufficient data for return value") + } + { + v, e := decodeUint256(data[offset : offset+32]) + if e != nil { + return result, fmt.Errorf("decoding return value: %w", e) + } + result = v + } + offset += 32 + return result, nil +} + +// Decode decodes return values for writeThenCallOtherWrite method +func (m *WriteThenCallOtherWriteMethod) Decode(data []byte) (WriteThenCallOtherWriteResult, error) { + return m.decodeImpl(data) +} + +// MustDecode decodes return values for writeThenCallOtherWrite method +func (m *WriteThenCallOtherWriteMethod) MustDecode(data []byte) WriteThenCallOtherWriteResult { + result, err := m.decodeImpl(data) + if err != nil { + panic(err) + } + return result +} + +// decodeImpl contains the actual decode logic +func (m *WriteThenCallOtherWriteMethod) decodeImpl(data []byte) (WriteThenCallOtherWriteResult, error) { + // Multiple return values - return as struct + var result WriteThenCallOtherWriteResult + offset := 0 + if len(data) < offset+32 { + return result, errors.New("insufficient data for return value 0") + } + { + v, e := decodeUint256(data[offset : offset+32]) + if e != nil { + return result, fmt.Errorf("decoding return value 0: %w", e) + } + result.OurValue = v + } + offset += 32 + if len(data) < offset+32 { + return result, errors.New("insufficient data for return value 1") + } + { + v, e := decodeUint256(data[offset : offset+32]) + if e != nil { + return result, fmt.Errorf("decoding return value 1: %w", e) + } + result.TheirValue = v + } + offset += 32 + return result, nil +} + +// Decode decodes return values for writeThenStaticRead method +func (m *WriteThenStaticReadMethod) Decode(data []byte) (WriteThenStaticReadResult, error) { + return m.decodeImpl(data) +} + +// MustDecode decodes return values for writeThenStaticRead method +func (m *WriteThenStaticReadMethod) MustDecode(data []byte) WriteThenStaticReadResult { + result, err := m.decodeImpl(data) + if err != nil { + panic(err) + } + return result +} + +// decodeImpl contains the actual decode logic +func (m *WriteThenStaticReadMethod) decodeImpl(data []byte) (WriteThenStaticReadResult, error) { + // Multiple return values - return as struct + var result WriteThenStaticReadResult + offset := 0 + if len(data) < offset+32 { + return result, errors.New("insufficient data for return value 0") + } + { + v, e := decodeBool(data[offset : offset+32]) + if e != nil { + return result, fmt.Errorf("decoding return value 0: %w", e) + } + result.Ok = v + } + offset += 32 + if len(data) < offset+32 { + return result, errors.New("insufficient data for return value 1") + } + { + v, e := decodeUint256(data[offset : offset+32]) + if e != nil { + return result, fmt.Errorf("decoding return value 1: %w", e) + } + result.Value = v + } + offset += 32 + return result, nil +} + +// Decode decodes log data for Loaded event +func (e *LoadedEventDecoder) Decode(data []byte) (LoadedEvent, error) { + return e.decodeImpl(data) +} + +// MustDecode decodes log data for Loaded event +func (e *LoadedEventDecoder) MustDecode(data []byte) LoadedEvent { + result, err := e.decodeImpl(data) + if err != nil { + panic(err) + } + return result +} + +// DecodeLog decodes a full log into LoadedEvent: non-indexed parameters are +// read from data, while indexed parameters are read from topics (topics[0] is the +// event signature, so indexed params start at topics[1]) in ABI order. +// +// Indexed dynamic types (string, bytes, arrays) are stored as the keccak hash of +// their value in the topic and cannot be recovered to their original value; such +// fields are left at their zero value. +func (e *LoadedEventDecoder) DecodeLog(topics [][32]byte, data []byte) (LoadedEvent, error) { + // Non-indexed parameters live in the data section. + result, err := e.decodeImpl(data) + if err != nil { + return result, err + } + // Indexed parameters live in the log topics, after the signature topic. + topicIndex := 1 + if len(topics) <= topicIndex { + return result, errors.New("missing topic for indexed parameter key") + } + { + word := topics[topicIndex][:] + v, e := decodeUint256(word) + if e != nil { + return result, fmt.Errorf("decoding indexed parameter key: %w", e) + } + result.Key = v + } + topicIndex++ + return result, nil +} + +// decodeImpl contains the actual decode logic +func (e *LoadedEventDecoder) decodeImpl(data []byte) (LoadedEvent, error) { + // Decode event parameters (only non-indexed parameters are in data) + var result LoadedEvent + offset := 0 + if len(data) < offset+32 { + return result, errors.New("insufficient data for event parameter value") + } + { + v, e := decodeUint256(data[offset : offset+32]) + if e != nil { + return result, fmt.Errorf("decoding event parameter value: %w", e) + } + result.Value = v + } + offset += 32 + return result, nil +} + +// Decode decodes error data for Boom error +func (e *BoomErrorDecoder) Decode(data []byte) (BoomError, error) { + return e.decodeImpl(data) +} + +// MustDecode decodes error data for Boom error +func (e *BoomErrorDecoder) MustDecode(data []byte) BoomError { + result, err := e.decodeImpl(data) + if err != nil { + panic(err) + } + return result +} + +// decodeImpl contains the actual decode logic +func (e *BoomErrorDecoder) decodeImpl(data []byte) (BoomError, error) { + // Skip the 4-byte selector + if len(data) < 4 { + return BoomError{}, errors.New("insufficient data for error selector") + } + var result BoomError + return result, nil +} diff --git a/tests/eip1153/frames_test.go b/tests/eip1153/frames_test.go new file mode 100644 index 0000000..dd26aec --- /dev/null +++ b/tests/eip1153/frames_test.go @@ -0,0 +1,266 @@ +package eip1153 + +// EIP-1153 semantics that only appear once more than one call frame is +// involved. transient_storage_test.go covers the single-frame opcode behaviour; +// this file covers the four rules that need a caller and a callee: +// +// (1) TSTORE raises an exception under STATICCALL; TLOAD is permitted. +// (2) A reverting frame rolls back its transient writes, including those made +// by its inner calls. +// (3) CALL/STATICCALL make the callee the owner of the transient storage; +// DELEGATECALL makes the caller the owner. +// (4) Transient storage survives across frames for the whole transaction. +// +// All of these are intra-transaction properties, so they are exercised through +// InspectClauses: one simulated clause per test, with the probe contract making +// the nested calls internally. +// +// Not covered: CALLCODE, which the specification names alongside DELEGATECALL. +// Solidity has emitted no CALLCODE since 0.5.0, so reaching it would mean +// hand-assembling bytecode for a rule DELEGATECALL already pins down. + +import ( + "math/big" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/vechain/interstellar-e2e/tests/eip1153/contracts/generated/transientprobe" +) + +// --------------------------------------------------------------------------- +// (1) STATICCALL restriction +// --------------------------------------------------------------------------- + +// TestEIP1153_StaticCall_TSTOREIsException checks the one hard prohibition in +// the specification: +// +// "If the TSTORE opcode is called within the context of a STATICCALL, it will +// result in an exception instead of performing the modification." +// +// The probe sends one identical payload to write(k, v) twice — once by CALL, +// once by STATICCALL — and reports both outcomes plus the returndata size. +// +// The CALL leg is the control, and it is what makes this test mean anything: a +// STATICCALL to an unknown selector would also come back as "failed, no +// returndata", so a bare STATICCALL failure would be indistinguishable from a +// typo. Asserting the returndata size is separately what makes this a test of +// "exception" rather than merely "did not succeed" — an exception returns +// nothing, a plain revert would carry a 4-byte error selector. +func TestEIP1153_StaticCall_TSTOREIsException(t *testing.T) { + _, probeB := deployedProbes(t) + + data := transientprobe.Methods().CallThenStaticWriteMethod().MustPack( + probeAddr(probeB), big.NewInt(7), big.NewInt(0xBEEF)) + + result := callProbe(t, probeA, data, 300_000) + require.False(t, result.Reverted, + "the outer frame must survive to report the inner failure (vmError: %s)", result.VMError) + + words := decodeWords(t, result.Data, 3) + callOk, staticOk, staticRetLen := words[0], words[1], words[2] + + require.Equal(t, uint64(1), callOk.Uint64(), + "control: the same payload sent by CALL must succeed, otherwise the STATICCALL "+ + "result below says nothing about TSTORE") + + assert.Equal(t, uint64(0), staticOk.Uint64(), + "TSTORE inside a STATICCALL must fail, but the inner call reported success") + assert.Equal(t, uint64(0), staticRetLen.Uint64(), + "TSTORE inside a STATICCALL must raise an exception, which returns no data; "+ + "%d bytes of returndata means it reverted normally instead", staticRetLen.Uint64()) +} + +// TestEIP1153_StaticCall_TLOADAllowed covers the other half of the same +// sentence: "TLOAD is allowed within the context of a STATICCALL." +// +// The probe first CALLs probeB.write(k, v) — a normal call, so probeB owns and +// keeps the value — then reads the same slot back through a STATICCALL. Reading +// a non-zero value proves both that TLOAD executed under STATICCALL and that it +// saw the right namespace. +func TestEIP1153_StaticCall_TLOADAllowed(t *testing.T) { + _, probeB := deployedProbes(t) + + const value = 0xC0FFEE + data := transientprobe.Methods().WriteThenStaticReadMethod().MustPack( + probeAddr(probeB), big.NewInt(9), big.NewInt(value)) + + result := callProbe(t, probeA, data, 300_000) + require.False(t, result.Reverted, "writeThenStaticRead must not revert (vmError: %s)", result.VMError) + + words := decodeWords(t, result.Data, 2) + ok, got := words[0], words[1] + + assert.Equal(t, uint64(1), ok.Uint64(), "TLOAD under STATICCALL must succeed") + assert.Equal(t, uint64(value), got.Uint64(), + "TLOAD under STATICCALL must return the value the preceding CALL stored") +} + +// --------------------------------------------------------------------------- +// (2) Revert rollback +// --------------------------------------------------------------------------- + +// TestEIP1153_Revert_RollsBackFrameWrites covers the first half of: +// +// "If a frame reverts, all writes to transient storage that took place between +// entry to the frame and the return are reverted..." +// +// The probe writes `pre`, self-CALLs a function that writes `inner` and then +// reverts, and swallows the failure. Caller and callee are the same address, so +// both writes hit the same transient slot: without rollback the final read +// would return `inner`. +func TestEIP1153_Revert_RollsBackFrameWrites(t *testing.T) { + deployedProbes(t) + + const ( + pre = 0x1111 + inner = 0x2222 + ) + data := transientprobe.Methods().RollbackAfterInnerRevertMethod().MustPack( + big.NewInt(1), big.NewInt(pre), big.NewInt(inner)) + + result := callProbe(t, probeA, data, 300_000) + require.False(t, result.Reverted, + "rollbackAfterInnerRevert must not revert — it swallows the inner failure (vmError: %s)", + result.VMError) + + got := decodeWords(t, result.Data, 1)[0] + assert.Equal(t, uint64(pre), got.Uint64(), + "a reverting frame's transient write must be rolled back: expected the pre-call value 0x%x, got 0x%x", + uint64(pre), got.Uint64()) +} + +// TestEIP1153_Revert_RollsBackInnerCallWrites covers the clause the previous +// test does not reach: "...including those that took place in inner calls." +// +// The reverting frame writes `a`, then makes a *successful* inner call that +// writes `b`, and only then reverts. The successful inner call's write must be +// undone too, so the surviving value is still `pre`. +func TestEIP1153_Revert_RollsBackInnerCallWrites(t *testing.T) { + deployedProbes(t) + + const ( + pre = 0x1111 + a = 0x2222 + b = 0x3333 + ) + data := transientprobe.Methods().RollbackIncludesInnerCallWritesMethod().MustPack( + big.NewInt(2), big.NewInt(pre), big.NewInt(a), big.NewInt(b)) + + result := callProbe(t, probeA, data, 400_000) + require.False(t, result.Reverted, + "rollbackIncludesInnerCallWrites must not revert (vmError: %s)", result.VMError) + + got := decodeWords(t, result.Data, 1)[0] + assert.Equal(t, uint64(pre), got.Uint64(), + "a revert must roll back writes made by the reverting frame (0x%x) and by its "+ + "successful inner calls (0x%x); expected 0x%x, got 0x%x", + uint64(a), uint64(b), uint64(pre), got.Uint64()) +} + +// --------------------------------------------------------------------------- +// (3) Ownership +// --------------------------------------------------------------------------- + +// TestEIP1153_Ownership_CallCalleeOwns checks that under CALL "the owning +// contract of the transient storage is the contract that is the target of the +// CALL or STATICCALL instruction (the callee)". +// +// probeA writes `mine` into its own slot k, then CALLs probeB.write(k, theirs). +// Both values must survive at their own address. Reading back both sides is +// what gives the test teeth: if the namespaces were shared, probeA's slot would +// come back holding `theirs`. +func TestEIP1153_Ownership_CallCalleeOwns(t *testing.T) { + _, probeB := deployedProbes(t) + + const ( + mine = 0xAAAA + theirs = 0xBBBB + ) + data := transientprobe.Methods().WriteThenCallOtherWriteMethod().MustPack( + probeAddr(probeB), big.NewInt(3), big.NewInt(mine), big.NewInt(theirs)) + + result := callProbe(t, probeA, data, 400_000) + require.False(t, result.Reverted, + "writeThenCallOtherWrite must not revert (vmError: %s)", result.VMError) + + words := decodeWords(t, result.Data, 2) + ourValue, theirValue := words[0], words[1] + + assert.Equal(t, uint64(mine), ourValue.Uint64(), + "a CALL writes into the callee's namespace, so the caller's own slot must be untouched") + assert.Equal(t, uint64(theirs), theirValue.Uint64(), + "the callee must own and keep the value its own TSTORE wrote") +} + +// TestEIP1153_Ownership_DelegateCallCallerOwns checks the inverse rule: under +// DELEGATECALL "the owning contract of the transient storage is the contract +// that issued the DELEGATECALL or CALLCODE instruction (the caller)". +// +// probeA DELEGATECALLs probeB's write(k, v). The code runs in probeA's context, +// so the value must land in probeA's namespace and probeB's must stay zero. +func TestEIP1153_Ownership_DelegateCallCallerOwns(t *testing.T) { + _, probeB := deployedProbes(t) + + const value = 0xD00D + data := transientprobe.Methods().DelegateWriteMethod().MustPack( + probeAddr(probeB), big.NewInt(4), big.NewInt(value)) + + result := callProbe(t, probeA, data, 400_000) + require.False(t, result.Reverted, "delegateWrite must not revert (vmError: %s)", result.VMError) + + words := decodeWords(t, result.Data, 2) + ourValue, implValue := words[0], words[1] + + assert.Equal(t, uint64(value), ourValue.Uint64(), + "under DELEGATECALL the caller owns the transient storage, so the write must land here") + assert.Equal(t, uint64(0), implValue.Uint64(), + "the DELEGATECALL implementation's own namespace must stay untouched, got 0x%x", + implValue.Uint64()) +} + +// --------------------------------------------------------------------------- +// (4) Survival across frames within one transaction +// --------------------------------------------------------------------------- + +// TestEIP1153_SurvivesAcrossFrames_Reentrancy exercises the pattern EIP-1153 +// exists for. probeA writes slot k, calls probeB, and probeB calls straight back +// into probeA to read k. The re-entrant frame must observe the value — that +// visibility is exactly what a transient re-entrancy guard depends on. +// +// This complements TestTransientStorage_ClearedBetweenTransactions, which pins +// the other end of the lifetime: values are gone in the *next* transaction. +func TestEIP1153_SurvivesAcrossFrames_Reentrancy(t *testing.T) { + _, probeB := deployedProbes(t) + + const value = 0xFEED + data := transientprobe.Methods().ReentrantReadMethod().MustPack( + probeAddr(probeB), big.NewInt(5), big.NewInt(value)) + + result := callProbe(t, probeA, data, 500_000) + require.False(t, result.Reverted, "reentrantRead must not revert (vmError: %s)", result.VMError) + + got := decodeWords(t, result.Data, 1)[0] + assert.Equal(t, uint64(value), got.Uint64(), + "a re-entrant frame must see the transient value written by the outer frame") +} + +// TestEIP1153_SurvivesAcrossFrames_InnerWritePersists is the mirror image of the +// rollback tests: when an inner call returns *successfully*, its transient +// writes stay visible to the caller. Without this the rollback tests would pass +// trivially on an implementation that simply discarded every inner write. +func TestEIP1153_SurvivesAcrossFrames_InnerWritePersists(t *testing.T) { + deployedProbes(t) + + const value = 0x5A5A + data := transientprobe.Methods().InnerWritePersistsMethod().MustPack( + big.NewInt(6), big.NewInt(value)) + + result := callProbe(t, probeA, data, 300_000) + require.False(t, result.Reverted, "innerWritePersists must not revert (vmError: %s)", result.VMError) + + got := decodeWords(t, result.Data, 1)[0] + assert.Equal(t, uint64(value), got.Uint64(), + "a successful inner call's transient write must remain visible to the caller") +} diff --git a/tests/eip1153/multiclause_test.go b/tests/eip1153/multiclause_test.go new file mode 100644 index 0000000..8ac20d4 --- /dev/null +++ b/tests/eip1153/multiclause_test.go @@ -0,0 +1,148 @@ +package eip1153 + +// EIP-1153 says transient storage is "discarded at the end of the transaction", +// but the specification was written for Ethereum, where a transaction is a +// single call. A VeChain transaction carries several clauses, each of which is +// its own top-level call, so "the end of the transaction" has two defensible +// readings: +// +// shared — one transient namespace spans every clause of the tx, which is +// the literal reading of the EIP; +// per-clause — each clause starts with a clean namespace, treating a clause as +// the equivalent of an Ethereum transaction. +// +// The choice is a VeChain semantic decision, not something the EIP settles. +// These tests pin down whichever behaviour INTERSTELLAR actually implements, on +// both the mined path (a real multi-clause transaction) and the simulated path +// (InspectClauses with several clauses), so a silent change to either is caught. + +import ( + "encoding/hex" + "math/big" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/vechain/thor/v2/api" + "github.com/vechain/thor/v2/thorclient" + "github.com/vechain/thor/v2/tx" + + "github.com/vechain/interstellar-e2e/tests/eip1153/contracts/generated/transientprobe" + "github.com/vechain/interstellar-e2e/tests/helper" +) + +// multiClauseKey and multiClauseValue are shared by both directions of the test. +const ( + multiClauseKey = 0x1153 + multiClauseValue = 0xABCDEF +) + +// TestEIP1153_MultiClause_MinedTransaction sends one transaction whose first +// clause writes a transient slot and reads it straight back, and whose second +// clause reads the same slot again. Both clauses report through an event: a +// Thor receipt records events, not return data, so that is the only way to +// observe what a non-final clause saw. +// +// Clause 0 is the positive control. It must report the value it just wrote, +// which rules out the failure mode where clause 1's zero says nothing about +// transient storage because the write or the event path never worked at all. +func TestEIP1153_MultiClause_MinedTransaction(t *testing.T) { + deployedProbes(t) + + client := helper.NewClient(nodeURL) + + writeAndReadData := transientprobe.Methods().WriteThenReadToEventMethod().MustPack( + big.NewInt(multiClauseKey), big.NewInt(multiClauseValue)) + readData := transientprobe.Methods().ReadToEventMethod().MustPack(big.NewInt(multiClauseKey)) + + trx := helper.BuildTx(t, client, 300_000, + tx.NewClause(&probeA).WithData(writeAndReadData.Bytes()), + tx.NewClause(&probeA).WithData(readData.Bytes()), + ) + + result, err := client.SendTransaction(trx) + require.NoError(t, err, "multi-clause tx must be accepted by the txpool") + + receipt := helper.WaitForReceipt(t, client, result.ID, 60*time.Second) + require.False(t, receipt.Reverted, "multi-clause tx must not revert") + require.Len(t, receipt.Outputs, 2, "both clauses must produce an output") + + withinClause := findLoadedEvent(t, receipt.Outputs[0].Events) + require.Equal(t, uint64(multiClauseValue), withinClause.Uint64(), + "control: a TLOAD in the same clause as its TSTORE must return the stored value; "+ + "got 0x%x, so the clause-1 result below would be meaningless", withinClause.Uint64()) + + acrossClauses := findLoadedEvent(t, receipt.Outputs[1].Events) + assert.Equal(t, uint64(0), acrossClauses.Uint64(), + "clause 1 read 0x%x from the slot clause 0 wrote — INTERSTELLAR clears transient "+ + "storage at each clause boundary, so a later clause must not observe an earlier "+ + "clause's transient writes", acrossClauses.Uint64()) +} + +// TestEIP1153_MultiClause_Simulated is the same experiment on the simulation +// path. It must agree with the mined path: a divergence between what +// InspectClauses predicts and what a mined transaction does would break every +// caller that uses simulation to estimate gas or preview a result. +func TestEIP1153_MultiClause_Simulated(t *testing.T) { + deployedProbes(t) + + client := helper.NewClient(nodeURL) + + writeData := transientprobe.Methods().WriteMethod().MustPack( + big.NewInt(multiClauseKey), big.NewInt(multiClauseValue)) + readData := transientprobe.Methods().ReadMethod().MustPack(big.NewInt(multiClauseKey)) + // Clause 2 is the positive control: it writes and reads the same slot inside + // one clause, so it must return the value even though clause 1 does not. + controlData := transientprobe.Methods().InnerWritePersistsMethod().MustPack( + big.NewInt(multiClauseKey), big.NewInt(multiClauseValue)) + + results, err := client.InspectClauses(&api.BatchCallData{ + Clauses: api.Clauses{ + {To: &probeA, Data: string(writeData)}, + {To: &probeA, Data: string(readData)}, + {To: &probeA, Data: string(controlData)}, + }, + Gas: 300_000, + }, thorclient.Revision("best")) + require.NoError(t, err) + require.Len(t, results, 3) + for i, r := range results { + require.False(t, r.Reverted, "clause %d must not revert (vmError: %s)", i, r.VMError) + } + + control := decodeWords(t, results[2].Data, 1)[0] + require.Equal(t, uint64(multiClauseValue), control.Uint64(), + "control: a write and read inside one simulated clause must return the stored value; "+ + "got 0x%x, so the clause-1 result below would be meaningless", control.Uint64()) + + got := decodeWords(t, results[1].Data, 1)[0] + assert.Equal(t, uint64(0), got.Uint64(), + "simulated clause 1 read 0x%x, but the mined path clears transient storage between "+ + "clauses; simulation must predict the same result", got.Uint64()) +} + +// findLoadedEvent returns the value carried by the probe's Loaded event. +// The key is indexed, so it travels in topics[1] and the value is the whole of +// the data field. +func findLoadedEvent(t *testing.T, events []*api.Event) *big.Int { + t.Helper() + + topic := transientprobe.Events().LoadedEventDecoder().Topic + for _, ev := range events { + if len(ev.Topics) < 2 || !strings.EqualFold(ev.Topics[0].String(), topic.String()) { + continue + } + require.Equal(t, uint64(multiClauseKey), new(big.Int).SetBytes(ev.Topics[1][:]).Uint64(), + "Loaded event must report the key that was read") + + raw, err := hex.DecodeString(strings.TrimPrefix(ev.Data, "0x")) + require.NoError(t, err, "event data must be valid hex: %q", ev.Data) + require.Len(t, raw, 32, "Loaded event must carry one 32-byte value") + return new(big.Int).SetBytes(raw) + } + + t.Fatalf("clause 1 emitted no Loaded event; got %d events", len(events)) + return nil +} diff --git a/tests/eip1153/probe_test.go b/tests/eip1153/probe_test.go new file mode 100644 index 0000000..6ed5969 --- /dev/null +++ b/tests/eip1153/probe_test.go @@ -0,0 +1,116 @@ +package eip1153 + +// Shared fixtures for the multi-frame EIP-1153 tests. +// +// The probe contract lives in contracts/TransientProbe.sol; the Go binding is +// generated from it (cd tests/eip1153/contracts && go generate). + +import ( + "encoding/hex" + "math/big" + "strings" + "sync" + "testing" + "time" + + "github.com/stretchr/testify/require" + "github.com/vechain/thor/v2/api" + "github.com/vechain/thor/v2/thor" + "github.com/vechain/thor/v2/thorclient" + "github.com/vechain/thor/v2/tx" + + "github.com/vechain/interstellar-e2e/tests/eip1153/contracts/generated/transientprobe" + "github.com/vechain/interstellar-e2e/tests/helper" +) + +// Two probe instances at two addresses own two independent transient +// namespaces. probeA is the contract under test; probeB is its counterparty — +// the CALL target, the DELEGATECALL implementation, and the re-entrancy relay. +var ( + probeOnce sync.Once + probeA thor.Address + probeB thor.Address + probeErr error +) + +// deployedProbes deploys both instances once per test binary and reuses them. +// Deployment is a real transaction, so sharing it keeps the suite from paying a +// block wait per test; the tests themselves never write persistent state, and +// transient storage is discarded between transactions, so no test can observe +// another's leftovers. +func deployedProbes(t *testing.T) (thor.Address, thor.Address) { + t.Helper() + + probeOnce.Do(func() { + client := helper.NewClient(nodeURL) + + // Both instances are deployed by one two-clause transaction: two + // creations, one block wait. + initCode := transientprobe.Bytecode.Bytes() + deployTx := helper.BuildTx(t, client, 3_000_000, + tx.NewClause(nil).WithData(initCode), + tx.NewClause(nil).WithData(initCode), + ) + + result, err := client.SendTransaction(deployTx) + if err != nil { + probeErr = err + return + } + + receipt := helper.WaitForReceipt(t, client, result.ID, 60*time.Second) + require.False(t, receipt.Reverted, "TransientProbe deployment must not revert") + require.Len(t, receipt.Outputs, 2, "both deployment clauses must produce an output") + require.NotNil(t, receipt.Outputs[0].ContractAddress) + require.NotNil(t, receipt.Outputs[1].ContractAddress) + + probeA = *receipt.Outputs[0].ContractAddress + probeB = *receipt.Outputs[1].ContractAddress + }) + + require.NoError(t, probeErr, "TransientProbe deployment must be accepted by the node") + require.NotEqual(t, probeA, probeB, "the two probes must be distinct contracts") + return probeA, probeB +} + +// callProbe simulates a single call to `to` and returns the result. It runs at +// the "best" revision rather than helper.PostForkRevision: the probes are +// deployed by a transaction mined well after block 1, so they are not in state +// at the fork block. Everything under test here is post-fork behaviour, and +// "best" is always post-fork. +func callProbe(t *testing.T, to thor.Address, data transientprobe.HexData, gas uint64) *api.CallResult { + t.Helper() + + client := helper.NewClient(nodeURL) + results, err := client.InspectClauses(&api.BatchCallData{ + Clauses: api.Clauses{{To: &to, Data: string(data)}}, + Gas: gas, + }, thorclient.Revision("best")) + require.NoError(t, err) + require.Len(t, results, 1) + return results[0] +} + +// probeAddr converts a thor.Address into the address type the generated binding +// encodes. +func probeAddr(a thor.Address) transientprobe.Address { + return transientprobe.Address(a) +} + +// decodeWords splits ABI return data into exactly n 32-byte words. Every probe +// method returns a fixed number of uint256/bool values, so a head-only decode +// is sufficient and avoids depending on decoder helpers the generator does not +// emit for return values. +func decodeWords(t *testing.T, data string, n int) []*big.Int { + t.Helper() + + raw, err := hex.DecodeString(strings.TrimPrefix(data, "0x")) + require.NoError(t, err, "return data must be valid hex: %q", data) + require.Len(t, raw, n*32, "expected %d return words, got %d bytes: %q", n, len(raw), data) + + words := make([]*big.Int, n) + for i := range words { + words[i] = new(big.Int).SetBytes(raw[i*32 : (i+1)*32]) + } + return words +} diff --git a/tests/helper/client.go b/tests/helper/client.go index 55d3a67..61a424e 100644 --- a/tests/helper/client.go +++ b/tests/helper/client.go @@ -40,9 +40,12 @@ func NewClient(nodeURL string) *thorclient.Client { return thorclient.New(nodeURL) } -// BuildTx constructs and signs a legacy transaction with the given gas limit and clause. -func BuildTx(t testing.TB, client *thorclient.Client, gas uint64, clause *tx.Clause) *tx.Transaction { +// BuildTx constructs and signs a legacy transaction with the given gas limit and +// clauses. Passing more than one clause builds a genuine multi-clause VeChain +// transaction, which the single-clause callers are unaffected by. +func BuildTx(t testing.TB, client *thorclient.Client, gas uint64, clauses ...*tx.Clause) *tx.Transaction { t.Helper() + require.NotEmpty(t, clauses, "BuildTx requires at least one clause") chainTag, err := client.ChainTag() require.NoError(t, err) @@ -50,16 +53,18 @@ func BuildTx(t testing.TB, client *thorclient.Client, gas uint64, clause *tx.Cla best, err := client.Block("best") require.NoError(t, err) - trx := tx.NewBuilder(tx.TypeLegacy). + builder := tx.NewBuilder(tx.TypeLegacy). ChainTag(chainTag). - Clause(clause). Gas(gas). BlockRef(tx.NewBlockRefFromID(best.ID)). Expiration(100). - Nonce(uint64(time.Now().UnixNano())). - Build() + Nonce(uint64(time.Now().UnixNano())) - signed, err := tx.Sign(trx, TestSenderKey) + for _, clause := range clauses { + builder = builder.Clause(clause) + } + + signed, err := tx.Sign(builder.Build(), TestSenderKey) require.NoError(t, err) return signed }