Skip to main content

Test case: string storage

Representing a Solidity string storage using an ethdebug/format/pointer requires the use of conditional logic to identify the one or more regions that correspond to raw UTF-8 Solidity string data. The dereference() function should behave as expected for such a pointer and observe the changing string value.

Test source​

const stringStorageTest: ObserveTraceTest<string> = {
pointer: findExamplePointer("string-storage-contract-variable-slot"),

compileOptions: stringStorageCompileOptions,

expectedValues: [
"",
"hello world",
"solidity storage is a fun lesson in endianness",
],

async observe({ regions, read }: Cursor.View): Promise<string> {
// collect all the regions corresponding to string contents
const strings = regions.named("string");

// read each region and concatenate all the bytes
const stringData: Data = Data.zero().concat(
...(await Promise.all(strings.map(read))),
);

// decode into JS string
return new TextDecoder().decode(stringData);
},
};

Tested pointer​

# example `string storage` allocation
define:
"string-storage-contract-variable-slot": 0
in:
group:
# for short strings, the length is stored as 2n in the last byte of slot
- name: "length-flag"
location: storage
slot: "string-storage-contract-variable-slot"
offset:
~difference: [ ~wordsize, 1 ]
length: 1

# define the region representing the string data itself conditionally
# based on odd or even length data
- if:
~remainder:
- ~sum:
- ~read: "length-flag"
- 1
- 2

# short string case (flag is even)
then:
define:
"string-length":
~quotient: [ { ~read: "length-flag" }, 2 ]
in:
name: "string"
location: storage
slot: "string-storage-contract-variable-slot"
offset: 0
length: "string-length"

# long string case (flag is odd)
else:
group:
# long strings may use full word to describe length as 2n+1
- name: "long-string-length-data"
location: storage
slot: "string-storage-contract-variable-slot"
offset: 0
length: ~wordsize

- define:
"string-length":
~quotient:
- ~difference:
- ~read: "long-string-length-data"
- 1
- 2

"start-slot":
~keccak256:
- ~wordsized: "string-storage-contract-variable-slot"

"total-slots":
# account for both zero and nonzero slot remainders by adding
# ~wordsize-1 to the length before dividing
~quotient:
- ~sum: [ "string-length", { ~difference: [ ~wordsize, 1 ] } ]
- ~wordsize
in:
list:
count: "total-slots"
each: "i"
is:
define:
"current-slot":
~sum: [ "start-slot", "i" ]
"previous-length":
~product: [ "i", ~wordsize ]
in:
# conditional based on whether this is the last slot:
# is the string length longer than the previous length
# plus this whole slot?
if:
~difference:
- "string-length"
- ~sum: [ "previous-length", "~wordsize" ]
then:
# include the whole slot
name: "string"
location: storage
slot: "current-slot"
else:
# include only what's left in the string
name: "string"
location: storage
slot: "current-slot"
offset: 0
length:
~difference: [ "string-length", "previous-length" ]