Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

GMS-2051 Multi-caller v2 #241

Merged
merged 19 commits into from
Sep 2, 2024
Merged

GMS-2051 Multi-caller v2 #241

merged 19 commits into from
Sep 2, 2024

Conversation

hiep-immutable
Copy link
Contributor

@hiep-immutable hiep-immutable commented Aug 22, 2024

This PR aims to improve the current implementation of Guarded Multi-caller

Regarding naming a new version, we follow <Contract><VersionNumber> style. This style is adopted by CREATE factory (CREATE, CREATE2, and CREATE3).

Improve transaction's visibility with function signatures

In the v1 implementation, call data is a concatenation of a function selector and its arguments. This is done for efficiency and to align with multi-call standards. However, function selectors provide almost no information about what the functions do, which makes it very complex to understand the flow of a crafting transaction and what it does. We fix this issue by passing function signatures separately instead of encoding function selectors into the _data param right away.

Use struct

Instead of separating _targets and _data into different arrays, we can define a struct with one array for extensibility.

Removal of function permits

https://immutable.atlassian.net/wiki/spaces/~6376e9423c26ca7fa0d17406/pages/2701230264/Multi-caller+Function+Permit+Removal
The function permits degrading developer experience while providing little security improvements. This PR removes the function permits.

Test Coverage

File % Lines % Statements % Branches % Funcs
contracts/multicall/GuardedMulticaller2.sol 97.22% (35/36) 97.78% (44/45) 100.00% (20/20) 85.71% (6/7)

Copy link

openzeppelin-code bot commented Aug 22, 2024

GMS-2051 Multi-caller v2

Generated at commit: 2af8c046f91c38202f640a49dfe80645df5d0c25

🚨 Report Summary

Severity Level Results
Contracts Critical
High
Medium
Low
Note
Total
2
0
0
11
27
40
Dependencies Critical
High
Medium
Low
Note
Total
0
0
0
0
0
0

For more details view the full report in OpenZeppelin Code Inspector

@hiep-immutable hiep-immutable marked this pull request as ready for review August 26, 2024 13:46
@hiep-immutable hiep-immutable changed the title Multi-caller v2 GMS-2051 Multi-caller v2 Aug 26, 2024
@hiep-immutable hiep-immutable requested review from a team and drinkcoffee August 27, 2024 02:39
Copy link

@audityourcontracts audityourcontracts left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:lgtm:

revert EmptyCallArray();
}
for (uint256 i = 0; i < _calls.length; i++) {
if (bytes(_calls[i].functionSignature).length == 0) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code coverage report indicates this branch isn't tested

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that I used the following, and then searched for GuardedMulticaller2:
forge coverage --report debug > x.txt

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found the debug report not reliable. I added a test that should cover that line:
https://github.com/immutable/contracts/pull/241/files#diff-7891bd472e64eb648ea5f38d97917eacbfc82723eeb5beffe2c65ee58c4c6b30R268

calls[0] = GuardedMulticaller2.Call(address(target), "", abi.encodePacked(uint256(42)));

if (bytes(_calls[i].functionSignature).length == 0) {
revert InvalidFunctionSignature(_calls[i]);
}
if (_calls[i].target.code.length == 0) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code coverage report indicates this branch isn't tested

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to the comment above. There is a test that should cover that line:
https://github.com/immutable/contracts/pull/241/files#diff-7891bd472e64eb648ea5f38d97917eacbfc82723eeb5beffe2c65ee58c4c6b30R152

calls[0] = GuardedMulticaller2.Call(address(0), "succeedWithUint256(uint256)", abi.encodePacked(uint256(42)));

// solhint-disable avoid-low-level-calls
// slither-disable-next-line calls-loop
(bool success, bytes memory returnData) = _calls[i].target.call(callData);
if (!success) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the coverage report is indicating that this branch isn't being tested

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(bool success, bytes memory returnData) = _calls[i].target.call(callData);
if (!success) {
// Look for revert reason and bubble it up if present
if (returnData.length == 0) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this return data length is less that four bytes, then the error returned is a non-valid ABI encoded revert result: that is, not a four byte function selector followed by encoded parameters. This would probably because the call was made to a contract that doesn't follow ABI encoding rules. Given this, the "revert(add(returnData, 32), mload(returnData))" will reference a memory location, likely never used thus far containing zeros.

I suggest changing to:
if (returnData.length < 4) {

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This logic is similar to the logic used by OpenZeppelin Address library. Not sure if this is something they failed to catch.

https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/Address.sol#L138

function _revert(bytes memory returndata) private pure {
        // Look for revert reason and bubble it up if present
        if (returndata.length > 0) {
            // The easiest way to bubble the revert reason is using memory via assembly
            /// @solidity memory-safe-assembly
            assembly {
                let returndata_size := mload(returndata)
                revert(add(32, returndata), returndata_size)
            }
        } else {
            revert Errors.FailedCall();
        }
    }

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have updated the logic to use 4 bytes for comparison and also extend FailedCall error to accept returnData as its param in case there is data returned from the function call and having less than 4 bytes.

@drinkcoffee
Copy link
Contributor

One potential scenario that could be guarded against that isn't: The last account with DEFAULT_ADMIN_ROLE mistakenly calls renounceRole, thus leaving the contract with no account with DEFAULT_ADMIN_ROLE. The only reason for allowing this is so that the contract could become permissionless. However, I don't think any game studio would want this.

@drinkcoffee drinkcoffee self-requested a review August 28, 2024 23:27
Copy link
Contributor

@drinkcoffee drinkcoffee left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please address comments

@hiep-immutable
Copy link
Contributor Author

One potential scenario that could be guarded against that isn't: The last account with DEFAULT_ADMIN_ROLE mistakenly calls renounceRole, thus leaving the contract with no account with DEFAULT_ADMIN_ROLE. The only reason for allowing this is so that the contract could become permissionless. However, I don't think any game studio would want this.

@drinkcoffee I have updated our threat model with the following steps if such scenario happens:

  • Accounts with MULTICALL_SIGNER_ROLE should renounce their roles
  • A new Guarded Multi-caller contract should be deployed.
  • All roles/accesses granted to the contract should be revoked.

@hiep-immutable hiep-immutable merged commit 71d1d5f into main Sep 2, 2024
6 of 7 checks passed
@hiep-immutable hiep-immutable deleted the multi-caller-improvements branch September 2, 2024 03:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

5 participants