diff --git a/tests/eip6780/contracts/Destructible.sol b/tests/eip6780/contracts/Destructible.sol new file mode 100644 index 0000000..1b90c0a --- /dev/null +++ b/tests/eip6780/contracts/Destructible.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.24; + +/// @title Destructible +/// @notice A minimal contract that exposes SELFDESTRUCT to any recipient. +/// +/// Post EIP-6780 (Cancun / INTERSTELLAR fork): +/// - If called on a **pre-existing** contract: only the balance is +/// transferred; code and storage are preserved. +/// - If called on a contract **created in the same transaction**: the +/// contract is fully deleted (code + storage removed). +contract Destructible { + /// @notice Allow plain VET transfers into the contract so it can hold a balance. + receive() external payable {} + + /// @notice Transfer this contract's balance to `recipient` via SELFDESTRUCT. + /// @param recipient The address that receives the balance. + function destroy(address payable recipient) external { + selfdestruct(recipient); + } +} diff --git a/tests/eip6780/contracts/Factory.sol b/tests/eip6780/contracts/Factory.sol new file mode 100644 index 0000000..78291ba --- /dev/null +++ b/tests/eip6780/contracts/Factory.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.24; + +import "./Destructible.sol"; + +/// @title Factory +/// @notice Deploys a Destructible child and immediately calls SELFDESTRUCT on it +/// within the same transaction. +/// +/// This exercises the EIP-6780 **same-transaction** deletion path: +/// because the child was created in the same tx as the SELFDESTRUCT call, +/// it is fully deleted (code + storage removed) post-INTERSTELLAR fork. +contract Factory { + /// @notice Deploy a child Destructible and self-destruct it in the same tx. + /// @return child The address of the newly created (and immediately destroyed) child. + function deployAndDestroy() external returns (address child) { + Destructible d = new Destructible(); + child = address(d); + d.destroy(payable(msg.sender)); + } +} diff --git a/tests/eip6780/contracts/gen.go b/tests/eip6780/contracts/gen.go new file mode 100644 index 0000000..c35abcd --- /dev/null +++ b/tests/eip6780/contracts/gen.go @@ -0,0 +1,10 @@ +// 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 + +// //go:generate docker run --rm -v ./:/sources ghcr.io/argotorg/solc:0.8.28 --evm-version cancun --optimize --optimize-runs 200 -o /sources/compiled --overwrite --abi --bin --bin-runtime /sources/Destructible.sol /sources/Factory.sol + +//go:generate sh -c "docker run --rm -v $(pwd):/src ghcr.io/argotorg/solc:stable --combined-json abi,bin,bin-runtime,hashes /src/Destructible.sol | docker run --rm -i -v $(pwd):/src otherview/solgen:latest --out /src/generated" +//go:generate sh -c "docker run --rm -v $(pwd):/src ghcr.io/argotorg/solc:stable --combined-json abi,bin,bin-runtime,hashes /src/Factory.sol | docker run --rm -i -v $(pwd):/src otherview/solgen:latest --out /src/generated" diff --git a/tests/eip6780/contracts/generated/destructible/destructible.go b/tests/eip6780/contracts/generated/destructible/destructible.go new file mode 100644 index 0000000..0344952 --- /dev/null +++ b/tests/eip6780/contracts/generated/destructible/destructible.go @@ -0,0 +1,722 @@ +// Code generated by github.com/otherview/solgen. DO NOT EDIT. +// SPDX-License-Identifier: MIT +// Contract: Destructible (solc 0.8.34+commit.80d5c536.Linux.clang) + +package destructible + +import ( + "encoding/hex" + "errors" + "fmt" + "math/big" + "strings" +) + +// Contract metadata +var _abiJSON = "[{\"inputs\":[{\"internalType\":\"address payable\",\"name\":\"recipient\",\"type\":\"address\"}],\"name\":\"destroy\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"stateMutability\":\"payable\",\"type\":\"receive\"}]" + +// ABI returns the contract ABI as a JSON string +func ABI() string { + return _abiJSON +} + +// Bytecode contains the contract creation bytecode +var Bytecode = HexData("0x6080604052348015600e575f5ffd5b506101168061001c5f395ff3fe608060405260043610601d575f3560e01c8062f55d9d146027576023565b36602357005b5f5ffd5b3480156031575f5ffd5b50604860048036038101906044919060ba565b604a565b005b8073ffffffffffffffffffffffffffffffffffffffff16ff5b5f5ffd5b5f73ffffffffffffffffffffffffffffffffffffffff82169050919050565b5f608e826067565b9050919050565b609c816086565b811460a5575f5ffd5b50565b5f8135905060b4816095565b92915050565b5f6020828403121560cc5760cb6063565b5b5f60d78482850160a8565b9150509291505056fea264697066735822122040f2cb45a2a4ee6961708994d71092033805fd18e1e368b6c0ee458cbccf9b5a64736f6c63430008220033") + +// DeployedBytecode contains the contract runtime bytecode +var DeployedBytecode = HexData("0x608060405260043610601d575f3560e01c8062f55d9d146027576023565b36602357005b5f5ffd5b3480156031575f5ffd5b50604860048036038101906044919060ba565b604a565b005b8073ffffffffffffffffffffffffffffffffffffffff16ff5b5f5ffd5b5f73ffffffffffffffffffffffffffffffffffffffff82169050919050565b5f608e826067565b9050919050565b609c816086565b811460a5575f5ffd5b50565b5f8135905060b4816095565b92915050565b5f6020828403121560cc5760cb6063565b5b5f60d78482850160a8565b9150509291505056fea264697066735822122040f2cb45a2a4ee6961708994d71092033805fd18e1e368b6c0ee458cbccf9b5a64736f6c63430008220033") + +// 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)) +} + +// 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) +} + +// 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 GetDestroyMethod() MethodInfo { + return MethodInfo{ + Name: "destroy", + Signature: "destroy(address)", + Selector: HexData("0x00f55d9d"), + } +} + +// Event information + +// Error information + +// 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 { + var data []byte + var dynamic bool + var err error + switch v := arg.(type) { + case *big.Int: + if v.Sign() < 0 { + data, err = encodeInt256(v) + } else { + data, err = encodeUint256(v) + } + if err != nil { + return "", fmt.Errorf("encoding big.Int arg %d: %w", i, err) + } + case uint8: + data, err = encodeUint256(uint64(v)) + if err != nil { + return "", fmt.Errorf("encoding uint8 arg %d: %w", i, err) + } + case uint16: + data, err = encodeUint256(uint64(v)) + if err != nil { + return "", fmt.Errorf("encoding uint16 arg %d: %w", i, err) + } + case uint32: + data, err = encodeUint256(uint64(v)) + if err != nil { + return "", fmt.Errorf("encoding uint32 arg %d: %w", i, err) + } + case uint64: + data, err = encodeUint256(v) + if err != nil { + return "", fmt.Errorf("encoding uint64 arg %d: %w", i, err) + } + case int8: + data, err = encodeInt256(big.NewInt(int64(v))) + if err != nil { + return "", fmt.Errorf("encoding int8 arg %d: %w", i, err) + } + case int16: + data, err = encodeInt256(big.NewInt(int64(v))) + if err != nil { + return "", fmt.Errorf("encoding int16 arg %d: %w", i, err) + } + case int32: + data, err = encodeInt256(big.NewInt(int64(v))) + if err != nil { + return "", fmt.Errorf("encoding int32 arg %d: %w", i, err) + } + case int64: + data, err = encodeInt256(big.NewInt(v)) + if err != nil { + return "", fmt.Errorf("encoding int64 arg %d: %w", i, err) + } + case Address: + data, err = encodeAddress(v) + if err != nil { + return "", fmt.Errorf("encoding address arg %d: %w", i, err) + } + case bool: + data, err = encodeBool(v) + if err != nil { + return "", fmt.Errorf("encoding bool arg %d: %w", i, err) + } + case string: + data, err = encodeString(v) + if err != nil { + return "", fmt.Errorf("encoding string arg %d: %w", i, err) + } + dynamic = true + case []byte: + data, err = encodeBytes(v) + if err != nil { + return "", fmt.Errorf("encoding bytes arg %d: %w", i, err) + } + dynamic = true + default: + return "", fmt.Errorf("unsupported argument type: %T", arg) + } + 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 +} + +// DestroyMethod returns a packable method for destroy +func (mr MethodRegistry) DestroyMethod() *DestroyMethod { + return &DestroyMethod{ + PackableMethod: PackableMethod{ + Name: "destroy", + Signature: "destroy(address)", + Selector: HexData("0x00f55d9d"), + }, + } +} + +// Methods returns the method registry +func Methods() MethodRegistry { + return MethodRegistry{} +} + +// DestroyMethod represents the destroy method with type-safe decode functionality +type DestroyMethod struct { + PackableMethod +} + +// Events returns the event registry +func Events() EventRegistry { + return EventRegistry{} +} + +// Errors returns the error registry +func Errors() ErrorRegistry { + return ErrorRegistry{} +} diff --git a/tests/eip6780/contracts/generated/factory/factory.go b/tests/eip6780/contracts/generated/factory/factory.go new file mode 100644 index 0000000..d449106 --- /dev/null +++ b/tests/eip6780/contracts/generated/factory/factory.go @@ -0,0 +1,746 @@ +// Code generated by github.com/otherview/solgen. DO NOT EDIT. +// SPDX-License-Identifier: MIT +// Contract: Factory (solc 0.8.34+commit.80d5c536.Linux.clang) + +package factory + +import ( + "encoding/hex" + "errors" + "fmt" + "math/big" + "strings" +) + +// Contract metadata +var _abiJSON = "[{\"inputs\":[],\"name\":\"deployAndDestroy\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"child\",\"type\":\"address\"}],\"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("0x6080604052348015600e575f5ffd5b506102e78061001c5f395ff3fe608060405234801561000f575f5ffd5b5060043610610029575f3560e01c8063bc44b3ef1461002d575b5f5ffd5b61003561004b565b604051610042919061012d565b60405180910390f35b5f5f604051610059906100e1565b604051809103905ff080158015610072573d5f5f3e3d5ffd5b5090508091508073ffffffffffffffffffffffffffffffffffffffff1662f55d9d336040518263ffffffff1660e01b81526004016100b09190610166565b5f604051808303815f87803b1580156100c7575f5ffd5b505af11580156100d9573d5f5f3e3d5ffd5b505050505090565b6101328061018083390190565b5f73ffffffffffffffffffffffffffffffffffffffff82169050919050565b5f610117826100ee565b9050919050565b6101278161010d565b82525050565b5f6020820190506101405f83018461011e565b92915050565b5f610150826100ee565b9050919050565b61016081610146565b82525050565b5f6020820190506101795f830184610157565b9291505056fe6080604052348015600e575f5ffd5b506101168061001c5f395ff3fe608060405260043610601d575f3560e01c8062f55d9d146027576023565b36602357005b5f5ffd5b3480156031575f5ffd5b50604860048036038101906044919060ba565b604a565b005b8073ffffffffffffffffffffffffffffffffffffffff16ff5b5f5ffd5b5f73ffffffffffffffffffffffffffffffffffffffff82169050919050565b5f608e826067565b9050919050565b609c816086565b811460a5575f5ffd5b50565b5f8135905060b4816095565b92915050565b5f6020828403121560cc5760cb6063565b5b5f60d78482850160a8565b9150509291505056fea264697066735822122040f2cb45a2a4ee6961708994d71092033805fd18e1e368b6c0ee458cbccf9b5a64736f6c63430008220033a26469706673582212201f42df89360aa7d15ded83f8978fb79acc182b6a683ed67ecc01b887d68be3dd64736f6c63430008220033") + +// DeployedBytecode contains the contract runtime bytecode +var DeployedBytecode = HexData("0x608060405234801561000f575f5ffd5b5060043610610029575f3560e01c8063bc44b3ef1461002d575b5f5ffd5b61003561004b565b604051610042919061012d565b60405180910390f35b5f5f604051610059906100e1565b604051809103905ff080158015610072573d5f5f3e3d5ffd5b5090508091508073ffffffffffffffffffffffffffffffffffffffff1662f55d9d336040518263ffffffff1660e01b81526004016100b09190610166565b5f604051808303815f87803b1580156100c7575f5ffd5b505af11580156100d9573d5f5f3e3d5ffd5b505050505090565b6101328061018083390190565b5f73ffffffffffffffffffffffffffffffffffffffff82169050919050565b5f610117826100ee565b9050919050565b6101278161010d565b82525050565b5f6020820190506101405f83018461011e565b92915050565b5f610150826100ee565b9050919050565b61016081610146565b82525050565b5f6020820190506101795f830184610157565b9291505056fe6080604052348015600e575f5ffd5b506101168061001c5f395ff3fe608060405260043610601d575f3560e01c8062f55d9d146027576023565b36602357005b5f5ffd5b3480156031575f5ffd5b50604860048036038101906044919060ba565b604a565b005b8073ffffffffffffffffffffffffffffffffffffffff16ff5b5f5ffd5b5f73ffffffffffffffffffffffffffffffffffffffff82169050919050565b5f608e826067565b9050919050565b609c816086565b811460a5575f5ffd5b50565b5f8135905060b4816095565b92915050565b5f6020828403121560cc5760cb6063565b5b5f60d78482850160a8565b9150509291505056fea264697066735822122040f2cb45a2a4ee6961708994d71092033805fd18e1e368b6c0ee458cbccf9b5a64736f6c63430008220033a26469706673582212201f42df89360aa7d15ded83f8978fb79acc182b6a683ed67ecc01b887d68be3dd64736f6c63430008220033") + +// 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)) +} + +// 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) +} + +// 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 GetDeployAndDestroyMethod() MethodInfo { + return MethodInfo{ + Name: "deployAndDestroy", + Signature: "deployAndDestroy()", + Selector: HexData("0xbc44b3ef"), + } +} + +// Event information + +// Error information + +// 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 { + var data []byte + var dynamic bool + var err error + switch v := arg.(type) { + case *big.Int: + if v.Sign() < 0 { + data, err = encodeInt256(v) + } else { + data, err = encodeUint256(v) + } + if err != nil { + return "", fmt.Errorf("encoding big.Int arg %d: %w", i, err) + } + case uint8: + data, err = encodeUint256(uint64(v)) + if err != nil { + return "", fmt.Errorf("encoding uint8 arg %d: %w", i, err) + } + case uint16: + data, err = encodeUint256(uint64(v)) + if err != nil { + return "", fmt.Errorf("encoding uint16 arg %d: %w", i, err) + } + case uint32: + data, err = encodeUint256(uint64(v)) + if err != nil { + return "", fmt.Errorf("encoding uint32 arg %d: %w", i, err) + } + case uint64: + data, err = encodeUint256(v) + if err != nil { + return "", fmt.Errorf("encoding uint64 arg %d: %w", i, err) + } + case int8: + data, err = encodeInt256(big.NewInt(int64(v))) + if err != nil { + return "", fmt.Errorf("encoding int8 arg %d: %w", i, err) + } + case int16: + data, err = encodeInt256(big.NewInt(int64(v))) + if err != nil { + return "", fmt.Errorf("encoding int16 arg %d: %w", i, err) + } + case int32: + data, err = encodeInt256(big.NewInt(int64(v))) + if err != nil { + return "", fmt.Errorf("encoding int32 arg %d: %w", i, err) + } + case int64: + data, err = encodeInt256(big.NewInt(v)) + if err != nil { + return "", fmt.Errorf("encoding int64 arg %d: %w", i, err) + } + case Address: + data, err = encodeAddress(v) + if err != nil { + return "", fmt.Errorf("encoding address arg %d: %w", i, err) + } + case bool: + data, err = encodeBool(v) + if err != nil { + return "", fmt.Errorf("encoding bool arg %d: %w", i, err) + } + case string: + data, err = encodeString(v) + if err != nil { + return "", fmt.Errorf("encoding string arg %d: %w", i, err) + } + dynamic = true + case []byte: + data, err = encodeBytes(v) + if err != nil { + return "", fmt.Errorf("encoding bytes arg %d: %w", i, err) + } + dynamic = true + default: + return "", fmt.Errorf("unsupported argument type: %T", arg) + } + 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 +} + +// DeployAndDestroyMethod returns a packable method for deployAndDestroy +func (mr MethodRegistry) DeployAndDestroyMethod() *DeployAndDestroyMethod { + return &DeployAndDestroyMethod{ + PackableMethod: PackableMethod{ + Name: "deployAndDestroy", + Signature: "deployAndDestroy()", + Selector: HexData("0xbc44b3ef"), + }, + } +} + +// Methods returns the method registry +func Methods() MethodRegistry { + return MethodRegistry{} +} + +// DeployAndDestroyMethod represents the deployAndDestroy method with type-safe decode functionality +type DeployAndDestroyMethod struct { + PackableMethod +} + +// Events returns the event registry +func Events() EventRegistry { + return EventRegistry{} +} + +// Errors returns the error registry +func Errors() ErrorRegistry { + return ErrorRegistry{} +} + +// Decode decodes return values for deployAndDestroy method +func (m *DeployAndDestroyMethod) Decode(data []byte) (Address, error) { + return m.decodeImpl(data) +} + +// MustDecode decodes return values for deployAndDestroy method +func (m *DeployAndDestroyMethod) MustDecode(data []byte) Address { + result, err := m.decodeImpl(data) + if err != nil { + panic(err) + } + return result +} + +// decodeImpl contains the actual decode logic +func (m *DeployAndDestroyMethod) decodeImpl(data []byte) (Address, error) { + // Single return value - use unified decoding approach + offset := 0 + if len(data) < offset+32 { + return Address{}, errors.New("insufficient data for return value") + } + return decodeAddress(data[offset : offset+32]) +} diff --git a/tests/eip6780/eip6780_test.go b/tests/eip6780/eip6780_test.go new file mode 100644 index 0000000..075cd86 --- /dev/null +++ b/tests/eip6780/eip6780_test.go @@ -0,0 +1,233 @@ +// 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 eip6780 tests EIP-6780: SELFDESTRUCT only in same transaction. +// +// EIP-6780 (Cancun / VeChain INTERSTELLAR fork) restricts SELFDESTRUCT: +// - Pre-existing contract: only the balance is transferred; code and storage persist. +// - Contract created in the same transaction: full deletion (code + storage removed). +// +// Contracts under test (see contracts/): +// - Destructible.sol — exposes a destroy(recipient) function that calls SELFDESTRUCT. +// - Factory.sol — deploys a Destructible child and calls destroy on it in one tx, +// exercising the same-transaction deletion path. +// +// To recompile contracts: +// +// cd contracts && go generate +package eip6780 + +import ( + "math/big" + "testing" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/assert" + "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/eip6780/contracts/generated/destructible" + "github.com/vechain/interstellar-e2e/tests/eip6780/contracts/generated/factory" + "github.com/vechain/interstellar-e2e/tests/helper" +) + +// --------------------------------------------------------------------------- +// Compiled contract bytecode (generated by contracts/gen.go) +// Recompile with: cd tests/eip6780/contracts && go generate +// --------------------------------------------------------------------------- + +// --------------------------------------------------------------------------- +// Test 1: Pre-fork — SELFDESTRUCT opcode is valid (does not revert) +// --------------------------------------------------------------------------- + +// TestEIP6780_PreFork_SelfDestructOpcodeValid verifies that the SELFDESTRUCT +// opcode (0xff) was already valid before the INTERSTELLAR fork. Deploying +// Destructible and calling destroy() at pre-fork revision must not revert. +// This establishes the baseline: the opcode exists; the fork only changes its semantics. +func TestEIP6780_PreFork_SelfDestructOpcodeValid(t *testing.T) { + client := helper.NewClient(nodeURL) + + callData := &api.BatchCallData{ + Clauses: api.Clauses{ + // Clause 0: deploy Destructible + {Data: destructible.Bytecode.Hex()}, + }, + Gas: 300_000, + } + + results, err := client.InspectClauses(callData, thorclient.Revision(helper.PreForkRevision)) + require.NoError(t, err) + require.Len(t, results, 1) + assert.False(t, results[0].Reverted, + "pre-fork: Destructible deployment must not revert (vmError: %s)", results[0].VMError) +} + +// --------------------------------------------------------------------------- +// Test 2: Post-fork — SELFDESTRUCT on a pre-existing contract does NOT delete it +// --------------------------------------------------------------------------- + +// TestEIP6780_PostFork_PreExistingContractNotDeleted deploys a Destructible +// contract, waits for it to be mined (making it a pre-existing contract), then +// calls destroy() in a separate transaction. +// +// Per EIP-6780: only the balance is transferred; the contract code and storage +// must survive because SELFDESTRUCT was not called in the same tx as creation. +func TestEIP6780_PostFork_PreExistingContractNotDeleted(t *testing.T) { + client := helper.NewClient(nodeURL) + + // Step 1: Deploy Destructible (Tx 1 — establishes it as a pre-existing contract). + deployTx := helper.BuildTx(t, client, 300_000, tx.NewClause(nil).WithData(destructible.Bytecode.Bytes())) + deployResult, err := client.SendTransaction(deployTx) + require.NoError(t, err, "deploy tx must be accepted by the node") + + deployReceipt := helper.WaitForReceipt(t, client, deployResult.ID, 30*time.Second) + require.False(t, deployReceipt.Reverted, "Destructible deploy must not revert") + require.Len(t, deployReceipt.Outputs, 1) + require.NotNil(t, deployReceipt.Outputs[0].ContractAddress, "deploy must produce a contract address") + + contractAddr := *deployReceipt.Outputs[0].ContractAddress + + // Confirm the contract has code immediately after deployment. + // We use the default "best" revision here — not PostForkRevision (block 1) — + // because the deploy tx may be mined in a later block; block 1 wouldn't have + // the contract in state yet. + acctAfterDeploy, err := client.Account(&contractAddr) + require.NoError(t, err) + require.True(t, acctAfterDeploy.HasCode, "Destructible must have code after deployment") + + // Step 2: Call destroy(address(0)) on the pre-existing contract (Tx 2). + callTx := helper.BuildTx(t, client, 100_000, + tx.NewClause(&contractAddr).WithData(destructible.Methods().DestroyMethod().MustPack(common.Big0).Bytes()), + ) + callResult, err := client.SendTransaction(callTx) + require.NoError(t, err, "destroy() call tx must be accepted") + + callReceipt := helper.WaitForReceipt(t, client, callResult.ID, 30*time.Second) + require.False(t, callReceipt.Reverted, + "destroy() must not revert — EIP-6780 still executes SELFDESTRUCT, only deletion is restricted") + + // Step 3: Contract must still have code (EIP-6780 protection for pre-existing contracts). + acctAfterDestroy, err := client.Account(&contractAddr) + require.NoError(t, err) + assert.True(t, acctAfterDestroy.HasCode, + "post-fork: pre-existing contract must retain its code after SELFDESTRUCT (EIP-6780)") +} + +// --------------------------------------------------------------------------- +// Test 3: Post-fork — contract created and self-destructed in the same tx IS deleted +// --------------------------------------------------------------------------- + +// TestEIP6780_PostFork_SameTxCreationIsDeleted deploys the Factory contract, +// then calls deployAndDestroy() which — in a single transaction: +// 1. Creates a new Destructible child via CREATE. +// 2. Calls destroy(msg.sender) on the child. +// +// Since the child was created in the same transaction as the SELFDESTRUCT call, +// EIP-6780 allows full deletion. After the tx, the child must have no code. +func TestEIP6780_PostFork_SameTxCreationIsDeleted(t *testing.T) { + client := helper.NewClient(nodeURL) + + // Step 1: Deploy the Factory (Tx 1). + deployTx := helper.BuildTx(t, client, 500_000, tx.NewClause(nil).WithData(factory.Bytecode.Bytes())) + deployResult, err := client.SendTransaction(deployTx) + require.NoError(t, err, "Factory deploy tx must be accepted") + + deployReceipt := helper.WaitForReceipt(t, client, deployResult.ID, 30*time.Second) + require.False(t, deployReceipt.Reverted, "Factory deploy must not revert") + require.Len(t, deployReceipt.Outputs, 1) + require.NotNil(t, deployReceipt.Outputs[0].ContractAddress) + + factoryAddr := *deployReceipt.Outputs[0].ContractAddress + + // Step 2: Call deployAndDestroy() — child is created AND destroyed in one tx (Tx 2). + callTx := helper.BuildTx(t, client, 500_000, + tx.NewClause(&factoryAddr).WithData(factory.Methods().DeployAndDestroyMethod().MustPack().Bytes()), + ) + callResult, err := client.SendTransaction(callTx) + require.NoError(t, err, "deployAndDestroy() call tx must be accepted") + + callReceipt := helper.WaitForReceipt(t, client, callResult.ID, 30*time.Second) + require.False(t, callReceipt.Reverted, "deployAndDestroy() must not revert") + + // The child contract address is deterministic: it was the first contract + // created inside clause 0 of callTx (creation counter = 1; the factory + // call itself is not a creation, the child CREATE inside it is counter 1). + childAddr := thor.CreateContractAddress(*callResult.ID, 0, 1) + + // Step 3: Child must have no code — same-tx creation allows full EIP-6780 deletion. + childAcct, err := client.Account(&childAddr) + require.NoError(t, err) + assert.False(t, childAcct.HasCode, + "post-fork: child created and self-destructed in the same tx must have no code (EIP-6780 same-tx exception)") +} + +// --------------------------------------------------------------------------- +// Test 4: Post-fork — SELFDESTRUCT to self on a pre-existing contract is a no-op +// --------------------------------------------------------------------------- + +// TestEIP6780_PostFork_SelfDestructToSelfIsNoop deploys a Destructible contract, +// funds it with 1 VET, then calls destroy(contractAddress) — passing the contract's +// own address as the recipient. +// +// Per EIP-6780, on a pre-existing contract: +// - The contract must not be deleted (code persists). +// - The balance must not change (self-transfer is a no-op). +func TestEIP6780_PostFork_SelfDestructToSelfIsNoop(t *testing.T) { + client := helper.NewClient(nodeURL) + + // Step 1: Deploy Destructible (Tx 1). + deployTx := helper.BuildTx(t, client, 300_000, tx.NewClause(nil).WithData(destructible.Bytecode.Bytes())) + deployResult, err := client.SendTransaction(deployTx) + require.NoError(t, err) + + deployReceipt := helper.WaitForReceipt(t, client, deployResult.ID, 30*time.Second) + require.False(t, deployReceipt.Reverted, "Destructible deploy must not revert") + require.Len(t, deployReceipt.Outputs, 1) + require.NotNil(t, deployReceipt.Outputs[0].ContractAddress) + + contractAddr := *deployReceipt.Outputs[0].ContractAddress + + // Step 2: Fund the contract with 1 VET so we can assert balance stability (Tx 2). + // Use 50_000 gas — 21_000 (the base clause cost) is not enough because calling + // receive() has additional EVM execution cost (~21055 used in practice). + fundTx := helper.BuildTx(t, client, 50_000, + tx.NewClause(&contractAddr).WithValue(big.NewInt(1e18)), + ) + fundResult, err := client.SendTransaction(fundTx) + require.NoError(t, err) + fundReceipt := helper.WaitForReceipt(t, client, fundResult.ID, 30*time.Second) + require.False(t, fundReceipt.Reverted, "funding transfer must not revert") + + balanceBefore, err := client.Account(&contractAddr) + require.NoError(t, err) + + // Step 3: Call destroy(contractAddr) — self as recipient (Tx 3). + // ABI encode destroy(address) with the contract's own address as argument. + addrPadded := make([]byte, 32) + copy(addrPadded[12:], contractAddr.Bytes()) + selfDestroyCalldata := append([]byte{0x00, 0xf5, 0x5d, 0x9d}, addrPadded...) + + callTx := helper.BuildTx(t, client, 100_000, + tx.NewClause(&contractAddr).WithData(selfDestroyCalldata), + ) + callResult, err := client.SendTransaction(callTx) + require.NoError(t, err) + + callReceipt := helper.WaitForReceipt(t, client, callResult.ID, 30*time.Second) + require.False(t, callReceipt.Reverted, + "destroy(self) must not revert — EIP-6780 executes SELFDESTRUCT but restricts deletion") + + // Step 4: Assert the contract still has code and balance is unchanged. + acctAfter, err := client.Account(&contractAddr) + require.NoError(t, err) + + assert.True(t, acctAfter.HasCode, + "post-fork: pre-existing contract calling SELFDESTRUCT to self must retain its code (EIP-6780 no-op)") + assert.Equal(t, balanceBefore.Balance, acctAfter.Balance, + "post-fork: SELFDESTRUCT to self must not change the contract balance (EIP-6780 no-op)") +} diff --git a/tests/eip6780/main_test.go b/tests/eip6780/main_test.go new file mode 100644 index 0000000..c8dac9a --- /dev/null +++ b/tests/eip6780/main_test.go @@ -0,0 +1,14 @@ +package eip6780 + +import ( + "os" + "testing" + + "github.com/vechain/interstellar-e2e/tests/helper" +) + +var nodeURL string + +func TestMain(m *testing.M) { + os.Exit(helper.RunTestMain(m, &nodeURL, nil)) +}