sherlock-audit / 2024-09-orderly-network-solana-contract-judging

0 stars 0 forks source link

Sunny Syrup Worm - Unchecked `deposit_token` Allows Malicious Token Substitution During Withdrawals #100

Open sherlock-admin4 opened 4 days ago

sherlock-admin4 commented 4 days ago

Sunny Syrup Worm

High

Unchecked deposit_token Allows Malicious Token Substitution During Withdrawals

Summary

A verified withdrawal message can be maliciously intercepted, replacing the expected token with a different, low-value token (e.g., BONK) by changing the deposit_token account. This enables the attacker to trick the vault into sending BONK instead of the intended token, causing the recipient to receive significantly less than the expected amount.

Root Cause

When the ledger processes a withdrawal through the SolConnector::withdraw function, the message is sent via LayerZero to the Orderly vault on Solana. In Solana, the message is verified by the endpoint::verify function, and upon successful verification, the PacketVerifiedEvent is emitted, making the message executable. A malicious user listening to PacketVerifiedEvent for the oApp receiver can then deliver the message to solana_vault::lz_receive with the same message as verified in the LayerZero endpoint but with a different deposit_token account:

#[account()]
pub deposit_token: Account<'info, Mint>,

If the vault authority currently holds other tokens up to the transfer amount, this would allow the attacker to send this other token to the receiver instead of the expected allowed token by setting deposit_token to this alternative token. If the vault authority only has a USDC-associated token account, the attacker could create an associated account for the vault authority, and then send $1 worth of a meme coin like BONK to the associated token account. The amount sent would depend on the set token amount in the constructed withdrawal message in SolConnector:

struct AccountWithdrawSol {
    bytes32 accountId;
    bytes32 sender;
    bytes32 receiver;  
    bytes32 brokerHash;
    bytes32 tokenHash;
    uint128 tokenAmount; //<-----  @
    uint128 fee;
    uint256 chainId;
    uint64 withdrawNonce;
}

As of the time of writing this report, 1 USDC is equivalent to 44,898.87640449438 BONK.

Since BONK in Solana has 5 decimal precision and USDC has 6 decimal precision, sending 1 USDC worth of BONK to the vault authority’s associated token account will process up to a 4,489 USDC withdrawal amount. If the receiver is expected to receive 4,000 USDC, they will instead receive 4,000 BONK, which is worth approximately 0.89 USDC.

Note that a BONK associated account can be easily created for the vault authority. The attacker only needs to cover a one-time creation cost:

#[account(init_if_needed, payer = attacker, associated_token::mint = deposit_token, associated_token::authority = vault_authority)]

Leveraging another issue, the account could also be created for the vault authority by depositing BONK to the vault authority via solana_vault::deposit

Internal Preconditions

None

External Preconditions

None

Attack Path

  1. The attacker listens for the PacketVerifiedEvent, which signals that a message has been verified and can be executed.
  2. If the vault authority has an associated token account for BONK, the attacker deposits an amount up to the set withdrawal amount in the message to this account. If there isn’t an account, the attacker creates one for the vault authority, then deposits BONK.
  3. The attacker sends a crafted message to solana_vault::lz_receive, modifying the deposit_token account to use BONK instead of the expected token.
  4. Due to the low value of BONK, the recipient receives far below the expected withdrawal value, resulting in a significant loss.

Impact

The attacker can intercept and manipulate USDC token withdrawals to instead send BONK to the receivers, significantly reducing the expected withdrawal amount's value.

Proof of Concept (PoC)

https://github.com/LayerZero-Labs/LayerZero-v2/blob/7bcfb4d5dac4192570af5e51dbc67413a6116a14/packages/layerzero-v2/solana/programs/programs/endpoint/src/lib.rs#L175-L177

https://solscan.io/token/DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263

Mitigation

Consider ensuring that the deposit_token account is indeed the expected allowed token:

#[account(constraint = deposit_token.key() == oapp_config.usdc_mint)]
pub deposit_token: Account<'info, Mint>,