Skip to content

Commit 7ef7c93

Browse files
committed
more test cases
1 parent 69fd0a8 commit 7ef7c93

File tree

15 files changed

+708
-438
lines changed

15 files changed

+708
-438
lines changed

devbox.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
66
77
"git@latest",
8-
"go@latest",
8+
"go@1.24.0",
99
"goreleaser@latest",
1010
"postgresql@15",
1111
"python@latest",

devbox.lock

Lines changed: 355 additions & 379 deletions
Large diffs are not rendered by default.

framework/.changeset/v0.8.0.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
- Solidity storage layout helper for anvil_setStorageAt + tests + guide

framework/evm_storage/README.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,24 @@
22

33
This code is used in e2e tests where we need to modify production contracts with `Anvil`'s `anvil_setStorageAt`.
44

5-
See a simple example where we override an array [struct](layout_api_test.go)
5+
See a simple example where we override different types [struct](layout_api_test.go)
66

77
Run it with
88
```
99
./setup.sh
1010
go test -v -run TestLayoutAPI
1111
./teardown.sh
1212
```
13-
Figure out proper encoding for your `encodeFunc` and use in your e2e tests.
13+
This test is more like a playground for you to figure out proper encoding for `encodeFunc` and use in your e2e tests.
1414

15-
See more [tests](layout_test.go) as examples.
15+
See more package [tests](layout_test.go) as examples.
1616

1717
Layout in `testdata/layout.json` can be found in `out` after `forge build` for any contract.
1818

19+
To double-check the layout you can also use `forge inspect <ContractName> storageLayout` in your `forge` directory.
20+
21+
To add more types for tests use `forge build && forge inspect Counter storageLayout --json > ../testdata/layout.json`
22+
1923
## Useful Debug Commands
2024

2125
```

framework/evm_storage/forge/lib/forge-std/scripts/vm.py

100755100644
File mode changed.

framework/evm_storage/forge/src/Counter.sol

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,12 @@
22
pragma solidity ^0.8.13;
33

44
contract Counter {
5-
uint256 public number;
5+
address private _owner;
6+
uint256 public number_uint256;
7+
int256 public number_int256;
8+
uint8 public number_uint8;
9+
bool public boolean;
10+
bytes32 public some_bytes;
611
uint256[] public values;
712
mapping(address => uint256) public scores;
813

@@ -12,11 +17,14 @@ contract Counter {
1217
uint8 index;
1318
uint8 group;
1419
}
20+
1521
mapping(address => Signer) s_signers;
1622
Signer[] a_signers;
1723

1824
constructor() public {
19-
number = 1;
25+
number_uint256 = 1;
26+
number_int256 = 1;
27+
number_uint8 = 1;
2028
values = [1, 2, 3];
2129
scores[address(0x5FbDB2315678afecb367f032d93F642f64180aa3)] = 1;
2230

@@ -33,12 +41,17 @@ contract Counter {
3341
}
3442
}
3543

44+
// this function is needed to check private field mutation
45+
function getOwner() external view returns (address) {
46+
return _owner;
47+
}
48+
3649
function setNumber(uint256 newNumber) public {
37-
number = newNumber;
50+
number_uint256 = newNumber;
3851
}
3952

4053
function increment() public {
41-
number++;
54+
number_uint256++;
4255
}
4356

4457
function pushValue(uint256 value) public {

framework/evm_storage/forge/test/Counter.t.sol

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,11 @@ contract CounterTest is Test {
1414

1515
function test_Increment() public {
1616
counter.increment();
17-
assertEq(counter.number(), 1);
17+
assertEq(counter.number_uint256(), 1);
1818
}
1919

2020
function testFuzz_SetNumber(uint256 x) public {
2121
counter.setNumber(x);
22-
assertEq(counter.number(), x);
22+
assertEq(counter.number_uint256(), x);
2323
}
2424
}

framework/evm_storage/layout.go

Lines changed: 107 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,25 @@ package evm_storage
33
import (
44
"encoding/hex"
55
"encoding/json"
6+
"errors"
67
"fmt"
78
"math/big"
89
"os"
910
"strings"
1011

12+
"github.com/ethereum/go-ethereum/accounts/abi"
13+
"github.com/ethereum/go-ethereum/common"
1114
"github.com/ethereum/go-ethereum/crypto"
1215
)
1316

1417
const (
1518
StorageSlotSizeBytes = 32
1619
)
1720

21+
var (
22+
ErrNoSlot = errors.New("no such slot found in layout JSON")
23+
)
24+
1825
type StorageEntry struct {
1926
Label string `json:"label"`
2027
Slot string `json:"slot"`
@@ -47,19 +54,28 @@ func (s *StorageLayout) GetSlots() map[string]string {
4754
return slots
4855
}
4956

50-
// Slot calculates a slot in Solidity mapping for storage field and a key
51-
func (s *StorageLayout) Slot(label string) string {
57+
// MustSlot calculates a slot in Solidity mapping for storage field and a key
58+
func (s *StorageLayout) MustSlot(label string) string {
59+
if _, ok := s.GetSlots()[label]; !ok {
60+
panic(fmt.Errorf("layout label: %s, %w", label, ErrNoSlot))
61+
}
5262
return s.GetSlots()[label]
5363
}
5464

55-
// MapSlot calculates a slot in Solidity mapping for storage field and a key
56-
func (s *StorageLayout) MapSlot(label, key string) string {
65+
// MustMapSlot calculates a slot in Solidity mapping for storage field and a key
66+
func (s *StorageLayout) MustMapSlot(label, key string) string {
67+
if _, ok := s.GetSlots()[label]; !ok {
68+
panic(fmt.Errorf("layout label: %s, %w", label, ErrNoSlot))
69+
}
5770
baseSlot := s.GetSlots()[label]
5871
return mapSlot(baseSlot, key)
5972
}
6073

61-
// ArraySlot calculates a slot in Solidity array for storage field and a key
62-
func (s *StorageLayout) ArraySlot(label string, index int64) string {
74+
// MustArraySlot calculates a slot in Solidity array for storage field and a key
75+
func (s *StorageLayout) MustArraySlot(label string, index int64) string {
76+
if _, ok := s.GetSlots()[label]; !ok {
77+
panic(fmt.Errorf("layout label: %s, %w", label, ErrNoSlot))
78+
}
6379
baseSlot := s.GetSlots()[label]
6480
return arraySlot(baseSlot, index)
6581
}
@@ -84,3 +100,88 @@ func mapSlot(baseSlot string, key string) string {
84100
hash := crypto.Keccak256(buf)
85101
return "0x" + hex.EncodeToString(hash)
86102
}
103+
104+
func ShiftHexByOffset(hexStr string, offset int) string {
105+
// Strip "0x"
106+
hexStr = strings.TrimPrefix(hexStr, "0x")
107+
108+
// Parse into big.Int
109+
n := new(big.Int)
110+
n.SetString(hexStr, 16)
111+
112+
// Shift left by offset * 8 bits
113+
n.Lsh(n, uint(offset*8))
114+
115+
// Return as 0x-prefixed, 32-byte hex
116+
return fmt.Sprintf("0x%064x", n)
117+
}
118+
119+
// MustEncodeStorageSlot encodes a value for Solidity storage slots based on type
120+
// Panics if encoding fails
121+
func MustEncodeStorageSlot(solidityType string, value interface{}) string {
122+
// Handle address type specially
123+
if solidityType == "address" {
124+
switch v := value.(type) {
125+
case common.Address:
126+
return fmt.Sprintf("0x%064x", v.Big())
127+
case string:
128+
if !common.IsHexAddress(v) {
129+
panic(fmt.Sprintf("invalid address format: %v", v))
130+
}
131+
return fmt.Sprintf("0x%064x", common.HexToAddress(v).Big())
132+
default:
133+
panic(fmt.Sprintf("unsupported address type: %T", value))
134+
}
135+
}
136+
137+
// Create the ABI type
138+
typ, err := abi.NewType(solidityType, "", nil)
139+
if err != nil {
140+
panic(fmt.Sprintf("invalid solidity type %q: %v", solidityType, err))
141+
}
142+
143+
// Special handling for bytes32 strings
144+
if solidityType == "bytes32" {
145+
if s, ok := value.(string); ok {
146+
if len(s) > 32 {
147+
panic("string too long for bytes32")
148+
}
149+
var b [32]byte
150+
copy(b[:], s)
151+
value = b
152+
}
153+
}
154+
155+
// Encode the value
156+
encoded, err := abi.Arguments{{Type: typ}}.Pack(value)
157+
if err != nil {
158+
panic(fmt.Sprintf("encoding failed for %v (%T) as %s: %v", value, value, solidityType, err))
159+
}
160+
161+
// For uint256 and int256, we need to take the last 32 bytes
162+
if solidityType == "uint256" || solidityType == "int256" {
163+
if len(encoded) > 32 {
164+
encoded = encoded[len(encoded)-32:]
165+
}
166+
}
167+
168+
return fmt.Sprintf("0x%064x", new(big.Int).SetBytes(encoded))
169+
}
170+
171+
// MergeHex merges two hex strings with bitwise "OR"
172+
// should be used when you see values with offsets in smart contract storage layout.json file
173+
// example:
174+
//
175+
// |----------------+-------------------------------------------+------+--------+-------+-------------------------|
176+
// | number_uint8 | uint8 | 3 | 0 | 1 | src/Counter.sol:Counter |
177+
// |----------------+-------------------------------------------+------+--------+-------+-------------------------|
178+
// | boolean | bool | 3 | 1 | 1 | src/Counter.sol:Counter |
179+
// |----------------+-------------------------------------------+------+--------+-------+-------------------------|
180+
func MergeHex(a, b string) string {
181+
ai := new(big.Int)
182+
bi := new(big.Int)
183+
ai.SetString(a[2:], 16)
184+
bi.SetString(b[2:], 16)
185+
ai.Or(ai, bi)
186+
return fmt.Sprintf("0x%064x", ai)
187+
}

framework/evm_storage/layout_api_test.go

Lines changed: 91 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@ package evm_storage_test
33
import (
44
"encoding/hex"
55
"fmt"
6+
"math/big"
67
"strings"
78
"testing"
89

10+
"github.com/ethereum/go-ethereum/common"
911
"github.com/stretchr/testify/require"
1012

1113
"github.com/smartcontractkit/chainlink-testing-framework/framework/evm_storage"
@@ -14,7 +16,7 @@ import (
1416

1517
// TestLayoutAPI that's the example of using helpers to override storage in your contracts
1618
func TestLayoutAPI(t *testing.T) {
17-
t.Skip("this test is for manual debugging and figuring out layout of custom structs")
19+
//t.Skip("this test is for manual debugging and figuring out layout of custom structs")
1820
// load contract layout file, see testdata/layout.json
1921
// more docs here - https://docs.soliditylang.org/en/latest/internals/layout_in_storage.html#
2022
layout, err := evm_storage.New(layoutFile)
@@ -41,12 +43,92 @@ func TestLayoutAPI(t *testing.T) {
4143
return "0x" + hex.EncodeToString(buf)
4244
}
4345

44-
slot := layout.ArraySlot("a_signers", 1)
45-
data := encodeFunc("0x00000000000000000000000000000000000000a5", 255, 42)
46-
fmt.Printf("setting slot: %s with data: %s\n", slot, data)
47-
r := rpc.New(testRPCURL, nil)
48-
err = r.AnvilSetStorageAt([]interface{}{contractAddr, slot, data})
49-
require.NoError(t, err)
50-
// verify it manually
51-
// cast call 0x5FbDB2315678afecb367f032d93F642f64180aa3 "getASigner(uint256)(address,uint8,uint8)" --rpc-url http://localhost:8545 1
46+
{
47+
slot := layout.MustSlot("number_uint256")
48+
data := evm_storage.MustEncodeStorageSlot("uint256", big.NewInt(222))
49+
fmt.Printf("setting slot: %s with data: %s\n", slot, data)
50+
r := rpc.New(testRPCURL, nil)
51+
err = r.AnvilSetStorageAt([]interface{}{contractAddr, slot, data})
52+
require.NoError(t, err)
53+
// cast call 0x5FbDB2315678afecb367f032d93F642f64180aa3 "number_uint256()(uint256)" --rpc-url http://localhost:8545
54+
}
55+
{
56+
slot := layout.MustSlot("number_uint8")
57+
data := evm_storage.MustEncodeStorageSlot("uint8", uint8(8))
58+
fmt.Printf("setting slot: %s with data: %s\n", slot, data)
59+
r := rpc.New(testRPCURL, nil)
60+
err = r.AnvilSetStorageAt([]interface{}{contractAddr, slot, data})
61+
require.NoError(t, err)
62+
// cast call 0x5FbDB2315678afecb367f032d93F642f64180aa3 "number_uint8()(uint8)" --rpc-url http://localhost:8545
63+
}
64+
{
65+
slot := layout.MustSlot("number_int256")
66+
data := evm_storage.MustEncodeStorageSlot("uint256", big.NewInt(221))
67+
fmt.Printf("setting slot: %s with data: %s\n", slot, data)
68+
r := rpc.New(testRPCURL, nil)
69+
err = r.AnvilSetStorageAt([]interface{}{contractAddr, slot, data})
70+
require.NoError(t, err)
71+
// cast call 0x5FbDB2315678afecb367f032d93F642f64180aa3 "number_int256()(int256)" --rpc-url http://localhost:8545
72+
}
73+
{
74+
slot := layout.MustSlot("_owner")
75+
data := evm_storage.MustEncodeStorageSlot("address", common.HexToAddress("0x5FbDB2315678afecb367f032d93F642f64180aa3"))
76+
fmt.Printf("setting slot: %s with data: %s\n", slot, data)
77+
r := rpc.New(testRPCURL, nil)
78+
err = r.AnvilSetStorageAt([]interface{}{contractAddr, slot, data})
79+
require.NoError(t, err)
80+
// private fields like _owner can only be verified by getter
81+
// cast call 0x5FbDB2315678afecb367f032d93F642f64180aa3 "getOwner()(address)" --rpc-url http://localhost:8545
82+
}
83+
{
84+
slot := layout.MustArraySlot("a_signers", 1)
85+
data := encodeFunc("0x00000000000000000000000000000000000000a5", 255, 42)
86+
fmt.Printf("setting slot: %s with data: %s\n", slot, data)
87+
r := rpc.New(testRPCURL, nil)
88+
err = r.AnvilSetStorageAt([]interface{}{contractAddr, slot, data})
89+
require.NoError(t, err)
90+
// cast call 0x5FbDB2315678afecb367f032d93F642f64180aa3 "getASigner(uint256)(address,uint8,uint8)" --rpc-url http://localhost:8545 1
91+
}
92+
{
93+
slot := layout.MustMapSlot("s_signers", "0x00000000000000000000000000000000000000a5")
94+
data := encodeFunc("0x00000000000000000000000000000000000000a5", 254, 40)
95+
fmt.Printf("setting slot: %s with data: %s\n", slot, data)
96+
r := rpc.New(testRPCURL, nil)
97+
err = r.AnvilSetStorageAt([]interface{}{contractAddr, slot, data})
98+
require.NoError(t, err)
99+
// cast call 0x5FbDB2315678afecb367f032d93F642f64180aa3 "getSSigner(address)(address,uint8,uint8)" 0x00000000000000000000000000000000000000a5 --rpc-url http://localhost:8545 1
100+
}
101+
{
102+
// offset example
103+
slot := layout.MustSlot("boolean")
104+
data := evm_storage.MustEncodeStorageSlot("bool", true)
105+
boolValue := evm_storage.ShiftHexByOffset(data, 1)
106+
uint8Value := evm_storage.MustEncodeStorageSlot("uint8", uint8(8))
107+
data = evm_storage.MergeHex(uint8Value, boolValue)
108+
fmt.Printf("setting slot: %s with data: %s\n", slot, data)
109+
r := rpc.New(testRPCURL, nil)
110+
err = r.AnvilSetStorageAt([]interface{}{contractAddr, slot, data})
111+
require.NoError(t, err)
112+
// Contract code:
113+
// contract Counter {
114+
// address private _owner;
115+
// uint256 public number_uint256;
116+
// int256 public number_int256;
117+
// uint8 public number_uint8; <-- we need to change this
118+
// bool public boolean; <-- and this
119+
//
120+
// Example layout:
121+
// ╭----------------+-------------------------------------------+------+--------+-------+-------------------------╮
122+
// | Name | Type | Slot | Offset | Bytes | Contract |
123+
// |----------------+-------------------------------------------+------+--------+-------+-------------------------|
124+
// | number_uint8 | uint8 | 3 | 0 | 1 | src/Counter.sol:Counter |
125+
// |----------------+-------------------------------------------+------+--------+-------+-------------------------|
126+
// | boolean | bool | 3 | 1 | 1 | src/Counter.sol:Counter |
127+
// |----------------+-------------------------------------------+------+--------+-------+-------------------------|
128+
// Resulting value with offsets: 0x0000000000000000000000000000000000000000000000000000000000000108
129+
// cast call 0x5FbDB2315678afecb367f032d93F642f64180aa3 "boolean()(bool)" --rpc-url http://localhost:8545
130+
// true
131+
// cast call 0x5FbDB2315678afecb367f032d93F642f64180aa3 "number_uint8()(uint8)" --rpc-url http://localhost:8545
132+
// 8
133+
}
52134
}

0 commit comments

Comments
 (0)