Platform: Code4rena
Start Date: 07/10/2022
Pot Size: $50,000 USDC
Total HM: 4
Participants: 62
Period: 5 days
Judge: 0xean
Total Solo HM: 2
Id: 169
League: ETH
Rank: 42/62
Findings: 1
Award: $20.79
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: IllIllI
Also found by: 0x1f8b, 0xNazgul, 0xSmartContract, 0xdeadbeef, B2, Bnke0x0, Deivitto, ElKu, Jujic, KoKo, Pheonix, RaymondFam, RedOneN, RockingMiles, Rolezn, Saintcode_, Shinchan, TomJ, Tomio, __141345__, ajtra, aysha, c3phas, carlitox477, catchup, delfin454000, emrekocak, erictee, fatherOfBlocks, gerdusx, gianganhnguyen, gogo, martin, mcwildy, medikko, oyc_109, pedr02b2, rbserver, ret2basic, rotcivegaf, saian, sakman, zishansami
20.7905 USDC - $20.79
Total of 11 issues found.
 
SLOADs cost 100 gas where MLOADs/MSTOREs cost only 3 gas. Whenever function reads storage value more than once, it should be cached to save gas.
Total of 3 instances found.
Cache l1GRT
of outboundTransfer() of L2GraphTokenGateway.sol
2 SLOADs to 1 SLOAD, 1 MSTORE and 2 MLOAD
https://github.com/code-423n4/2022-10-thegraph/blob/309a188f7215fa42c745b136357702400f91b4ff/contracts/l2/gateway/L2GraphTokenGateway.sol#L145
https://github.com/code-423n4/2022-10-thegraph/blob/309a188f7215fa42c745b136357702400f91b4ff/contracts/l2/gateway/L2GraphTokenGateway.sol#L156
Cache l1GRT
of finalizeInboundTransfer() of L2GraphTokenGateway.sol
2 SLOADs to 1 SLOAD, 1 MSTORE and 2 MLOAD
https://github.com/code-423n4/2022-10-thegraph/blob/309a188f7215fa42c745b136357702400f91b4ff/contracts/l2/gateway/L2GraphTokenGateway.sol#L233
https://github.com/code-423n4/2022-10-thegraph/blob/309a188f7215fa42c745b136357702400f91b4ff/contracts/l2/gateway/L2GraphTokenGateway.sol#L236
Cache escrow
of finalizeInboundTransfer() of L1GraphTokenGateway.sol
2 SLOADs to 1 SLOAD, 1 MSTORE and 2 MLOAD
https://github.com/code-423n4/2022-10-thegraph/blob/309a188f7215fa42c745b136357702400f91b4ff/contracts/gateway/L1GraphTokenGateway.sol#L273
https://github.com/code-423n4/2022-10-thegraph/blob/309a188f7215fa42c745b136357702400f91b4ff/contracts/gateway/L1GraphTokenGateway.sol#L276
When certain state variable is read more than once, cache it to local variable to save gas.
 
Certain variables is defined even though they are used only once. Remove these unnecessary variables to save gas. For cases where it will reduce the readability, one can use comments to help describe what the code is doing.
Total of 1 instance found.
escrowBalance
variable of finalizeInboundTransfer function of L1GraphTokenGateway.sol
https://github.com/code-423n4/2022-10-thegraph/blob/309a188f7215fa42c745b136357702400f91b4ff/contracts/gateway/L1GraphTokenGateway.sol#L273-L275
Mitigation:
Delete line 273 and replace line 275 with below coderequire(_amount <= token.balanceOf(escrow), "BRIDGE_OUT_OF_FUNDS");
Don't define variable that is used only once. Details are listed on above PoC.
 
Below has redundant use of variables. Instead of defining a new variable, emit the event first and then update the variable.
Total of 4 instances found.
oldPendingGovernor
of transferOwnership function of Governed.sol
https://github.com/code-423n4/2022-10-thegraph/blob/309a188f7215fa42c745b136357702400f91b4ff/contracts/governance/Governed.sol#L40-L47
Change it tofunction transferOwnership(address _newGovernor) external onlyGovernor { require(_newGovernor != address(0), "Governor must be set"); emit NewPendingOwnership(pendingGovernor, _newGovernor); pendingGovernor = _newGovernor; }
oldGovernor
and oldPendingGovernor
of acceptOwnership function of Governed.sol
https://github.com/code-423n4/2022-10-thegraph/blob/309a188f7215fa42c745b136357702400f91b4ff/contracts/governance/Governed.sol#L53-L67
Change it tofunction acceptOwnership() external { require( pendingGovernor != address(0) && msg.sender == pendingGovernor, "Caller must be pending governor" ); emit NewOwnership(governor, pendingGovernor); emit NewPendingOwnership(pendingGovernor, address(0)); governor = pendingGovernor; pendingGovernor = address(0); }
oldPauseGuardian
of _setPauseGuardian function of Pausable.sol
https://github.com/code-423n4/2022-10-thegraph/blob/309a188f7215fa42c745b136357702400f91b4ff/contracts/governance/Pausable.sol#L55-L59
Change it tofunction _setPauseGuardian(address newPauseGuardian) internal { emit NewPauseGuardian(pauseGuardian, newPauseGuardian); pauseGuardian = newPauseGuardian; }
Instead of defining a new variable, emit the event first and then update the variable like shown in above PoC.
 
Since below require checks are used more than once, I recommend making these to a modifier or a function.
Total of 5 instances found.
./GraphProxyAdmin.sol:34: require(success); ./GraphProxyAdmin.sol:47: require(success); ./GraphProxyAdmin.sol:59: require(success);
./L1GraphTokenGateway.sol:200: require(_l1Token == address(token), "TOKEN_NOT_GRT"); ./L1GraphTokenGateway.sol:271: require(_l1Token == address(token), "TOKEN_NOT_GRT");
./L2GraphTokenGateway.sol:145: require(_l1Token == l1GRT, "TOKEN_NOT_GRT"); ./L2GraphTokenGateway.sol:233: require(_l1Token == l1GRT, "TOKEN_NOT_GRT");
./L2GraphTokenGateway.sol:147: require(msg.value == 0, "INVALID_NONZERO_VALUE"); ./L2GraphTokenGateway.sol:234: require(msg.value == 0, "INVALID_NONZERO_VALUE");
./Managed.sol:44: require(!controller.paused(), "Paused"); ./Managed.sol:49: require(!controller.paused(), "Paused");
I recommend making duplicate require statement into modifier or a function.
 
When there are multiple conditions in require statement, break down the require statement into multiple require statements instead of using && can save gas.
Total of 3 instances found.
./Governed.sol:54: require( pendingGovernor != address(0) && msg.sender == pendingGovernor, "Caller must be pending governor" ); ./GraphProxy.sol:142: require( _pendingImplementation != address(0) && msg.sender == _pendingImplementation, "Caller must be the pending implementation" ); ./L1GraphTokenGateway.sol:142: require(_escrow != address(0) && Address.isContract(_escrow), "INVALID_ESCROW");
Break down into several require statement.
 
Certain function is defined even though it is called only once. Inline it instead to where it is called to avoid usage of extra gas.
Total of 3 instances found.
_notPartialPaused() of Managed.sol _getCurrentDelegated function called only once at line 61 https://github.com/code-423n4/2022-10-thegraph/blob/309a188f7215fa42c745b136357702400f91b4ff/contracts/governance/Managed.sol#L43 https://github.com/code-423n4/2022-10-thegraph/blob/309a188f7215fa42c745b136357702400f91b4ff/contracts/governance/Managed.sol#L61
_onlyGovernor() of Managed.sol _getCurrentDelegated function called only once at line 78 https://github.com/code-423n4/2022-10-thegraph/blob/309a188f7215fa42c745b136357702400f91b4ff/contracts/governance/Managed.sol#L52 https://github.com/code-423n4/2022-10-thegraph/blob/309a188f7215fa42c745b136357702400f91b4ff/contracts/governance/Managed.sol#L78
_onlyController() of Managed.sol _getCurrentDelegated function called only once at line 72 https://github.com/code-423n4/2022-10-thegraph/blob/309a188f7215fa42c745b136357702400f91b4ff/contracts/governance/Managed.sol#L56 https://github.com/code-423n4/2022-10-thegraph/blob/309a188f7215fa42c745b136357702400f91b4ff/contracts/governance/Managed.sol#L72
I recommend to not define above functions and instead inline it at place it is called.
 
It is more gas expensive to compare boolean with "variable == true" or "variable == false" than directly checking the returned boolean value.
Total of 1 instance found.
./L1GraphTokenGateway.sol:214: extraData.length == 0 || callhookWhitelist[msg.sender] == true,
Simply check by returned boolean value. Change it to
require(extraData.length == 0 || callhookWhitelist[msg.sender])
 
Since EVM operates on 32 bytes at a time, if the element is smaller than that, the EVM must use more operations in order to reduce the elements from 32 bytes to specified size. Therefore it is more gas saving to use 32 bytes unless it is used in a struct or state variable in order to reduce storage slot.
Reference: https://docs.soliditylang.org/en/v0.8.15/internals/layout_in_storage.html
Total of 18 instances found.
./GraphTokenUpgradeable.sol:79: uint8 _v, ./ICuration.sol:10: function setDefaultReserveRatio(uint32 _defaultReserveRatio) external; ./ICuration.sol:14: function setCurationTaxPercentage(uint32 _percentage) external; ./ICuration.sol:57: function curationTaxPercentage() external view returns (uint32); ./IGraphToken.sol:33: uint8 _v, ./IStaking.sol:32: function setThawingPeriod(uint32 _thawingPeriod) external; ./IStaking.sol:34: function setCurationPercentage(uint32 _percentage) external; ./IStaking.sol:36: function setProtocolPercentage(uint32 _percentage) external; ./IStaking.sol:38: function setChannelDisputeEpochs(uint32 _channelDisputeEpochs) external; ./IStaking.sol:40: function setMaxAllocationEpochs(uint32 _maxAllocationEpochs) external; ./IStaking.sol:42: function setRebateRatio(uint32 _alphaNumerator, uint32 _alphaDenominator) external; ./IStaking.sol:44: function setDelegationRatio(uint32 _delegationRatio) external; ./IStaking.sol:47: uint32 _indexingRewardCut, ./IStaking.sol:48: uint32 _queryFeeCut, ./IStaking.sol:49: uint32 _cooldownBlocks ./IStaking.sol:52: function setDelegationParametersCooldown(uint32 _blocks) external; ./IStaking.sol:54: function setDelegationUnbondingPeriod(uint32 _delegationUnbondingPeriod) external; ./IStaking.sol:56: function setDelegationTaxPercentage(uint32 _percentage) external;
I suggest using uint256 instead of anything smaller and downcast where needed.
 
!= 0 costs less gas when optimizer is enabled and is used for unsigned integers in require statement.
Total of 3 instances found.
./L1GraphTokenGateway.sol:201: require(_amount > 0, "INVALID_ZERO_AMOUNT"); ./L1GraphTokenGateway.sol:217: require(maxSubmissionCost > 0, "NO_SUBMISSION_COST"); ./L2GraphTokenGateway.sol:146: require(_amount > 0, "INVALID_ZERO_AMOUNT");
I suggest changing > 0 to != 0
 
Since each storage slot is size of 32 bytes, error messages that is longer than this will need extra storage slot leading to more gas cost.
Total of 6 instances found.
./GraphProxy.sol:105: require(_newAdmin != address(0), "Cannot change the admin of a proxy to the zero address"); ./GraphProxy.sol:141: require(Address.isContract(_pendingImplementation), "Implementation must be a contract"); ./GraphProxy.sol:142: require( _pendingImplementation != address(0) && msg.sender == _pendingImplementation, "Caller must be the pending implementation" ); ./GraphUpgradeable.sol:32: require(msg.sender == _implementation(), "Caller must be the implementation"); ./GraphTokenGateway.sol:19: require( msg.sender == controller.getGovernor() || msg.sender == pauseGuardian, "Only Governor or Guardian can call" ); ./Managed.sol:53: require(msg.sender == controller.getGovernor(), "Caller must be Controller governor");
Simply keep the error messages within 32 bytes to avoid extra storage slot cost.
 
Custom errors from Solidity 0.8.4 are cheaper than revert strings. Details are explained here: https://blog.soliditylang.org/2021/04/21/custom-errors/
Total of 64 instances found.
./Governed.sol:24: require(msg.sender == governor, "Only Governor can call"); ./Governed.sol:41: require(_newGovernor != address(0), "Governor must be set"); ./Governed.sol:54: require( pendingGovernor != address(0) && msg.sender == pendingGovernor, "Caller must be pending governor" ); ./GraphProxyAdmin.sol:34: require(success); ./GraphProxyAdmin.sol:47: require(success); ./GraphProxyAdmin.sol:59: require(success); ./GraphProxy.sol:105: require(_newAdmin != address(0), "Cannot change the admin of a proxy to the zero address"); ./GraphProxy.sol:133: require(success); ./GraphProxy.sol:141: require(Address.isContract(_pendingImplementation), "Implementation must be a contract"); ./GraphProxy.sol:142: require( _pendingImplementation != address(0) && msg.sender == _pendingImplementation, "Caller must be the pending implementation" ); ./GraphProxy.sol:157: require(msg.sender != _admin(), "Cannot fallback to proxy target"); ./L2GraphToken.sol:36: require(msg.sender == gateway, "NOT_GATEWAY"); ./L2GraphToken.sol:49: require(_owner != address(0), "Owner must be set"); ./L2GraphToken.sol:60: require(_gw != address(0), "INVALID_GATEWAY"); ./L2GraphToken.sol:70: require(_addr != address(0), "INVALID_L1_ADDRESS"); ./GraphTokenUpgradeable.sol:60: require(isMinter(msg.sender), "Only minter can call"); ./GraphTokenUpgradeable.sol:94: require(_owner == recoveredAddress, "GRT: invalid permit"); ./GraphTokenUpgradeable.sol:95: require(_deadline == 0 || block.timestamp <= _deadline, "GRT: expired permit"); ./GraphTokenUpgradeable.sol:106: require(_account != address(0), "INVALID_MINTER"); ./GraphTokenUpgradeable.sol:115: require(_minters[_account], "NOT_A_MINTER"); ./GraphTokenUpgradeable.sol:123: require(_minters[msg.sender], "NOT_A_MINTER"); ./GraphUpgradeable.sol:24: require(msg.sender == _proxy.admin(), "Caller must be the proxy admin"); ./GraphUpgradeable.sol:32: require(msg.sender == _implementation(), "Caller must be the implementation"); ./GraphProxyStorage.sol:62: require(msg.sender == _admin(), "Caller must be admin"); ./GraphTokenGateway.sol:19: require( msg.sender == controller.getGovernor() || msg.sender == pauseGuardian, "Only Governor or Guardian can call" ); ./GraphTokenGateway.sol:31: require(_newPauseGuardian != address(0), "PauseGuardian must be set"); ./GraphTokenGateway.sol:40: require(!_paused, "Paused (contract)"); ./L1GraphTokenGateway.sol:74: require(inbox != address(0), "INBOX_NOT_SET"); ./L1GraphTokenGateway.sol:78: require(msg.sender == address(bridge), "NOT_FROM_BRIDGE"); ./L1GraphTokenGateway.sol:82: require(l2ToL1Sender == l2Counterpart, "ONLY_COUNTERPART_GATEWAY"); ./L1GraphTokenGateway.sol:110: require(_inbox != address(0), "INVALID_INBOX"); ./L1GraphTokenGateway.sol:111: require(_l1Router != address(0), "INVALID_L1_ROUTER"); ./L1GraphTokenGateway.sol:122: require(_l2GRT != address(0), "INVALID_L2_GRT"); ./L1GraphTokenGateway.sol:132: require(_l2Counterpart != address(0), "INVALID_L2_COUNTERPART"); ./L1GraphTokenGateway.sol:142: require(_escrow != address(0) && Address.isContract(_escrow), "INVALID_ESCROW"); ./L1GraphTokenGateway.sol:153: require(_newWhitelisted != address(0), "INVALID_ADDRESS"); ./L1GraphTokenGateway.sol:154: require(!callhookWhitelist[_newWhitelisted], "ALREADY_WHITELISTED"); ./L1GraphTokenGateway.sol:165: require(_notWhitelisted != address(0), "INVALID_ADDRESS"); ./L1GraphTokenGateway.sol:166: require(callhookWhitelist[_notWhitelisted], "NOT_WHITELISTED"); ./L1GraphTokenGateway.sol:200: require(_l1Token == address(token), "TOKEN_NOT_GRT"); ./L1GraphTokenGateway.sol:201: require(_amount > 0, "INVALID_ZERO_AMOUNT"); ./L1GraphTokenGateway.sol:202: require(_to != address(0), "INVALID_DESTINATION"); ./L1GraphTokenGateway.sol:213: require( extraData.length == 0 || callhookWhitelist[msg.sender] == true, "CALL_HOOK_DATA_NOT_ALLOWED" ); ./L1GraphTokenGateway.sol:217: require(maxSubmissionCost > 0, "NO_SUBMISSION_COST"); ./L1GraphTokenGateway.sol:224: require(msg.value >= expectedEth, "WRONG_ETH_VALUE"); ./L1GraphTokenGateway.sol:271: require(_l1Token == address(token), "TOKEN_NOT_GRT"); ./L1GraphTokenGateway.sol:275: require(_amount <= escrowBalance, "BRIDGE_OUT_OF_FUNDS"); ./L2GraphTokenGateway.sol:69: require( msg.sender == AddressAliasHelper.applyL1ToL2Alias(l1Counterpart), "ONLY_COUNTERPART_GATEWAY" ); ./L2GraphTokenGateway.sol:98: require(_l2Router != address(0), "INVALID_L2_ROUTER"); ./L2GraphTokenGateway.sol:108: require(_l1GRT != address(0), "INVALID_L1_GRT"); ./L2GraphTokenGateway.sol:118: require(_l1Counterpart != address(0), "INVALID_L1_COUNTERPART"); ./L2GraphTokenGateway.sol:145: require(_l1Token == l1GRT, "TOKEN_NOT_GRT"); ./L2GraphTokenGateway.sol:146: require(_amount > 0, "INVALID_ZERO_AMOUNT"); ./L2GraphTokenGateway.sol:147: require(msg.value == 0, "INVALID_NONZERO_VALUE"); ./L2GraphTokenGateway.sol:148: require(_to != address(0), "INVALID_DESTINATION"); ./L2GraphTokenGateway.sol:153: require(outboundCalldata.extraData.length == 0, "CALL_HOOK_DATA_NOT_ALLOWED"); ./L2GraphTokenGateway.sol:233: require(_l1Token == l1GRT, "TOKEN_NOT_GRT"); ./L2GraphTokenGateway.sol:234: require(msg.value == 0, "INVALID_NONZERO_VALUE"); ./Managed.sol:44: require(!controller.paused(), "Paused"); ./Managed.sol:45: require(!controller.partialPaused(), "Partial-paused"); ./Managed.sol:49: require(!controller.paused(), "Paused"); ./Managed.sol:53: require(msg.sender == controller.getGovernor(), "Caller must be Controller governor"); ./Managed.sol:57: require(msg.sender == address(controller), "Caller must be Controller"); ./Managed.sol:104: require(_controller != address(0), "Controller must be set");
I suggest implementing custom errors to save gas.