This article will break down the Ethernaut Level 16: Preservation contract, point out its flaws, and show how the Hack contract takes advantage of these weaknesses. Let’s dive into how delegatecall can be risky by looking at a solution for Ethernaut Challenge 16. The goal here is to take control of a contract by exploiting its use of delegatecall.
The Preservation Contract
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Preservation {
// public library contracts
address public timeZone1Library;
address public timeZone2Library;
address public owner;
uint256 storedTime;
// Sets the function signature for delegatecall
bytes4 constant setTimeSignature = bytes4(keccak256("setTime(uint256)"));
constructor(address _timeZone1LibraryAddress, address_timeZone2LibraryAddress{
timeZone1Library = _timeZone1LibraryAddress;
timeZone2Library = _timeZone2LibraryAddress;
owner = msg.sender;
}
// set the time for timezone 1
function setFirstTime(uint256 _timeStamp) public {
timeZone1Library.delegatecall(abi.encodePacked(setTimeSignature,_timeStamp));
}
// set the time for timezone 2
function setSecondTime(uint256 _timeStamp) public {
timeZone2Library.delegatecall(abi.encodePacked(setTimeSignature,
_timeStamp));
}
}
contract LibraryContract {
// stores a timestamp
uint256 storedTime;
function setTime(uint256 _time) public {
storedTime = _time;
}
}
Contract Analysis
- Library Contracts: The Preservation contract uses two external library contracts to manage time zones. These libraries are referenced via timeZone1Library and timeZone2Library.
- Owner Variable: The contract includes an owner variable to track the contract’s owner.
- delegatecall: The contract uses delegatecall to invoke the setTime function on the library contracts. The setTimeSignature is calculated using the function’s signature hash.
- Vulnerability: The use of delegatecall allows the called function to modify the calling contract’s state. If the library function writes to storage, it can overwrite the Preservation contract’s state variables.
Problem Analysis
- State Variable Overwriting: Using delegatecall, the context of the caller (Preservation) is used, which means the storage layout of the Preservation contract is used by the LibraryContract. If the LibraryContract’s setTime function is called, it can inadvertently overwrite any storage slot in the
Preservation contract that matches its own storage layout. - Security Risks: The primary risk is that an attacker can exploit delegatecall to manipulate the state of the Preservation contract. Specifically, an attacker can replace the library contract address with their
own malicious contract that modifies critical state variables like owner. - Lack of Restrictions: The Preservation contract does not restrict who can call the setFirstTime and setSecondTime functions, making it possible for anyone to trigger the vulnerable delegatecall.
The Attack Contract
To successfully take ownership of the Preservation contract, an attacker must exploit the delegatecall mechanism. Let’s look at the Hack contract designed to achieve this:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Hack {
Preservation target;
address public owner;
constructor(address _target) {
target = Preservation(_target);
}
function attack() external {
target.setFirstTime(uint160(address(this)));
target.setFirstTime(uint160(msg.sender));
}
function setTime(uint256 addr) external {
owner = address(uint160(addr));
}
}
Attack Strategy
1. Deploy the Hack Contract: The attacker deploys the Hack contract, passing the address of the target Preservation contract to its constructor.
2. Overwrite the Library Address: The attack function in the Hack contract is called, which:
- First, sets the timeZone1Library address of the Preservation contract to the address of the Hack contract.
- Then, sets the owner variable of the Preservation contract to the attacker’s address by calling setFirstTime again.
Detailed Explanation of the Hack Contract
1. Overwriting the Library and Owner:
- target.setFirstTime(uint160(address(this))): This call changes the timeZone1Library address in the Preservation contract to the Hack contract’s address. Here’s why this is significant:
- delegatecall Context: When Preservation calls delegatecall, it uses the storage layout of the Preservation contract but the code of the library. By changing timeZone1Library to point to the Hack contract, we can run the Hack contract’s code in the context of Preservation.
- Setting Up for Takeover: Once the Hack contract is set as the library, any delegatecall to it will execute its setTime function but affect the state of the Preservation contract.
- target.setFirstTime(uint160(msg.sender)): After setting the Hack contract as the library, this call runs the setTime function of the Hack contract in the context of Preservation. This is how it works:
- Casting Address: The setFirstTime function is called with the attacker’s address cast to a uint160.
- Changing Owner: The setTime function in the Hack contract assigns the owner variable of the Preservation contract to the attacker’s address.
2. setTime Function:
- setTime(uint256 addr): This function in the Hack contract gets called via delegatecall when setFirstTime is executed. Here’s what happens:
- Parameter Handling: The function takes a uint256 parameter which is supposed to be a timestamp but is actually used as an address here.
- Changing Owner: The function then casts the uint256 parameter to an address and assigns it to the owner variable of the Preservation contract. This is possible because delegatecall allows
the Hack contract’s state changes to affect the Preservation contract’s storage directly.
Best Practices
- Use library Keyword: When writing libraries, use the library keyword, which ensures that the library cannot have its own state.
- Minimize delegatecall Use: Avoid using delegatecall unless absolutely necessary and ensure strict access controls are in place.
- Thorough Testing: Conduct extensive testing to identify and mitigate potential vulnerabilities, ensuring that all edge cases are considered.
This challenge underscores the necessity of using the library keyword for libraries to prevent them from accessing or modifying state variables and highlights the importance of thorough testing and consideration of potential vulnerabilities when designing smart contracts.
Conclusion: Ethernaut Level 16
The Preservation challenge demonstrates the risks associated with using delegatecall to call libraries that can modify state variables. By carefully crafting the Hack contract, an attacker can take ownership of the Preservation contract, showcasing the critical importance of understanding how delegatecall works and ensuring proper security measures are in place.
FAQs
What is the main vulnerability in the Preservation contract?
- The main vulnerability in the Preservation contract is the use of delegatecall to call functions in external library contracts. Since delegatecall executes the code of the library contract in the context of the calling contract (Preservation), it can overwrite state variables in the Preservation contract, leading to potential security breaches.
How does the delegatecall function work in Solidity?
- The delegatecall function in Solidity allows a contract to execute code from another contract (library) but in the context of the calling contract. This means that any state changes made by the library’s code affect the storage of the calling contract, not the library. This can be risky if the library is not trusted or if it can overwrite important state variables.
What is the significance of the library keyword in Solidity?
- The library keyword in Solidity is used to define library contracts that do not have their own state and cannot modify the state of the calling contract. This makes libraries safer to use because they cannot accidentally or maliciously alter the storage of the calling contract.
How can we mitigate the risks associated with delegatecall?
- To mitigate the risks associated with delegatecall:
- Use the library keyword for libraries to ensure they do not have their own state and cannot modify the state of the calling contract.
- Avoid using delegatecall unless absolutely necessary.
- Implement strict access controls to restrict who can call functions that use delegatecall.
- Conduct thorough testing and code reviews to identify and mitigate potential vulnerabilities.
Why is the setFirstTime function used twice in the Hack contract?
- The setFirstTime function is used twice in the Hack contract to first set the timeZone1Library address to the Hack contract’s address, and then to use the setTime function in the context of the Preservation contract to change the owner variable to the attacker’s address.
How does the storage layout affect the delegatecall vulnerability?
- The storage layout is critical because delegatecall uses the storage layout of the calling contract (Preservation). If the library contract has functions that write to storage, these writes will affect the storage slots in the calling contract, potentially overwriting important variables like the owner.