3.35 Proxy Pitfalls
The next set of security pitfalls and best practices that we'll discuss is related to Proxy-based contracts.
Remember that Proxy-based architectures are used for upgradability and other aspects desired in smart contract applications. In this Proxy setup, there is typically a Proxy contract that does a delegatecall
to a logic contract, and because of the delegatecall
, the logic contract gets to implement logic that executes on the state of the Proxy contract.
Under this specific setup, due to the delegatecall
the data, the logic aspects has specific requirements that need to be met by both the Proxy as well as the logic contract. These lead to some security pitfalls and best practices.
Initializers
In this particular pitfall, initializer functions should not be callable multiple times: they should be callable only once, and by the authorized Proxy contract as soon as the logic contract has been deployed.
Remember that under a Proxy setup, the implementation contract can't use a constructor to initialize its state, because it is working with the state of the Proxy contract that does a delegatecall
. So instead, implementation contract is expected to declare an initializer function which does all the required initializations for it. Such functions need to have an external
or public
visibility because they need to be callable from an external contract (which is the Proxy contract).
The deployment is typically done from a deploy script or from a factory contract. Preventing multiple invocations is critical because such invocations could happen from unauthorized contracts (or unauthorized users). In those cases, they could re-initialize the contract with values that let them exploit some of the contract functionality.
The best practice here is to use OpenZeppelin
's Initializable
library, which provides an initializer modifier that can be applied to the initialize function, preventing it from being called multiple times.
State Variables
This is a pitfall related to the previous one discussed. This specifically applies to initializing state variables in the Proxy-based setup.
Constructors should not be used in the implementation (or logic contracts) to initialize its state, but using an initializer function instead. State variables in the implementation contract, similarly, should not be initialized in their declarations themselves because such initializations will not be reflected when the Proxy contract makes a delegatecall
to this implementation.
So instead, the state variables should be initialized within the initializer function because otherwise they would not be set when the delegatecall
happens.
Import Contracts
The contracts used in a Proxy setup may also derive from other libraries or other contracts within the project itself, which could be defined in other files. In this case they are imported to be used in the Proxy contract.
These imported contracts that the Proxy contracts derive from, should also adhere to the same rules discussed: the base contracts should also not use a constructor, they should be using an initializer function. In addition, such contacts should also not initialize state variables during declaration.
The best practice is to make sure that the imported contracts also follow those rules, because if not, the state would be uninitialized and using that state could result in undefined behavior or potentially even serious vulnerabilities.
selfdestruct
selfdestruct
If a Data Proxy calls a logic implementation contract that has a selfdestruct
call in it, then that logic contract would end up getting destroyed and thereafter all calls to that logic contract will end up delegating calls to an address without any code.
Similarly, the use of delegatecall
may also cause issues because the logic implementation works with the state of the Data Proxy.
The best practice is to avoid entirely the use of selfdestruct
or delegatecall
s with Proxy-based contracts.
State Variables
In Proxy-based contracts, the order layout type and mutability of state variables declared in the proxy, the corresponding implementation (or different versions of the implementation), should be preserved exactly while upgrading. This is to prevent storage layout mismatch errors. These ones can lead to very critical errors if they are inherited.
The best practice is to make sure that these aspects of state variables are exactly the same in Proxy-based setups.
Function Id
Remember that Solidity
and EVM have the notion of a function selector which is the keccak256()
hash of the function signatures.
These selectors are used to determine which contact function is being called, so at runtime the function dispatcher in the contract byte code should determine (by looking at the function selector) if one of the functions in the proxy is being called or if this call needs to be delegated to the implementation contract.
In Proxy-based setups, a malicious Proxy contract may declare a function such that its function Id collides (is the same as) with one of the Proxy Functions. So even though the call was targeting an implementation function, the malicious proxy, hijacks that call and could lead to the execution of a function that can cause an exploit.
The best practice here is to pay attention to the proxy, any trust assumptions related to the proxy and implementation contracts. Also, to check if the proxy has or can declare a function whose Id might collide with one of the implementation contract functions.
Shadowing
Instead of the Proxy contract trying to hijack calls meant for the implementation by declaring functions whose Id collides with the implementation contract functions, they could simply shadow the functions in the implementation contract.
This means that a Proxy contract can declare functions that have the same name, the same parameter numbers and types as functions in the implementation contract. In such a scenario, the function dispatcher would simply call the proxy contact function instead of forwarding it to the implementation contract.
This way, a malicious proxy can intercept (or hijack) calls instead of delegating it to the implementation contract and exploit this aspect to cause malicious behavior.
The best practice here is again to pay attention to the Proxy contract, the implementation contract, look at all the functions declared in both these contracts to see if any of the Proxy Functions are indeed Shadowing those in the implementation contract. If that is the case, recognize that such functions will be executed in the context of the proxy without being forwarded to the implementation contract.
Last updated