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: 10/62
Findings: 2
Award: $622.91
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: 0xSmartContract
Also found by: 0x1f8b, 0x4non, 0xNazgul, Bnke0x0, Chom, IllIllI, Josiah, Rahoz, RaymondFam, Trust, Waze, ajtra, bobirichman, brgltd, bulej93, c3phas, cccz, chrisdior4, delfin454000, fatherOfBlocks, gogo, ladboy233, mcwildy, mics, nicobevi, oyc_109, rbserver, rotcivegaf, zzzitron
602.1213 USDC - $602.12
* @param _amount Amount of tokens to tranfer
Change tranfer
to transfer
* @param _to Recepient address on L1
Change Recepient
to Recipient
In theory, comments over 80 characters should wrap using multi-line comment syntax. Even if somewhat longer comments (up to 120 characters) are acceptable and a scroll bar is provided, there are cases where very long comment lines interfere with readability. Below are two sets of comments including extra-long lines whose readability could be improved, as shown.
* TIP: To get this value clients can read directly from the storage slot shown below (specified by EIP1967) using the * https://eth.wiki/json-rpc/API#eth_getstorageat[`eth_getStorageAt`] RPC call.
Suggestion:
* TIP: To get this value clients can read directly from the storage slot shown below (specified by EIP1967) * using the https://eth.wiki/json-rpc/API#eth_getstorageat[`eth_getStorageAt`] RPC call.
Similarly for the following sets of comments:
L2GraphTokenGateway.sol: L214-219
* Note that whitelisted senders (some protocol contracts) can include additional calldata * for a callhook to be executed on the L2 side when the tokens are received. In this case, the L2 transaction * can revert if the callhook reverts, potentially locking the tokens on the bridge if the callhook * never succeeds. This requires extra care when adding contracts to the whitelist, but is necessary to ensure that * the tickets can be retried in the case of a temporary failure, and to ensure the atomicity of callhooks * with token transfers.
Suggestion:
* Note that whitelisted senders (some protocol contracts) can include additional calldata * for a callhook to be executed on the L2 side when the tokens are received. In this case, * the L2 transaction can revert if the callhook reverts, potentially locking the tokens on the bridge * if the callhook never succeeds. This requires extra care when adding contracts to the whitelist, * but is necessary to ensure that the tickets can be retried in the case of a temporary failure, * and to ensure the atomicity of callhooks with token transfers.
Similarly for the following set of comments:
L1GraphTokenGateway.sol: L177-182
Code that contains open items should be addressed and modified or removed
L2GraphTokenGateway.sol: L131-144
* @param _l1Token L1 Address of GRT (needed for compatibility with Arbitrum Gateway Router) * @param _to Recipient address on L1 * @param _amount Amount of tokens to burn * @param _data Contains sender and additional data (always empty) to send to L1 * @return ID of the withdraw transaction */ function outboundTransfer( address _l1Token, address _to, uint256 _amount, uint256, // unused on L2 uint256, // unused on L2 bytes calldata _data ) public payable override notPaused nonReentrant returns (bytes memory) {
Suggestion: Remove the line uint256, // unused on L2
, which occurs twice
Terms incorporating "black," "white," "slave" or "master" are potentially problematic. Substituting more neutral terminology is becoming common practice. I see that "blacklist" has been eliminated by using denylist
. However, the contracts still use whitelist
and master
, as shown in the two examples below:
L1GraphTokenGateway.sol: L152-157
function addToCallhookWhitelist(address _newWhitelisted) external onlyGovernor { require(_newWhitelisted != address(0), "INVALID_ADDRESS"); require(!callhookWhitelist[_newWhitelisted], "ALREADY_WHITELISTED"); callhookWhitelist[_newWhitelisted] = true; emit AddedToCallhookWhitelist(_newWhitelisted); }
function setCurationTokenMaster(address _curationTokenMaster) external;
Suggestion: Replace whitelist
with allowlist
, and TokenMaster
with TokenPrimary
or TokenAdmin
. Similarly for variations of these terms.
require
revert strings should be consistentFor the sake of readability, revert strings should have consistent punctuation throughout The Graph L2
Some error messages are written using ALL CAPS with underscores between words, as the example below shows:
require(_l2Router != address(0), "INVALID_L2_ROUTER");
Other error messages are written in a more conversational style without using ALL CAPS or underscores:
require(msg.sender == _proxy.admin(), "Caller must be the proxy admin");
Suggestion: Convert ALL CAP + underscore messages to the more conversational capital + lower case without underscores style
require
revert stringA require
message should be included to enable users to understand the reason for failure
Recommendation: Add a brief (<= 32 char) explanatory message to each of the four requires
referenced below. Also, consider using custom error codes (which would be cheaper than revert strings).
function getProxyImplementation(IGraphProxy _proxy) public view returns (address) { // We need to manually run the static call since the getter cannot be flagged as view // bytes4(keccak256("implementation()")) == 0x5c60da1b (bool success, bytes memory returndata) = address(_proxy).staticcall(hex"5c60da1b"); require(success); return abi.decode(returndata, (address)); }
Similarly for the following:
Specific NatSpec statements are missing for the 15 functions below:
* @dev Accept to be an implementation of proxy. */ function acceptProxy(IGraphProxy _proxy) external onlyProxyAdmin(_proxy) {
Missing: @param _proxy
* @dev Accept to be an implementation of proxy and then call a function from the new * implementation as specified by `_data`, which should be an encoded function call. This is * useful to initialize new storage variables in the proxied contract. */ function acceptProxyAndCall(IGraphProxy _proxy, bytes calldata _data)
Missing: @param _proxy
and @param _data
* @dev Initialize the governor to the contract caller. */ function _initialize(address _initGovernor) internal {
Missing: @param _initGovernor
* @notice Change the partial paused state of the contract */ function _setPartialPaused(bool _toPause) internal {
Missing: @param _toPause
* @notice Change the paused state of the contract */ function _setPaused(bool _toPause) internal {
Missing: @param _toPause
* @dev Returns the current implementation of a proxy. * This is needed because only the proxy admin can query it. * @return The address of the current implementation of the proxy. */ function getProxyImplementation(IGraphProxy _proxy) public view returns (address) {
Missing: @param _proxy
* @dev Returns the pending implementation of a proxy. * This is needed because only the proxy admin can query it. * @return The address of the pending implementation of the proxy. */ function getProxyPendingImplementation(IGraphProxy _proxy) public view returns (address) {
Missing: @param _proxy
* @dev Returns the admin of a proxy. Only the admin can query it. * @return The address of the current admin of the proxy. */ function getProxyAdmin(IGraphProxy _proxy) public view returns (address) {
Missing: @param _proxy
* @dev Returns the current admin. * * NOTE: Only the admin and implementation can call this function. * * TIP: To get this value clients can read directly from the storage slot shown below (specified by EIP1967) using the * https://eth.wiki/json-rpc/API#eth_getstorageat[`eth_getStorageAt`] RPC call. * `0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103` */ function admin() external ifAdminOrPendingImpl returns (address) {
Missing: @return
. @dev
should contain some of the information in the notes so that @return
is not redundant
* @dev Returns the current implementation. * * NOTE: Only the admin can call this function. * * TIP: To get this value clients can read directly from the storage slot shown below (specified by EIP1967) using the * https://eth.wiki/json-rpc/API#eth_getstorageat[`eth_getStorageAt`] RPC call. * `0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc` */ function implementation() external ifAdminOrPendingImpl returns (address) {
Missing: @return
. Again, @dev
should contain some of the information in the notes so that @return
is not redundant
* @dev Changes the admin of the proxy. * * NOTE: Only the admin can call this function. */ function setAdmin(address _newAdmin) external ifAdmin {
Missing: @param _newAdmin
* @dev Admin function for new implementation to accept its role as implementation. */ function acceptUpgradeAndCall(bytes calldata data) external ifAdminOrPendingImpl {
Missing: @param data
* @dev Initialize the controller. */ function _initialize(address _controller) internal {
Missing: @param _controller
* @dev Resolve a contract address from the cache or the Controller if not found. * @return Address of the contract */ function _resolveContract(bytes32 _nameHash) internal view returns (address) {
Missing: @param _nameHash
* @notice Getter to access paused state of this contract */ function paused() external view returns (bool) {
Missing: @return
NatSpec statements are entirely missing for the functions listed below. In some cases, a group of functions is proceeded by a comment such as // -- Configuration --
or // -- Getters --
. However, such comments, which serve as explanatory headings for the functions, are not as helpful as would be @notice
statements (to explain to end users what a function does) and @param
and @return
statements (which describe the roles of individual variables).
Managed.sol
IGraphCurationToken.sol
IGraphProxy.sol
L6; L8; L10; L12; L14; L16; L18
IEpochManager.sol
L8; L12; L16; L18; L20; L22; L24; L26; L28; L30
IController.sol
L6; L10; L12; L14; L16; L20; L22; L24; L26; L28
IGraphToken.sol
L10; L12; L14; L18; L20; L22; L24; L28-36; L40; L42
IRewardsManager.sol
L18; L20; L24; L26; L28; L31; L35; L37; L39-42; L44-47; L49; L53; L55; L59; L61
ICuration.sol
L10; L12; L14; L16; L20-24; L26-30; L32; L36; L38-41; L43; L45; L47-50; L52-55; L57
IStaking.sol
L30; L32; L34; L36; L38; L40; L42; L44; L46-50; L52; L54; L56; L58; L60; L64; L66; L70; L72; L74; L76-81; L83; L85; L89; L91; L93; L97-103; L105-112; L114; L116; L118-127; L129; L131; L133; L137; L139; L141; L143; L145; L147; L149-152; L154-157; L159
🌟 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
Require
revert string is too longThe revert strings below can be shortened to 32 characters or fewer (as shown) to save gas (or else consider using custom error codes, which could save even more gas)
require(msg.sender == _implementation(), "Caller must be the implementation");
Change message to Caller must be the impl
require(Address.isContract(_pendingImplementation), "Implementation must be a contract");
Change message to Implementation must be contract
require( _pendingImplementation != address(0) && msg.sender == _pendingImplementation, "Caller must be the pending implementation" );
Change message to Caller must be the pending impl
require(msg.sender == controller.getGovernor(), "Caller must be Controller governor");
Change message to Caller must be Controller gov
"Only Governor or Guardian can call"
Change message to Only Gov or Guardian can call
The revert string below is also longer than 32 characters but it's not clear how to shorten it
require(_newAdmin != address(0), "Cannot change the admin of a proxy to the zero address");
require
functionSplitting into separate require()
statements saves gas
require( pendingGovernor != address(0) && msg.sender == pendingGovernor, "Caller must be pending governor" );
Recommendation:
require(pendingGovernor != address(0), "Caller must be pending governor"); require(msg.sender == pendingGovernor, "Caller must be pending governor");
require( _pendingImplementation != address(0) && msg.sender == _pendingImplementation, "Caller must be the pending implementation" );
Recommendation:
require(_pendingImplementation != address(0), "Caller must be the pending impl"); require(msg.sender == _pendingImplementation, "Caller must be the pending impl");
require(_escrow != address(0) && Address.isContract(_escrow), "INVALID_ESCROW");
Recommendation:
require(_escrow != address(0), "Invalid escrow"); require(Address.isContract(_escrow), "Invalid escrow");
!= 0
instead of > 0
in a require
statement if variable is an unsigned integer!= 0
should be used where possible since > 0
costs more gas
require(maxSubmissionCost > 0, "NO_SUBMISSION_COST");
Change maxSubmissionCost > 0
to maxSubmissionCost != 0
It costs less gas to inline the code of functions that are only called once. Doing so saves the cost of allocating the function selector, and the cost of the jump when the function is called.
// Check if sender is controller. modifier onlyController() { _onlyController(); _; }
Since onlyController()
is used only once in this contract (in function setController()
) as shown below, it should be inlined to save gas
function setController(address _controller) external onlyController { _setController(_controller); }
GraphTokenUpgradeable.sol: L59-62
modifier onlyMinter() { require(isMinter(msg.sender), "Only minter can call"); _; }
Since onlyMinter()
is used only once in this contract (in function mint()
) as shown below, it should be inlined to save gas
GraphTokenUpgradeable.sol: L132-134
function mint(address _to, uint256 _amount) external onlyMinter { _mint(_to, _amount); }
L2GraphTokenGateway.sol: L68-74
modifier onlyL1Counterpart() { require( msg.sender == AddressAliasHelper.applyL1ToL2Alias(l1Counterpart), "ONLY_COUNTERPART_GATEWAY" ); _; }
Since onlyL1Counterpart()
is used only once in this contract (in function finalizeInboundTransfer()
) as shown below, it should be inlined to save gas
L2GraphTokenGateway.sol: L226-232
function finalizeInboundTransfer( address _l1Token, address _from, address _to, uint256 _amount, bytes calldata _data ) external payable override notPaused onlyL1Counterpart nonReentrant {
L1GraphTokenGateway.sol: L73-74
modifier onlyL2Counterpart() { require(inbox != address(0), "INBOX_NOT_SET");
Since onlyL2Counterpart()
is used only once in this contract (in function finalizeInboundTransfer()
) as shown below, it should be inlined to save gas
L1GraphTokenGateway.sol: L263-269
function finalizeInboundTransfer( address _l1Token, address _from, address _to, uint256 _amount, bytes calldata // _data, contains exitNum, unused by this contract ) external payable override notPaused onlyL2Counterpart {
modifier onlyGovernorOrGuardian() { require( msg.sender == controller.getGovernor() || msg.sender == pauseGuardian, "Only Governor or Guardian can call" );
Since onlyGovernorOrGuardian()
is used only once in this contract (in function setPaused()
) as shown below, it should be inlined to save gas
function setPaused(bool _newPaused) external onlyGovernorOrGuardian { _setPaused(_newPaused); }
The following two modifiers are never used and should be reviewed:
* @dev Modifier to check whether the `msg.sender` is the admin. */ modifier onlyAdmin() { require(msg.sender == _admin(), "Caller must be admin"); _; }
modifier notPartialPaused() { _notPartialPaused(); _; }