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

0 stars 0 forks source link

Plain Corduroy Goblin - Protocol Fund Drain via LayerZero Fee Exhaustion Through Invalid Cross-Chain Withdrawals #106

Open sherlock-admin4 opened 4 days ago

sherlock-admin4 commented 4 days ago

Plain Corduroy Goblin

Medium

Protocol Fund Drain via LayerZero Fee Exhaustion Through Invalid Cross-Chain Withdrawals

Summary

A malicious user will cause fund loss and potential denial of service to the protocol and its users by initiating small withdrawals that are guaranteed to fail on the Solana side after consuming LayerZero fees paid by the protocol.

Root Cause

The withdraw() function being non-payable and Protocol pays fees via internal _payNative():

function _payNative(uint256 _nativeFee) internal virtual returns (uint256 nativeFee) {
    // Protocol balance is used to pay fees
    if (msg.value < _nativeFee && address(this).balance < _nativeFee) revert NotEnoughNative(msg.value);
    return _nativeFee;
}

LayerZero message initiation showing fee payment from protocol: https://github.com/sherlock-audit/2024-09-orderly-network-solana-contract/blob/main/sol-cc/contracts/SolConnector.sol#L97

bytes memory lzWithdrawMsg = MsgCodec.encodeLzMsg(uint8(MsgCodec.MsgType.Withdraw), payload);
bytes memory withdrawOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(
    msgOptions[uint8(MsgCodec.MsgType.Withdraw)].gas,
    msgOptions[uint8(MsgCodec.MsgType.Withdraw)].value
);
MessagingFee memory _msgFee = _quote(solEid, lzWithdrawMsg, withdrawOptions, false);
// Protocol pays fee through _lzSend
_lzSend(solEid, lzWithdrawMsg, withdrawOptions, _msgFee, address(this));

Solana side trying to take fee from very small token_amount that will fail:

if lz_message.msg_type == MsgType::Withdraw as u8 {
    let withdraw_params = AccountWithdrawSol::decode_packed(&lz_message.payload).unwrap();

    let amount_to_transfer = withdraw_params.token_amount - withdraw_params.fee;
    transfer(
        ctx.accounts
            .transfer_token_ctx()
            .with_signer(&[&vault_authority_seeds[..]]),
        amount_to_transfer,
    )?;
}

Internal pre-conditions

No response

External pre-conditions

No response

Attack Path

  1. Attacker initiates multiple withdrawals of 0.000001 USDC each
  2. Protocol pays LayerZero fees on EVM side for message relay from its own balance
  3. Messages get relayed to Solana
  4. Transactions revert on Solana due to underflow in amount_to_transfer = token_amount - fee
  5. Protocol loses fee with no successful transfer
  6. Repeat to amplify impact and drain protocol's fee reserves

Impact

The protocol suffers financial losses equal to the LayerZero message fees for each failed transaction, with fees being paid from protocol's own balance rather than users'. Other users experience denial of service as the attack consumes funds meant for fee. The attacker loses minimal funds (just EVM transaction gas fees) while causing significant protocol losses.

PoC

No response

Mitigation

No response