Storage across contracts

Hi there, just wondering if it is possible to to update a state variable in another contract by specifying data location as storage.

The following example cannot compile, but it shows the goal I’d like to achieve, modifying a variable in a struct in another contract. A working solution would be getting a copy of the struct and making change to it and setting the new value to the original contract. Basically, I’m wondering if this can be simplified

// SPDX-License-Identifier: MIT

pragma solidity 0.8.4;

library Helper {
    struct Info {
        uint256 _height;
        uint256 _weight;
        bool _isFemale;
    }
}

contract Storage {

    Helper.Info _info;

    function getInfo() public view returns(Helper.Info storage) {
        return _info;
    }
}

contract Test {
    address storageAddr;

    constructor (address storageAddr_) {
        storageAddr = storageAddr_;
    }

    function updateHeight(uint256 newHeight) public {
        Helper.Info storage info = Storage(storageAddr).getInfo();
        info._height = newHeight;
    }
}

No. storage variables are pointers into your contract’s own storage.

It’s by design - if you could modify another contract’s storage just like that, contracts could not protect it from modification. So you can only ask another contract to modify its own storage for you by calling its external functions (if it has them).

You can do this the other way around though - you can use DELEGATECALL to let another contract modify your storage. The language will only let you do that with libraries - they can take storage parameters:

library L {
    function updateHeight(Info storage info, uint newHeight) public {
        info._height = newHeight;
    }
}

EVM actually lets you do DELEGATECALL between contracts too but there’s no syntax for that in Solidity other than the low-level delegatecall().

And the storage in the library is still referring to where this library is embeded in, correct?

This was just a flash of thought in my mind. Is it possible to add this as a new functionality? Here is the use case. For an external storage pattern, to update a struct in the storage contract, as illustrated above, one has to retrieve a copy of it in this contract, and store it in memory as a new struct instance, and get it updated and send it back to the storage contract and call setter in the storage contract.

If a storage pointer can be accessed by another contract, the above process can be reduced to just two steps: read and modify.

And the storage in the library is still referring to where this library is embeded in, correct?

Yes. It’s a pointer into the storage of the calling contract. Actually, it’s a pointer into the storage space of the library but DELEGATECALL maps contract’s storage into that space so it’s one and the same in such a call.

Is it possible to add this as a new functionality?

No, because it’s not just a case of a missing feature. It’s a restriction imposed by the EVM. When a contract is given access to another contract’s storage through DELEGATECALL it loses access to its own storage. That’s the reason why Solidity does not let you put storage variables in libraries - you would not be able to access them. There are no special EVM instructions for accessing other contract’s storage.

1 Like

Actually, it’s a pointer into the storage space of the library but DELEGATECALL maps contract’s storage into that space so it’s one and the same in such a call.

I see. This is actually answering a question buried in my mind a while back. The question is if functions in a library are being called via DELEGATECALL, how the state variable layout gets handled. Does a library generate a storage layout in memory in run-time? A library can be called and used by multiple contracts. I would assume a library would generate multiple layouts?

The library does not know the layout. If you want to let a library access a storage variable in your contract, you must pass the variable into a storage parameter of its function, i.e. explicitly tell the library where the variable is located.

library L {
    function foo(uint[] storage array) public {}
}

contract C {
    uint[] a;

    function test() public {
        L.foo(a);
    }
}

contract D {
    address a;
    C c;
    uint[] b;

    function test() public {
        L.foo(b);
    }
}

Hi @cameel thanks for this. This way of using a library is obvious. I’ve also come across alternative ways of using a library. See the following snippets for an example. The library TransferHelper and Helper can be used in multiple contracts with different storage layouts.

TransferHelper.safeTransfer(tokenAddr_, receiver_, 1000);

Helper.concatStrings(stateVariable1, stateVariable2);

In scenarios like the two comands above, how does a delegatecall made from a calling contract to a library? Thanks.

// SPDX-License-Identifier: MIT

pragma solidity 0.8.4;

library TransferHelper {
    function safeApprove(address token, address to, uint value) internal {
        require(token.code.length > 0);
        // bytes4(keccak256(bytes('approve(address,uint256)')));
        (bool success, bytes memory data) = token.call(abi.encodeWithSelector(0x095ea7b3, to, value));
        require(success && (data.length == 0 || abi.decode(data, (bool))), 'TransferHelper: APPROVE_FAILED');
    }

    function safeTransfer(address token, address to, uint value) internal {
        require(token.code.length > 0);
        // bytes4(keccak256(bytes('transfer(address,uint256)')));
        (bool success, bytes memory data) = token.call(abi.encodeWithSelector(0xa9059cbb, to, value));
        require(success && (data.length == 0 || abi.decode(data, (bool))), 'TransferHelper: TRANSFER_FAILED');
    }
}
// SPDX-License-Identifier: MIT

pragma solidity 0.8.4;

library Helper {
    function compareStrings(
        string memory str1,
        string memory str2
    )
        internal
        pure
        returns (bool)
    {
        return keccak256(abi.encodePacked(str1)) == keccak256(abi.encodePacked(str2));
    }

    function concatStrings(
        string memory str1,
        string memory str2
    )
        internal
        pure
        returns (string memory)
    {
        return string(abi.encodePacked(str1, str2));
    }

Now I’ve come to realize there are two ways of using a library. If functions are internal in a library, they are embedded into a contract. If they are external or public, then they need to get ‘linked’ and are called by delegatecall.

1 Like

Yeah, exactly. internal and private functions work just like internal and private functions in contracts. I.e. they are not callable from the outside and are compiled into any contract that calls them. Same is true for free functions.

external library functions can only be called via DELEGATECALL and their ABI encoding for parameters is slightly different than in contracts because they support more types (storage parameters, mappings, etc.). public functions result in an internal call only when called from within the same library. They result in a DELEGATECALL when called from anywhere else (contract, another library, free function, etc.).

The library TransferHelper and Helper can be used in multiple contracts with different storage layouts.

Note that your functions do not access storage at all since they do not have any storage parameters. If you pass a storage variable via a normal parameter you get a copy of the value rather than a pointer that the function could use to modify the variable.

2 Likes

Hi. I had one small query regarding the discussion. If we mark the library function as internal does that mean that the keyword storage can be used in library and store the variables in context of the contract thats importing the library?