refactor(cross-zone): fund bridge-lock holdings with a real native balance

This commit is contained in:
moudyellaz 2026-07-21 18:28:04 +02:00
parent f4a5d85c65
commit e278437266
5 changed files with 35 additions and 58 deletions

View File

@ -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,

View File

@ -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 =

View File

@ -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)

View File

@ -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];

View File

@ -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()));