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: singleSourceCompilation({
path: "StringStorage.sol",
contractName: "StringStorage",
content: `contract StringStorage {
string storedString;
bool done;

event Done();

constructor() {
storedString = "hello world";
storedString = "solidity storage is a fun lesson in endianness";

done = true;
}
}
`
}),

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" ]