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