1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
package state
import (
"errors"
"fmt"
"math/big"
"reflect"
"regexp"
"strings"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum-optimism/optimism/op-bindings/solc"
"github.com/ethereum-optimism/optimism/op-service/eth"
)
// EncodeStorageKeyValue encodes the key value pair that is stored in state
// given a StorageLayoutEntry and StorageLayoutType. A single input may result
// in multiple outputs. Unknown or unimplemented types will return an error.
// Note that encoding uints is *not* overflow safe, so be sure to check
// the ABI before setting very large values
func EncodeStorageKeyValue(value any, entry solc.StorageLayoutEntry, storageType solc.StorageLayoutType) ([]*EncodedStorage, error) {
label := storageType.Label
encoded := make([]*EncodedStorage, 0)
key := encodeSlotKey(entry)
switch storageType.Encoding {
case "inplace":
switch label {
case "bool":
val, err := EncodeBoolValue(value, entry.Offset)
if err != nil {
return nil, fmt.Errorf("cannot encode %s: %w", storageType.Encoding, err)
}
encoded = append(encoded, &EncodedStorage{key, val})
case "address":
val, err := EncodeAddressValue(value, entry.Offset)
if err != nil {
return nil, fmt.Errorf("cannot encode %s: %w", storageType.Encoding, err)
}
encoded = append(encoded, &EncodedStorage{key, val})
case "bytes":
return nil, fmt.Errorf("%w: %s", errUnimplemented, label)
case "bytes32":
val, err := EncodeBytes32Value(value, entry.Offset)
if err != nil {
return nil, fmt.Errorf("cannot encode %s: %w", storageType.Encoding, err)
}
encoded = append(encoded, &EncodedStorage{key, val})
default:
switch true {
case strings.HasPrefix(label, "contract"):
val, err := EncodeAddressValue(value, entry.Offset)
if err != nil {
return nil, fmt.Errorf("cannot encode %s: %w", storageType.Encoding, err)
}
encoded = append(encoded, &EncodedStorage{key, val})
case strings.HasPrefix(label, "uint"):
val, err := EncodeUintValue(value, entry.Offset)
if err != nil {
return nil, fmt.Errorf("cannot encode %s: %w", storageType.Encoding, err)
}
encoded = append(encoded, &EncodedStorage{key, val})
default:
// structs are not supported
return nil, fmt.Errorf("cannot encode %s: %w", storageType.Encoding, errUnimplemented)
}
}
case "dynamic_array":
case "bytes":
switch label {
case "string":
val, err := EncodeStringValue(value, entry.Offset)
if err != nil {
return nil, fmt.Errorf("cannot encode %s: %w", storageType.Encoding, errUnimplemented)
}
encoded = append(encoded, &EncodedStorage{key, val})
default:
return nil, fmt.Errorf("%w: %s", errUnimplemented, label)
}
case "mapping":
if strings.HasPrefix(storageType.Value, "mapping") {
return nil, fmt.Errorf("%w: %s", errUnimplemented, "nested mappings")
}
values, ok := value.(map[any]any)
if !ok {
return nil, fmt.Errorf("mapping must be map[any]any")
}
keyEncoder, err := getElementEncoder(storageType, "key")
if err != nil {
return nil, err
}
valueEncoder, err := getElementEncoder(storageType, "value")
if err != nil {
return nil, err
}
// Mapping values have 0 offset
for rawKey, rawVal := range values {
encodedKey, err := keyEncoder(rawKey, 0)
if err != nil {
return nil, err
}
encodedSlot := encodeSlotKey(entry)
preimage := [64]byte{}
copy(preimage[0:32], encodedKey.Bytes())
copy(preimage[32:64], encodedSlot.Bytes())
hash := crypto.Keccak256(preimage[:])
key := common.BytesToHash(hash)
val, err := valueEncoder(rawVal, 0)
if err != nil {
return nil, err
}
encoded = append(encoded, &EncodedStorage{key, val})
}
default:
return nil, fmt.Errorf("unknown encoding %s: %w", storageType.Encoding, errUnimplemented)
}
return encoded, nil
}
// encodeSlotKey will encode the storage slot key. This does not
// support mappings.
func encodeSlotKey(entry solc.StorageLayoutEntry) common.Hash {
slot := new(big.Int).SetUint64(uint64(entry.Slot))
return common.BigToHash(slot)
}
// ElementEncoder is a function that can encode an element
// based on a solidity type
type ElementEncoder func(value any, offset uint) (common.Hash, error)
// getElementEncoder will return the correct ElementEncoder
// given a solidity type. The kind refers to the key or the value
// when getting an encoder for a mapping. This is only useful
// if the key itself is not populated for some reason.
func getElementEncoder(storageType solc.StorageLayoutType, kind string) (ElementEncoder, error) {
var target string
if kind == "key" {
target = storageType.Key
} else if kind == "value" {
target = storageType.Value
} else {
return nil, fmt.Errorf("unknown storage %s", kind)
}
switch target {
case "t_address":
return EncodeAddressValue, nil
case "t_bool":
return EncodeBoolValue, nil
case "t_bytes32":
return EncodeBytes32Value, nil
default:
if strings.HasPrefix(target, "t_uint") {
return EncodeUintValue, nil
}
}
// Special case fallback if the target is empty, pull it
// from the label. This requires knowledge of whether we want
// the key or the value in the label.
if target == "" {
r := regexp.MustCompile(`mapping\((?P<key>[[:alnum:]]*) => (?P<value>[[:alnum:]]*)\)`)
result := r.FindStringSubmatch(storageType.Label)
for i, key := range r.SubexpNames() {
if kind == key {
res := "t_" + result[i]
layout := solc.StorageLayoutType{}
if kind == "key" {
layout.Key = res
} else if kind == "value" {
layout.Value = res
} else {
return nil, fmt.Errorf("unknown storage %s", kind)
}
return getElementEncoder(layout, kind)
}
}
}
return nil, fmt.Errorf("unsupported type: %s", target)
}
// EncodeBytes32Value will encode a bytes32 value. The offset
// is included so that it can implement the ElementEncoder
// interface, but the offset must always be 0.
func EncodeBytes32Value(value any, offset uint) (common.Hash, error) {
if offset != 0 {
return common.Hash{}, errors.New("offset must be 0")
}
return encodeBytes32Value(value)
}
// encodeBytes32Value implements the encoding of a bytes32
// value into a common.Hash that is suitable for storage.
func encodeBytes32Value(value any) (common.Hash, error) {
name := reflect.TypeOf(value).Name()
switch name {
case "string":
str, ok := value.(string)
if !ok {
return common.Hash{}, errInvalidType
}
val, err := hexutil.Decode(str)
if err != nil {
return common.Hash{}, err
}
return common.BytesToHash(val), nil
case "Hash":
hash, ok := value.(common.Hash)
if !ok {
return common.Hash{}, errInvalidType
}
return hash, nil
default:
return common.Hash{}, errInvalidType
}
}
// EncodeStringValue will encode a string to a type suitable
// for storage in state. The offset must be 0.
func EncodeStringValue(value any, offset uint) (common.Hash, error) {
if offset != 0 {
return common.Hash{}, errors.New("offset must be 0")
}
return encodeStringValue(value)
}
// encodeStringValue implements the string encoding. Values larger
// than 31 bytes are not supported because they will be stored
// in multiple storage slots.
func encodeStringValue(value any) (common.Hash, error) {
name := reflect.TypeOf(value).Name()
switch name {
case "string":
str, ok := value.(string)
if !ok {
return common.Hash{}, errInvalidType
}
data := []byte(str)
// Values that are 32 bytes or longer are not supported
if len(data) >= 32 {
return common.Hash{}, errors.New("string value too long")
}
// The data is right padded with 2 * the length
// of the data in the final byte
padded := common.RightPadBytes(data, 32)
padded[len(padded)-1] = byte(len(data) * 2)
return common.BytesToHash(padded), nil
default:
return common.Hash{}, errInvalidType
}
}
// EncodeBoolValue will encode a boolean value given a storage
// offset.
func EncodeBoolValue(value any, offset uint) (common.Hash, error) {
val, err := encodeBoolValue(value)
if err != nil {
return common.Hash{}, fmt.Errorf("invalid bool: %w", err)
}
return handleOffset(val, offset), nil
}
// encodeBoolValue will encode a boolean value into a type
// suitable for solidity storage.
func encodeBoolValue(value any) (common.Hash, error) {
name := reflect.TypeOf(value).Name()
switch name {
case "bool":
boolean, ok := value.(bool)
if !ok {
return common.Hash{}, errInvalidType
}
if boolean {
return common.BigToHash(common.Big1), nil
} else {
return common.Hash{}, nil
}
case "string":
boolean, ok := value.(string)
if !ok {
return common.Hash{}, errInvalidType
}
if boolean == "true" {
return common.BigToHash(common.Big1), nil
} else {
return common.Hash{}, nil
}
default:
return common.Hash{}, errInvalidType
}
}
// EncodeAddressValue will encode an address like value given a
// storage offset.
func EncodeAddressValue(value any, offset uint) (common.Hash, error) {
val, err := encodeAddressValue(value)
if err != nil {
return common.Hash{}, fmt.Errorf("invalid address: %w", err)
}
return handleOffset(val, offset), nil
}
// encodeAddressValue will encode an address value into
// a type suitable for solidity storage.
func encodeAddressValue(value any) (common.Hash, error) {
typ := reflect.TypeOf(value)
if typ.Kind() == reflect.Ptr {
typ = typ.Elem()
}
name := typ.Name()
switch name {
case "Address":
if reflect.TypeOf(value).Kind() == reflect.Ptr {
address, ok := value.(*common.Address)
if !ok {
return common.Hash{}, errInvalidType
}
return eth.AddressAsLeftPaddedHash(*address), nil
} else {
address, ok := value.(common.Address)
if !ok {
return common.Hash{}, errInvalidType
}
return eth.AddressAsLeftPaddedHash(address), nil
}
case "string":
address, ok := value.(string)
if !ok {
return common.Hash{}, errInvalidType
}
return eth.AddressAsLeftPaddedHash(common.HexToAddress(address)), nil
default:
return common.Hash{}, errInvalidType
}
}
// EncodeUintValue will encode a uint value given a storage offset
func EncodeUintValue(value any, offset uint) (common.Hash, error) {
val, err := encodeUintValue(value)
if err != nil {
return common.Hash{}, fmt.Errorf("invalid uint: %w", err)
}
return handleOffset(val, offset), nil
}
// encodeUintValue will encode a uint like type into a
// type suitable for solidity storage.
func encodeUintValue(value any) (common.Hash, error) {
val := reflect.ValueOf(value)
if val.Kind() == reflect.Ptr {
val = val.Elem()
}
name := val.Type().Name()
switch name {
case "uint":
val, ok := value.(uint)
if !ok {
return common.Hash{}, errInvalidType
}
result := new(big.Int).SetUint64((uint64(val)))
return common.BigToHash(result), nil
case "int":
val, ok := value.(int)
if !ok {
return common.Hash{}, errInvalidType
}
result := new(big.Int).SetUint64(uint64(val))
return common.BigToHash(result), nil
case "uint64":
val, ok := value.(uint64)
if !ok {
return common.Hash{}, errInvalidType
}
result := new(big.Int).SetUint64(val)
return common.BigToHash(result), nil
case "uint32":
val, ok := value.(uint32)
if !ok {
return common.Hash{}, errInvalidType
}
result := new(big.Int).SetUint64(uint64(val))
return common.BigToHash(result), nil
case "uint16":
val, ok := value.(uint16)
if !ok {
return common.Hash{}, errInvalidType
}
result := new(big.Int).SetUint64(uint64(val))
return common.BigToHash(result), nil
case "uint8":
val, ok := value.(uint8)
if !ok {
return common.Hash{}, errInvalidType
}
result := new(big.Int).SetUint64(uint64(val))
return common.BigToHash(result), nil
case "string":
val, ok := value.(string)
if !ok {
return common.Hash{}, errInvalidType
}
number, err := hexutil.DecodeBig(val)
if err != nil {
if errors.Is(err, hexutil.ErrMissingPrefix) {
number, ok = new(big.Int).SetString(val, 10)
if !ok {
return common.Hash{}, errInvalidType
}
} else if errors.Is(err, hexutil.ErrLeadingZero) {
number, ok = new(big.Int).SetString(val[2:], 16)
if !ok {
return common.Hash{}, errInvalidType
}
}
}
return common.BigToHash(number), nil
case "bool":
val, ok := value.(bool)
if !ok {
return common.Hash{}, errInvalidType
}
if val {
return common.Hash{31: 0x01}, nil
} else {
return common.Hash{}, nil
}
case "Int":
val, ok := value.(*big.Int)
if !ok {
return common.Hash{}, errInvalidType
}
return common.BigToHash(val), nil
default:
return common.Hash{}, errInvalidType
}
}
// handleOffset will offset a value in storage by shifting
// it to the left. This is useful for when multiple variables
// are tightly packed in a storage slot.
func handleOffset(hash common.Hash, offset uint) common.Hash {
if offset == 0 {
return hash
}
number := hash.Big()
shifted := new(big.Int).Lsh(number, offset*8)
return common.BigToHash(shifted)
}