Open sherlock-admin3 opened 6 months ago
1 comment(s) were left on this issue during the judging contest.
takarez commented:
seem valid; high(6)
The protocol team fixed this issue in PR/commit https://github.com/Tapioca-DAO/tapioca-periph/pull/200.
0xadrii
high
Wrong parameter in remote transfer makes it possible to steal all USDO balance from users
Summary
Setting a wrong parameter when performing remote transfers enables an attack flow where USDO can be stolen from users.
Vulnerability Detail
The following bug describes a way to leverage Tapioca’s remote transfers in order to drain any user’s USDO balance. Before diving into the issue, a bit of background regarding compose calls is required in order to properly understand the attack.
Tapioca allows users to leverage LayerZero’s compose calls, which enable complex interactions between messages sent across chains. Compose messages are always preceded by a sender address in order for the destination chain to understand who the sender of the compose message is. When the compose message is received,
TapiocaOmnichainReceiver.lzCompose()
will decode the compose message, extract thesrcChainSender_
and trigger the internal_lzCompose()
call with the decodedsrcChainSender_
as the sender:One of the type of compose calls supported in tapioca are remote transfers. When the internal
_lzCompose()
is triggered, users who specify a msgType equal toMSG_REMOTE_TRANSFER
will make the_remoteTransferReceiver()
internal call be executed:Remote transfers allow users to burn tokens in one chain and mint them in another chain by executing a recursive
_lzSend()
call. In order to burn the tokens, they will first be transferred from an arbitrary owner set by the function caller via the_internalTransferWithAllowance()
function.After transferring the tokens via
_internalTransferWithAllowance()
,_internalRemoteTransferSendPacket()
will be triggered, which is the function that will actually burn the tokens and execute the recursive_lzSend()
call:As we can see, the
_lzSend()
call performed inside_internalRemoteTransferSendPacket()
allows to trigger the remote call with another compose message (built using the_buildOFTMsgAndOptionsMemory()
function). If there is an actual_composeMsg
to be appended, the sender of such message will be set to the_internalRemoteTransferSendPacket()
function’s_srcChainSender
parameter.The problem is that when
_internalRemoteTransferSendPacket()
is called, the parameter passed as the source chain sender is set to the arbitrary owner address supplied by the caller in the initial compose call, instead of the actual source chain sender:This makes it possible for an attacker to create an attack vector that allows to drain any user’s USDO balance. The attack path is as follows:
owner
to any victim’s address. It is important to also set the amount to be transferred in this first compose call to 0 so that the attacker can bypass the allowance check performed inside the_remoteTransferReceiver()
call._internalRemoteTransferSendPacket()
. This second compose message will be sent from chain B to chain A, and the source chain sender will be set to the arbitraryowner
address that the attacker wants to drain due to the incorrect parameter being passed. It will also be a remote transfer action._lzReceive()
triggered in chain A, the composed message will instruct to transfer and burn a certain amount of tokens (selected by the attacker when crafting the attack). Because the source chain sender is the victim address and theowner
specified is also the victim, the_internalTransferWithAllowance()
executed in chain A will not check for allowances because the owner and the spender are the same address (the victim’s address). This will burn the attacker’s desired amount from the victim’s wallet._lzSend()
will be triggered to chain B, where the burnt tokens in chain A will be minted. Because the compose calls allow to set a specific recipient address, the receiver of the minted tokens will be theattacker
.As a summary: the attack allows to combine several compose calls recursively so that an attacker can burn victim’s tokens in Chain A, and mint them in chain B to a desired address. The following diagram summarizes the attack for clarity:
Proof of concept
The following proof of concept illustrates how the mentioned attack can take place. In order to execute the PoC, the following steps must be performed:
EnpointMock.sol
file inside thetest
folder insideTapioca-bar
and paste the following code (the current tests are too complex, this imitates LZ’s endpoint contracts and reduces the poc’s complexity):Usdo.t.sol
fileUsdo.sol
’s implementation so that the endpoint variable is not immutable and add asetEndpoint()
function so that the endpoint configured insetUp()
can be chainged to the newly deployed endpointsUsdo.t.sol
:Run the poc with the following command:
forge test --mt testVuln_stealUSDOFromATargetUserDueToWrongParameter
The proof of concept shows how in the end, the victim’s
aUsdo
balance will become 0, while all thebUsdo
in chain B will be minted to the attacker.Impact
High. An attacker can drain any USDO holder’s balance and transfer it to themselves.
Code Snippet
https://github.com/sherlock-audit/2024-02-tapioca/blob/main/Tapioca-bar/gitmodule/tapioca-periph/contracts/tapiocaOmnichainEngine/TapiocaOmnichainReceiver.sol#L224
Tool used
Manual Review, foundry
Recommendation
Change the parameter passed in the
_internalRemoteransferSendPacket()
call so that the sender in the compose call built inside it is actually the real source chain sender. This will make it be kept along all the possible recursive calls that might take place: