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: 110/175
Findings: 1
Award: $25.68
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 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
https://github.com/code-423n4/2023-09-maia/blob/f5ba4de628836b2a29f9b5fff59499690008c463/src/BranchBridgeAgent.sol#L882 https://github.com/code-423n4/2023-09-maia/blob/f5ba4de628836b2a29f9b5fff59499690008c463/src/BranchBridgeAgent.sol#L832 https://github.com/code-423n4/2023-09-maia/blob/f5ba4de628836b2a29f9b5fff59499690008c463/src/BranchBridgeAgent.sol#L354
In the event of a failed deposit the has not been executed yet, the user may not be able to call retryDeposit(...)
if the user does not specify his account as the refundee
.
When _createDeposit(...)
is called as shown below, the deposit.owner
variable is updated to _refundee
.
function _createDeposit( uint32 _depositNonce, address payable _refundee, address _hToken, address _token, uint256 _amount, uint256 _deposit ) internal { // Update Deposit Nonce depositNonce = _depositNonce + 1; ... // Save deposit to storage ... // @audit SUGGESTION change deposit.owner = _refundee; to deposit.owner = msg.sender; deposit.owner = _refundee; }
When retryDeposit(...)
is called, as shownn below, if the user did not use his address as the refundee
during deposit creation, his the call to retryDeposit(...)
will revert.
function retryDeposit( bool _isSigned, uint32 _depositNonce, bytes calldata _params, GasParams calldata _gParams, bool _hasFallbackToggled ) external payable override lock { ... //Check if deposit belongs to message sender if (deposit.owner != msg.sender) revert NotDepositOwner(); ... // Perform Call _performCall(payable(msg.sender), payload, _gParams); }
Manual review
Consider making the msg.sender
the owner during the deposir creation stage as shown below
function _createDeposit( uint32 _depositNonce, address payable _refundee, address _hToken, address _token, uint256 _amount, uint256 _deposit ) internal { ... // Save deposit to storage ... deposit.owner = msg.sender; }
Invalid Validation
#0 - c4-pre-sort
2023-10-11T12:37:44Z
0xA5DF marked the issue as primary issue
#1 - c4-pre-sort
2023-10-11T12:37:49Z
0xA5DF marked the issue as sufficient quality report
#2 - 0xA5DF
2023-10-11T12:38:01Z
Seems like a design choice, but will leave open for sponsor to comment
#3 - c4-sponsor
2023-10-17T20:19:26Z
0xBugsy (sponsor) disputed
#4 - 0xLightt
2023-10-17T20:20:29Z
This is intended, you can set the owner of the deposit to your own address if desired. Removing this would break functionality, like our current router interactions.
#5 - alcueca
2023-10-25T13:00:24Z
It might be intended, but that is far from what the docs suggest.
<img width="731" alt="image" src="https://github.com/code-423n4/2023-09-maia-findings/assets/38806121/fd4969ef-bfd9-43fe-a421-4581be423622">It doesn't seem far-fetched to think that users might want to set _refundee
to some address other than msg.sender
. It doesn't make sense either that if the user decides to set the _refundee
to something else than msg.sender
then they lose some functionality unrelated to gas refunds.
#6 - alcueca
2023-10-25T13:01:42Z
Maybe the deposit owner and the refundee should be separate concepts, and dealt with separately.
#7 - 0xBugsy
2023-10-25T16:19:09Z
We believe this relies on API misuse. Someone integrating with our contracts should be conscious of what's the role of a _refundee
so as to prevent setting ownership over user deposits or settlements to other addresses.
The only use case we can see where having both addresses exposed, would be if someone were to create a router contract that is intended to retain ownership over user deposits (e.g. needs to perform some extra action before giving tokens back to user) and wanted to refund excess gas to the caller, this can also be achieved by other means without having to increase the number of parameters for everyone else.
In addition, for signed calls letting the user decide the _owner
could lead to unexpected outcomes in our opinion since we would be using different Virtual Accounts in the first call and the second.
Regarding the natspec we 100% agree it is not up to par and should be updated to match the detail used in IRootBridgeAgent
: https://github.com/code-423n4/2023-09-maia/blob/f5ba4de628836b2a29f9b5fff59499690008c463/src/interfaces/IRootBridgeAgent.sol#L178C1-L179C120
#8 - alcueca
2023-10-26T06:12:11Z
I'll judge it as QA grade-a, but please fix the natspec and possibly the variable name. It is very misleading. I'd argue that the fact that the variable denotes the deposit owner is much more relevant than the fact that it also denotes the recipient for gas refunds.
#9 - c4-judge
2023-10-26T06:13:00Z
alcueca changed the severity to QA (Quality Assurance)
#10 - c4-judge
2023-10-26T06:14:21Z
alcueca marked the issue as grade-a
#11 - 0xLightt
2023-11-01T01:21:03Z
As bugsy stated:
Someone integrating with our contracts should be conscious of what's the role of a
_refundee
so as to prevent setting ownership over user deposits or settlements to other addresses.
The _refundee
is meant to be the owner, especially for non-signed calls that are router/contract facing functions. We should rename that variable and/or split it into two.
In addition, for signed calls letting the user decide the _owner could lead to unexpected outcomes in our opinion since we would be using different Virtual Accounts in the first call and the second.
For signed calls, the msg.sender should be the owner or what bugsy described would happen. We are not doing this part correctly, the _refundee
is being used instead of msg.sender here for example: https://github.com/code-423n4/2023-09-maia/blob/f5ba4de628836b2a29f9b5fff59499690008c463/src/BranchBridgeAgent.sol#L299C39-L299C48
I still believe this would only happen due to API misuse, a UI should always pass the caller's address as _refundee
. And if a EOA is able to build proper callOutSignedAndBridge
parameters, it is expected that they would also put their own or a trusted/owned address in _refundee
. But in case anyone ever makes the mistake, to avoid any issues, we should only use the _refundee
address for gas refunds or take out _refundee
parameter completely for signed calls and use msg.sender for all purposes.
#12 - viktorcortess
2023-11-01T16:22:13Z
As bugsy stated:
Someone integrating with our contracts should be conscious of what's the role of a
_refundee
so as to prevent setting ownership over user deposits or settlements to other addresses.The
_refundee
is meant to be the owner, especially for non-signed calls that are router/contract facing functions. We should rename that variable and/or split it into two.In addition, for signed calls letting the user decide the _owner could lead to unexpected outcomes in our opinion since we would be using different Virtual Accounts in the first call and the second.
For signed calls, the msg.sender should be the owner or what bugsy described would happen. We are not doing this part correctly, the
_refundee
is being used instead of msg.sender here for example: https://github.com/code-423n4/2023-09-maia/blob/f5ba4de628836b2a29f9b5fff59499690008c463/src/BranchBridgeAgent.sol#L299C39-L299C48I still believe this would only happen due to API misuse, a UI should always pass the caller's address as
_refundee
. And if a EOA is able to build propercallOutSignedAndBridge
parameters, it is expected that they would also put their own or a trusted/owned address in_refundee
. But in case anyone ever makes the mistake, to avoid any issues, we should only use the_refundee
address for gas refunds or take out_refundee
parameter completely for signed calls and use msg.sender for all purposes.
Hello 0xLightt , I have one of the duplicates of this issue and after reading your comments I'd like to add a comment. In "real life" it's clear that the dApp and API will be set up correctly. But we audit the code with all possible interactions and the problem here is not in the names of the variables.
If we want to use a Branch side we can use ether callOutAndBridge() function from BaseBranchRouter contract or callOutAndBridge from BranchBridge contract(). Both of them don't have any modifier so a user can call any of them and in case if he calls it from the BaseBranchRouter the refundee is indeed msg.sender:
//Perform call to bridge agent. IBridgeAgent(localBridgeAgentAddress).callOutAndBridge{value: msg.value}( payable(msg.sender), _params, _dParams, _gParams );
But if a user uses BranchBridge contract he can set up whatever he wants as a refundee and this can later lead to problems described in the issues linked with retry functions.
function callOutAndBridge( address payable _refundee, bytes calldata _params, DepositInput memory _dParams, GasParams calldata _gParams ) external payable override lock {
Even if it looks like a user error at first I noticed that similar Root functions are protected by modifiers, so the agent only can be called by a router, not by a user:
RootBridgeAgent:
175: function callOutAndBridge( address payable _refundee, address _recipient, uint16 _dstChainId, bytes calldata _params, SettlementInput calldata _sParams, GasParams calldata _gParams, bool _hasFallbackToggled ) external payable override lock requiresRouter { /// @notice Internal function to verify msg sender is Bridge Agent's Router. modifier requiresRouter() { if (msg.sender != localRouterAddress) revert UnrecognizedRouter(); _; }
I am leading to the point that maybe bridgeAgent contract should have the same modifier then the user won't be able to call bridge contract's call functions without router? At the current implementation, the bridge has 2 entry points if a user wants to use call() functions. Maybe it's the designed behavior but I think these 2 possibilities to call the same function attracted the attention of the wardens and I personally assumed that it was a forgotten modifier, not just the name of a variable.
#13 - 0xLightt
2023-11-01T18:08:39Z
Your recommendation to add requiresRouter
to every non-signed callOut in the branch and remove the system calls makes complete sense.
I believe the reason for QA is that due to happening from a user error, so it follows the first point: https://github.com/code-423n4/2023-09-maia-findings/discussions/906#discussioncomment-7408115
#14 - c4-sponsor
2023-11-01T18:10:22Z
0xLightt (sponsor) confirmed
#15 - viktorcortess
2023-11-01T18:20:41Z
Hello @alcueca, may I request your attention to the comments above, and I kindly ask for your final decision on this matter.
#16 - alcueca
2023-11-03T10:01:16Z
Again, a user calling this function can only hurt themselves if they call it with the wrong parameters. To be consistent in judging this contest I'm classifying users hurting themselves as QA, unless it is explicitly said that the users should do something, and that something hurts them.
To be a High or a Medium in this contest:
The sponsor modifying the code to remove footguns is consistent with a QA grade-a severity, as well.