storage layout of EVM Solidity type structure
storage slot structure based on target contract analysis
storage slot structure utilizing delegatecall and fallback
storage slot structure within the upgrade proxy pattern
Mitigation and addressing storage collision vulnerabilities.
State variables are stored in storage in a compact manner, using the same storage slot for multiple values.
The size of each variable is determined by its type.
Types less than 32 bytes are packed according to the following rules
The first item in the storage slot is ordered by lower-order byte.
Only as many bytes as are necessary to store the value type are used.
If the value type does not fit in the remaining part of the storage slot, it is stored in the next storage slot.
Struct and array data always starts on a new slot.
contract SimpleStorageLayout {
// slots: 0
uint16 a;
uint16 b;
uint16 c;
uint16 d;
uint16 e;
uint16 f;
uint16 g;
uint16 h;
uint16 i;
uint16 j;
uint16 k;
uint16 l;
uint16 m;
uint16 n;
uint16 o;
uint16 p;
// slots: 1
bytes12 calice;
address alice;
// slots: 2
address bob;
// slots: 3
uint256 balance;
}
State variables "a-p" specified as uint16
type use 2 bytes each, for a total of 16 variables that are packed into a single 32-byte sized slot using the 32 byte context. (slots: 0)
State variables specified as bytes12
and address
types use 12 bytes and 20 bytes respectively. Similarly, they are packed into a single 32-byte context slot. (slots: 1)
Next, state variables specified as uint256
type use a fixed-size 32-byte context slot. (slots: 3)
256 bit can be used for all 256-bit hash addresses in Solidity storage and the data storage access method is different from what can be applied to webassembly.
C3 Linearization
, so that a derived variable should come after its base variable. The state variables of other contracts will share the same storage slots.Due to sizes that cannot be predicted at compile-time, array types cannot be stored in between other state variables in storage slots.
Instead, they are treated as taking up only 32 bytes (header slots) and the elements within them are stored in other storage slots, starting with a keccak hash calculation.
Contracts start storing slots from slot 0 and as previously mentioned, basic fixed types occupy one slot (e.g. uint256), if an array is to be stored, the number of elements in the array is stored in a slot referred to as a header slot
which is 32 bytes.
You can access the internal data by adding the number of the header slot
to the keccak hashing of the element's address. This is similar to the way dynamic arrays are stored in C/C++ etc.
Solidity does not maintain these pointers anywhere and as a result, memory can be written to all storage locations without allocating memory.
contract ArrayStorage {
uint256 a;
uint256 b;
uint256[] blocks;
function init() public {
blocks[0] = 0x41414141;
blocks[1] = 0x42424242;
blocks[2] = 0x43434343;
}
}
As seen in the diagram, state variables a and b have fixed size slots of 32 bytes and therefore occupy slots 0 and 1.
The dynamic array uint256[]
state variable first forms 'header slots' based on the number of internal elements and occupies a slot to store the total length value.
The internal element data can be accessed in the form of keccak(slots(2)) + 0
. Similarly, data that exists at index 0, 1 and 2 can be accessed in the same way.
Mappings have similar mechanisms, with the exception that all values are located separately and the key value is included in the hash calculation.
Next, using the example of the struct pos
being declared and being managed in the 2-dimensional mapping structure positions
. There is a state variable dimension
of 32 byte size in the middle.
contract StructStorage {
struct pos {
uint128 x;
uint128 y;
uint256 z;
}
uint256 dimension;
mapping(uint256 => mapping(uint256 => pos)) positions;
}
data[2][3].z
, it can be calculated using the following method.0. slots:0 => uint256 dimension
1. positions[2] == keccak256(uint256(2).uint256(1))
- reference: index 2
- first state variable is slot 0
2. positions[2][3] == keccak256(uint256(3).keccak256(uint256(2).uint256(1)))
3. positions[2][3].z == keccak256(uint256(3).keccak256(uint256(2).uint256(1))) + 1
- Since uint128 x and y are 16 bytes each, we can compress them to 32 bytes and use +1
Bytes and strings are encoded in the same way, and the encoding generally uses the storage slot for the array itself and the data stored at the corresponding storage slot position, which is similar to bytes1[]
.
If the length of the element is less than 32 bytes, it is stored in the same slot with the length.
If the length of the data is a maximum of 31 bytes, the element is stored in the upper byte (left-aligned) and the least significant byte stores the value of length * 2
.
For byte arrays that store data, if the length is 32 or more bytes, the main storage slot p
stores length * 2 + 1
, and the data is stored in the usual way in keccak256(p)
.
0x00 ~ 0x3f (64 byte) => hashing method scratch space
0x40 ~ 0x5f (32 byte) => allocated memory size (free memory space)
0x60 ~ 0x7f (32 byte) => zer0 slots
Scratch space can be used in between instructions (inside inline assembly).
Slot 0 is used as the initial value of dynamic memory arrays and must never be written to. (Available memory pointers point to the initial address of 0x80)
New objects are always placed in spare memory pointers and memory is never released.
Elements in memory arrays in Solidity always occupy a multiple of 32 bytes (this also applies to bytes1[]
, but not for bytes
and string
types)
Multidimensional memory arrays consist of a pointer to the memory array, the length of the dynamic array is stored in the first slot of the array and the array elements follow.
Temporary memory areas larger than 64 bytes are needed.
There are some tasks that do not fit in the scratch space.
Use of memory arrays or dynamic memory allocation is required.
In this case, memory is allocated using a free memory pointer and the size is stored in the first slot.
The memory pointer is incremented by the size of the allocated memory and the next allocation is done using this memory pointer.
The allocated memory is cleared when the function exits.
Array Type Layout
uint8[4] a;
Storage => 32 bytes (1 slot)
Memory => 128 bytes (4 slots, 32 bytes each)
Struct Type Layout
struct s {
uint a;
uint b;
uint8 c;
uint8 d;
}
Storage => 96 bytes (3 slots: 32 bytes each)
Memory => 128 bytes (4 slots: 32 bytes each)
When operating EVM-based smart contracts on various chains, various bugs may occur or updates may be necessary. However, since it is on the blockchain block, it is impossible to modify. In the past, the process of directly deploying new code and entering the newly created contract address for all associated services was cumbersome due to lack of flexibility, but now various proxy design patterns have emerged, making it possible to use updatable smart contracts.
The idea is to keep the smart contract address and code in an unchangeable state and have the code executed by another contract and return the result.
It is easy to change the contract state as much as the variable that saves the other contract address, but the code cannot be changed.
Because there may be several smart contract versions, recording a new version address while keeping the old version address allows users to switch to a new version at any time.
Next, we will learn the basic delegatecall, fallback
commands that are essential to understand in Upgradable Proxy Pattern, and analyze the storage slots on a unit basis.
We will use the foundry framework to analyze the code structure and conduct transaction tests.
contract StorageLayout {
address immutable owner;
uint256 public flag;
mapping(address => uint256) public users;
bytes public key;
constructor(bytes memory _key) {
owner = msg.sender;
key = _key;
}
function setFlags(address _user, uint256 _flag) external {
require(msg.sender == owner, "[ERR-01] Only owner can set flags");
users[_user] = _flag;
}
}
As seen in the above contract storage layout, the address immutable owner
variable is not stored in storage as it uses the immutable
keyword, instead it is stored in the contract's memory during execution.
The next variable, flag
, occupies 1 storage slot and stores its data. The next mapping type, users
, occupies 1 storage slot as well, with the dynamic type rule resulting in the use of a header slot, which stores the length of elements within the users
mapping.
slots:0 ------- flag
sltos:1 ------- users <header slots:length>
;; users[addr] reference addr index
...
slots:keccak256(users)+1 ------- users[addr]
contract StorageLayoutTest is DSTest {
Vm internal immutable vm = Vm(HEVM_ADDRESS);
StorageLayout internal storageLayout;
address internal minsu;
function setUp() public {
minsu = address(uint160(uint256(keccak256(abi.encodePacked("owner")))));
vm.label(minsu, "Owner");
}
function testSetFlags() public {
vm.startPrank(address(minsu));
storageLayout = new StorageLayout("BBBB");
storageLayout.setFlags(minsu, uint256(0x41414141));
vm.stopPrank();
bytes32 minsuStorageSlot = keccak256(
abi.encodePacked(uint256(uint160(minsu)), uint256(1))
);
uint256 minsuStorageSlotValue = uint256(
vm.load(address(storageLayout), minsuStorageSlot)
);
assertEq(minsuStorageSlotValue, storageLayout.users(minsu));
}
}
The setFlags
function is used to map the value of 0x41414141 to the address "minsu" in the users
mapping variable. Afterwards, users[address(minsu)]
will contain the value 0x41414141.
Therefore, we can directly reference the slot number by calculating it through dynamic mapping slot calculation. By referencing the storage in the format keccak256(abi.encodePacked(uint256(uint160(minsu)), uint256(1)));
, we can confirm that the value is the same by calculating the corresponding mapping slot number and checking the storage.
[PASS] testSetFlags() (gas: 248370)
Traces:
[248370] StorageLayoutTest::testSetFlags()
ββ [0] VM::startPrank(Owner: [0x9E326f4F4C1B9057164Ef592671cF0D37C8040C0])
β ββ β ()
ββ [180298] β new StorageLayout@0x8b10C21F6dEE107A616205e39313147Ee8e0e19A
β ββ β 785 bytes of code
ββ [22600] StorageLayout::setFlags(Owner: [0x9E326f4F4C1B9057164Ef592671cF0D37C8040C0], 1094795585)
β ββ β ()
ββ [0] VM::stopPrank()
β ββ β ()
ββ [0] VM::load(StorageLayout: [0x8b10C21F6dEE107A616205e39313147Ee8e0e19A], 0x80b63dade9d4ca379daeb336da1ac63122f9446a350c48d3ecd78eda052432eb)
β ββ β 0x0000000000000000000000000000000000000000000000000000000041414141
ββ [541] StorageLayout::users(Owner: [0x9E326f4F4C1B9057164Ef592671cF0D37C8040C0]) [staticcall]
β ββ β 1094795585
ββ β ()
For simple contracts like StorageLayout
, the variables can be manually and easily calculated, however, for more complex contracts that inherit or have multiple variables stored in the same slot, it can become more difficult to work with.
For more complex contract level, there is an option provided by the foundry command 'storage-layout' that allows you to check the storage layout.
β‘ forge inspect --pretty StorageLayout storage-layout
| Name | Type | Slot | Offset | Bytes | Contract |
|-------|-----------------------------|------|--------|-------|--------------------------------------------|
| flag | uint256 | 0 | 0 | 32 | src/test/StorageLayout.t.sol:StorageLayout |
| users | mapping(address => uint256) | 1 | 0 | 32 | src/test/StorageLayout.t.sol:StorageLayout |
| key | bytes | 2 | 0 | 32 | src/test/StorageLayout.t.sol:StorageLayout |
contract ImplFlagManagerV1 {
uint256 public flag;
function getFlag() external view returns (uint256) {
return flag;
}
function setFlag(uint256 _flag) external {
require(0 < _flag, "[ERR-01] Invalid flag");
flag = _flag;
}
}
contract DelegateCallFlagManager {
uint256 public flag;
function getFlag() external view returns (uint256) {
return flag;
}
function delegateCallSet(ImplFlagManagerV1 _impl, uint256 _flag) external {
bytes memory payload = abi.encodeWithSignature(
"setFlag(uint256)",
_flag
);
console.logBytes(payload);
(bool _success, ) = address(_impl).delegatecall(payload);
require(_success, "[ERR-02] Delegate call failed");
}
}
The ImplFlagManagerV1
contract uses the 'flag' state variable and allows for the retrieval of the 'flag' variable through the use of the getFlag()
function and the updating of the 'flag' variable with values greater than zero through the use of the setFlag(uint256)
function.
The DelegateCallFlagManager
contract is similar to the ImplFlagManagerV1
contract, with the exception of the delegateCallSet
function.
The delegateCallSet
function in the DelegateCallFlagManager
contract uses the delegatecall
to perform the logic of the setFlag
function in the ImplFlagManagerV1
contract, using the storage of the DelegateCallFlagManager
contract.
Both contracts should have compatible storage layouts, meaning they should have the same variables assigned to the same storage slots.
The delegatecall
is a low-level primitive and possess the following specifications.
When calling a function in a contract, both the function and the parameters being passed need to be specified. This information needs to be encoded in a well-known format, so that the target contract can interpret it. (ABI encoding)
In the case of normal function calls, Solidity performs this encoding automatically, but when using delegatecall
, it must be done manually.
To understand the internal logic of the delegateCallSet
function, you should check the description below.
bytes memory payload = abi.encodeWithSignature(
"setFlag(uint256)",
_flag
);
// <abi.encodeWithSignature>
// parms1 : function signature
// parms2 : function parameter
// 0xd40c79f00000000000000000000000000000000000000000000000000000000041414141
cast keccak "setFlag(uint256)"
0xd40c79f00eec2930b946749c3d40887e066d1cf5a1d633e066df20cd9e50491d
delegatecall
.(bool _success, ) = address(_impl).delegatecall(payload);
This line of code is executing the ImplFlagManagerV1.setFlag(uint256)
function in the context of the current contract.
Because it is executed using delegatecall
, it uses the storage of the calling contract, DelegateCallFlagManager
.
delegatecall
returns two values: a boolean indicating whether the call was successful, and a byte array containing the returned data.
As there is no separate return handling in this case, you only need to check the success of the call. This is important because when using delegatecall
, the call's failures are not automatically propagated.
//...
function delegateCallGet(ImplFlagManagerV1 _impl)
external
returns (uint256)
{
bytes memory payload = abi.encodeWithSignature("getFlag()");
console.logBytes(payload);
(bool _success, bytes memory _ret) = address(_impl).delegatecall(
payload
);
require(_success, "[ERR-02] Delegate call failed");
return abi.decode(_ret, (uint256));
}
//...
Next, we will similarly process the getFlag()
function through the delegatecall
command. In this case, since the function retrieves the flag
data, we will need to handle the return.
Depending on the return type of the called function, we need to decode it to obtain the byte array in the correct order.
To handle the data returned by the delegatecall
call stored in _ret
, we will cast it to the return type of getFlag()
which is uint256
, it's a fixed width type and it's simply represented by big endian, so it's encoded as 32 bytes padded, so we can cast it and decode it to get the result.
return abi.decode(_ret, (uint256));
abi.decode()
function uses two parameters, a byte array containing the encoded value, and a tuple containing the encoded types.Generalizing delegatecall return handling
delegatecall
with any function, it must be able to handle arbitrary return data as well. This must be implemented at the assembly level.assembly {
let data := add(_ret, 0x20)
let size := mload(_ret)
return(data, size)
}
DelegateCallFlagManager
contract uses the delegatecall
command to execute the logic of the ImplFlagManagerV1
contract's setFlag
and getFlag
functions, while using its own storage. This allows for the logic of the ImplFlagManagerV1
contract to be updated without modifying its code. The abi.decode()
function is used to decode the returned data from the delegatecall
command, and the return(data, size)
command is used to copy a specified amount of memory starting at a certain address. The returned data can be accessed by referencing memory variables such as _ret
. This technique allows for the data returned from a delegatecall
to be easily passed without knowing the encoded value's type.//...
function delegateCallGenericGet(ImplFlagManagerV1 _impl)
external
returns (uint256)
{
bytes memory payload = abi.encodeWithSignature("getFlag()");
(bool _success, bytes memory _ret) = address(_impl).delegatecall(
payload
);
require(_success, "[ERR-02] Delegate call failed");
assembly {
let data := add(_ret, 0x20)
let size := mload(_ret)
return(data, size)
}
}
//...
interface IFallBack {
function client() external;
function server() external;
}
contract System {
event Log(string message);
function client() external {
emit Log("client, Hello");
}
fallback() external {
emit Log("[Fallback] call server");
}
}
IFallBack
interface has defined the client()
and server()
functions, and a contract named System
has been defined that includes the client()
function and the fallback function.IFallBack ifallback = IFallBack(address(new System()));
ifallback.client(); // => Log("client, Hello")
ifallback.server(); // => Log("[Fallback] call server")
A new instance of the contract System
can be created and cast as IFallBack
to call both the client()
and server()
functions.
If the server()
function, which is not defined in the contract System
, is called, the fallback function will be called instead.
One useful fact is that you can access the original call data that triggered the fallback function by using msg.data.
For example, if the fallback function of the contract System
is triggered, by checking msg.data
you can find the signature of the server()
function, which means that you can determine what function the user intended to call by inspecting msg.data.
By using both delegatecall and fallback functions, a general solution for upgradeable contracts based on proxy can be obtained.
Two contracts, the proxy contract and the logic contract, are actually deployed for each contract that can be upgraded.
The proxy contract is the one that stores all the data and the logic contract contains the functions that operate on that data.
Users interact only with the proxy contract
When a user calls a function on the proxy, the proxy uses delegatecall to pass the call to the logic contract
Because the proxy uses delegatecall, the logic contract can execute the function and affect the storage of the proxy contract.
When using upgradeable contracts, the proxy holds the state and the logic contract holds the code.
From the user's perspective, the proxy behaves the same as the logic contract. Upgrading the contract simply means that the proxy points to a new logic contract.
contract FallBackProxy {
address public impl;
function upgradeTo(address _impl) external {
impl = _impl;
}
fallback() external payable {
(bool _success, bytes memory _ret) = impl.delegatecall(msg.data);
require(_success, "[ERR-02] Delegate call failed");
assembly {
let data := add(_ret, 0x20)
let size := mload(_ret)
return(data, size)
}
}
}
Based on the theory previously described, the proxy is set up, but there are a few errors.
The proxy has a single storage variable implementation that stores the address of the logic contract.
By calling the upgradeTo
function, the logic contract can be changed and upgraded. (Of course, as the authorization system is not set up now, anyone can call it, which is not desirable.)
The fallback function uses delegatecall to pass all calls to the logic contract. (Calls to the upgradeTo() function, which is processed by the proxy itself, are excluded) But how can we know which function the user intended to call?
The original origin calldata that triggered the fallback function can be accessed via msg.data.
Calldata includes both the function signature and the parameter values, so it is easy to pass msg.data to delegatecall and it is essential to handle exceptions to see if the call was successful.
function testFallBackProxy() public {
ImplFlagManagerV1 impl = new ImplFlagManagerV1();
FallBackProxy proxy = new FallBackProxy();
vm.expectEmit(false, false, false, true);
proxy.upgradeTo(address(impl));
vm.expectEmit(false, false, false, true);
ImplFlagManagerV1(address(proxy)).setFlag(0x41414141);
ImplFlagManagerV1(address(proxy)).getFlag();
}
The ImplFlagManagerV1
and FallBackProxy
contracts are set up based on logic and proxy contracts, respectively. The upgradeTo
function is used to update the address of the logic contract, address public impl;
, in the proxy contract.
When the ImplFlagManagerV1
is casted to the proxy contract address and the setFlag(0x41414141)
function is called, the fallback function is executed because the setFlag()
function is not defined. In the msg.data, the following data is included.
0xd40c79f00000000000000000000000000000000000000000000000000000000041414141
The setFlag(uint256)
function signature and the parameter 0x41414141.
Updating the proxy contract storage also updates the logic contract storage, as the storage slots are the same. This results in manipulating the value of the incorrect address public impl;
state variable.
Finally, calling ImplFlagManagerV1(address(proxy)).getFlag()
will result in an exception, causing the transaction to fail.
When comparing the storage layout of the two contracts, it can be seen that different variables are stored in slot 0. This results in a unfortunate outcome where the ImplFlagManagerV1.setFlag()
function modifies the 0th slot of the storage of the proxy contract when executed in order to update.
In the proxy contract, the 0th slot contains the address of the logic contract for upgrading and thus, when ImplFlagManagerV1(address(proxy)).setFlag(0x41414141)
is executed, the actual logic contract address becomes the address + 0x41414141 and references a different address.
Specifically, it now points to an undeployed logic contract address, so a delegatecall call is made to a blank account but no data is returned.
One solution to this issue is to use an unstructured storage pattern, which provides a solution to the problem without requiring changes to the logic contract.
The idea is for the proxy to store the logic contract address in a slot that is far enough away from the possible slot collision, so that the proxy can ignore the possibility of collision.
Other solutions include using design patterns like UUPS, transparent proxies, and diamond patterns, but these require changes to the logic contract.
However, these design patterns can be difficult to implement, especially if the logic contract is inherited from other contracts.
A simple but not optimal solution would be to insert dummy storage variables in the conflicting section, but this may be difficult to apply if the logic contract is inherited from other contracts.
contract UpgradeProxy {
bytes32 constant RAND_IMPL_SLOT =
bytes32(uint256(keccak256("eip1967.proxy.implementation")) - 1);
function upgradeTo(address _newImplementation) external {
bytes32 slot = RAND_IMPL_SLOT;
assembly {
sstore(slot, _newImplementation)
}
}
function implementation() public view returns (address _impl) {
bytes32 slot = RAND_IMPL_SLOT;
assembly {
_impl := sload(slot)
}
}
fallback() external payable {
(bool _success, bytes memory _ret) = implementation().delegatecall(
msg.data
);
require(_success, "[ERR-02] Delegate call failed");
assembly {
let data := add(_ret, 0x20)
let size := mload(_ret)
return(data, size)
}
}
}
The main difference between this proxy contract and previous ones is that it does not declare any storage variables. Instead, the address of the logic contract is stored in the RAND_IMPL_SLOT
slot, which is calculated by taking the keccak256 hash of the eip1967.proxy.implementation
string and subtracting 1.
According to the EIP1967 standard, if a custom slot is used to store the logic contract, services like Etherscan can automatically detect the contract proxy and display information about both the proxy and logic contract.
To upgrade the contract, the upgradeTo()
function must modify the address of the slot provided by RAND_IMPL_SLOT
using the sstore
command.
To prevent proxy storage conflicts, it is necessary to check that the layout is clearly compatible.
In fact, the proxy is completely independent of the specific logic contract used. It is enough to write a single proxy that can be used by everyone.
On the other hand, for types such as dynamic arrays that are matched and dynamically resized using keccak hash, the handling may be slightly different.
Slots calculated in this way can actually cause collisions with RAND_IMPL_SLOT
.
The general consensus is that these occurrences are not considered a problem as the likelihood of this happening is low.
Proxies are vulnerable to function signature collisions that can lead to unexpected behavior.
UpgradeTo
function allow anyone to upgrade the contract, and since the behavior of the contract can change significantly during an upgrade, this poses a significant security threat that needs to be addressed in all proxy implementations.
In the previous contract ImplFlagManagerV1
, it was used as a logic contract, but it was simple enough to not have a user-defined constructor. This allows important limitations that occur when using a proxy to be successfully ignored.
The constructor is not actually a function, so it cannot be called by delegatecall. Therefore, a separate initialization function is used.
contract ImplFlagManagerV1 {
bool public Initialized;
uint256 public flag;
function initialize(uint256 _flag) external {
require(!Initialized, "[ERR-01] Already initialized");
Initialized = true;
flag = _flag;
}
function getFlag() external view returns (uint256) {
return flag;
}
function setFlag(uint256 _flag) external {
require(0 < _flag, "[ERR-01] Invalid flag");
flag = _flag;
}
}
To make the proxy pattern upgradeable, we added an Initialized
storage variable and initialize(uint256)
function. Unlike the constructor, the initialize(uint256)
function is a regular function and can be called multiple times.
Because security-sensitive parameters are often set during initialization, we use the Initialized
variable to prevent re-initialization.
contract ImplFlagManagerV2 {
bool public Initialized;
uint256 public flag;
function initialize(uint256 _flag) external {
require(!Initialized, "[ERR-01] Already initialized");
Initialized = true;
flag = _flag;
}
function getFlag() external view returns (uint256) {
return flag;
}
function setFlag(uint256 _flag) external {
require(0 < _flag, "[ERR-01] Invalid flag");
flag = _flag >> 1;
}
}
function testUpgradeProxy() public {
ImplFlagManagerV1 logicV1 = new ImplFlagManagerV1();
UpgradeProxy Uproxy = new UpgradeProxy();
Uproxy.upgradeTo(address(logicV1));
ImplFlagManagerV1 Uproxy_logicV1 = ImplFlagManagerV1(address(Uproxy));
Uproxy_logicV1.initialize(0x41414141);
assertEq(Uproxy_logicV1.getFlag(), 0x41414141);
Uproxy_logicV1.setFlag(0x42424242);
assertEq(Uproxy_logicV1.getFlag(), 0x42424242);
ImplFlagManagerV2 logicV2 = new ImplFlagManagerV2();
Uproxy.upgradeTo(address(logicV2));
Uproxy_logicV1.setFlag(0x41414141);
assertEq(Uproxy_logicV1.getFlag(), (0x41414141 >> 1));
}
The previous steps involve deploying the logic and proxy contracts, and using the proxy contract to connect to the logic contract using the upgradeTo
function (as per EIP1967).
After initializing the ImplFlagManagerV1
logic contract through the proxy contract using the initialize(0x41414141)
function, the getFlag()
function is called to confirm that it returns the same value.
The ImplFlagManagerV2
contract, which has new features, can then be upgraded to through the proxy contract and the verification conditions can be confirmed to have been passed.
How to solve storage collisions between proxy and logic contracts?
Eternal Storage
Inherited Storage
Unstructured Storage
How to solve function signature collisions between proxy and logic contracts?
Function signatures are derived from the keccak hash and internally identified by the 4-byte function signature. This can cause signature collisions when different functions are mapped to the same function signature.
Single contract's two function signature collisions will cause a compiler error and stop, so this is generally not a problem. However, when two functions from different contracts collide, no exception occurs and a problem occurs.
https://medium.com/nomic-foundation-blog/malicious-backdoors-in-ethereum-proxies-62629adf3357
To solve this, the recommended proxy design patterns are as follows:
https://blog.openzeppelin.com/the-transparent-proxy-pattern/
https://eips.ethereum.org/EIPS/eip-1822
https://docs.openzeppelin.com/contracts/4.x/api/proxy#beacon