Platform: Code4rena
Start Date: 22/09/2023
Pot Size: $100,000 USDC
Total HM: 15
Participants: 175
Period: 14 days
Judge: alcueca
Total Solo HM: 4
Id: 287
League: ETH
Rank: 15/175
Findings: 3
Award: $1,269.83
🌟 Selected for report: 0
🚀 Solo Findings: 0
1165.9616 USDC - $1,165.96
https://github.com/code-423n4/2023-09-maia/blob/main/src/CoreRootRouter.sol#L481-L487 https://github.com/code-423n4/2023-09-maia/blob/main/src/RootPort.sol#L266-L267
The CoreRootRouter._setLocalToken
function is used to set the local token on a specific chain for a global token. The function initially checks whether the local token is is already added as shown below:
if (IPort(rootPortAddress).isLocalToken(_localAddress, _dstChainId)) revert TokenAlreadyAdded();
If the token is not already added then the global token's new branch chain address
is set as shown below:
IPort(rootPortAddress).setLocalAddress(_globalAddress, _localAddress, _dstChainId);
The IPort.setLocalAddress
function performs two mapping state updates as shown below:
getGlobalTokenFromLocal[_localAddress][_srcChainId] = _globalAddress; getLocalTokenFromGlobal[_globalAddress][_srcChainId] = _localAddress;
Now let's consider the following scenario:
Assume _globalAddress = 0x01, _localAddress = 0x02, _srcChainId = 1.
The CoreRootRouter._setLocalToken
function is called with above input parameter values.
The getGlobalTokenFromLocal[0x02][1]
is equal to address(0)
since it is not initialized.
Hence the IPort(rootPortAddress).isLocalToken(_localAddress, _dstChainId)
condition will return false
and the _setLocalToken
function will proceed executing.
But the getLocalTokenFromGlobal[0x01][1]
has value 0x03
since it is already assigned during a previous call to the CoreRootRouter._setLocalToken
function.
There is no check to verify whether getLocalTokenFromGlobal[_globalAddress][_srcChainId]
is already set.
Hence the transaction execution will proceed to the IPort.setLocalAddress
function and will update the getGlobalTokenFromLocal
and getLocalTokenFromGlobal
for the corresponding values provided.
This will overwrite the existing value of ``getLocalTokenFromGlobal[0x01][1]which is
0x03with the new passed in
_localAddressvalue
0x02` as shown below:
getLocalTokenFromGlobal[0x01][1] = 0x02;
But this is not the expected behaviour of the IPort.setLocalAddress
function since above vulnerability could overwrite the getLocalTokenFromGlobal
mapping values for already set _globalAddress
and _srcChainId
pairs. This can break the internal logic execution and accounting of the hTokens
defined in the system.
function _setLocalToken(address _globalAddress, address _localAddress, uint16 _dstChainId) internal { // Verify if the token already added if (IPort(rootPortAddress).isLocalToken(_localAddress, _dstChainId)) revert TokenAlreadyAdded(); // Set the global token's new branch chain address IPort(rootPortAddress).setLocalAddress(_globalAddress, _localAddress, _dstChainId); }
https://github.com/code-423n4/2023-09-maia/blob/main/src/CoreRootRouter.sol#L481-L487
getGlobalTokenFromLocal[_localAddress][_srcChainId] = _globalAddress; getLocalTokenFromGlobal[_globalAddress][_srcChainId] = _localAddress;
https://github.com/code-423n4/2023-09-maia/blob/main/src/RootPort.sol#L266-L267
Manual Review and VSCode
Hence it is recommended to perform an additional check in the CoreRootRouter._setLocalToken
to ensure that the getLocalTokenFromGlobal
mapping value for the _globalAddress
and _srcChainId
pair is not already set as shown below:
if (IPort(rootPortAddress).isLocalToken(_localAddress, _dstChainId)) revert TokenAlreadyAdded(); if (IPort(rootPortAddress).isGlobalToken(_globalAddress, _dstChainId)) revert TokenAlreadyAdded();
Other
#0 - c4-pre-sort
2023-10-10T06:42:16Z
0xA5DF marked the issue as primary issue
#1 - c4-pre-sort
2023-10-10T06:42:21Z
0xA5DF marked the issue as sufficient quality report
#2 - 0xBugsy
2023-10-17T20:02:35Z
This has already been checked in the _addGlobalToken
step of this execution flow.
#3 - c4-sponsor
2023-10-17T20:02:38Z
0xBugsy (sponsor) disputed
#4 - alcueca
2023-10-25T14:14:52Z
Many issues around
addGlobalToken
due to lack of input validation when linking a global token to local token #860
Would you please point out where? I can't find where the _localAddress
is obtained.
#5 - 0xBugsy
2023-10-25T16:56:20Z
After calling addGlobalToken
- requesting a new local token representation in a new chain for an existing global token - we perform these steps:
1- https://github.com/code-423n4/2023-09-maia/blob/f5ba4de628836b2a29f9b5fff59499690008c463/src/CoreRootRouter.sol#L406 (where we check if the global token exists and if it has already been added to the destination chain)
2- https://github.com/code-423n4/2023-09-maia/blob/f5ba4de628836b2a29f9b5fff59499690008c463/src/CoreBranchRouter.sol#L166 (creation of the new branch hToken for the existing root/global hToken) _localAddress is deployed in this step
3- https://github.com/code-423n4/2023-09-maia/blob/f5ba4de628836b2a29f9b5fff59499690008c463/src/CoreRootRouter.sol#L481 (check if we have not added the new local token to system already and request update to root port)
4- Ends here: https://github.com/code-423n4/2023-09-maia/blob/f5ba4de628836b2a29f9b5fff59499690008c463/src/RootPort.sol#L259 (updating the root port token mapping namely getLocalTokenFromGlobal
)
#6 - alcueca
2023-10-26T08:28:13Z
As a rule, you should validate the arguments in internal functions that when those arguments change state in the same function and some values of those arguments could break the logic represented by that state.
#7 - c4-judge
2023-10-26T08:28:25Z
alcueca changed the severity to QA (Quality Assurance)
#8 - c4-judge
2023-10-26T08:28:37Z
alcueca marked the issue as grade-a
#9 - c4-judge
2023-10-26T08:31:13Z
This previously downgraded issue has been upgraded by alcueca
#10 - c4-judge
2023-10-26T08:40:05Z
alcueca changed the severity to QA (Quality Assurance)
#11 - c4-judge
2023-11-04T05:54:18Z
This previously downgraded issue has been upgraded by alcueca
#12 - c4-judge
2023-11-04T05:54:50Z
alcueca marked the issue as duplicate of #610
#13 - c4-judge
2023-11-07T13:53:45Z
alcueca marked the issue as satisfactory
🌟 Selected for report: MrPotatoMagic
Also found by: 0xAadi, 0xDING99YA, 0xDemon, 0xRstStn, 0xSmartContract, 0xStriker, 0xWaitress, 0xbrett8571, 0xfuje, 0xsagetony, 0xsurena, 33BYTEZZZ, 3docSec, 7ashraf, ABA, ABAIKUNANBAEV, Aamir, Audinarey, Bauchibred, Black_Box_DD, Daniel526, DanielArmstrong, DanielTan_MetaTrust, Dinesh11G, Eurovickk, Franklin, Inspecktor, John, Jorgect, Joshuajee, K42, Kek, Koolex, LokiThe5th, MIQUINHO, Myd, NoTechBG, QiuhaoLi, SanketKogekar, Sathish9098, Sentry, Soul22, SovaSlava, Stormreckson, Tendency, Topmark, Udsen, V1235816, Viktor_Cortess, Viraz, Yanchuan, ZdravkoHr, Zims, albahaca, albertwh1te, alexweb3, alexxander, ast3ros, audityourcontracts, bareli, bin2chen, bronze_pickaxe, c0pp3rscr3w3r, cartlex_, castle_chain, chaduke, debo, ether_sky, gumgumzum, imare, its_basu, jaraxxus, jasonxiale, josephdara, kodyvim, ladboy233, lanrebayode77, lsaudit, mert_eren, minhtrng, n1punp, nadin, niroh, nmirchev8, orion, peakbolt, perseverancesuccess, pfapostol, ptsanev, rvierdiiev, saneryee, shaflow2, te_aut, terrancrypt, twcctop, unsafesol, ustas, versiyonbir, windhustler, yongskiws, zhaojie, ziyou-
25.6785 USDC - $25.68
_refundee
VARIABLE HAS REDUNDANT DECLARATION AS payable
The BranchBridgeAgent._performCall
function is used to perform calls to LayerZero messaging layer Endpoint for cross-chain messaging. The _refundee
is a payable address which is used to refund excess gas to. The _refundee
is declared as a payable
address when passed in as an input parameter to the _performCall
function. The issue is _refundee
is again recasted to a payable
address inside the call to the layerzero endpoint which is not required as shown below:
ILayerZeroEndpoint(lzEndpointAddress).send{value: msg.value}( //@audit-info - uses layer zero messaging layer endpoint for cross-chain messaging rootChainId, rootBridgeAgentPath, _payload, payable(_refundee), address(0), abi.encodePacked(uint16(2), _gParams.gasLimit, _gParams.remoteBranchExecutionGas, rootBridgeAgentAddress) );
Hence it is recommneded to remove the redundant recasting of the _refundee
address to a payable
address inside the call to the layerzero
endpoint for cross chain messaging.
https://github.com/code-423n4/2023-09-maia/blob/main/src/BranchBridgeAgent.sol#L774 https://github.com/code-423n4/2023-09-maia/blob/main/src/RootBridgeAgent.sol#L919
address(0)
COULD LEAD TO LOSS OF FUNDSThe BranchBridgeAgent._clearToken
is an internal function which is used to request balance clearance from a Port to a given user. The _clearToken
is called by the BranchBridgeAgent.clearToken
function by passing in the _recipient
address to whom the balance clearance should be sent to.
The issue is during the execution flow of the BranchBridgeAgent.clearToken
function there is no check to verifty whether the _recipient != address(0)
. Hence if the address(0) is passed in as the _recipient
address the balance clearance will be sent to the address(0) which is loss of funds.
The _clearToken
function calls the IPort.bridgeIn
function and IPort.withdraw
function. Inside the bridgeIn
function and withdraw
function ERC20
_mint
and safeTransfer
functions are used. But event inside these functions no address(0)
check is performed on the passed in _recipient
address.
Hence it is recommended to perform the input validation check inside the BranchBridgeAgent._clearToken
function to ensure the passed in _recipient
address is not equal to address(0)
. It is recommended to add the following line of code at the beginning of the _clearToken
function.
require( _recipient != address(0), "_recipient can not be address(0)");
https://github.com/code-423n4/2023-09-maia/blob/main/src/BranchBridgeAgent.sol#L906-L914 https://github.com/code-423n4/2023-09-maia/blob/main/src/BranchBridgeAgent.sol#L494-L571
_recipient
ADDRESS IS NOT DECLARED AS A payable
ADDRESS IN THE BranchBridgeAgentExecutor.executeWithSettlement
FUNCTIONThe BranchBridgeAgentExecutor.executeWithSettlement
function is used to execute a cross-chain request with a single settlement. The function accepts the _recipient
address which is the the recipient of the settlement as shown below:
function executeWithSettlement(address _recipient, address _router, bytes calldata _payload)
But the issue is _recipient
is not declared as a payable
address. Since _recipient
is expected to receive the native ETH
it is recommneded to declare the _recipient
address as a payable address in the executeWithSettlement
function signature. The modified line of code is shown below:
function executeWithSettlement(address payable _recipient, address _router, bytes calldata _payload)
https://github.com/code-423n4/2023-09-maia/blob/main/src/BranchBridgeAgentExecutor.sol#L66
BranchPort._bridgeOut
TRANSACTION COULD REVERT IF THE _amount < _deposit
The BranchPort._bridgeOut
is an internal function used to bridge out hTokens and underlying tokens. Within the function implementation the _hTokenAmount
to bridge out is calculated as follows:
uint256 _hTokenAmount = _amount - _deposit;
The issue here is that if the _amount < _deposit
the arithmetic operation will revert due to underflow
error but the cause for the revert will not be clear. Hence it is recommended to add the below conditional check to verify (_amount > _deposit) before calculating the _hTokenAmount.
require(_amount > _deposit, "Arithmetic Underflow"); uint256 _hTokenAmount = _amount - _deposit;
https://github.com/code-423n4/2023-09-maia/blob/main/src/BranchPort.sol#L522
settlement.status
IS BY DEFAULT SET TO THE STATUS_SUCCESS
STATE SINCE THE STATUS_SUCCESS == 0
The RootBridgeAgent.redeemSettlement
function is used to enable an user to redeem thier settlement. Here initially the settlement.status
is checked to ensure whether the settlement is redeemable as shown below:
if (settlement.status == STATUS_SUCCESS) revert SettlementRedeemUnavailable();
The STATUS_SUCCESS
is declared in the BridgeAgentConstant.sol
as shown below:
uint8 internal constant STATUS_SUCCESS = 0;
At the end of the RootBridgeAgent.redeemSettlement
function the settlement corresponding to the _settlementNonce
is deleted as shown below:
delete getSettlement[_settlementNonce];
Due to the above deletion the settlement.status
will be equal to 0
which is the default value. Hence as a result this is still equal to the STATUS_SUCCESS
state. Hence even for any non-existing settlement and for a settlement that has already being deleted the settlement.status
will be equal to the STATUS_SUCCESS
state.
Hence it is recommended to update the constant STATUS_SUCCESS = 2
because in that case the default value of the settlement.status
will not be equal to the STATUS_SUCCESS
state by default.
https://github.com/code-423n4/2023-09-maia/blob/main/src/interfaces/BridgeAgentConstants.sol#L23 https://github.com/code-423n4/2023-09-maia/blob/main/src/RootBridgeAgent.sol#L307 https://github.com/code-423n4/2023-09-maia/blob/main/src/RootBridgeAgent.sol#L343
address(0)
FOR THE _recipient
ADDRESS IN THE RootBridgeAgent.bridgeIn
FUNCTIONThe RootBridgeAgent.bridgeIn
function is used to bridge in hTokens
from branch to root. The bridgeIn
function calls the IPort.bridgeToRoot
function by passing in the _recipient
address to whom the _amount - _deposit
amount of _hTokens
will be transferred to and the _deposit
amount of _hTokens
will be minted to inside the bridgeToRoot
function.
But the issue here is that in none of the bridgeIn
, bridgeToRoot
or the mint
functions the _recipient
address is not checked for address(0)
. Hence if the address(0)
is passed in as the _recipient
address to the RootBridgeAgent.bridgeIn
function (address(0) is the default value), the _hTokens
transferred to the _recipient address (address(0))
will be permenantly lost.
Hence it is recommneded to perform the address(0)
check inside the ``IPort.bridgeToRoot` function as an input validation check.
https://github.com/code-423n4/2023-09-maia/blob/main/src/RootBridgeAgent.sol#L351 https://github.com/code-423n4/2023-09-maia/blob/main/src/RootBridgeAgent.sol#L377-L378 https://github.com/code-423n4/2023-09-maia/blob/main/src/RootPort.sol#L277-L291
RootPort
CONTRACTIn the RootPort.sol
contract there are four mappings declared on hTokens
. They are as follows:
/// @notice ChainId -> Local Address -> Global Address mapping(address chainId => mapping(uint256 localAddress => address globalAddress)) public getGlobalTokenFromLocal; /// @notice ChainId -> Global Address -> Local Address mapping(address chainId => mapping(uint256 globalAddress => address localAddress)) public getLocalTokenFromGlobal; /// @notice ChainId -> Underlying Address -> Local Address mapping(address chainId => mapping(uint256 underlyingAddress => address localAddress)) public getLocalTokenFromUnderlying; /// @notice Mapping from Local Address to Underlying Address. mapping(address chainId => mapping(uint256 localAddress => address underlyingAddress)) public getUnderlyingTokenFromLocal;
As it is evident in each of the above mappings there is a mismatch between chainId
and localAddress
names used.
These mappings declare that the first key as an address
(which is named chainId
but it's actually an address), and the second key as a uint256
(which is named localAddress
, which is misleading because it's a uint256
, not an address
). This can be confusing to both developers and auditors and can lead to serious vulenarabilities if the naming mismatch is not understood properly.
Hence it is recommended to correct the above key data type and naming mismatch, and update the mapping declaration as shown below:
/// @notice Local Address -> ChainId -> Global Address mapping(address localAddress => mapping(uint256 chainId => address globalAddress)) public getGlobalTokenFromLocal; /// @notice Global Address -> ChainId -> Local Address mapping(address globalAddress => mapping(uint256 chainId => address localAddress)) public getLocalTokenFromGlobal; /// @notice Underlying Address -> ChainId -> Local Address mapping(address underlyingAddress => mapping(uint256 chainId => address localAddress)) public getLocalTokenFromUnderlying; /// @notice Mapping from Local Address to Underlying Address. mapping(address localAddress => mapping(uint256 chainId => address underlyingAddress)) public getUnderlyingTokenFromLocal;
https://github.com/code-423n4/2023-09-maia/blob/main/src/RootPort.sol#L89-L101
ERC20hTokenRootFactory
CONTRACT DOES NOT HAVE FUNCTIONALITY TO REMOVE UNWANTED hTokens
IF THE NEED ARISES IN THE FUTUREThe ERC20hTokenRootFactory
contract is a factory contract used to create new hTokens
. The new hTokens are create using the createToken
function and the newly created hTokens
are added to the hTokens
array as shown below:
newToken = new ERC20hTokenRoot( localChainId, address(this), rootPortAddress, _name, _symbol, _decimals ); hTokens.push(newToken);
The issue here is that even if the hTokens
are added to the hTokens
array there is no mechanism to remove the hTokens
if the need arises in the future. As a result the hTokens
array will keep on growing.
Since there is a ERC20hTokenRootFactory.getHTokens
function to retrieve the hTokens
array, the retreiver could be using the hTokens
array to loop through the array to execute a different logic. If the hTokens
array is extremely large then the retriever will get transaction out-of-gas exception. Hence the hTokens
array returned by the ERC20hTokenRootFactory.getHTokens
function will not be useful for any other logic execution.
Hence it is recommended to add a functionality remove the unwanted global hTokens
from the hTokens
array in the ERC20hTokenRootFactory
contract and update the corresponding token mappings (Eg : getGlobalTokenFromLocal) in the RootPort
contract.
https://github.com/code-423n4/2023-09-maia/blob/main/src/factories/ERC20hTokenRootFactory.sol#L81-L89 https://github.com/code-423n4/2023-09-maia/blob/main/src/factories/ERC20hTokenRootFactory.sol#L63-L65 https://github.com/code-423n4/2023-09-maia/blob/main/src/factories/ERC20hTokenBranchFactory.sol#L72 https://github.com/code-423n4/2023-09-maia/blob/main/src/factories/ERC20hTokenBranchFactory.sol#L87-L89
address(0)
CHECK IS NOT PERFORMED ON THE _newBranchBridgeAgent
ADDRESS IN THE RootPort.syncBranchBridgeAgentWithRoot
FUNCTIONThe RootPort.syncBranchBridgeAgentWithRoot
function is used to sync the branch bridge agent for a given _branchChainId
with the root bridge agent. The function calls the IBridgeAgent.syncBranchBridgeAgent
function to perform the _newBranchBridgeAgent
update.
But the issue is there is no address(0)
check performed on the _newBranchBridgeAgent
address for which the branch bridge agent
will be set to.
Hence it is recommended to perform the address(0) check on the _newBranchBridgeAgent
value inside the RootPort.syncBranchBridgeAgentWithRoot
to ensure address(0)
is not as the branch bridge agent address for the given _branchChainId
in the respective _rootBridgeAgent
.
Following line of code can be added to the RootPort.syncBranchBridgeAgentWithRoot
function before the BridgeAgent.syncBranchBridgeAgent
is called.
require(_newBranchBridgeAgent != address(0), "Can not be address(0)");
https://github.com/code-423n4/2023-09-maia/blob/main/src/RootPort.sol#L404 https://github.com/code-423n4/2023-09-maia/blob/main/src/RootBridgeAgent.sol#L1181-L1182
approve
TRANSACTION IS NOT CHECKED FOR SUCCESS IN THE BaseBranchRouter._transferAndApproveToken
FUNCTIONThe BaseBranchRouter._transferAndApproveToken
is an internal function used to transfer tokens to the Branch Router
during callOutAndBridge
transaction. During the _transferAndApproveToken
function execution both the local branch tokens (_hToken
) and underlying tokens (_token
) are transferred to the BaseBranchRouter
contract.
Then both the _hToken
and the _token
are approved by the BaseBranchRouter
contract to the _localPortAddress
contract to be stored in the port
. It is shown below:
ERC20(_hToken).approve(_localPortAddress, _amount - _deposit); ERC20(_token).approve(_localPortAddress, _deposit);
This issue here is certain ERC20 tokens communicate the approve
transaction failure by returning a boolean false
value. Since the above lines of code does not check the return boolean value of the approve
function it could lead to failed transactions being accepted as succesful without approving anything.
Hence it is recommneded to check the return boolean value of the approve
transaction and revert if the success
value of the returned boolean is false
.
Note : In the automated report one instance (BaseBranchRouter.sol#L175) is mentioned. But There is another instance of this issue in BaseBranchRouter.sol#L168
. As a result included this as a quality assurance vulnerabilitiy.
https://github.com/code-423n4/2023-09-maia/blob/main/src/BaseBranchRouter.sol#L168
_refundee
CAN UNFAIRLY PROFIT FROM THE REFUNDED GAS SINCE address(this).balance
IS USED TO CALCULATE THE BALANCE GAS AMOUNT TO BE SENT TO THE _refundee
The RootBridgeAgent
contract and BranchBridgeAgent
contracts use address(this).balance
to calculate the existing balance of the contract to pass as the gas amount during execution of the RootBridgeAgent._execute
, RootBridgeAgent._performFallbackCall
functions and while calling the _performRetrySettlementCall
function.
The issue is, if there is native Eth
transferred to RootBridgeAgent
or BranchBridgeAgent
by another contract or an EOA
by mistake those funds will also be transferred to the _refundee
address of the respective transaction. Hence the _refundee
gets an undue advantage and will profit with extra native Eth
from the mistake of another individual.
Hence it is recommended to calculate the eth balance
before and after the function call which transfers native eth
to either RootBridgeAgent
or BranchBridgeAgent
contracts and then use the actually transferred eth amount
to execute the cross-chain transaction. This can be done by declaring a state variable to keep track of the current eth balance of the contract at all times. And then use that variable to calculate the actually transferred eth amount to the contract. This will refund the correct amount of gas
to the _refundee
address if excessive gas remained after the transaction execution.
Once above change is made it is recommneded to add a withdraw
function to withdraw the locked eth
amount from the RootBridgeAgent
contract and BranchBridgeAgent
contracts. This will ensure fair operation of the system for all users.
https://github.com/code-423n4/2023-09-maia/blob/main/src/RootBridgeAgent.sol#L695 https://github.com/code-423n4/2023-09-maia/blob/main/src/RootBridgeAgent.sol#L754 https://github.com/code-423n4/2023-09-maia/blob/main/src/RootBridgeAgent.sol#L779 https://github.com/code-423n4/2023-09-maia/blob/main/src/RootBridgeAgent.sol#L940 https://github.com/code-423n4/2023-09-maia/blob/main/src/BranchBridgeAgent.sol#L717 https://github.com/code-423n4/2023-09-maia/blob/main/src/BranchBridgeAgent.sol#L737 https://github.com/code-423n4/2023-09-maia/blob/main/src/BranchBridgeAgent.sol#L787
BranchPort.initialize
FUNCTION CAN BE CALLED MULTIPLE TIMES BY THE owner
THUS MAKING THE CONTRACT VULNERABLE TO CENTRALIZATION AND SINGLE POINT OF FAILUREThe BranchPort.initialize
function is onlyOwner
controlled function which is used to initialize the coreBranchRouterAddress
and isBridgeAgentFactory[_bridgeAgentFactory]
state variables as shown below:
coreBranchRouterAddress = _coreBranchRouter; isBridgeAgentFactory[_bridgeAgentFactory] = true; bridgeAgentFactories.push(_bridgeAgentFactory);
The issue here is that this function can be called by the owner any number of times thus changing the coreBranchRouterAddress
address as he wants. This is not the expected behaviour of the initialize
function which is expected to be called only once to initialze the states of the smart contract. Due to the above behaviour of the initialize
function the owner
is getting a greater control over the BranchPort
contract which makes the contract vulnerable to single point of failure.
Hence it is recommended to import
the openzeppelin Initializable.sol
contract and add the initializer
modifier to the BranchPort.initialize
function so it can be called only once by the owner to initialize the smart contract.
https://github.com/code-423n4/2023-09-maia/blob/main/src/BranchPort.sol#L122
BranchPort._checkTimeLimit
TRANSACTION REVERT DUE TO ARITHMETIC UNDERFLOW IS NOT CORRECTLY HANDLEDThe BranchPort._checkTimeLimit
function is used to check if a Port Strategy has reached its daily management limit. The remaining daily limit is updated at the end of the _checkTimeLimit
function as shown below:
strategyDailyLimitRemaining[msg.sender][_token] = dailyLimit - _amount;
But there is no check performed to ensure dailyLimit > _amount
. Hence if the dailyLimit < _amount
transaction could revert silently. Hence it is recommended to add the following require
statement before the strategyDailyLimitRemaining[msg.sender][_token]
is updated.
require(dailyLimit > _amount, "Daily limit has exceeded"); strategyDailyLimitRemaining[msg.sender][_token] = dailyLimit - _amount;
https://github.com/code-423n4/2023-09-maia/blob/main/src/BranchPort.sol#L493
Natspect
COMMENTS AND ACTUAL LOGIC IMPLEMENTATION IN THE hToken Address
PARSING IN THE RootBridgeAgentExecutor._bridgeInMultiple
FUNCTIONThe RootBridgeAgentExecutor._bridgeInMultiple
function parses the input bytes array _dParams
into the respective token details. The Natspect
comment on the parsing of the hToken address
is given as below:
* 1. PARAMS_TKN_START [hToken address has no offset from token information start]
In the Natspec
comment is says there is no offset from the token information start. But as it is evident from the logic implementation there is an offset of ADDRESS_END_OFFSET
byte amount as shown below:
_dParams[ PARAMS_TKN_START + (PARAMS_ENTRY_SIZE * i) + ADDRESS_END_OFFSET: PARAMS_TKN_START + (PARAMS_ENTRY_SIZE * currentIterationOffset) ]
This discrepency between the Natspec
comment and actual logic implementation could be confusing to both developers and auditors.
Hence the Natspec
comment for the hToken address
should be corrected to mention there is an offset of ADDRESS_END_OFFSET
from token information start.
https://github.com/code-423n4/2023-09-maia/blob/main/src/RootBridgeAgentExecutor.sol#L262 https://github.com/code-423n4/2023-09-maia/blob/main/src/RootBridgeAgentExecutor.sol#L286-L297
BranchBridgeAgent._performCall
FUNCTIONThe BranchBridgeAgent._performCall
function is used to perform calls to LayerZero messaging layer Endpoint for cross-chain messaging. _gParams
is passed in as a GasParams
struct which includes the LayerZero gas information
. The Natspec
comments for the _gParams
input parameter is given as shown below:
* @param _gParams LayerZero gas information. (_gasLimit,_remoteBranchExecutionGas,_nativeTokenRecipientOnDstChain)
But the _gParams
is a variable of type GasParams
struct and GasParams
struct does not has a parameter named _nativeTokenRecipientOnDstChain
within its scope. Hence above Natspec
comments is errorneous and should be corrected as follows:
* @param _gParams LayerZero gas information. (_gasLimit,_remoteBranchExecutionGas)
https://github.com/code-423n4/2023-09-maia/blob/main/src/BranchBridgeAgent.sol#763
* `Naive` assets are deposited and hTokens are bridgedOut.
The above Natspec comment should be corrected as follows:
* `Native` assets are deposited and hTokens are bridgedOut.
https://github.com/code-423n4/2023-09-maia/blob/main/src/BranchBridgeAgent.sol#L803
/// @notice `Deposit` nonce used for identifying transaction.
The above Natspec comment should be corrected as follows:
/// @notice `Settlement` nonce used for identifying transaction.
https://github.com/code-423n4/2023-09-maia/blob/main/src/RootBridgeAgent.sol#L74
* @param _localRouterAddress Local `Port` Address.
The above Natspec comment should be corrected as follows:
* @param _localRouterAddress Local `Router` Address.
https://github.com/code-423n4/2023-09-maia/blob/main/src/RootBridgeAgent.sol#L103
* @notice Internal function performs call to Layerzero `Enpoint` Contract for cross-chain messaging.
The above Natspec comment should be corrected as follows:
* @notice Internal function performs call to Layerzero `Endpoint` Contract for cross-chain messaging.
https://github.com/code-423n4/2023-09-maia/blob/main/src/RootBridgeAgent.sol#L934
/// @notice `Deposit` nonce used for identifying transaction.
The above Natspec comment should be corrected as follows:
/// @notice `Settlement` nonce used for identifying transaction.
https://github.com/code-423n4/2023-09-maia/blob/main/src/RootBridgeAgent.sol#L74
#0 - c4-pre-sort
2023-10-15T09:01:18Z
0xA5DF marked the issue as sufficient quality report
#1 - c4-judge
2023-10-20T13:32:33Z
alcueca marked the issue as grade-a
#2 - c4-judge
2023-10-20T13:33:22Z
alcueca marked the issue as selected for report
#3 - c4-judge
2023-10-27T10:39:47Z
alcueca marked the issue as grade-b
#4 - c4-judge
2023-10-27T10:39:52Z
alcueca marked the issue as not selected for report
#5 - alcueca
2023-10-27T10:40:15Z
Now that I understand the codebase, many of these are obviously wrong.
🌟 Selected for report: rvierdiiev
Also found by: 0x11singh99, 0xAnah, 0xta, Aamir, DavidGiladi, DevABDee, Eurovickk, JCK, K42, MrPotatoMagic, Pessimistic, Raihan, Rolezn, SM3_SS, SY_S, Sathish9098, Udsen, ayo_dev, blutorque, c3phas, clara, dharma09, hihen, hunter_w3b, jamshed, koxuan, lsaudit, marqymarq10, oualidpro, pfapostol, sivanesh_808, tabriz, wahedtalash77, zabihullahazadzoi, ziyou-
78.1884 USDC - $78.19
BranchBridgeAgent.redeemDeposit
CAN BE OMITTED TO SAVE GASThe BranchBridgeAgent.redeemDeposit
function is used to redeem the deposit by a given msg.sender
. The redeemDeposit
function performs the following input validation checks using the require
statements.
if (deposit.owner == address(0)) revert DepositRedeemUnavailable(); if (deposit.owner != msg.sender) revert NotDepositOwner();
As it is seen above the deposit.owner == address(0)
conditional checks is redundant since it is anyway verified that the deposit.owner
is not a zero address in the conditional check which follows it.
If the deposit.owner == address(0)
then deposit.owner != msg.sender
is also true since the msg.sender can not be address zero
. Hence it is recommended to remove the redundant deposit.owner == address(0)
conditional check to save gas. The modified code is shown below:
if (deposit.status == STATUS_SUCCESS) revert DepositRedeemUnavailable(); if (deposit.owner != msg.sender) revert NotDepositOwner();
https://github.com/code-423n4/2023-09-maia/blob/main/src/BranchBridgeAgent.sol#L439-L441
delete
CAN BE USED TO OBTAIN A GAS REFUND INSTEAD OF RESETTING TO ADDRESS(0)The BranchBridgeAgent.redeemDeposit
function is used to redeem the deposit by a given msg.sender
. Inside the redeemDeposit
function the deposit.owner
is assigned the address(0)
in order to zero out the owner as shown below:
deposit.owner = address(0);
But the delete
can be used to obtain a gas refund instead of assigning to address(0)
. Hence the modified line of code is shown below:
delete deposit.owner;
https://github.com/code-423n4/2023-09-maia/blob/main/src/BranchBridgeAgent.sol#L444
ADD
ARITHMETIC OPERATION CAN BE REPLACED TO SAVE GASThe BranchBridgeAgentExecutor.executeWithSettlementMultiple
function is used to execute a cross-chain request with multiple settlements. In the function execution if the _payload.length > settlementEndOffset
condition is satisfied (which means there is calldata
to be executed) executeSettlementMultiple
function of the Router
contract is called as shown below:
IRouter(_router).executeSettlementMultiple{value: msg.value}( _payload[PARAMS_END_SIGNED_OFFSET + assetsOffset:], sParams );
The bytes
range of the _payload
is determined as PARAMS_END_SIGNED_OFFSET + assetsOffset:
. But the arithmetic addition used in the above range calculation can be replaced by using the settlementEndOffset
value since the settlementEndOffset == PARAMS_END_SIGNED_OFFSET + assetsOffset
. Hence this will save gas on one +
arithmetic operation. Hence the above code snippet can be modified as shown below:
IRouter(_router).executeSettlementMultiple{value: msg.value}( _payload[settlementEndOffset:], sParams );
https://github.com/code-423n4/2023-09-maia/blob/main/src/BranchBridgeAgentExecutor.sol#L121-L123
The RootBridgeAgent._performRetrySettlementCall
function performs call to Layer Zero Endpoint Contract for cross-chain messaging. During the function execution there is conditional check to ensure that the remoteBridgeAgent
is a valid address and is not address(0)
as shown below:
address callee = getBranchBridgeAgent[_dstChainId]; // Check if valid destination if (callee == address(0)) revert UnrecognizedBridgeAgent();
But the problem here is that this conditional check is performed after the payload
computation for the remote call for either single asset settlement or multiple asset settlement. Hence if the callee == address(0)
condition results in true
the transaction will revert thus wasting the gas consumed on the payload
computation.
Hence it is recommended to perform the callee == address(0)
conditional check at the beggining of the RootBridgeAgent._performRetrySettlementCall
execution thus saving gas if the transaction reverts due to the callee == address(0)
condition resulting in false
.
https://github.com/code-423n4/2023-09-maia/blob/main/src/RootBridgeAgent.sol#L906-L910
RootBridgeAgentExecutor.executeWithDepositMultiple
CAN BE OMITTED TO SAVE GASThe RootBridgeAgentExecutor.executeWithDepositMultiple
function is used to execute a remote call with multiple deposits. The encoded data payload is passed to the executeWithDepositMultiple
as _payload
and the byte array is sliced to obtain the required portion of the _payload
to execute the remote call.
The following calculation is performed to calculate the ending index of the _payload
which represents the deposit details of the assets.
PARAMS_END_OFFSET + uint256(uint8(bytes1(_payload[PARAMS_START]))) * PARAMS_TKN_SET_SIZE_MULTIPLE
But the issue is this calculation is performed three times with in the executeWithDepositMultiple
function redundantly.
Other two locations are as follows:
if (length > PARAMS_END_OFFSET + (numOfAssets * PARAMS_TKN_SET_SIZE_MULTIPLE)) { IRouter(_router).executeDepositMultiple{value: msg.value}( _payload[PARAMS_END_OFFSET + uint256(numOfAssets) * PARAMS_TKN_SET_SIZE_MULTIPLE:], dParams, _srcChainId );
Hence it is recommended to perform the above mentioned calculation and assign the result into a memory variable and read the value from the memory to other two locations where the above calculation is used. This will save gas which is spent on redundant arithmetic operations to calculation the above mentioned bytes array index.
https://github.com/code-423n4/2023-09-maia/blob/main/src/RootBridgeAgentExecutor.sol#L125 https://github.com/code-423n4/2023-09-maia/blob/main/src/RootBridgeAgentExecutor.sol#L134 https://github.com/code-423n4/2023-09-maia/blob/main/src/RootBridgeAgentExecutor.sol#L136-L138 https://github.com/code-423n4/2023-09-maia/blob/main/src/RootBridgeAgentExecutor.sol#L210-L214 https://github.com/code-423n4/2023-09-maia/blob/main/src/RootBridgeAgentExecutor.sol#L220-L222 https://github.com/code-423n4/2023-09-maia/blob/main/src/RootBridgeAgentExecutor.sol#L226-L229
BaseBranchRouter.callOutAndBridgeMultiple
FUNCTION WHICH COULD LEAD TO GAS WASTAGE IN THE EVENT OF A REVERTThe BaseBranchRouter.callOutAndBridgeMultiple
function is used to bridge in multiple tokens to the root. The function calls the _transferAndApproveMultipleTokens
internal function which is used to transfer multiple tokens to the BaseBranchRouter
contract and approve the tokens to the _localPortAddress
contract. This is performed by iterating through a for loop
till _hTokens.length
is reached.
The issue here is if there is a length mismatch in any of the _hTokens
, _tokens
, _amounts
or _deposits
the transaction will revert inside the for loop
due to index out of bound
execption thus wasting the user's gas spent on the computations performed till that point.
Hence it is recommended to perform an array length equality check for the _hTokens
, _tokens
, _amounts
and _deposits
arrays inside the BaseBranchRouter.callOutAndBridgeMultiple
function, before _transferAndApproveMultipleTokens
function is called. Hence if there is an array length mismath then the transaction will revert at the very beginning of its execution without wasting gas on further computation.
https://github.com/code-423n4/2023-09-maia/blob/main/src/BaseBranchRouter.sol#L104-L110 https://github.com/code-423n4/2023-09-maia/blob/main/src/BaseBranchRouter.sol#L192-L198
deposit.status
IS NOT CHECKED AT THE BEGINNING OF THE BranchBridgeAgent.retryDeposit
FUNCTION WHICH ALLOWS THE retryDeposit
TO BE CALLED FOR THE SECOND TIME THUS WASTING GASThe BranchBridgeAgent.retryDeposit
function is used to retry the deposit transaction when the transaction is failed before. In the execution of the BranchBridgeAgent.retryDeposit
the deposit.status
will be updated to STATUS_SUCCESS
as shown below:
deposit.status = STATUS_SUCCESS;
But the issue here is the deposit.status
is not checked for STATUS_SUCCESS
at the beginning of the retryDeposit
function which would allow a deposit.owner
to mistakenly call the retryDeposit
on the _depositNonce
for the second time even after succesfully executing it the first time. As a result the second call will only revert in the RootBridgeAgent.lzReceiveNonBlocking
function when _payload[0] == 0x03
occurs and the following condition occurs:
if (executionState[_srcChainId][nonce] != STATUS_READY) { revert AlreadyExecutedTransaction(); }
But the gas spent on the second retryDeposit
transaction till it reverts after reaching the RootBridgeAgent.lzReceiveNonBlocking
revert conditon will be lost. But this gas wastage can be avoided by performing the status check on the deposit.status
value at the beginning of the BranchBridgeAgent.retryDeposit
function as shown below:
require(deposit.status != STATUS_SUCCESS, "Deposit already executed");
https://github.com/code-423n4/2023-09-maia/blob/main/src/BranchBridgeAgent.sol#L415 https://github.com/code-423n4/2023-09-maia/blob/main/src/RootBridgeAgent.sol#L518-L520
unchecked
DUE TO PREVIOUS CONDITIONAL CHECKS, TO SAVE GASThe BranchPort.replenishReserves
function is used to replenish the minimum reserves required for a particular strategy token
. The strategy tokens
will be withdrawn from the specific _strategy
to replenish the minimum reserve amount.
getPortStrategyTokenDebt
and getStrategyTokenDebt
mappings are updated accordingly by the replenished strategy token amount as shown below:
// Update Port Strategy Token Debt getPortStrategyTokenDebt[_strategy][_token] = portStrategyTokenDebt - amountToWithdraw; // Update Strategy Token Global Debt getStrategyTokenDebt[_token] = strategyTokenDebt - amountToWithdraw;
Both of the above arithmetic operations can not underflow due to previous conditonal check which is given below:
uint256 amountToWithdraw = portStrategyTokenDebt < reservesLacking ? portStrategyTokenDebt : reservesLacking;
Hence it is recommended to unchecked
the getPortStrategyTokenDebt
and getStrategyTokenDebt
updates to save gas.
https://github.com/code-423n4/2023-09-maia/blob/main/src/BranchPort.sol#L201-L207
require
STATEMENT CAN BE REMOVED TO SAVE GAS IN THE BranchPort.setCoreRouter
FUNCTIONThe BranchPort.setCoreRouter
is used to set a new coreBranchRouterAddress
address as shown below:
function setCoreRouter(address _newCoreRouter) external override requiresCoreRouter { require(coreBranchRouterAddress != address(0), "CoreRouter address is zero"); require(_newCoreRouter != address(0), "New CoreRouter address is zero"); coreBranchRouterAddress = _newCoreRouter; }
There is a redundant check of coreBranchRouterAddress != address(0)
check which is redundant and hence can be removed to save gas. Since the setCoreRouter
function is controlled by the requiresCoreRouter
modifier, only the coreBranchRouterAddress
can call the setCoreRouter
function. Hence the coreBranchRouterAddress
can never be address(0)
.
Hence the coreBranchRouterAddress != address(0)
require statement check is redundant and should be removed to save gas.
https://github.com/code-423n4/2023-09-maia/blob/main/src/BranchPort.sol#L331-L335
#0 - c4-pre-sort
2023-10-15T16:57:31Z
0xA5DF marked the issue as sufficient quality report
#1 - c4-judge
2023-10-26T13:48:02Z
alcueca marked the issue as grade-a