The delegatecall
method is a powerful low-level function that enables one contract (the "Caller") to execute code in another contract (the "Callee") while preserving the storage and context of the Caller. Unlike the call
method, which executes code in a different contract but does not retain the Caller’s state, delegatecall
maintains the context of the Caller’s storage, allowing for more flexible contract design. However, as with any low-level operation, improper use of delegatecall
can introduce significant security risks. For a deeper understanding of the call
method, be sure to check out my previous article, The call Function in Solidity.
What is delegatecall?
The delegatecall
function in Solidity enables a contract to call another contract’s function but retain its own storage, sender, and msg.value
. This is useful when you want to separate logic from storage, allowing the Caller to use the Callee’s logic while maintaining its own state.
Common Use Cases:
Upgradeable Contracts: The Proxy pattern uses delegatecall to upgrade contract logic without affecting stored data.
Modular Contracts: Break down complex functionality into separate contracts to improve code reusability and maintainability.
Basic Example of delegatecall
In this example, the Caller contract calls setNum
in the Callee contract to set a number. The state change happens in the Caller’s storage rather than the Callee’s.
Callee Contract
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Callee {
uint public num;
function setNum(uint _num) public {
num = _num;
}
}
Caller Contract
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Caller {
uint public num;
address public calleeAddress;
uint public num; // State variable that MUST be in storage slot 0 to matche the storage layout of the Callee contract
function setCalleeAddress(address _calleeAddress) public {
calleeAddress = _calleeAddress;
}
/**
* @dev Calls the setNum function of the Callee contract using delegatecall.
* The logic of the Callee's setNum function is executed in the context of the Caller contract.
* This updates the `num` variable in the Caller contract's storage, NOT the Callee's.
*
* NOTE: The `num` variable in both the Caller and Callee contracts MUST occupy the same storage slot.
* In this case, `num` is the first declared state variable in both contracts, which means it is in slot 0.
*
* @param _num The value to set the Caller contract's `num` variable to.
*/
function delegateSetNum(uint _num) public {
(bool success, ) = calleeAddress.delegatecall(
abi.encodeWithSignature("setNum(uint256)", _num)
);
require(success, "delegatecall failed");
}
}
Explanation:
The
setCalleeAddress
function stores the address of the Callee contract.delegateSetNum
usesdelegatecall
to executesetNum
in Callee, but any state changes are applied in Caller’s storage.
Risks and Best Practices of Using delegatecall
While delegatecall
is powerful, improper use can introduce serious security risks. Here’s what to consider:
-
Storage Layout Consistency
- The storage layout between the Caller and Callee must match exactly. Any mismatch can cause unexpected behavior and data corruption.
-
Reentrancy Attacks
- Using
delegatecall
with untrusted contracts can expose vulnerabilities, such as reentrancy attacks. Apply the checks-effects-interactions pattern to minimize risk.
- Using
-
Avoid Unauthorized Access
- Ensure
delegatecall
does not inadvertently expose sensitive functions. Use access control to limit function calls as needed.
- Ensure
Example of a Security Vulnerability
In the following example, HackMe
calls Lib's pwn
function with delegatecall
, allowing anyone to take over the HackMe contract by changing the owner.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Lib {
address public owner;
function pwn() public {
owner = msg.sender;
}
}
contract HackMe {
address public owner;
Lib public lib;
constructor(Lib _lib) {
owner = msg.sender;
lib = Lib(_lib);
}
// Delegate all external calls to Lib contract, including unauthorized access to pwn
fallback() external payable {
address(lib).delegatecall(msg.data);
}
}
Explanation:
- Anyone calling
pwn
can change the owner ofHackMe
becauseHackMe
uses Lib’s storage layout, unintentionally exposing itsowner
variable.
Practical Tips for Using delegatecall
Safely
To maximize the benefits of delegatecall
while minimizing risks, follow these tips:
Check Storage Compatibility: Ensure that the Caller and Callee contracts have matching storage layouts to avoid corruption.
Validate
delegatecall
Targets: Only use trusted contracts as targets fordelegatecall
.Use Access Control: Restrict access to functions that use
delegatecall
to avoid unauthorized actions.
Conclusion
The delegatecall
function in Solidity provides flexibility for modular and upgradeable smart contracts. However, due to its powerful nature, it’s essential to use delegatecall
with caution, carefully managing storage layouts and implementing security checks. Proper use of delegatecall
can open up many possibilities for flexible contract development, but with poor implementation, it can introduce serious vulnerabilities.
Top comments (1)
Interesting source!