mirror of
https://github.com/logos-blockchain/logos-execution-zone.git
synced 2026-06-30 02:49:53 +00:00
feat(cross-zone)!: add wrapped_token and bridge_lock programs
BREAKING CHANGE: registers the wrapped_token and bridge_lock builtin programs and seeds the wrapped_token config account at genesis, changing the genesis state.
This commit is contained in:
parent
60741a7191
commit
5d77359fd8
30
Cargo.lock
generated
30
Cargo.lock
generated
@ -1145,6 +1145,15 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bridge_lock_core"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"lee",
|
||||
"lee_core",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bs58"
|
||||
version = "0.5.1"
|
||||
@ -1801,8 +1810,10 @@ name = "cross_zone_inbox_core"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"borsh",
|
||||
"bridge_lock_core",
|
||||
"lee",
|
||||
"lee_core",
|
||||
"ping_core",
|
||||
"risc0-zkvm",
|
||||
"serde",
|
||||
]
|
||||
@ -3823,6 +3834,7 @@ dependencies = [
|
||||
"async-stream",
|
||||
"authenticated_transfer_core",
|
||||
"borsh",
|
||||
"bridge_lock_core",
|
||||
"common",
|
||||
"cross_zone_inbox_core",
|
||||
"futures",
|
||||
@ -3988,8 +4000,10 @@ dependencies = [
|
||||
"authenticated_transfer_core",
|
||||
"borsh",
|
||||
"bridge_core",
|
||||
"bridge_lock_core",
|
||||
"bytesize",
|
||||
"common",
|
||||
"cross_zone_inbox_core",
|
||||
"cross_zone_outbox_core",
|
||||
"faucet_core",
|
||||
"futures",
|
||||
@ -4019,6 +4033,7 @@ dependencies = [
|
||||
"vault_core",
|
||||
"wallet",
|
||||
"wallet-ffi",
|
||||
"wrapped_token_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -4530,8 +4545,10 @@ dependencies = [
|
||||
"authenticated_transfer_core",
|
||||
"borsh",
|
||||
"bridge_core",
|
||||
"bridge_lock_core",
|
||||
"clock_core",
|
||||
"cross_zone_inbox_core",
|
||||
"cross_zone_outbox_core",
|
||||
"env_logger",
|
||||
"faucet_core",
|
||||
"hex",
|
||||
@ -4551,6 +4568,7 @@ dependencies = [
|
||||
"test_program_methods",
|
||||
"thiserror 2.0.18",
|
||||
"token_core",
|
||||
"wrapped_token_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -7421,6 +7439,7 @@ dependencies = [
|
||||
"ata_program",
|
||||
"authenticated_transfer_core",
|
||||
"bridge_core",
|
||||
"bridge_lock_core",
|
||||
"clock_core",
|
||||
"cross_zone_inbox_core",
|
||||
"cross_zone_outbox_core",
|
||||
@ -7432,6 +7451,7 @@ dependencies = [
|
||||
"token_core",
|
||||
"token_program",
|
||||
"vault_core",
|
||||
"wrapped_token_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -8867,6 +8887,7 @@ dependencies = [
|
||||
"anyhow",
|
||||
"borsh",
|
||||
"bridge_core",
|
||||
"bridge_lock_core",
|
||||
"bytesize",
|
||||
"chrono",
|
||||
"common",
|
||||
@ -11475,6 +11496,15 @@ dependencies = [
|
||||
"wasmparser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wrapped_token_core"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"lee_core",
|
||||
"risc0-zkvm",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "writeable"
|
||||
version = "0.6.3"
|
||||
|
||||
@ -27,6 +27,8 @@ members = [
|
||||
"programs/cross_zone_outbox/core",
|
||||
"programs/cross_zone_inbox/core",
|
||||
"programs/ping/core",
|
||||
"programs/wrapped_token/core",
|
||||
"programs/bridge_lock/core",
|
||||
"lez/sequencer/core",
|
||||
"lez/sequencer/service",
|
||||
"lez/sequencer/service/protocol",
|
||||
@ -86,6 +88,8 @@ vault_core = { path = "programs/vault/core" }
|
||||
cross_zone_outbox_core = { path = "programs/cross_zone_outbox/core" }
|
||||
cross_zone_inbox_core = { path = "programs/cross_zone_inbox/core" }
|
||||
ping_core = { path = "programs/ping/core" }
|
||||
wrapped_token_core = { path = "programs/wrapped_token/core" }
|
||||
bridge_lock_core = { path = "programs/bridge_lock/core" }
|
||||
test_program_methods = { path = "test_program_methods" }
|
||||
testnet_initial_state = { path = "lez/testnet_initial_state" }
|
||||
keycard_wallet = { path = "lez/keycard_wallet" }
|
||||
|
||||
BIN
artifacts/program_methods/bridge_lock.bin
Normal file
BIN
artifacts/program_methods/bridge_lock.bin
Normal file
Binary file not shown.
BIN
artifacts/program_methods/wrapped_token.bin
Normal file
BIN
artifacts/program_methods/wrapped_token.bin
Normal file
Binary file not shown.
@ -12,6 +12,7 @@ lee_core = { workspace = true, features = ["host"] }
|
||||
clock_core.workspace = true
|
||||
faucet_core.workspace = true
|
||||
bridge_core.workspace = true
|
||||
wrapped_token_core.workspace = true
|
||||
|
||||
anyhow.workspace = true
|
||||
thiserror.workspace = true
|
||||
|
||||
@ -14,7 +14,7 @@ use crate::{
|
||||
CLOCK_ID, CROSS_ZONE_INBOX_ELF, CROSS_ZONE_INBOX_ID, CROSS_ZONE_OUTBOX_ELF,
|
||||
CROSS_ZONE_OUTBOX_ID, FAUCET_ELF, FAUCET_ID, PINATA_ELF, PINATA_ID, PING_RECEIVER_ELF,
|
||||
PING_RECEIVER_ID, PING_SENDER_ELF, PING_SENDER_ID, TOKEN_ELF, TOKEN_ID, VAULT_ELF,
|
||||
VAULT_ID,
|
||||
VAULT_ID, BRIDGE_LOCK_ELF, BRIDGE_LOCK_ID, WRAPPED_TOKEN_ELF, WRAPPED_TOKEN_ID,
|
||||
},
|
||||
};
|
||||
|
||||
@ -207,6 +207,22 @@ impl Program {
|
||||
elf: PING_SENDER_ELF.to_vec(),
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn bridge_lock() -> Self {
|
||||
Self {
|
||||
id: BRIDGE_LOCK_ID,
|
||||
elf: BRIDGE_LOCK_ELF.to_vec(),
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn wrapped_token() -> Self {
|
||||
Self {
|
||||
id: WRAPPED_TOKEN_ID,
|
||||
elf: WRAPPED_TOKEN_ELF.to_vec(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Testnet only. Refactor to prevent compilation on mainnet.
|
||||
|
||||
@ -201,6 +201,23 @@ impl V03State {
|
||||
this.insert_program(Program::cross_zone_inbox());
|
||||
this.insert_program(Program::ping_sender());
|
||||
this.insert_program(Program::ping_receiver());
|
||||
this.insert_program(Program::bridge_lock());
|
||||
this.insert_program(Program::wrapped_token());
|
||||
|
||||
// Seed the wrapped-token config with its authorized minter (the cross-zone
|
||||
// inbox), so the guest can pin its caller without importing the inbox id.
|
||||
let wrapped_token_id = Program::wrapped_token().id();
|
||||
this.public_state.insert(
|
||||
wrapped_token_core::config_account_id(wrapped_token_id),
|
||||
Account {
|
||||
program_owner: wrapped_token_id,
|
||||
data: wrapped_token_core::minter_bytes(Program::cross_zone_inbox().id())
|
||||
.to_vec()
|
||||
.try_into()
|
||||
.expect("minter id fits in account data"),
|
||||
..Account::default()
|
||||
},
|
||||
);
|
||||
|
||||
this
|
||||
}
|
||||
@ -705,6 +722,17 @@ pub mod tests {
|
||||
},
|
||||
);
|
||||
}
|
||||
this.insert(
|
||||
wrapped_token_core::config_account_id(Program::wrapped_token().id()),
|
||||
Account {
|
||||
program_owner: Program::wrapped_token().id(),
|
||||
data: wrapped_token_core::minter_bytes(Program::cross_zone_inbox().id())
|
||||
.to_vec()
|
||||
.try_into()
|
||||
.unwrap(),
|
||||
..Account::default()
|
||||
},
|
||||
);
|
||||
this
|
||||
};
|
||||
let expected_builtin_programs = {
|
||||
@ -727,6 +755,8 @@ pub mod tests {
|
||||
this.insert(Program::cross_zone_inbox().id(), Program::cross_zone_inbox());
|
||||
this.insert(Program::ping_sender().id(), Program::ping_sender());
|
||||
this.insert(Program::ping_receiver().id(), Program::ping_receiver());
|
||||
this.insert(Program::bridge_lock().id(), Program::bridge_lock());
|
||||
this.insert(Program::wrapped_token().id(), Program::wrapped_token());
|
||||
this
|
||||
};
|
||||
|
||||
|
||||
@ -23,5 +23,7 @@ vault_core.workspace = true
|
||||
cross_zone_outbox_core.workspace = true
|
||||
cross_zone_inbox_core.workspace = true
|
||||
ping_core.workspace = true
|
||||
wrapped_token_core.workspace = true
|
||||
bridge_lock_core.workspace = true
|
||||
risc0-zkvm.workspace = true
|
||||
serde = { workspace = true, default-features = false }
|
||||
|
||||
117
program_methods/guest/src/bin/bridge_lock.rs
Normal file
117
program_methods/guest/src/bin/bridge_lock.rs
Normal file
@ -0,0 +1,117 @@
|
||||
use bridge_lock_core::{Instruction, balance_bytes, escrow_account_id, escrow_seed, read_balance};
|
||||
use cross_zone_outbox_core::Instruction as OutboxInstruction;
|
||||
use lee_core::{
|
||||
account::AccountWithMetadata,
|
||||
program::{AccountPostState, ChainedCall, Claim, ProgramInput, ProgramOutput, read_lee_inputs},
|
||||
};
|
||||
use wrapped_token_core::Instruction as WrappedInstruction;
|
||||
|
||||
fn main() {
|
||||
let (
|
||||
ProgramInput {
|
||||
self_program_id,
|
||||
caller_program_id,
|
||||
pre_states,
|
||||
instruction,
|
||||
},
|
||||
instruction_words,
|
||||
) = read_lee_inputs::<Instruction>();
|
||||
|
||||
assert!(
|
||||
caller_program_id.is_none(),
|
||||
"bridge_lock is only invoked as a top-level user transaction"
|
||||
);
|
||||
|
||||
let Instruction::Lock {
|
||||
amount,
|
||||
target_zone,
|
||||
target_program_id,
|
||||
target_accounts,
|
||||
payload,
|
||||
outbox_program_id,
|
||||
ordinal,
|
||||
} = instruction;
|
||||
|
||||
// Value conservation: the forwarded payload must mint exactly what is locked.
|
||||
let WrappedInstruction::Mint {
|
||||
amount: mint_amount,
|
||||
..
|
||||
} = decode_mint(&payload);
|
||||
assert_eq!(
|
||||
mint_amount, amount,
|
||||
"locked amount must equal the wrapped mint amount"
|
||||
);
|
||||
|
||||
// pre_states: [holder holding (authorized), escrow PDA, outbox PDA].
|
||||
let [holder, escrow, outbox] = <[AccountWithMetadata; 3]>::try_from(pre_states)
|
||||
.expect("Lock requires holder, escrow, and outbox accounts");
|
||||
|
||||
assert!(holder.is_authorized, "holder must authorize the lock");
|
||||
assert_eq!(
|
||||
holder.account.program_owner, self_program_id,
|
||||
"holder account must be a bridge_lock holding"
|
||||
);
|
||||
assert_eq!(
|
||||
escrow.account_id,
|
||||
escrow_account_id(self_program_id),
|
||||
"second account must be the escrow PDA"
|
||||
);
|
||||
|
||||
let holder_new = read_balance(&holder.account.data.clone().into_inner())
|
||||
.checked_sub(amount)
|
||||
.expect("insufficient balance to lock");
|
||||
let escrow_new = read_balance(&escrow.account.data.clone().into_inner())
|
||||
.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");
|
||||
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");
|
||||
let escrow_post =
|
||||
AccountPostState::new_claimed_if_default(escrow_account, Claim::Pda(escrow_seed()));
|
||||
|
||||
let call = ChainedCall::new(
|
||||
outbox_program_id,
|
||||
vec![outbox.clone()],
|
||||
&OutboxInstruction::Emit {
|
||||
target_zone,
|
||||
target_program_id,
|
||||
target_accounts,
|
||||
payload,
|
||||
ordinal,
|
||||
},
|
||||
);
|
||||
|
||||
ProgramOutput::new(
|
||||
self_program_id,
|
||||
caller_program_id,
|
||||
instruction_words,
|
||||
vec![holder, escrow, outbox.clone()],
|
||||
vec![holder_post, escrow_post, AccountPostState::new(outbox.account)],
|
||||
)
|
||||
.with_chained_calls(vec![call])
|
||||
.write();
|
||||
}
|
||||
|
||||
/// Decodes the cross-zone payload (risc0 words, little-endian bytes) into the
|
||||
/// wrapped-token instruction it carries.
|
||||
fn decode_mint(payload: &[u8]) -> WrappedInstruction {
|
||||
assert!(
|
||||
payload.len() % 4 == 0,
|
||||
"payload must be u32-aligned instruction words"
|
||||
);
|
||||
let words: Vec<u32> = payload
|
||||
.chunks_exact(4)
|
||||
.map(|chunk| u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]))
|
||||
.collect();
|
||||
risc0_zkvm::serde::from_slice(&words).expect("payload decodes to a wrapped-token instruction")
|
||||
}
|
||||
68
program_methods/guest/src/bin/wrapped_token.rs
Normal file
68
program_methods/guest/src/bin/wrapped_token.rs
Normal file
@ -0,0 +1,68 @@
|
||||
use lee_core::{
|
||||
account::AccountWithMetadata,
|
||||
program::{AccountPostState, Claim, ProgramInput, ProgramOutput, read_lee_inputs},
|
||||
};
|
||||
use wrapped_token_core::{
|
||||
Instruction, balance_bytes, config_account_id, holding_account_id, holding_seed, read_balance,
|
||||
read_minter,
|
||||
};
|
||||
|
||||
fn main() {
|
||||
let (
|
||||
ProgramInput {
|
||||
self_program_id,
|
||||
caller_program_id,
|
||||
pre_states,
|
||||
instruction,
|
||||
},
|
||||
instruction_words,
|
||||
) = read_lee_inputs::<Instruction>();
|
||||
|
||||
let Instruction::Mint { recipient, amount } = instruction;
|
||||
|
||||
// pre_states: [config PDA, recipient holding PDA].
|
||||
let [config, holding] = <[AccountWithMetadata; 2]>::try_from(pre_states)
|
||||
.expect("Mint requires the config and recipient holding accounts");
|
||||
|
||||
// The config PDA is genesis-seeded with the authorized minter (the cross-zone
|
||||
// inbox). Pin the caller to it, since the guest cannot import the inbox id.
|
||||
assert_eq!(
|
||||
config.account_id,
|
||||
config_account_id(self_program_id),
|
||||
"first account must be the wrapped-token config PDA"
|
||||
);
|
||||
let minter = read_minter(&config.account.data.clone().into_inner())
|
||||
.expect("config account holds an authorized minter id");
|
||||
assert_eq!(
|
||||
caller_program_id,
|
||||
Some(minter),
|
||||
"Mint is only callable by the authorized minter (the cross-zone inbox)"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
holding.account_id,
|
||||
holding_account_id(self_program_id, &recipient),
|
||||
"second account must be the recipient holding PDA"
|
||||
);
|
||||
|
||||
let new_balance = read_balance(&holding.account.data.clone().into_inner())
|
||||
.checked_add(amount)
|
||||
.expect("wrapped-token balance overflow");
|
||||
let mut holding_account = holding.account.clone();
|
||||
holding_account.data = balance_bytes(new_balance)
|
||||
.to_vec()
|
||||
.try_into()
|
||||
.expect("balance fits in account data");
|
||||
let holding_post =
|
||||
AccountPostState::new_claimed_if_default(holding_account, Claim::Pda(holding_seed(&recipient)));
|
||||
let config_post = AccountPostState::new(config.account.clone());
|
||||
|
||||
ProgramOutput::new(
|
||||
self_program_id,
|
||||
caller_program_id,
|
||||
instruction_words,
|
||||
vec![config, holding],
|
||||
vec![config_post, holding_post],
|
||||
)
|
||||
.write();
|
||||
}
|
||||
17
programs/bridge_lock/core/Cargo.toml
Normal file
17
programs/bridge_lock/core/Cargo.toml
Normal file
@ -0,0 +1,17 @@
|
||||
[package]
|
||||
name = "bridge_lock_core"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
license = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
lee_core.workspace = true
|
||||
serde = { workspace = true, features = ["alloc"] }
|
||||
lee = { workspace = true, optional = true }
|
||||
|
||||
[features]
|
||||
# Host-only genesis helper; pulls `lee`, so the risc0 guest builds without it.
|
||||
host = ["dep:lee"]
|
||||
92
programs/bridge_lock/core/src/lib.rs
Normal file
92
programs/bridge_lock/core/src/lib.rs
Normal file
@ -0,0 +1,92 @@
|
||||
//! Core types for the bridge-lock program, the source side of the cross-zone
|
||||
//! token bridge. A holder locks part of their balance into an escrow and emits a
|
||||
//! cross-zone message minting the wrapped token on the target zone.
|
||||
|
||||
use lee_core::{
|
||||
account::AccountId,
|
||||
program::{PdaSeed, ProgramId},
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
const ESCROW_SEED_DOMAIN: [u8; 32] = *b"/LEZ/v0.3/BridgeLockEscrow/0000/";
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum Instruction {
|
||||
/// Lock `amount` of the holder's balance and emit a cross-zone message
|
||||
/// minting the wrapped token on `target_zone`. The emission fields mirror
|
||||
/// `cross_zone_outbox::Instruction::Emit` so the watcher reads them directly.
|
||||
///
|
||||
/// Required accounts (3): holder holding (authorized), escrow PDA, outbox PDA.
|
||||
Lock {
|
||||
amount: u128,
|
||||
target_zone: [u8; 32],
|
||||
target_program_id: ProgramId,
|
||||
target_accounts: Vec<[u8; 32]>,
|
||||
payload: Vec<u8>,
|
||||
outbox_program_id: ProgramId,
|
||||
ordinal: u32,
|
||||
},
|
||||
}
|
||||
|
||||
/// PDA accumulating all locked balance on this zone.
|
||||
#[must_use]
|
||||
pub fn escrow_account_id(bridge_lock_id: ProgramId) -> AccountId {
|
||||
AccountId::for_public_pda(&bridge_lock_id, &escrow_seed())
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub 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 fn balance_bytes(amount: u128) -> [u8; 16] {
|
||||
amount.to_le_bytes()
|
||||
}
|
||||
|
||||
/// Builds the genesis holding account funding a holder's bridgeable balance:
|
||||
/// owned by bridge_lock, data is the LE balance, at the holder's account id. It
|
||||
/// is not produced by any transaction, so the sequencer and the indexer both
|
||||
/// seed it through this one builder to keep their genesis states identical.
|
||||
#[cfg(feature = "host")]
|
||||
#[must_use]
|
||||
pub fn build_holding_account(
|
||||
holder: AccountId,
|
||||
amount: u128,
|
||||
) -> (AccountId, lee_core::account::Account) {
|
||||
let account = lee_core::account::Account {
|
||||
program_owner: lee::program::Program::bridge_lock().id(),
|
||||
data: balance_bytes(amount)
|
||||
.to_vec()
|
||||
.try_into()
|
||||
.expect("balance fits in account data"),
|
||||
..Default::default()
|
||||
};
|
||||
(holder, account)
|
||||
}
|
||||
|
||||
#[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];
|
||||
assert_eq!(escrow_account_id(id), escrow_account_id(id));
|
||||
}
|
||||
}
|
||||
13
programs/wrapped_token/core/Cargo.toml
Normal file
13
programs/wrapped_token/core/Cargo.toml
Normal file
@ -0,0 +1,13 @@
|
||||
[package]
|
||||
name = "wrapped_token_core"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
license = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
lee_core.workspace = true
|
||||
serde = { workspace = true, features = ["alloc"] }
|
||||
risc0-zkvm.workspace = true
|
||||
115
programs/wrapped_token/core/src/lib.rs
Normal file
115
programs/wrapped_token/core/src/lib.rs
Normal file
@ -0,0 +1,115 @@
|
||||
//! Core types for the wrapped-token program, the destination side of the
|
||||
//! cross-zone bridge. Only the cross-zone inbox may mint; the guest enforces
|
||||
//! this by reading the authorized minter from a genesis-seeded config account.
|
||||
|
||||
use lee_core::{
|
||||
account::AccountId,
|
||||
program::{PdaSeed, ProgramId},
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
const CONFIG_SEED_DOMAIN: [u8; 32] = *b"/LEZ/v0.3/WrappedTokenConfig/00/";
|
||||
const HOLDING_SEED_DOMAIN: [u8; 32] = *b"/LEZ/v0.3/WrappedTokenHold/00000";
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum Instruction {
|
||||
/// Credit `amount` wrapped tokens to `recipient`'s holding. Delivered only by
|
||||
/// the cross-zone inbox.
|
||||
///
|
||||
/// Required accounts (2): the wrapped-token config PDA, then the recipient's
|
||||
/// holding PDA.
|
||||
Mint { recipient: [u8; 32], amount: u128 },
|
||||
}
|
||||
|
||||
/// PDA holding the authorized minter program id (the cross-zone inbox), seeded at
|
||||
/// genesis so the guest can pin its caller without importing the inbox image id.
|
||||
#[must_use]
|
||||
pub fn config_account_id(wrapped_token_id: ProgramId) -> AccountId {
|
||||
AccountId::for_public_pda(&wrapped_token_id, &config_seed())
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn config_seed() -> PdaSeed {
|
||||
PdaSeed::new(CONFIG_SEED_DOMAIN)
|
||||
}
|
||||
|
||||
/// PDA holding one recipient's wrapped-token balance.
|
||||
#[must_use]
|
||||
pub fn holding_account_id(wrapped_token_id: ProgramId, recipient: &[u8; 32]) -> AccountId {
|
||||
AccountId::for_public_pda(&wrapped_token_id, &holding_seed(recipient))
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn holding_seed(recipient: &[u8; 32]) -> PdaSeed {
|
||||
use risc0_zkvm::sha::{Impl, Sha256 as _};
|
||||
|
||||
let mut bytes = Vec::with_capacity(HOLDING_SEED_DOMAIN.len() + recipient.len());
|
||||
bytes.extend_from_slice(&HOLDING_SEED_DOMAIN);
|
||||
bytes.extend_from_slice(recipient);
|
||||
let seed: [u8; 32] = Impl::hash_bytes(&bytes)
|
||||
.as_bytes()
|
||||
.try_into()
|
||||
.unwrap_or_else(|_| unreachable!());
|
||||
PdaSeed::new(seed)
|
||||
}
|
||||
|
||||
/// Encodes the authorized minter program id for the config account's data.
|
||||
#[must_use]
|
||||
pub fn minter_bytes(minter: ProgramId) -> [u8; 32] {
|
||||
let mut bytes = [0_u8; 32];
|
||||
for (word, chunk) in minter.iter().zip(bytes.chunks_exact_mut(4)) {
|
||||
chunk.copy_from_slice(&word.to_le_bytes());
|
||||
}
|
||||
bytes
|
||||
}
|
||||
|
||||
/// Decodes the authorized minter program id from the config account's data.
|
||||
#[must_use]
|
||||
pub fn read_minter(data: &[u8]) -> Option<ProgramId> {
|
||||
if data.len() < 32 {
|
||||
return None;
|
||||
}
|
||||
let mut minter = [0_u32; 8];
|
||||
for (word, chunk) in minter.iter_mut().zip(data[..32].chunks_exact(4)) {
|
||||
*word = u32::from_le_bytes(chunk.try_into().unwrap_or_else(|_| unreachable!()));
|
||||
}
|
||||
Some(minter)
|
||||
}
|
||||
|
||||
/// Reads a wrapped-token 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 fn balance_bytes(amount: u128) -> [u8; 16] {
|
||||
amount.to_le_bytes()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn minter_round_trips() {
|
||||
let minter: ProgramId = [1, 2, 3, 4, 5, 6, 7, 8];
|
||||
assert_eq!(read_minter(&minter_bytes(minter)), Some(minter));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn balance_round_trips() {
|
||||
assert_eq!(read_balance(&balance_bytes(42)), 42);
|
||||
assert_eq!(read_balance(&[]), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn holding_is_unique_per_recipient() {
|
||||
let id: ProgramId = [9; 8];
|
||||
assert_ne!(holding_account_id(id, &[1; 32]), holding_account_id(id, &[2; 32]));
|
||||
assert_eq!(holding_account_id(id, &[1; 32]), holding_account_id(id, &[1; 32]));
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user