1

I recently started development on a personal project, and one of the suggested methods to achieve what i want was using delegatecall. Essentially I have a main contract that the user interacts with and this main contract will using the implementations of other contracts. From what I read, delegatecall will call the target contract in the context of the calling contract, meaning it will retain the msg.sender/value of the original transaction and any changes made to storage will be in the context of the calling contract. For instance, I have the following code.

contract MainContract{

    address public otherContract;


    constructor (address _otherContract){
        otherContract = _otherContract;
    }

    function registerUser(string memory _username) external {
        (bool success,) = otherContract.delegatecall(
            abi.encodeWithSignature("createUser(string)", _username)
        );
        require(success, "createUser failed");
    }

}

Before i used delegatecall, i was directly calling the function as such OtherContract(otherContract).createUser(_username); and on my test file ran successfully other than the fact that the owner of account wasn't the EOA that originally started the transaction. Below is the function being called in OtherContract(Other contract inherits some events/enums from Activity). Note, I have tested the implementation contracts individually and they work as intended, only when I try to call the functions from a different contract is where I have issues:

contract OtherContract is Activity{
struct User{
        address user;
        string name;
        bool userCreated;
        Status status;
        uint256 creationDate;
    }

mapping(address => User) internal users;
uint256 public totalUsers = 0;

function createUser(string memory _name) external{
        require(!users[msg.sender].userCreated, "Address already in use by active user!");
        users[msg.sender] = User(msg.sender, _name, true, Status.Online, block.timestamp);
        totalUsers++;
        emit LogActivity(msg.sender, ActivityType.User, string.concat("New user created ", _name), block.timestamp);
    }
}

After running the update MainContract not only is the user not being successfully created, but the event is being logged 4 times. From what I understand there are reentry issues when using delegatecall so having a modifier to prevent this was suggested. Another suggestion was to add another parameter for address to pass the EOAs address, but seeing as I have other functions and contracts that rely on msg.sender, I would like to avoid that.

Any suggestions on what I should do? Also is my understanding of delegatecall correct?

0

1 Answer 1

1

The critical thing to know about delegate call is how storage is written.

The behaviour of the function you call will mimic everything including the storage variables are stored at. If the contract you mimic stores B at slot 3, then slot 3 will be used in your contract no matter the variable name. It is critical to know and and act carefully.

In the MainContract that is delegating from OtherContract you have a big storage problem.

Here are some useful links & resources:

enter image description here

enter image description here

7
  • Okay so, correct me if im wrong, although I don't have any variable names for storage slot greater than 2 (only having 1 state variable in MainContract) because in my 2nd contract that im making a delegatecall to, the storage slots in my original contract are updated based off any updates to the corresponding storage slots in OtherContract made in the delegatecall function, right? In OtherContract's case it would be slots 1,2 (users and totalUsers respectively)?
    – pierre96
    Sep 9 at 2:19
  • Sorry I've read it 5 times but I don't understand your question. Basically after the first call it will stop working since the first variable address public otherContract; will be overriden by the struct's pointer. If the storage layout of your two contracts are not the same, then there will be storage slot collision and overriding.
    – Torof
    Sep 9 at 7:13
  • One work around is to predefine at what slots the variable will be saved, using yul. The diamond storage pattern from the ERC2535 uses this.
    – Torof
    Sep 9 at 7:15
  • Apologies, what i was trying to say, OtherContract has 2 state variables, users and totalUsers (slot 1 and 2 respectively). MainContract only has 1 being otherContract and after the delegatecall, slot 1 and 2 for MainContract, will be overwritten by users and totalUsers?
    – pierre96
    Sep 10 at 8:09
  • something like this. users is a mapping so it doesn't use only 1 storage slot. In Solidity, mappings are stored in storage slots in a way that is different from arrays. Arrays are stored sequentially, starting from the first storage slot, while mappings are stored using a hash function. The hash function takes the key of the mapping element as input and produces a 256-bit hash value. This hash value is then used to calculate the storage slot index of the mapping element. Please look at the layout in the links I provided in my answer.
    – Torof
    Sep 10 at 8:17

Not the answer you're looking for? Browse other questions tagged or ask your own question.