The Merkle root_from_proof function below, will successfully skip the for-loop's body in case leaves_len == 1 and an empty proof, i.e. proof.len() == 0.
pubfnroot_from_proof<L>(
leaf:L,
leaf_idx:LeafIdx,
leaves_len:LeafIdx,
proof:&[[u8;32]],
digest_cb:implFn(L,&mutsha2::Sha256),
)->[u8;32]{
let ceil_log2_n:u8= leaves_len.checked_next_power_of_two().unwrap().ilog2()asu8;
letmut root =hash_leaf(leaf,&digest_cb);
letmut i:Height=0;
for h in0..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 asusize])
}else{
hash_pair(proof[i asusize], root)
};
i +=1;
}
assert_eq!(i asusize, 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_idx parameter is ignored and the return value is just the digest of the leaf.
Attack vector
The operator who proposes a block can arbitrarily set the withdrawal_root allowing it to be just the digest of the leaf_to_prove. In this case, it suffices to invoke the withdraw instruction below with claim.leaves_count == 1 and empty claim.proof to pass the root check and proceed with the withdrawal.
However, in case of an empty proof as discussed above, the claim.leaf_index is ignored in the root computation and therefore can be set to arbitrary values to bypass the withdrawal_nullifier allowing multiple withdrawals.
Recommendation
It is recommended to incorporate the leaf_idx into the root computation in any case and assert that leaves_len > leaf idx to also evaluate the leaf_idx in case of an empty proof.
Block finalization is immediate and permissionless
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_block instruction 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
The initialize instruction and its account context impose no restrictions on the signer.
Consequently, the first one to invoke initialize after deployment is in charge of setting the protocol's operator role.
Recommendation
It is recommended to add the following code to the Initialize context to require co-signing of the initialize instruction with the program's private key (keypair) that should only be known to the deployer.
The permissionless deposit_spl instruction imposes no restrictions on the amount.
Consequently, anyone can spam deposits with zero or dust amounts thereby unnecessarily increasing the last_deposit_index.
Recommendation
It is recommended to introduce a minimum deposit amount.
Insufficient validation of tree height when computing subtree offset