Attacker can block LayerZero channel due to variable gas cost of saving payload #1220
Labels
3 (High Risk)
Assets can be stolen/lost/compromised directly
bug
Something isn't working
edited-by-warden
H-16
primary issue
Highest quality submission among a set of duplicates
selected for report
This submission will be included/highlighted in the audit report
sponsor confirmed
Sponsor agrees this is a problem and intends to fix it (OK to use w/ "disagree with severity")
Lines of code
https://github.com/Tapioca-DAO/tapioca-bar-audit/blob/master/contracts/usd0/BaseUSDO.sol#L399
https://github.com/Tapioca-DAO/tapiocaz-audit/blob/master/contracts/tOFT/BaseTOFT.sol#L442
https://github.com/Tapioca-DAO/tap-token-audit/blob/main/contracts/tokens/BaseTapOFT.sol#L52
Vulnerability details
Impact
This is an issue that affects
BaseUSDO
,BaseTOFT
, andBaseTapOFT
or all the contracts which are sending and receiving LayerZero messages.The consequence of this is that anyone can with low cost and high frequency keep on blocking the pathway between any two chains, making the whole system unusable.
Proof of Concept
I will illustrate the concept of blocking the pathway on the example of sending a message through
BaseTOFT’s
sendToYAndBorrow
.This function allows the user to mint/borrow
USDO
with some collateral that is wrapped in aTOFT
and gives the option of transferring mintedUSDO
to another chain.The attack starts by invoking
sendToYBAndBorrow
which delegate calls intoBaseTOFTMarketModule
.If we look at the implementation inside the
BaseTOFTMarketModule
nothing is validated there except for thelzPayload
which has the packetType ofPT_YB_SEND_SGL_BORROW
.The only validation of the message happens inside the
LzApp
with the configuration which was set.What is restrained within this configuration is the
payload size
, which if not configured defaults to 10k bytes.The application architecture was set up in a way that all the messages regardless of their packetType go through the same
_lzSend
implementation.I’m mentioning that because it means that if the project decides to change the default payload size to something smaller(or bigger) it will be dictated by the message with the biggest possible payload size.
I’ve mentioned the minimum gas enforcement in my other issue but even if that is fixed and a high min gas is enforced this is another type of issue.
To execute the attack we need to pass the following parameters to the function mentioned above:
ICommonData.IApproval[] calldata approvals
are going to be fake data so max payload size limit is reached(10k). Thetarget
of the 1st approval in the array will be theGasDrainingContract
deployed on the receiving chain and thepermitBorrow = true
.Let’s take an example of an attacker sending a transaction on the home chain which specifies a 1 million gasLimit for the destination transaction.
Transaction is successfully received inside the
lzReceive
after which it reaches _blockingLzReceive.This is the first external call and according to
EIP-150
out of 1 million gas:The cost of saving a big payload into the
failedMessages
and emitting events is higher than 15k.When it comes to 10k bytes it is around 130k gas but even with smaller payloads, it is still significant. It can be tested with the following code:
If we can drain the 985k gas in the rest of the execution since storing
failedMessages
would fail the pathway would be blocked because this will fail at the level of LayerZero and result inStoredPayload
.Let’s continue the execution flow just to illustrate how this would occur, inside the implementation for
_nonblockingLzReceive
the_executeOnDestination
is invoked for the right packet type and there we have another external call which delegatecalls into the right module.Since it is also an external call only 63/64 gas is forwarded which is roughly:
This 970k gas is used for
borrow
, and it would be totally drained inside our malicious GasDraining contract from above, and then the execution would continue inside theexecuteOnDestination
which also fails due to 15k gas not being enough, and finally, it fails inside the_blockingLzReceive
due to out of gas, resulting in blocked pathway.Tools Used
Recommended Mitigation Steps
_executeOnDestination
storing logic is just code duplication and serves no purpose.Instead of that you should override the
_blockingLzReceive
.Create a new storage variable called
gasAllocation
which can be set only by the owner and change the implementation to:While ensuring that
gasleft() > gasAllocation
in each and every case. This should be enforced on the sending side.Now this is tricky because as I have shown the gas cost of storing payload varies with payload size meaning the
gasAllocation
needs to be big enough to cover storing max payload size.Other occurrences
This exploit is possible with all the packet types which allow arbitrary execution of some code on the receiving side with something like I showed with the
GasDrainingContract
. Since almost all packets allow this it is a common issue throughout the codebase, but anyway listing below where it can occur in various places:BaseTOFT
https://github.com/Tapioca-DAO/tapiocaz-audit/blob/master/contracts/tOFT/modules/BaseTOFTLeverageModule.sol#L205
https://github.com/Tapioca-DAO/tapiocaz-audit/blob/master/contracts/tOFT/modules/BaseTOFTMarketModule.sol#L204
https://github.com/Tapioca-DAO/tapiocaz-audit/blob/master/contracts/tOFT/modules/BaseTOFTLeverageModule.sol#L111
https://github.com/Tapioca-DAO/tapiocaz-audit/blob/master/contracts/tOFT/modules/BaseTOFTOptionsModule.sol#L221
https://github.com/Tapioca-DAO/tapiocaz-audit/blob/master/contracts/tOFT/modules/BaseTOFTOptionsModule.sol#L118
BaseUSDO
https://github.com/Tapioca-DAO/tapioca-bar-audit/blob/master/contracts/usd0/modules/USDOMarketModule.sol#L191
https://github.com/Tapioca-DAO/tapioca-bar-audit/blob/master/contracts/usd0/modules/USDOLeverageModule.sol#L190
https://github.com/Tapioca-DAO/tapioca-bar-audit/blob/master/contracts/usd0/modules/USDOMarketModule.sol#L104
https://github.com/Tapioca-DAO/tapioca-bar-audit/blob/master/contracts/usd0/modules/USDOLeverageModule.sol#L93
https://github.com/Tapioca-DAO/tapioca-bar-audit/blob/master/contracts/usd0/modules/USDOOptionsModule.sol#L206
https://github.com/Tapioca-DAO/tapioca-bar-audit/blob/master/contracts/usd0/modules/USDOOptionsModule.sol#L103
BaseTapOFT
IERC20[] memory rewardTokens
as an array of one award token which is our malicious token which implements theERC20
andISendFrom
interfaces.Since inside the
twTap.claimAndSendRewards(tokenID, rewardTokens)
there are no reverts in case therewardToken
isinvalid we can execute the gas draining attack inside the
sendFrom
whereby
rewardTokens[i]
is our malicious contract.Assessed type
DoS
The text was updated successfully, but these errors were encountered: