Organization
- @Layer-N
Engagement Type
Cantina Reviews
Period
-
Researchers
Findings
High Risk
2 findings
2 fixed
0 acknowledged
Low Risk
3 findings
3 fixed
0 acknowledged
Informational
1 findings
0 fixed
1 acknowledged
High Risk2 findings
Withdrawal roots can be specifically crafted such that multiple withdrawals are possible
Severity
- Severity: High
Submitted by
Mario Poneder
Description
Core vulnerability
The Merkle
root_from_prooffunction below, will successfully skip the for-loop's body in caseleaves_len == 1and an emptyproof, i.e.proof.len() == 0.pub fn root_from_proof<L>( leaf: L, leaf_idx: LeafIdx, leaves_len: LeafIdx, proof: &[[u8; 32]], digest_cb: impl Fn(L, &mut sha2::Sha256),) -> [u8; 32] { let ceil_log2_n: u8 = leaves_len.checked_next_power_of_two().unwrap().ilog2() as u8; let mut root = hash_leaf(leaf, &digest_cb); let mut i: Height = 0; for h in 0..ceil_log2_n { // @audit skipped if leaves_len == 1 let begin = subtree_ofs(leaf_idx, h); if begin >= leaves_len { continue; } let is_leaf_left = leaf_idx & (1 << h) == 0; root = if is_leaf_left { hash_pair(root, proof[i as usize]) } else { hash_pair(proof[i as usize], root) }; i += 1; } assert_eq!(i as usize, proof.len()); // @audit passes if proof.len() == 0 root // @audit returns hash_leaf(leaf, &digest_cb) if loop is skipped}In this case, the
leaf_idxparameter is ignored and the return value is just the digest of theleaf.Attack vector
The operator who proposes a block can arbitrarily set the
withdrawal_rootallowing it to be just the digest of theleaf_to_prove. In this case, it suffices to invoke thewithdrawinstruction below withclaim.leaves_count == 1and emptyclaim.proofto pass the root check and proceed with the withdrawal.pub fn withdraw(ctx: Context<Withdraw>, claim: WithdrawalClaim) -> Result<()> { let root = ctx.accounts.state_update.facts.withdrawal_root; let leaf_to_prove = TransferParams { user: claim.user, mint: ctx.accounts.to_account.mint, amount: claim.amount, }; let computed_root = crate::merkle::root_from_proof( &leaf_to_prove, claim.leaf_index, claim.leaves_count, &claim.proof, TransferParams::digest, ); if root != computed_root { // @audit will pass if withdrawal_root is digest of leaf_to_prove return err!(BridgeError::InvalidWithdrawalProof); } ...}In order to avoid multiple withdrawals reusing the same block, the
Withdrawcontext implements thewithdrawal_nullifierbelow.#[derive(Accounts)]#[instruction(claim: WithdrawalClaim)]pub struct Withdraw<'info> { ... #[account( init, payer = payer, space = 8, seeds = [WITHDRAWAL_NULLIFIER_SEED, &claim.block_id.to_le_bytes(), &claim.leaf_index.to_le_bytes()], bump, )] pub withdrawal_nullifier: Account<'info, WithdrawalNullifier>, ...}However, in case of an empty proof as discussed above, the
claim.leaf_indexis ignored in the root computation and therefore can be set to arbitrary values to bypass thewithdrawal_nullifierallowing multiple withdrawals.Recommendation
It is recommended to incorporate the
leaf_idxinto the root computation in any case and assert thatleaves_len > leaf idxto also evaluate theleaf_idxin case of an emptyproof.Block finalization is immediate and permissionless
Severity
- Severity: High
Submitted by
Mario Poneder
Description
Once a block is proposed by the operator, which might malicious e.g. containing a specifically crafted withdrawal root, anyone can immediately invoke the
finalize_blockinstruction to finalize it.Consequently, a malicious withdrawal can be performed immediately after proposing such a block.
Recommendation
It is recommended to introduce a challenge as well as a validity-proof mechanism that allows validators to challenge a block and prove its validity before it can be finalized.
Low Risk3 findings
Permissionless initialization allows anyone to set operator
Severity
- Severity: Low
Submitted by
Mario Poneder
Description
The
initializeinstruction and its account context impose no restrictions on the signer.
Consequently, the first one to invokeinitializeafter deployment is in charge of setting the protocol'soperatorrole.Recommendation
It is recommended to add the following code to the
Initializecontext to require co-signing of theinitializeinstruction with the program's private key (keypair) that should only be known to the deployer.#[account(address = crate::ID)]pub program: Signer<'info>Deposits can be spammed with zero amounts
Severity
- Severity: Low
Submitted by
Mario Poneder
Description
The permissionless
deposit_splinstruction imposes no restrictions on theamount.
Consequently, anyone can spam deposits with zero or dust amounts thereby unnecessarily increasing thelast_deposit_index.Recommendation
It is recommended to introduce a minimum deposit amount.
Insufficient validation of tree height when computing subtree offset
Severity
- Severity: Low
Submitted by
Mario Poneder
Description
The following shift operation in the
subtree_ofsfunction can be performed with any given tree heighth.let mask0: LeafIdx = 1 << h;However, the
LeafIdxtype is defined asu64. Consequently, any tree heighth >= 64leads to invalid results.Recommendation
It is recommended to assert that
h < 64.
Informational1 finding
Project relies on vulnerable crate dependencies
State
- Acknowledged
Severity
- Severity: Informational
Submitted by
Mario Poneder
Description
The following vulnerable crate dependencies were identified using
cargo audit:1. Timing variability in
curve25519-dalek'sScalar29::sub/Scalar52::subCrate: curve25519-dalekVersion: 3.2.0Title: Timing variability in `curve25519-dalek`'s `Scalar29::sub`/`Scalar52::sub`Date: 2024-06-18ID: RUSTSEC-2024-0344URL: https://rustsec.org/advisories/RUSTSEC-2024-0344Solution: Upgrade to >=4.1.32. Double Public Key Signing Function Oracle Attack on
ed25519-dalekCrate: ed25519-dalekVersion: 1.0.1Title: Double Public Key Signing Function Oracle Attack on `ed25519-dalek`Date: 2022-06-11ID: RUSTSEC-2022-0093URL: https://rustsec.org/advisories/RUSTSEC-2022-0093Solution: Upgrade to >=23.
ssl::select_next_protouse after freeCrate: opensslVersion: 0.10.68Title: `ssl::select_next_proto` use after freeDate: 2025-02-02ID: RUSTSEC-2025-0004URL: https://rustsec.org/advisories/RUSTSEC-2025-0004Solution: Upgrade to >=0.10.704. Marvin Attack: potential key recovery through timing sidechannels
Crate: rsaVersion: 0.9.7Title: Marvin Attack: potential key recovery through timing sidechannelsDate: 2023-11-22ID: RUSTSEC-2023-0071URL: https://rustsec.org/advisories/RUSTSEC-2023-0071Severity: 5.9 (medium)Solution: No fixed upgrade is available!Recommendation
It is recommended to upgrade the affected crate dependencies if applicable and viable in terms of compatibility.