Closed c4-submissions closed 1 year ago
0xA5DF marked the issue as duplicate of #785
0xA5DF marked the issue as sufficient quality report
alcueca changed the severity to 2 (Med Risk)
alcueca marked the issue as satisfactory
I would give best report with a working PoC. Without it, I have to check all other duplicates to make sure that the attack vector (using a reverting call, instead of out of gas) works.
From the sponsor:
the warden seems to have missed the fact that our execution is encapsulated by an excessivelySafeCall and as such the revert being described would not bubble up until the LayerZeroEndpoint and would be caught by the bridge agent.
alcueca marked the issue as partial-50
50% for lack of PoC (when others have delivered one for the same vulnerability) and for not correctly identifying a revert (out of gas).
The sponsor is right, and I was wrong :(
Lines of code
https://github.com/code-423n4/2023-09-maia/blob/main/src/RootBridgeAgent.sol#L423-L431
Vulnerability details
Description
Contract
Endpoint
, from LayerZero is the one responsible of sending/receiving messages to/from other chains. Specifically it has functionreceivePayload
, which is called by contractUltraLightNodeV2
(the current default library of the protocol) after validating the transaction proof.This function is the one that is responsible of executing the
lzReceive
function in the destination user application (UA in LayerZero terminology).The last lines of the function is where we have to put our eyes to understand the issue reported here.
First, it looks for a
StoredPayload
value in a mapping, for the source chain and source address of the message:Then, it checks that the payload hash stored is empty, otherwise it reverts and the message cannot be delivered.
Lastly, it tries to execute the function
lzReceive
in the destination address.The execution happens in a try-catch block.
If the execution succeeds, the message is considered delivered by LayerZero.
But if the execution fails, the keccac of the payload, along with its length and the destination address are stored in the
storedPayload
. This means that now we have a blocking message that will not allow the endpoint to process any more messages that come from_srcChainId
and_srcAddress
. Because of these two lines:So what now? How can we unblock the system? Well, LayerZero's
Endpoint
has a couple of functions that can help us out with this.Fisrt, we have
retryPayload
:This function can be called by anyone and it would retry the execution that failed the first time, but now without the gas limit that came in the message from the source chain.
And if the call continually fails, does it mean we have nothing else to do? No,
Endpoint
provides a last resort function:This function deletes the content of the
storedPayload
mapping for the_srcChainId
and_srcAddress
, thus unblocking our UA, that now will be able to receive messages again. But this function can only be called from our UA, that is Maia'sRootBridgeAgent
orBranchBridgeAgent
contracts.The thing is none of these contracts is prepared to execute a call to function forceResumeReceive. This means if anytime a message is sent that cannot be executed by whatever reason, the
Endpoint
would get stuck and will not be able to process any more messages from the source address and the source chain.LayerZero documentation states explicitly that:
(Source: https://layerzero.gitbook.io/docs/evm-guides/best-practice)
So not having this function that allows to unblock the
Endpoint
is a serious vulnerability.And that is because calls to
lzReceive
in the Maia code, in bothRootBridgeAgent
andBranchBridgeAgent
contracts, ultimately excute function(s) _execute. There are two of them, in both contracts, one just tries to execute and reverts on failure, and the other tries to execute and if it fails, it sends a message back to the source chain (provided_hasFallbackToggled
is set to true). To make it simple, we can check function _execute inRootBridgeAgent
:We can clearly see that if the call to the bridge agent executor with the payload provided fails, the excution reverts, and this would make the
Endpoint
store the payload for future execution, potentially blocking it indefinitely.Impact
Impact of this issue is Critical, as the LayerZero messaging between chains could get permanently blocked.
Proof of Concept
The fact that this is an issue that involves sending messages via LayerZero from one chain to another, along with the lack of such testing in the project's own tests has made it very difficult for me to code a PoC.
I hope the above description illustrates the issue with enough detail for the judge and sponsor to value its severity.
Now I will list some steps that could make one chain unusable for Maia:
callOut
in a BranchRouter:callOut
in the BranchBridgeAgent, which calls internal call_performCall
which callssend
in the LayerZero endpoint to send a message to the Root chain's RootBridgeAgent. The payload sent isabi.encodePacked(bytes1(0x01), depositNonce++, _params)
, with_params
being user supplied.UltraLightNodeV2
validates the proof and callsEndpoint
'sreceivePayload
, which in turn callsRootBridgeAgent
'slzReceive
.0x01
so// DEPOSIT FLAG: 1 (Call without Deposit)
. Function_execute
is called with the following values:executeNoDeposit
is called in theRootBridgeAgentExecutor
with, among others, the user supplied_payload
.execute
in contractlocalRouterAddress
passing in the user supplied payload (note below that the payload passed in is stripping the first 5 bytes, PARAMS_TKN_START, which correspond to the flag and the nonce) and the source chain id.execute
function, wether it beingMulticallRootRouter
orCoreRootRouter
, take the first byte and check the function id, if it is not 1 (CoreRootRouter
) or 1 to 3 (MulticallRootRouter
) the call reverts.Endpoint
would have added it to the stored payloads mapping. And if anyone tried to retry the execution viaretryPayload
, execution would fail again for the same reason.forceResumeReceive
would mean the source branch would be unable to send messages ever again to the Root branch.Tools Used
Manual review
Recommended Mitigation Steps
Implement
ILayerZeroApplicationConfig
interface in bothRootBridgeAgent
andBranchBridgeAgent
and implement a functionforceResumeReceive
that in turn callsforceResumeReceive
in the LayerZeroEndpoint
.Assessed type
Other