In the previous part, we explained some of typical proxy issues related to initialization, lack of state update or frontrunning. In this part, we would like to talk a bit about function and storage conflicts and also about decentralization.
Proxy function clashing
This vulnerability is unlikely to be found unless the proxy is a custom implementation. Using e.g. OpenZeppelin upgradeable contracts is much safer, however in the past, it was the place where this vulnerability has been found. It has been thoroughly explained in articles such as this and this, but let us summarize it in short. Look at the following code, which will not compile as we will get error as in first line:
// Error: DeclarationError: Function with same name and parameter types defined twice. pragma solidity ^0.8.0; contract clash { address owner; function setAdmin(address newOwner) public{ owner = newOwner; } function setAdmin(address newOwner) public { require (msg.sender == owner); owner = newOwner; } }
If such contract compiled, we would have run into trouble since unexpected things might have happened, including possibility of calling the insecure function instead of the secure one. But this is only for reference, as proxy clashing works exactly in the same way.
When a function is called by EVM, it is not however called by its name, but by its selector, which is the first 4 bytes of keccak256 hash of the string constructed of function signature (its name and arguments it takes). Below code from solidity-by-example shows how to get a signature of function:
// SPDX-License-Identifier: MIT pragma solidity ^0.8.17; contract FunctionSelector { /* "transfer(address,uint256)" 0xa9059cbb "transferFrom(address,address,uint256)" 0x23b872dd */ function getSelector(string calldata _func) external pure returns (bytes4) { return bytes4(keccak256(bytes(_func))); } }
So if there is a hash and not function name, shouldn’t it be unique? Well, if it were for full hash, maybe, but since its just first 4 bytes, it is easy to create a collision. But it again, will not work in the same contract, since solidity compiler will be able to detect such clash. However, when a proxy is used, it is possible that a proxy might implement a function (other than standard fallback etc.) that have the same signature as a function in the implementation. In such case, the function in the proxy will be called first, which might have unexpected security implications. This behavior is already well described here.
Proxy storage clash
Another “clash” type issue concerns storage. As you probably know, proxy separates data and logic (implementation) layer. Knowing this, we also highlight that during upgrade, the logic is changed. It may contain references to some variables, which is normal thing. However, these variables are not being referenced by name, but by its order in data contract storage. So in short, if contract v1 has two variables: Owner and Manager, and contract v2 wants to implement third one, it should also implement the previous variables in the same order.
pragma solidity ^0.8.0; contract Contractv1 { address Owner; address Manager; } contract Contractv2 { address User; address Owner; address Manager; }
Since solidity slots in memory, in simple terms, are just a “boxes” next to each other, then in above code, if a upgrade is performed from contract v1 to v2, the new version will have the User variable referring to the same slot which previously was reserved for Owner. This may result in unexpected circumstances. Also that behavior is extensively described here.
Decentralization concerns
It of course should be noted that upgradeability breaks the idea of immutability – if a contract can be upgraded, then its code can be changed by anyone who have access to proper deployer key. It is extremely important that the deployer address is not a standalone entity but rather a multisignature wallet, which especially in case of a compromise of single admin, will not lead to replacing the contract with malicious one.