Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 0 additions & 11 deletions contracts/L2/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion contracts/L2/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,4 @@
"ts-node": "^10.9.2",
"typescript": "^5.3.3"
}
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
}
88 changes: 57 additions & 31 deletions contracts/L2/src/core/ZeroXBridgeL2.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@ pub mod ZeroXBridgeL2 {
self.rates.write(rates);
let mmr: MMR = Default::default();
self.mmr.write(mmr);
self.leaves_count.write(0);
}

#[abi(embed_v0)]
Expand Down Expand Up @@ -399,8 +400,12 @@ pub mod ZeroXBridgeL2 {

fn get_last_peaks(self: @ContractState) -> Array<felt252> {
let mut peaks = array![];
for i in 0..self.last_peaks.len() {
peaks.append(self.last_peaks.at(i).read());
let len = self.last_peaks.len();
for i in 0..len {
let peak = self.last_peaks.at(i).read();
if peak != 0 { // Only include non-zero peaks
peaks.append(peak);
}
}
peaks
}
Expand All @@ -417,66 +422,87 @@ pub mod ZeroXBridgeL2 {
proof: Array<felt252>,
) -> Result<bool, felt252> {
let mmr = self.mmr.read();
mmr.verify_proof(index, commitment_hash, peaks.span(), proof.span())
MMRTrait::verify_proof(@mmr, index, commitment_hash, peaks.span(), proof.span())
}
}

#[generate_trait]
impl InternalFunctions of InternalFunctionsTrait {
fn append_withdrawal_hash(ref self: ContractState, commitment_hash: felt252) {
let mut mmr = self.mmr.read();
let last_peaks = self.get_last_peaks().span();
let last_peaks = self.get_last_peaks();
let mut leaves_count = self.leaves_count.read();

match mmr.append(commitment_hash, last_peaks) {
match MMRTrait::append(ref mmr, commitment_hash, last_peaks.span()) {
Result::Ok((
root_hash, peaks,
)) => {
// Store the new root and last position
self.node_index_to_root.write(mmr.last_pos, root_hash);
leaves_count += 1;

self.commitment_hash_to_index.write(commitment_hash, mmr.last_pos.into());
// Calculate the correct MMR leaf index
let correct_leaf_index = Self::leaf_count_to_mmr_index(leaves_count) + 1;
self.commitment_hash_to_index.write(commitment_hash, correct_leaf_index);

println!("Leaf data");
println!("{:?}", leaves_count);
println!("{:?}", correct_leaf_index);

leaves_count += 1;
self.leaves_count.write(leaves_count);

let last_peaks_len = last_peaks.len();
let peaks_len = peaks.len();

if last_peaks_len > peaks_len {
// Overwrite up to peaks_len, then set the rest to 0
for i in 0..peaks_len {
let mut storage_ptr = self.last_peaks.at(i.into());
storage_ptr.write(*peaks.at(i));
}
for i in peaks_len..last_peaks_len {
let mut storage_ptr = self.last_peaks.at(i.into());
storage_ptr.write(0);
};
} else {
// Overwrite up to last_peaks_len, then append the rest
for i in 0..last_peaks_len {
let mut storage_ptr = self.last_peaks.at(i.into());
storage_ptr.write(*peaks.at(i));
}
for i in last_peaks_len..peaks_len {
self.last_peaks.push(*peaks.at(i));
};
// Clear and update peaks storage properly
self.clear_peaks_storage();
for i in 0..peaks.len() {
self.last_peaks.push(*peaks.at(i));
}

self
.emit(
Event::WithdrawalHashAppended(
WithdrawalHashAppended {
index: mmr.last_pos.into(),
index: correct_leaf_index,
commitment_hash: commitment_hash,
root_hash: mmr.root,
root_hash: root_hash,
},
),
);

// Write the updated MMR back to storage
self.mmr.write(mmr);
},
Result::Err(err) => { panic(array![err]) },
}
}

fn clear_peaks_storage(ref self: ContractState) {
// Clear all existing peaks from storage
let current_len = self.last_peaks.len();
for _i in 0..current_len {
self.last_peaks.pop();
}
}

fn leaf_count_to_mmr_index(leaf_count: felt252) -> felt252 {
if leaf_count == 0 {
return 0;
}

// For MMR, the first leaf is at index 1, second at index 2, etc.
// But we need to account for internal nodes
let mut internal_nodes: u64 = 0;
let mut temp: u64 = leaf_count.try_into().unwrap();

// Count internal nodes created by building the MMR
let mut level = 1_u64;
while level < temp {
internal_nodes += temp / (level * 2);
level *= 2;
}

// The MMR index is leaf position + internal nodes before it
let mmr_index = leaf_count + internal_nodes.into();
mmr_index
}
}
}
160 changes: 160 additions & 0 deletions contracts/L2/tests/test_ZeroXBridgeL2.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use core::integer::u256;
use core::poseidon::PoseidonTrait;
use l2::core::ZeroXBridgeL2::ZeroXBridgeL2::{BurnData, BurnEvent, Event, MintData, MintEvent};
use l2::interfaces::IL2Oracle::{IL2OracleDispatcher, IL2OracleDispatcherTrait};
use l2::interfaces::IMerkleManager::{IMerkleManagerDispatcher, IMerkleManagerDispatcherTrait};
use l2::interfaces::IProofRegistry::{IProofRegistryDispatcher, IProofRegistryDispatcherTrait};
use l2::interfaces::IZeroXBridgeL2::{
IDynamicRateDispatcher, IDynamicRateDispatcherTrait, IZeroXBridgeL2Dispatcher,
Expand Down Expand Up @@ -467,3 +468,162 @@ fn test_insufficient_proof_data() {
.mint_and_claim_xzb(proof, commitment_hash, eth_address, r, s, y_parity);
}

#[test]
fn test_mmr_index_fix_single_withdrawal() {
// Test that the first withdrawal has correct index (should be 0)
let token_addr = deploy_xzb();
let proof_registry_addr = deploy_registry();
let oracle_addr = deploy_oracle();
let bridge_addr = deploy_bridge(token_addr, proof_registry_addr, oracle_addr);

let alice_addr = alice();
let owner_addr = owner();
let burn_amount = 1000_u256 * PRECISION;

// Setup: mint tokens to Alice
cheat_caller_address(token_addr, owner_addr, CheatSpan::TargetCalls(1));
IXZBERC20Dispatcher { contract_address: token_addr }.mint(alice_addr, burn_amount);

// Setup: set TVL
cheat_caller_address(oracle_addr, owner_addr, CheatSpan::TargetCalls(1));
IL2OracleDispatcher { contract_address: oracle_addr }.set_total_tvl(100_000_u256 * PRECISION);

// Approve and burn
cheat_caller_address(token_addr, alice_addr, CheatSpan::TargetCalls(1));
IERC20Dispatcher { contract_address: token_addr }.approve(bridge_addr, burn_amount);

let mut spy = spy_events();
cheat_caller_address(bridge_addr, alice_addr, CheatSpan::TargetCalls(1));
IZeroXBridgeL2Dispatcher { contract_address: bridge_addr }.burn_xzb_for_unlock(burn_amount);

// Verify the leaf count and MMR state
let merkle_manager = IMerkleManagerDispatcher { contract_address: bridge_addr };
let leaves_count = merkle_manager.get_leaves_count();
assert(leaves_count == 1, 'Expected 1 leaf');

// Verify MMR root was set (not zero)
let root_hash = merkle_manager.get_root_hash();
assert(root_hash != 0, 'Root hash should not be zero');
}

#[test]
fn test_mmr_index_fix_multiple_withdrawals() {
// Test that multiple withdrawals have correct MMR indices
// Expected MMR indices: leaf 0 -> 0, leaf 1 -> 1, leaf 2 -> 2, leaf 3 -> 4, leaf 4 -> 5, etc.
let token_addr = deploy_xzb();
let proof_registry_addr = deploy_registry();
let oracle_addr = deploy_oracle();
let bridge_addr = deploy_bridge(token_addr, proof_registry_addr, oracle_addr);

let alice_addr = alice();
let owner_addr = owner();
let burn_amount = 1000_u256 * PRECISION;

let mut spy = spy_events();
// Setup: mint tokens to Alice
cheat_caller_address(token_addr, owner_addr, CheatSpan::TargetCalls(1));
IXZBERC20Dispatcher { contract_address: token_addr }.mint(alice_addr, burn_amount * 5);

// Setup: set TVL
cheat_caller_address(oracle_addr, owner_addr, CheatSpan::TargetCalls(1));
IL2OracleDispatcher { contract_address: oracle_addr }.set_total_tvl(100_000_u256 * PRECISION);

// Approve all burns at once
cheat_caller_address(token_addr, alice_addr, CheatSpan::TargetCalls(1));
IERC20Dispatcher { contract_address: token_addr }.approve(bridge_addr, burn_amount * 5);

let merkle_manager = IMerkleManagerDispatcher { contract_address: bridge_addr };

// First burn (leaf 0 -> MMR index 0)
cheat_caller_address(bridge_addr, alice_addr, CheatSpan::TargetCalls(1));
IZeroXBridgeL2Dispatcher { contract_address: bridge_addr }.burn_xzb_for_unlock(burn_amount);
let leaves_count_1 = merkle_manager.get_leaves_count();
assert(leaves_count_1 == 1, 'Expected 1 leaf');

// Second burn (leaf 1 -> MMR index 1)
cheat_caller_address(bridge_addr, alice_addr, CheatSpan::TargetCalls(1));
IZeroXBridgeL2Dispatcher { contract_address: bridge_addr }.burn_xzb_for_unlock(burn_amount);
let leaves_count_2 = merkle_manager.get_leaves_count();
assert(leaves_count_2 == 2, 'Expected 2 leaves');

// Third burn (leaf 2 -> MMR index 2) - this is where the original bug would manifest
cheat_caller_address(bridge_addr, alice_addr, CheatSpan::TargetCalls(1));
IZeroXBridgeL2Dispatcher { contract_address: bridge_addr }.burn_xzb_for_unlock(burn_amount);
let leaves_count_3 = merkle_manager.get_leaves_count();
assert(leaves_count_3 == 3, 'Expected 3 leaves');

// Fourth burn (leaf 3 -> MMR index 4) - critical test case
cheat_caller_address(bridge_addr, alice_addr, CheatSpan::TargetCalls(1));
IZeroXBridgeL2Dispatcher { contract_address: bridge_addr }.burn_xzb_for_unlock(burn_amount);
let leaves_count_4 = merkle_manager.get_leaves_count();
assert(leaves_count_4 == 4, 'Expected 4 leaves');

// Fifth burn (leaf 4 -> MMR index 5)
cheat_caller_address(bridge_addr, alice_addr, CheatSpan::TargetCalls(1));
IZeroXBridgeL2Dispatcher { contract_address: bridge_addr }.burn_xzb_for_unlock(burn_amount);
let leaves_count_5 = merkle_manager.get_leaves_count();
assert(leaves_count_5 == 5, 'Expected 5 leaves');

// Verify MMR element count reflects the total number of nodes (leaves + internal nodes)
let element_count = merkle_manager.get_element_count();
let leaves_count_u256: u256 = leaves_count_5.into();
let element_count_u256: u256 = element_count.into();
assert(element_count_u256 > leaves_count_u256, 'Element count > leaf count');
}

#[test]
fn test_mmr_leaf_index_calculation_edge_cases() {
// Test edge cases for the leaf index calculation helper function
let token_addr = deploy_xzb();
let proof_registry_addr = deploy_registry();
let oracle_addr = deploy_oracle();
let bridge_addr = deploy_bridge(token_addr, proof_registry_addr, oracle_addr);

let alice_addr = alice();
let owner_addr = owner();
let burn_amount = 1000_u256 * PRECISION;

// Setup
cheat_caller_address(token_addr, owner_addr, CheatSpan::TargetCalls(1));
IXZBERC20Dispatcher { contract_address: token_addr }.mint(alice_addr, burn_amount * 8);

cheat_caller_address(oracle_addr, owner_addr, CheatSpan::TargetCalls(1));
IL2OracleDispatcher { contract_address: oracle_addr }.set_total_tvl(100_000_u256 * PRECISION);

cheat_caller_address(token_addr, alice_addr, CheatSpan::TargetCalls(1));
IERC20Dispatcher { contract_address: token_addr }.approve(bridge_addr, burn_amount * 8);

let merkle_manager = IMerkleManagerDispatcher { contract_address: bridge_addr };

// Test power-of-2 boundaries where MMR structure changes significantly
// These are critical test points for the leaf index calculation

// Burns 1-8 to test various MMR tree shapes
let mut i: u32 = 1;
loop {
if i > 8 {
break;
}
cheat_caller_address(bridge_addr, alice_addr, CheatSpan::TargetCalls(1));
IZeroXBridgeL2Dispatcher { contract_address: bridge_addr }.burn_xzb_for_unlock(burn_amount);

let current_leaves = merkle_manager.get_leaves_count();
let expected_leaves: felt252 = i.into();
assert(current_leaves == expected_leaves, 'Incorrect leaf count');

// Verify MMR is still valid after each burn
let root_hash = merkle_manager.get_root_hash();
assert(root_hash != 0, 'Root hash should not be zero');

i += 1;
}

// Final verification
let final_leaves_count = merkle_manager.get_leaves_count();
assert(final_leaves_count == 8, 'Expected 8 leaves total');

let final_element_count = merkle_manager.get_element_count();
let final_leaves_count_u256: u256 = final_leaves_count.into();
let final_element_count_u256: u256 = final_element_count.into();
assert(final_element_count_u256 > final_leaves_count_u256, 'Element count > leaf count');
}
Loading
Loading