mirror of
https://github.com/logos-blockchain/logos-execution-zone.git
synced 2026-08-27 04:11:08 +00:00
fix(cross-zone)!: cap a single wrapped-token mint
Closes #678. BREAKING CHANGE: `wrapped_token` and `bridge_lock` image ids move, relocating the wrapped-token config and every holding PDA. A lock above the cap is now refused at the source rather than escrowing balance the destination will not mint.
This commit is contained in:
@@ -97,14 +97,86 @@ fn seed_wrapped_config(state: &mut V03State) {
|
||||
/// The wrapped-token `Mint` the bridge forwards, serialized as the cross-zone
|
||||
/// payload (risc0 words, little-endian bytes).
|
||||
fn mint_payload() -> Vec<u8> {
|
||||
mint_payload_of(LOCK_AMOUNT)
|
||||
}
|
||||
|
||||
fn mint_payload_of(amount: u128) -> Vec<u8> {
|
||||
let mint = wrapped_token_core::Instruction::Mint {
|
||||
recipient: RECIPIENT,
|
||||
amount: LOCK_AMOUNT,
|
||||
amount,
|
||||
};
|
||||
let words = risc0_zkvm::serde::to_vec(&mint).expect("serialize mint");
|
||||
words.iter().flat_map(|word| word.to_le_bytes()).collect()
|
||||
}
|
||||
|
||||
/// Runs a bridge mint of `amount` through the inbox, as the watcher would.
|
||||
fn dispatch_mint(amount: u128) -> Result<ValidatedStateDiff, lee::error::LeeError> {
|
||||
let inbox_id = programs::cross_zone_inbox().id();
|
||||
let wrapped_token_id = programs::wrapped_token().id();
|
||||
let self_zone = [1_u8; 32];
|
||||
let src_zone = [2_u8; 32];
|
||||
let src_block_id = 5;
|
||||
|
||||
let mut state = base_state();
|
||||
seed_inbox_config(
|
||||
&mut state,
|
||||
self_zone,
|
||||
src_zone,
|
||||
[9_u32; 8],
|
||||
wrapped_token_id,
|
||||
);
|
||||
seed_wrapped_config(&mut state);
|
||||
|
||||
let msg = CrossZoneMessage {
|
||||
src_zone,
|
||||
src_block_id,
|
||||
src_block_hash: SRC_BLOCK_HASH,
|
||||
src_tx_index: 0,
|
||||
src_program_id: [9_u32; 8],
|
||||
target_program_id: wrapped_token_id,
|
||||
payload: mint_payload_of(amount),
|
||||
l1_inclusion_witness: None,
|
||||
};
|
||||
|
||||
let message = Message::try_new(
|
||||
inbox_id,
|
||||
vec![
|
||||
inbox_config_account_id(inbox_id),
|
||||
inbox_seen_shard_account_id(inbox_id, &src_zone, src_block_id),
|
||||
wrapped_token_core::config_account_id(wrapped_token_id),
|
||||
wrapped_token_core::holding_account_id(wrapped_token_id, &RECIPIENT),
|
||||
],
|
||||
vec![],
|
||||
InboxInstruction::Dispatch(msg),
|
||||
)
|
||||
.expect("build dispatch message");
|
||||
let tx = PublicTransaction::new(message, WitnessSet::from_raw_parts(vec![]));
|
||||
|
||||
ValidatedStateDiff::from_public_transaction(&tx, &state, 1, 0)
|
||||
}
|
||||
|
||||
/// One message must not be able to pin a holding near `u128::MAX`, which would
|
||||
/// make every later honest mint to that recipient overflow and fail for good.
|
||||
#[test]
|
||||
fn a_mint_above_the_cap_is_rejected() {
|
||||
assert!(
|
||||
dispatch_mint(wrapped_token_core::MAX_MINT_AMOUNT + 1).is_err(),
|
||||
"an amount over the per-mint cap must not execute"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_mint_at_the_cap_is_accepted() {
|
||||
let diff = dispatch_mint(wrapped_token_core::MAX_MINT_AMOUNT)
|
||||
.expect("the cap itself is a legitimate amount");
|
||||
let holding_id =
|
||||
wrapped_token_core::holding_account_id(programs::wrapped_token().id(), &RECIPIENT);
|
||||
let minted = wrapped_token_core::read_balance(
|
||||
&diff.public_diff()[&holding_id].data.clone().into_inner(),
|
||||
);
|
||||
assert_eq!(minted, wrapped_token_core::MAX_MINT_AMOUNT);
|
||||
}
|
||||
|
||||
/// Drives `cross_zone_inbox::Dispatch` directly through the state machine
|
||||
/// (no watcher) and asserts the message is delivered to `ping_receiver`, which
|
||||
/// records the payload into its own PDA.
|
||||
@@ -245,54 +317,9 @@ fn lock_escrows_balance_and_emits_to_outbox() {
|
||||
/// and asserts it chains into `wrapped_token::Mint`, crediting the recipient.
|
||||
#[test]
|
||||
fn inbox_dispatch_mints_wrapped_token() {
|
||||
let inbox_id = programs::cross_zone_inbox().id();
|
||||
let wrapped_token_id = programs::wrapped_token().id();
|
||||
|
||||
let self_zone = [1_u8; 32];
|
||||
let src_zone = [2_u8; 32];
|
||||
let src_block_id = 5;
|
||||
|
||||
let mut state = base_state();
|
||||
seed_inbox_config(
|
||||
&mut state,
|
||||
self_zone,
|
||||
src_zone,
|
||||
[9_u32; 8],
|
||||
wrapped_token_id,
|
||||
);
|
||||
seed_wrapped_config(&mut state);
|
||||
|
||||
let msg = CrossZoneMessage {
|
||||
src_zone,
|
||||
src_block_id,
|
||||
src_block_hash: SRC_BLOCK_HASH,
|
||||
src_tx_index: 0,
|
||||
src_program_id: [9_u32; 8],
|
||||
target_program_id: wrapped_token_id,
|
||||
payload: mint_payload(),
|
||||
l1_inclusion_witness: None,
|
||||
};
|
||||
|
||||
let seen_id = inbox_seen_shard_account_id(inbox_id, &src_zone, src_block_id);
|
||||
let wrapped_config_id = wrapped_token_core::config_account_id(wrapped_token_id);
|
||||
let holding_id = wrapped_token_core::holding_account_id(wrapped_token_id, &RECIPIENT);
|
||||
|
||||
let message = Message::try_new(
|
||||
inbox_id,
|
||||
vec![
|
||||
inbox_config_account_id(inbox_id),
|
||||
seen_id,
|
||||
wrapped_config_id,
|
||||
holding_id,
|
||||
],
|
||||
vec![],
|
||||
InboxInstruction::Dispatch(msg),
|
||||
)
|
||||
.expect("build dispatch message");
|
||||
let tx = PublicTransaction::new(message, WitnessSet::from_raw_parts(vec![]));
|
||||
|
||||
let diff = ValidatedStateDiff::from_public_transaction(&tx, &state, 1, 0)
|
||||
.expect("dispatch must validate and execute");
|
||||
let diff = dispatch_mint(LOCK_AMOUNT).expect("dispatch must validate and execute");
|
||||
let holding_id =
|
||||
wrapped_token_core::holding_account_id(programs::wrapped_token().id(), &RECIPIENT);
|
||||
let minted = wrapped_token_core::read_balance(
|
||||
&diff.public_diff()[&holding_id].data.clone().into_inner(),
|
||||
);
|
||||
|
||||
@@ -4,7 +4,7 @@ use lee_core::{
|
||||
account::AccountWithMetadata,
|
||||
program::{AccountPostState, ChainedCall, Claim, ProgramInput, ProgramOutput, read_lee_inputs},
|
||||
};
|
||||
use wrapped_token_core::Instruction as WrappedInstruction;
|
||||
use wrapped_token_core::{Instruction as WrappedInstruction, MAX_MINT_AMOUNT};
|
||||
|
||||
fn main() {
|
||||
let (
|
||||
@@ -44,6 +44,14 @@ fn main() {
|
||||
mint_amount, amount,
|
||||
"locked amount must equal the wrapped mint amount"
|
||||
);
|
||||
// Refused here rather than on the destination, where the mint would fail
|
||||
// after this side had already escrowed the balance. Nothing releases an
|
||||
// escrow, so an amount the destination will not mint has to fail before the
|
||||
// debit, in the submitter's own transaction where they can see it.
|
||||
assert!(
|
||||
amount <= MAX_MINT_AMOUNT,
|
||||
"locked amount exceeds what the wrapped token will mint"
|
||||
);
|
||||
|
||||
// pre_states: [holder holding (authorized), escrow PDA, outbox PDA].
|
||||
let [holder, escrow, outbox] = <[AccountWithMetadata; 3]>::try_from(pre_states)
|
||||
|
||||
@@ -8,6 +8,24 @@ use lee_core::{
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// The most one mint may credit.
|
||||
///
|
||||
/// The amount is chosen on the peer zone and the balance is a `u128`, so without
|
||||
/// a bound a single delivery can push a holding to within a hair of the maximum.
|
||||
/// Every honest mint to that recipient then overflows, and an overflow is a guest
|
||||
/// panic, so each one fails execution and is eventually given up on. The holding
|
||||
/// is unusable for inbound transfers for good, at a cost to the attacker of one
|
||||
/// message.
|
||||
///
|
||||
/// A cap does not put the maximum out of reach, it makes reaching it cost 2^64
|
||||
/// deliveries rather than one.
|
||||
///
|
||||
/// `u64::MAX` is a bound the bridge imposes rather than one native balances
|
||||
/// already obey: `Balance` is a `u128` and the faucet is seeded at its maximum,
|
||||
/// so a larger amount is representable. `bridge_lock` refuses one at the source
|
||||
/// so it fails in the submitter's transaction rather than after escrowing.
|
||||
pub const MAX_MINT_AMOUNT: u128 = 0xFFFF_FFFF_FFFF_FFFF;
|
||||
|
||||
const CONFIG_SEED_DOMAIN: [u8; 32] = *b"/LEZ/v0.3/WrappedTokenConfig/00/";
|
||||
const HOLDING_SEED_DOMAIN: [u8; 32] = *b"/LEZ/v0.3/WrappedTokenHold/00000";
|
||||
|
||||
|
||||
@@ -3,8 +3,8 @@ use lee_core::{
|
||||
program::{AccountPostState, Claim, ProgramInput, ProgramOutput, read_lee_inputs},
|
||||
};
|
||||
use wrapped_token_core::{
|
||||
Instruction, balance_bytes, config_account_id, config_seed, holding_account_id, holding_seed,
|
||||
minter_bytes, read_balance, read_minter,
|
||||
Instruction, MAX_MINT_AMOUNT, balance_bytes, config_account_id, config_seed,
|
||||
holding_account_id, holding_seed, minter_bytes, read_balance, read_minter,
|
||||
};
|
||||
|
||||
fn main() {
|
||||
@@ -70,6 +70,11 @@ fn mint(
|
||||
"second account must be the recipient holding PDA"
|
||||
);
|
||||
|
||||
assert!(
|
||||
amount <= MAX_MINT_AMOUNT,
|
||||
"mint amount exceeds the per-mint cap"
|
||||
);
|
||||
// The backstop against accumulation, which the per-mint cap does not bound.
|
||||
let new_balance = read_balance(&holding.account.data.clone().into_inner())
|
||||
.checked_add(amount)
|
||||
.expect("wrapped-token balance overflow");
|
||||
|
||||
Reference in New Issue
Block a user