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: 87/175
Findings: 2
Award: $25.79
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: 0xTheC0der
Also found by: 0x180db, 0xDING99YA, 0xRstStn, 0xTiwa, 0xWaitress, 0xblackskull, 0xfuje, 3docSec, Aamir, Black_Box_DD, HChang26, Hama, Inspecktor, John_Femi, Jorgect, Kek, KingNFT, Kow, Limbooo, MIQUINHO, MrPotatoMagic, NoTechBG, Noro, Pessimistic, QiuhaoLi, SovaSlava, SpicyMeatball, T1MOH, TangYuanShen, Vagner, Viktor_Cortess, Yanchuan, _eperezok, alexweb3, alexxander, ast3ros, ayden, bin2chen, blutorque, btk, ciphermarco, ether_sky, gumgumzum, gztttt, hals, imare, its_basu, joaovwfreire, josephdara, klau5, kodyvim, ladboy233, marqymarq10, mert_eren, minhtrng, n1punp, nobody2018, oada, orion, peakbolt, peritoflores, perseverancesuccess, pfapostol, rvierdiiev, stuxy, tank, unsafesol, ustas, windhustler, zambody, zzzitron
0.1127 USDC - $0.11
call() is restricted with the requiresApprovedCaller
modifier @ L66
payableCall() @ L85 does not have the requiresApprovedCaller
modifier even though it is largely the same function as call()
+ it also allows the caller to send ETH.
Since call()
is restructed, payableCall()
should be as well.
Without the modifier, anyone can call payableCall()
to send transactions as the VirtualAccount to arbitrary contracts, which may bypass some access conrols in 3rd party contracts.
Add requiresApprovedCaller
to payableCall()
Access Control
#0 - c4-pre-sort
2023-10-08T14:12:32Z
0xA5DF marked the issue as duplicate of #888
#1 - c4-pre-sort
2023-10-08T14:53:33Z
0xA5DF marked the issue as sufficient quality report
#2 - c4-judge
2023-10-26T11:30:04Z
alcueca marked the issue as satisfactory
#3 - c4-judge
2023-10-26T11:32:08Z
alcueca changed the severity to 3 (High Risk)
🌟 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
Change nonce = uint32(bytes4(_payload[22:26]));
to match other nonce values in this function: nonce = uint32(bytes4(_payload[PARAMS_START_SIGNED:PARAMS_TKN_START_SIGNED]));
PARAMS_START_SIGNED = 21 PARAMS_TKN_START_SIGNED = 25
Marking as low as I have not thought through or validated impact, perhaps a nonce collision is possible here? Based on the surrounding code, I believe these values should be updated.
_minimumReservesRatio
or adding to strategyTokens
arrayBranchRouters as currently implemented do not do this, marking as low.
BranchPort.togglePortStrategy() has no validation to check if a Port Strategy has been created before.
This would result in:
getMinimumTokenReserveRatio[_token]
returning 0 which affects _minimumReserves()
@ L473 and violates the MIN_RESERVE_RATIO = 3e3
state variable.strategyDailyLimitAmount
state variable is not set (defaults to 0)Allows callling manage()
to withdraw all tokens, leaving 0 in reserves. Will pass modifier check requiresPortStrategy
@ L559.
Also affects
replenishReserves
@ L188,_reservesLacking()
returns current balance?
This would also affect the public strategyTokens
variable which would no longer output accurate results.
It's worth noting a call to
addStrategyToken()
can resolve this issue, however, the protocol should have prevented this state from occuring.
BranchPort.addPortStrategy() has never been called for a given _portStrategy
and _token
Calling togglePortStrategy() will set isPortStrategy
true which will pass checks in other logic, however, portStrategies
and strategyDailyLimitAmount
state variables are never set like in addPortStrategy()
@ L388 and L389 respectively.
Validate inputs for BranchPort.togglePortStrategy() to ensure a Port Strategy has previously been created through the addPortStrategy()
function.
Note, other toggle functions in Branch Port could also implement validation logic as well.
createBridgeAgent
I did not have time to fully assess the impact of this, or if it is a problem. Below are some notes as I understand them which may be useful for further assessment.
A malicious user can create their own BranchAgent and add it to the isBridgeAgent
mapping of an arbitrary RootPort by finding the associated Root Port Factory's address.
RootBridgeAgentFactory.createBridgeAgent() (can be called by anyone) > RootPort.addBridgeAgent(). A RootPort owner must call RootPort.toggleBridgeAgentFactory() to disable the factory, however, this call can be frontrun if done using a public mempool in a separate transaction as the creation of the RootPort itself.
Functions with the requiresBridgeAgent
modifier in RootPort that may be callable:
RootPort.burn() @ #L318 which does not validate _srcChainId != localChainId
. I believe this is ok because there is a check in what I believe is the callstack before burn()
is called @ [RootBridgeAgent._performCall() @ L821(https://github.com/code-423n4/2023-09-maia/blob/f5ba4de628836b2a29f9b5fff59499690008c463/src/RootBridgeAgent.sol#L821).
RootPort.toggleVirtualAccountApproved() which would allow a RootPort to interact with an arbitrary VirtualAccount as the Virtual Account's access control @ L161 in the requiresApprovedCaller
queries the RootPort to check if the RootPort can access the VirtualAccount's functions.
Since the malicious users has deployed their own RootBridgeAgent which includes it's own BridgeAgentExecutor as well as deployed their own MultiCallRootRouter, they should be able to trigger function calls, specifically through the RootBridgeAgent.lzreceive()/RootBridgeAgent.lzreceiveNonBlocking() functions. However, calling functions is abstracted through LayerZero and I have no had a chance to dig deeper, I am not sure the range of functions the malicious user could actually do.
It may be possible, for instance, to cause a DOS by frontrunning calls to (eventually) execute RootPort.toggleVirtualAccountApproved() and cause other calls to fail such as deposits and withdraws.
For this test, we are just adding a new BridgeAgent that we control to an arbitrary RootPort.
Add test to: /2023-09-maia/test/ulysses-omnichain/ArbitrumBranchTest.t.sol
function testAddBridgeAgentArbitrum_asUser(address _user) public { //Get some gas hevm.deal(address(this), 2 ether); hevm.deal(_user, 4 ether); hevm.startPrank(_user); // Create MulticallRootRouter MulticallRootRouter testMulticallRouter; testMulticallRouter = new MulticallRootRouter( rootChainId, address(rootPort), multicallAddress ); // Create Bridge Agent // @audit bridgeAgentFactory's `createBridgeAgent` is a public function with no access control // @audit Note that this adds the new BridgeAgent onto the rootPort that uses the Factory. (i.e.: we now have a BranchAgent of our control added to the isBridgeAgent mapping of an arbitrary RootPort) RootBridgeAgent testRootBridgeAgent = RootBridgeAgent( payable(RootBridgeAgentFactory(bridgeAgentFactory).createBridgeAgent(address(testMulticallRouter))) ); //Initialize Router w/ new Bridge Agent testMulticallRouter.initialize(address(testRootBridgeAgent)); //Create Branch Router in FTM BaseBranchRouter arbTestRouter = new BaseBranchRouter(); //Allow new branch from root testRootBridgeAgent.approveBranchBridgeAgent(rootChainId); //Create Branch Bridge Agent rootCoreRouter.addBranchToBridgeAgent{value: 2 ether}( address(testRootBridgeAgent), address(localBranchBridgeAgentFactory), address(testMulticallRouter), address(this), rootChainId, [GasParams(6_000_000, 15 ether), GasParams(1_000_000, 0)] ); console2.log("new branch bridge agent", localPortAddress.bridgeAgents(2)); BranchBridgeAgent arbTestBranchBridgeAgent = BranchBridgeAgent(payable(localPortAddress.bridgeAgents(2))); arbTestRouter.initialize(address(arbTestBranchBridgeAgent)); require(testRootBridgeAgent.getBranchBridgeAgent(rootChainId) == address(arbTestBranchBridgeAgent)); hevm.stopPrank(); }
Run test with
forge test --match-test testAddBridgeAgentArbitrum_asUser
Consider adding access control to the RootBridgeAgentFactory.createBridgeAgent() function.
#0 - c4-pre-sort
2023-10-15T12:53:08Z
0xA5DF marked the issue as sufficient quality report
#1 - alcueca
2023-10-21T05:39:44Z
L-3 is explicitly allowed. L-1 might be valuable.
#2 - c4-judge
2023-10-21T05:39:48Z
alcueca marked the issue as grade-a