From e278437266e58437e9749869112f1e35798ec61d Mon Sep 17 00:00:00 2001 From: moudyellaz Date: Tue, 21 Jul 2026 18:28:04 +0200 Subject: [PATCH] refactor(cross-zone): fund bridge-lock holdings with a real native balance --- integration_tests/tests/cross_zone_bridge.rs | 21 ++++++++------- .../tests/cross_zone_state_machine.rs | 14 +++------- lez/cross_zone/src/lib.rs | 11 +++----- lez/programs/bridge_lock/core/src/lib.rs | 20 -------------- lez/programs/bridge_lock/src/main.rs | 27 +++++++++++-------- 5 files changed, 35 insertions(+), 58 deletions(-) diff --git a/integration_tests/tests/cross_zone_bridge.rs b/integration_tests/tests/cross_zone_bridge.rs index 83eefc00..63ce8cc4 100644 --- a/integration_tests/tests/cross_zone_bridge.rs +++ b/integration_tests/tests/cross_zone_bridge.rs @@ -72,10 +72,15 @@ async fn lock_on_zone_a_mints_wrapped_token_on_zone_b() -> Result<()> { let (seq_a, _seq_a_home) = setup_sequencer(partial, bedrock_addr, genesis_a, channel_a, None) .await .context("Failed to set up zone A sequencer")?; - let (_seq_b, _seq_b_home) = - setup_sequencer(partial, bedrock_addr, vec![], channel_b, Some(cross_zone.clone())) - .await - .context("Failed to set up zone B sequencer")?; + let (_seq_b, _seq_b_home) = setup_sequencer( + partial, + bedrock_addr, + vec![], + channel_b, + Some(cross_zone.clone()), + ) + .await + .context("Failed to set up zone B sequencer")?; let (idx_b, _idx_b_home) = setup_indexer(bedrock_addr, channel_b, Some(cross_zone)) .await .context("Failed to set up zone B indexer")?; @@ -106,16 +111,12 @@ async fn lock_on_zone_a_mints_wrapped_token_on_zone_b() -> Result<()> { // escrow now. let seq_a_client = sequencer_client(seq_a.addr())?; let escrow_id = bridge_lock_core::escrow_account_id(programs::bridge_lock().id()); - let escrowed = bridge_lock_core::read_balance( - &seq_a_client.get_account(escrow_id).await?.data.into_inner(), - ); + let escrowed = seq_a_client.get_account(escrow_id).await?.balance; assert_eq!( escrowed, LOCK_AMOUNT, "zone A escrow must hold the locked amount" ); - let remaining = bridge_lock_core::read_balance( - &seq_a_client.get_account(holder_id).await?.data.into_inner(), - ); + let remaining = seq_a_client.get_account(holder_id).await?.balance; assert_eq!( remaining, INITIAL_BALANCE - LOCK_AMOUNT, diff --git a/integration_tests/tests/cross_zone_state_machine.rs b/integration_tests/tests/cross_zone_state_machine.rs index 25583b9b..f1080c8c 100644 --- a/integration_tests/tests/cross_zone_state_machine.rs +++ b/integration_tests/tests/cross_zone_state_machine.rs @@ -174,12 +174,8 @@ fn lock_escrows_balance_and_emits_to_outbox() { holder_id, Account { program_owner: bridge_lock_id, - balance: 0, - data: bridge_lock_core::balance_bytes(INITIAL_BALANCE) - .to_vec() - .try_into() - .expect("balance fits in account data"), - nonce: 0_u128.into(), + balance: INITIAL_BALANCE, + ..Default::default() }, )]); @@ -214,16 +210,14 @@ fn lock_escrows_balance_and_emits_to_outbox() { .expect("lock must validate and execute"); let public_diff = diff.public_diff(); - let holder_after = - bridge_lock_core::read_balance(&public_diff[&holder_id].data.clone().into_inner()); + let holder_after = public_diff[&holder_id].balance; assert_eq!( holder_after, INITIAL_BALANCE - LOCK_AMOUNT, "holder debited" ); - let escrow_after = - bridge_lock_core::read_balance(&public_diff[&escrow_id].data.clone().into_inner()); + let escrow_after = public_diff[&escrow_id].balance; assert_eq!(escrow_after, LOCK_AMOUNT, "escrow credited"); let record = diff --git a/lez/cross_zone/src/lib.rs b/lez/cross_zone/src/lib.rs index 0a5993e6..06ebd8a0 100644 --- a/lez/cross_zone/src/lib.rs +++ b/lez/cross_zone/src/lib.rs @@ -176,17 +176,14 @@ pub fn build_inbox_init_config_tx( /// Builds the genesis holding account funding a holder's bridgeable balance. /// -/// Owned by `bridge_lock`, data is the LE balance. Not produced by any -/// transaction, so the sequencer and indexer both seed it through this one -/// builder. +/// A real native balance owned by `bridge_lock`, which can debit it on a lock; it +/// is conserved like any other balance. Not produced by any transaction, so the +/// sequencer and indexer both seed it through this one builder. #[must_use] pub fn build_holding_account(holder: AccountId, amount: Balance) -> (AccountId, Account) { let account = Account { program_owner: programs::bridge_lock().id(), - data: bridge_lock_core::balance_bytes(amount) - .to_vec() - .try_into() - .expect("balance fits in account data"), + balance: amount, ..Default::default() }; (holder, account) diff --git a/lez/programs/bridge_lock/core/src/lib.rs b/lez/programs/bridge_lock/core/src/lib.rs index e5a36113..6d2aaf49 100644 --- a/lez/programs/bridge_lock/core/src/lib.rs +++ b/lez/programs/bridge_lock/core/src/lib.rs @@ -39,30 +39,10 @@ pub const fn escrow_seed() -> PdaSeed { PdaSeed::new(ESCROW_SEED_DOMAIN) } -/// Reads a bridgeable balance from account data; empty data is a zero balance. -#[must_use] -pub fn read_balance(data: &[u8]) -> u128 { - if data.len() < 16 { - return 0; - } - u128::from_le_bytes(data[..16].try_into().unwrap_or_else(|_| unreachable!())) -} - -#[must_use] -pub const fn balance_bytes(amount: u128) -> [u8; 16] { - amount.to_le_bytes() -} - #[cfg(test)] mod tests { use super::*; - #[test] - fn balance_round_trips() { - assert_eq!(read_balance(&balance_bytes(7)), 7); - assert_eq!(read_balance(&[]), 0); - } - #[test] fn escrow_is_stable() { let id: ProgramId = [4; 8]; diff --git a/lez/programs/bridge_lock/src/main.rs b/lez/programs/bridge_lock/src/main.rs index 53dd1077..8b176ee5 100644 --- a/lez/programs/bridge_lock/src/main.rs +++ b/lez/programs/bridge_lock/src/main.rs @@ -1,4 +1,4 @@ -use bridge_lock_core::{Instruction, balance_bytes, escrow_account_id, escrow_seed, read_balance}; +use bridge_lock_core::{Instruction, escrow_account_id, escrow_seed}; use cross_zone_outbox_core::Instruction as OutboxInstruction; use lee_core::{ account::AccountWithMetadata, @@ -50,6 +50,10 @@ fn main() { .expect("Lock requires holder, escrow, and outbox accounts"); assert!(holder.is_authorized, "holder must authorize the lock"); + // The holder holding is bridge_lock-owned, so bridge_lock may debit its native + // balance directly (state-machine rule 5). This also pins the transfer to a + // genuine holding: a caller cannot substitute an account owned by some other + // program to emit the mint without an actual lock. assert_eq!( holder.account.program_owner, self_program_id, "holder account must be a bridge_lock holding" @@ -60,25 +64,26 @@ fn main() { "second account must be the escrow PDA" ); - let holder_new = read_balance(&holder.account.data.clone().into_inner()) + // Move the real native balance holder -> escrow. bridge_lock owns both accounts, + // so it debits the holder and credits the escrow directly; conservation holds + // because the same amount moves between them. + let holder_new = holder + .account + .balance .checked_sub(amount) .expect("insufficient balance to lock"); - let escrow_new = read_balance(&escrow.account.data.clone().into_inner()) + let escrow_new = escrow + .account + .balance .checked_add(amount) .expect("escrow balance overflow"); let mut holder_account = holder.account.clone(); - holder_account.data = balance_bytes(holder_new) - .to_vec() - .try_into() - .expect("balance fits in account data"); + holder_account.balance = holder_new; let holder_post = AccountPostState::new(holder_account); let mut escrow_account = escrow.account.clone(); - escrow_account.data = balance_bytes(escrow_new) - .to_vec() - .try_into() - .expect("balance fits in account data"); + escrow_account.balance = escrow_new; let escrow_post = AccountPostState::new_claimed_if_default(escrow_account, Claim::Pda(escrow_seed()));