Platform: Code4rena
Start Date: 18/02/2022
Pot Size: $125,000 USDC
Total HM: 13
Participants: 24
Period: 14 days
Judge: GalloDaSballo
Total Solo HM: 6
Id: 88
League: ETH
Rank: 21/24
Findings: 1
Award: $350.79
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: IllIllI
Also found by: 0x1f8b, CertoraInc, TerrierLover, Tomio, WatchPug, d4rk, gzeon, kenta, kyliek, m_smirnova2020, rfa, robee, saian, ye0lde
Let's take a look at the getContractRegisteredRange
function in the MessageProxy
contract:
function getContractRegisteredRange( bytes32 schainHash, uint256 from, uint256 to ) external view override returns (address[] memory contractsInRange) { require( from < to && to - from <= 10 && to <= _getRegistryContracts()[schainHash].length(), "Range is incorrect" ); contractsInRange = new address[](to - from); for (uint256 i = from; i < to; i++) { contractsInRange[i - from] = _getRegistryContracts()[schainHash].at(i); } }
In this function, the expression _getRegistryContracts()[schainHash]
is used and calculated multiple times, and it can be calculated once. The code will look something like this:
function getContractRegisteredRange( bytes32 schainHash, uint256 from, uint256 to ) external view override returns (address[] memory contractsInRange) { EnumerableSetUpgradeable.AddressSet storage set = _getRegistryContracts()[schainHash]; require( from < to && to - from <= 10 && to <= set.length(), "Range is incorrect" ); contractsInRange = new address[](to - from); for (uint256 i = from; i < to; i++) { contractsInRange[i - from] = set.at(i); } }
This can be done also in the registerExtraContractForAll
function of the MessageProxy
contract:
function registerExtraContractForAll(address extraContract) external override onlyExtraContractRegistrar { require(extraContract.isContract(), "Given address is not a contract"); require(!_getRegistryContracts()[bytes32(0)].contains(extraContract), "Extra contract is already registered"); _getRegistryContracts()[bytes32(0)].add(extraContract); emit ExtraContractRegistered(bytes32(0), extraContract); }
We can save the return value of _getRegistryContracts()[bytes32(0)] instead of calling it twice. The code will be:
function registerExtraContractForAll(address extraContract) external override onlyExtraContractRegistrar { require(extraContract.isContract(), "Given address is not a contract"); EnumerableSetUpgradeable.AddressSet storage set = _getRegistryContracts()[bytes32(0)]; require(!set.contains(extraContract), "Extra contract is already registered"); set.add(extraContract); emit ExtraContractRegistered(bytes32(0), extraContract); }
getContractRegisteredRange
(also saving a variable instead of i - from
)Loops can be optimized in several ways. Let's look for example at the loop in the getContractRegisteredRange
function of the MessageProxy
contract:
function getContractRegisteredRange( bytes32 schainHash, uint256 from, uint256 to ) external view override returns (address[] memory contractsInRange) { require( from < to && to - from <= 10 && to <= _getRegistryContracts()[schainHash].length(), "Range is incorrect" ); contractsInRange = new address[](to - from); for (uint256 i = from; i < to; i++) { contractsInRange[i - from] = _getRegistryContracts()[schainHash].at(i); } }
We can do several things here:
i - from
in every iteration by saving another index.
The code will look something like this:function getContractRegisteredRange( bytes32 schainHash, uint256 from, uint256 to ) external view override returns (address[] memory contractsInRange) { require( from < to && to - from <= 10 && to <= _getRegistryContracts()[schainHash].length(), "Range is incorrect" ); contractsInRange = new address[](to - from); uint index; for (uint256 i = from; i < to; ++i) { contractsInRange[index++] = _getRegistryContracts()[schainHash].at(i); } }
Another thing that can be done in this specific function (getContractRegisteredRange
function of the MessageProxy
contract) is that the expression to - from
can be calculated once instead of twice, and can be calculated using unchecked becuase of the check above. The code will look like this:
// ... require(from < to, "Range is incorrect"); unchecked { uint len = to - from; require(len <= 10, "Range is incorrect"); contractsInRange = new address[](len); } // ...
contains
in most casesIn a lot of places in the contracts you call the contains function to check if an element in the set, and only then removing or adding it. But you can just check the return value of the add or remove function - add returns true if the element was added successfully, which means that he wasn't in the set before. The remove function returns true if the element was removed successfully, which means that he wasn in the set before. So instead of writing something like this:
require(!_getRegistryContracts()[bytes32(0)].contains(extraContract), "Extra contract is already registered"); _getRegistryContracts()[bytes32(0)].add(extraContract);
You can simply use:
require(_getRegistryContracts()[bytes32(0)].add(extraContract), "Extra contract is already registered");
This will save the gas spent on the call to the contains function.
This repeats all over the contracts, the places I saw this happening
registerExtraContractForAll
, removeExtraContractForAll
, _registerExtraContract
, _removeExtraContract
functionsMessageProxyForMainnet
contractIn the MessageProxyForMainnet
contrcat, there are some loops that can be optimize. Let's take a look at the loop in the initializeAllRegisteredContracts
function:
for (uint256 i = 0; i < contracts.length; i++) { if ( deprecatedRegistryContracts[schainHash][contracts[i]] && !_registryContracts[schainHash].contains(contracts[i]) ) { _registryContracts[schainHash].add(contracts[i]); delete deprecatedRegistryContracts[schainHash][contracts[i]]; } }
We can do several things here:
contracts
array length in a local variable instead of accessing it in every iteration.contracts[i]
in a local variable instead of accessing it 4 times in every iteration. This will save accssing the array's ith element 4 times in every iteration ,which requires an address calculation.address contracts_i; uint len = contracts.length; for (uint256 i; i < len; ++i) { contracts_i = contracts[i]; if ( deprecatedRegistryContracts[schainHash][contracts_i] && _registryContracts[schainHash].add(contracts_i) ) { delete deprecatedRegistryContracts[schainHash][contracts_i]; } }
Another loop that can be optimized is in the postIncomingMessages
of this contract:
for (uint256 i = 0; i < messages.length; i++) { gasTotal = gasleft(); if (isContractRegistered(bytes32(0), messages[i].destinationContract)) { address receiver = _getGasPayer(fromSchainHash, messages[i], startingCounter + i); _callReceiverContract(fromSchainHash, messages[i], startingCounter + i); notReimbursedGas += communityPool.refundGasByUser( fromSchainHash, payable(msg.sender), receiver, gasTotal - gasleft() + additionalGasPerMessage ); } else { _callReceiverContract(fromSchainHash, messages[i], startingCounter + i); notReimbursedGas += gasTotal - gasleft() + additionalGasPerMessage; } }
The things we can do here:
messages
array length in a local variable instead of accessing it in every iteration.messages[i]
in a local variable instead of accessing it multiple times in every iteration.startingCounter
instead of calculating startingCounter + i
in every iteration.modifier messageInProgressLocker() { require(!messageInProgress, "Message is in progress"); messageInProgress = true; _; messageInProgress = false; }
The reenterncy guard in the MessageProxyForMainnet
contract can be optimized. In solidity, changing a variable value from its default value to another value costs more gas then changing it from a non-default value to a non-default value (really weird, I know). So in the current implementation the operation of changing the lock from false to true can be optimized. In order to optimize it, you can use uint8 instead of bool, and use values 1 and 2 for the lock (1 for locked, 2 for unlocked).
This will look like this:
uint8 messageInProgress = 1; modifier messageInProgressLocker() { require(messageInProgress != 2, "Message is in progress"); messageInProgress = 2; _; messageInProgress = 1; }
There are lots of loops in these contrcats, and the tricks I explained before will work here too. Every optimization will save gas - even changing the i++
to ++i
.
getSchainToAllERC721
In the mentioned function, the hash is calculated in order to access a mapping element. But instead of calculating it once and save the hash value, it's being calculated in every iteration, and that will cost extra gas for nothing. More than that, we can save all set instead of accessing the mapping in every interation.
Code before:
function getSchainToAllERC721( string calldata schainName, uint256 from, uint256 to ) external view override returns (address[] memory tokensInRange) { require( from < to && to - from <= 10 && to <= _schainToERC721[keccak256(abi.encodePacked(schainName))].length(), "Range is incorrect" ); tokensInRange = new address[](to - from); for (uint256 i = from; i < to; i++) { tokensInRange[i - from] = _schainToERC721[keccak256(abi.encodePacked(schainName))].at(i); } }
Code after:
function getSchainToAllERC721( string calldata schainName, uint256 from, uint256 to ) external view override returns (address[] memory tokensInRange) { EnumerableSetUpgradeable.AddressSet set = _schainToERC721[keccak256(abi.encodePacked(schainName))]; require( from < to && to - from <= 10 && to <= set.length(), "Range is incorrect" ); tokensInRange = new address[](to - from); for (uint256 i = from; i < to; i++) { tokensInRange[i - from] = set.at(i); } }
#0 - yavrsky
2022-03-16T02:28:11Z
Only marginal gas improvements.
#1 - GalloDaSballo
2022-04-28T15:25:28Z
I'm not convinced that storing the storage pointer will actually save gas
I believe this would save 6 gas per iteraton
Will save 100 gas per instance -> 5 = 500
6 gas
This is a pretty meaningful gas saving in that the current one will cost 20k to set and refund 15k (gas refunds being capped at 1/5 call cost), while the proposed version will cost 5k to set and 5k to unset, with a refund because the original value is set back, this should bring the cost down meaningfully, saving 15k gas
15 gas
Total gas saved: 15527