Skip to content

Fix: Allow appending to struct at the end of storage layout #1136 #1147

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 14 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions packages/core/contracts/test/StructAppend.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is missing test cases for namespaced structs as described in #1136

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added test cases for namespaced structs as requested in the latest commit. Let me know if there are any more suggestions


contract StructAppendV1 {
struct MyStruct {
uint256 x;
uint256 y;
}

MyStruct public myStruct;
}

contract StructAppendV2_Ok {
struct MyStruct {
uint256 x;
uint256 y;
uint256 z; // appended field
}

MyStruct public myStruct;
}

contract StructAppendBeforeStorageEndV1 {
struct MyStruct {
uint256 x;
uint256 y;
}

MyStruct public myStruct;
uint256 public zz;
}

contract StructAppendBeforeStorageEndV2_Bad {
struct MyStruct {
uint256 x;
uint256 y;
uint256 z; // appended field
}

MyStruct public myStruct;
uint256 public zz;
}
26 changes: 26 additions & 0 deletions packages/core/src/storage/compare.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,32 @@ export class StorageLayoutComparator {
// Gap shrink or gap replacement that finishes on the same slot is safe
return false;
} else if (o.kind === 'append' && allowAppend) {
// Allow append operation if the allowAppend flag is set
// This applies to both direct storage appends and appends within structs
return false;
} else if (o.kind === 'typechange' && o.change?.kind === 'struct members' && o.change.allowAppend) {
const structOps = o.change.ops;
const hasNonAppendOps = structOps.some(op => op.kind !== 'append');
if (hasNonAppendOps) {
return true;
}

const structEnd = storageFieldEnd(o.original);
if (structEnd === undefined) {
return true;
}

const hasFieldsAfter = original
.filter(field => !isGap(field))
.some(field => {
const fieldStart = storageFieldBegin(field);
if (field === o.original || fieldStart === undefined) {
return false;
}
return fieldStart > structEnd;
});

return hasFieldsAfter;
Comment on lines +189 to +204
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not clear what this section of code achieves. There should be testcases and changing/removing any of this code should cause those testcases to fail.

}
return true;
});
Expand Down Expand Up @@ -324,6 +349,7 @@ export class StorageLayoutComparator {
}
}
assert(isStructMembers(originalMembers) && isStructMembers(updatedMembers));

const ops = this.layoutLevenshtein(originalMembers, updatedMembers, { allowAppend });
if (ops.length > 0) {
return { kind: 'struct members', ops, original, updated, allowAppend };
Expand Down
73 changes: 73 additions & 0 deletions packages/core/src/storage/struct-append.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import _test, { TestFn } from 'ava';
import { ContractDefinition } from 'solidity-ast';
import { findAll, astDereferencer } from 'solidity-ast/utils';
import { artifacts } from 'hardhat';

import { SolcOutput } from '../solc-api';
import { getStorageUpgradeErrors } from '../storage';
import { StorageLayout } from './layout';
import { extractStorageLayout } from './extract';

interface Context {
extractStorageLayout: (contract: string) => StorageLayout;
}

const test = _test as TestFn<Context>;

test.before(async t => {
const buildInfo = await artifacts.getBuildInfo('contracts/test/StructAppend.sol:StructAppendV1');
if (buildInfo === undefined) {
throw new Error('Build info not found');
}
const solcOutput: SolcOutput = buildInfo.output;
const contracts: Record<string, ContractDefinition> = {};
const storageLayouts: Record<string, StorageLayout> = {};
for (const def of findAll('ContractDefinition', solcOutput.sources['contracts/test/StructAppend.sol'].ast)) {
contracts[def.name] = def;
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
storageLayouts[def.name] = solcOutput.contracts['contracts/test/StructAppend.sol'][def.name].storageLayout!;
}
const deref = astDereferencer(solcOutput);
const dummyDecodeSrc = () => 'file.sol:1';
t.context.extractStorageLayout = name =>
extractStorageLayout(contracts[name], dummyDecodeSrc, deref, storageLayouts[name]);
});

test('struct append at end of storage - ok', t => {
const v1 = t.context.extractStorageLayout('StructAppendV1');
const v2 = t.context.extractStorageLayout('StructAppendV2_Ok');
const comparison = getStorageUpgradeErrors(v1, v2);
t.deepEqual(comparison, [], JSON.stringify(comparison, null, 2));
});

test('struct append not at end of storage - bad', t => {
const v1 = t.context.extractStorageLayout('StructAppendBeforeStorageEndV1');
const v2 = t.context.extractStorageLayout('StructAppendBeforeStorageEndV2_Bad');
const comparison = getStorageUpgradeErrors(v1, v2);
t.like(comparison, {
length: 2,
0: {
kind: 'typechange',
change: {
kind: 'struct members',
ops: {
length: 1,
0: { kind: 'append' },
},
},
original: { label: 'myStruct' },
updated: { label: 'myStruct' },
},
1: {
kind: 'layoutchange',
change: {
slot: {
from: '2',
to: '3',
},
},
original: { label: 'zz' },
updated: { label: 'zz' },
},
});
});

Check warning on line 73 in packages/core/src/storage/struct-append.test.ts

View workflow job for this annotation

GitHub Actions / lint

Insert `⏎`
Loading