mirror of
https://github.com/logos-blockchain/logos-execution-zone.git
synced 2026-08-26 11:51:13 +00:00
fix(cross-zone)!: claim the source authority, and guard its precondition
BREAKING CHANGE: the targets claim the authority account on their first use of it, so both target image ids move.
This commit is contained in:
@@ -1313,6 +1313,41 @@ fn the_authority_can_renounce_itself_and_only_itself() {
|
||||
"rejected for the wrong reason: {err:?}"
|
||||
);
|
||||
|
||||
// Named but not signing. Separate from the case above, which fails one line
|
||||
// earlier on the address.
|
||||
let Err(unsigned) =
|
||||
ValidatedStateDiff::from_public_transaction(&renounce(authority, &other_key), &state, 1, 0)
|
||||
else {
|
||||
panic!("naming the authority without its signature must not renounce it");
|
||||
};
|
||||
assert!(
|
||||
format!("{unsigned:?}").contains("must authorize renouncing it"),
|
||||
"rejected for the wrong reason: {unsigned:?}"
|
||||
);
|
||||
|
||||
// Substituting another account for the config is refused. Renounce is the one
|
||||
// instruction that destroys the authority for good, so this guard matters most
|
||||
// here.
|
||||
let substituted = Message::try_new(
|
||||
wrapped_token_id,
|
||||
vec![ping_record_pda(wrapped_token_id), authority],
|
||||
vec![0_u128.into()],
|
||||
wrapped_token_core::Instruction::RenounceAuthority,
|
||||
)
|
||||
.expect("build renounce message");
|
||||
let swapped_tx = PublicTransaction::new(
|
||||
substituted.clone(),
|
||||
WitnessSet::for_message(&substituted, &[&key]),
|
||||
);
|
||||
let Err(swapped) = ValidatedStateDiff::from_public_transaction(&swapped_tx, &state, 1, 0)
|
||||
else {
|
||||
panic!("a renounce over a substituted config account must not execute");
|
||||
};
|
||||
assert!(
|
||||
format!("{swapped:?}").contains("must be the wrapped-token config PDA"),
|
||||
"rejected for the wrong reason: {swapped:?}"
|
||||
);
|
||||
|
||||
let diff =
|
||||
ValidatedStateDiff::from_public_transaction(&renounce(authority, &key), &state, 1, 0)
|
||||
.expect("the authority renounces itself");
|
||||
@@ -1342,8 +1377,9 @@ fn the_authority_can_renounce_itself_and_only_itself() {
|
||||
wrapped_token_core::Instruction::UpdateSources { sources: vec![] },
|
||||
)
|
||||
.expect("build update message");
|
||||
let tx = PublicTransaction::new(update.clone(), WitnessSet::for_message(&update, &[&key]));
|
||||
let Err(frozen) = ValidatedStateDiff::from_public_transaction(&tx, &state, 2, 0) else {
|
||||
let frozen_tx =
|
||||
PublicTransaction::new(update.clone(), WitnessSet::for_message(&update, &[&key]));
|
||||
let Err(frozen) = ValidatedStateDiff::from_public_transaction(&frozen_tx, &state, 2, 0) else {
|
||||
panic!("a renounced authority must not still change sources");
|
||||
};
|
||||
assert!(
|
||||
@@ -1352,6 +1388,253 @@ fn the_authority_can_renounce_itself_and_only_itself() {
|
||||
);
|
||||
}
|
||||
|
||||
/// The receiver's authority path is a mirror of the token's, and a mirror is
|
||||
/// exactly where a copy-paste slip hides. Same battery, run against it.
|
||||
#[test]
|
||||
fn the_receiver_authority_path_holds() {
|
||||
let receiver_id = programs::ping_receiver().id();
|
||||
let config_id = receiver_config_account_id(receiver_id);
|
||||
let src_zone = [2_u8; 32];
|
||||
let sender_id = programs::ping_sender().id();
|
||||
|
||||
let key = PrivateKey::try_new([7; 32]).expect("valid key");
|
||||
let authority = AccountId::from(&PublicKey::new_from_private_key(&key));
|
||||
let other_key = PrivateKey::try_new([8; 32]).expect("valid key");
|
||||
let other = AccountId::from(&PublicKey::new_from_private_key(&other_key));
|
||||
|
||||
let update = |account: AccountId, signer: &PrivateKey, nonce: u128| {
|
||||
let message = Message::try_new(
|
||||
receiver_id,
|
||||
vec![config_id, account],
|
||||
vec![nonce.into()],
|
||||
ping_core::ReceiverInstruction::UpdateSources {
|
||||
sources: vec![(src_zone, sender_id)],
|
||||
},
|
||||
)
|
||||
.expect("build update message");
|
||||
let witness = WitnessSet::for_message(&message, &[signer]);
|
||||
PublicTransaction::new(message, witness)
|
||||
};
|
||||
let renounce = |account: AccountId, signer: &PrivateKey, nonce: u128| {
|
||||
let message = Message::try_new(
|
||||
receiver_id,
|
||||
vec![config_id, account],
|
||||
vec![nonce.into()],
|
||||
ping_core::ReceiverInstruction::RenounceAuthority,
|
||||
)
|
||||
.expect("build renounce message");
|
||||
let witness = WitnessSet::for_message(&message, &[signer]);
|
||||
PublicTransaction::new(message, witness)
|
||||
};
|
||||
let rejects = |state: &V03State, tx: &PublicTransaction, expected: &str| {
|
||||
let Err(err) = ValidatedStateDiff::from_public_transaction(tx, state, 1, 0) else {
|
||||
panic!("expected a rejection mentioning {expected}");
|
||||
};
|
||||
assert!(
|
||||
format!("{err:?}").contains(expected),
|
||||
"rejected for the wrong reason: {err:?}"
|
||||
);
|
||||
};
|
||||
|
||||
// With no authority configured, nothing moves.
|
||||
let mut unset = base_state();
|
||||
seed_receiver_config(&mut unset, None, vec![]);
|
||||
rejects(&unset, &update(authority, &key, 0), "fixed at genesis");
|
||||
rejects(&unset, &renounce(authority, &key, 0), "already renounced");
|
||||
|
||||
// With one configured: the wrong account, and the right account without its
|
||||
// own signature, are both refused for their own reasons.
|
||||
let mut state = base_state();
|
||||
seed_receiver_config(&mut state, Some(authority), vec![]);
|
||||
rejects(
|
||||
&state,
|
||||
&update(other, &other_key, 0),
|
||||
"must be the configured authority",
|
||||
);
|
||||
rejects(
|
||||
&state,
|
||||
&renounce(other, &other_key, 0),
|
||||
"must be the configured authority",
|
||||
);
|
||||
rejects(
|
||||
&state,
|
||||
&update(authority, &other_key, 0),
|
||||
"must authorize a source change",
|
||||
);
|
||||
rejects(
|
||||
&state,
|
||||
&renounce(authority, &other_key, 0),
|
||||
"must authorize renouncing it",
|
||||
);
|
||||
|
||||
// The authority itself works, and renouncing is one-way.
|
||||
let diff =
|
||||
ValidatedStateDiff::from_public_transaction(&update(authority, &key, 0), &state, 1, 0)
|
||||
.expect("the configured authority changes sources");
|
||||
state.apply_state_diff(diff);
|
||||
let cfg = ping_core::ReceiverConfig::from_bytes(
|
||||
&state.get_account_by_id(config_id).data.into_inner(),
|
||||
)
|
||||
.expect("config decodes");
|
||||
assert_eq!(cfg.sources, vec![(src_zone, sender_id)]);
|
||||
assert_eq!(cfg.deliverer, programs::cross_zone_inbox().id());
|
||||
|
||||
let renounce_diff =
|
||||
ValidatedStateDiff::from_public_transaction(&renounce(authority, &key, 1), &state, 2, 0)
|
||||
.expect("the authority renounces itself");
|
||||
state.apply_state_diff(renounce_diff);
|
||||
let renounced_cfg = ping_core::ReceiverConfig::from_bytes(
|
||||
&state.get_account_by_id(config_id).data.into_inner(),
|
||||
)
|
||||
.expect("config decodes");
|
||||
assert_eq!(renounced_cfg.authority, None, "the authority is gone");
|
||||
assert_eq!(
|
||||
renounced_cfg.sources,
|
||||
vec![(src_zone, sender_id)],
|
||||
"renouncing freezes the list it had"
|
||||
);
|
||||
rejects(&state, &update(authority, &key, 2), "fixed at genesis");
|
||||
rejects(&state, &renounce(authority, &key, 2), "already renounced");
|
||||
}
|
||||
|
||||
/// The guards that survive a deletion otherwise: the caller pins on renounce for
|
||||
/// both targets and on the receiver's update, the config-address checks the
|
||||
/// substitution cases miss, and the token's already-renounced branch.
|
||||
#[test]
|
||||
fn the_remaining_authority_guards_hold() {
|
||||
let wrapped_token_id = programs::wrapped_token().id();
|
||||
let receiver_id = programs::ping_receiver().id();
|
||||
let inbox_id = programs::cross_zone_inbox().id();
|
||||
let self_zone = [1_u8; 32];
|
||||
let src_zone = [2_u8; 32];
|
||||
|
||||
let key = PrivateKey::try_new([7; 32]).expect("valid key");
|
||||
let authority = AccountId::from(&PublicKey::new_from_private_key(&key));
|
||||
|
||||
let rejects = |state: &V03State, tx: &PublicTransaction, expected: &str| {
|
||||
let Err(err) = ValidatedStateDiff::from_public_transaction(tx, state, 1, 0) else {
|
||||
panic!("expected a rejection mentioning {expected}");
|
||||
};
|
||||
assert!(
|
||||
format!("{err:?}").contains(expected),
|
||||
"rejected for the wrong reason: {err:?}"
|
||||
);
|
||||
};
|
||||
let signed = |program: lee_core::program::ProgramId,
|
||||
accounts: Vec<AccountId>,
|
||||
instruction_words: Vec<u32>| {
|
||||
let message =
|
||||
Message::new_preserialized(program, accounts, vec![0_u128.into()], instruction_words);
|
||||
let witness = WitnessSet::for_message(&message, &[&key]);
|
||||
PublicTransaction::new(message, witness)
|
||||
};
|
||||
|
||||
let mut state = base_state();
|
||||
seed_inbox_config(&mut state, self_zone);
|
||||
seed_wrapped_config(&mut state, Some(authority), vec![]);
|
||||
seed_receiver_config(&mut state, Some(authority), vec![]);
|
||||
|
||||
// Config address, on the instruction each target's substitution case misses.
|
||||
rejects(
|
||||
&state,
|
||||
&signed(
|
||||
wrapped_token_id,
|
||||
vec![ping_record_pda(wrapped_token_id), authority],
|
||||
risc0_zkvm::serde::to_vec(&wrapped_token_core::Instruction::UpdateSources {
|
||||
sources: vec![(src_zone, programs::bridge_lock().id())],
|
||||
})
|
||||
.expect("serialize"),
|
||||
),
|
||||
"must be the wrapped-token config PDA",
|
||||
);
|
||||
for (words, expected) in [
|
||||
(
|
||||
risc0_zkvm::serde::to_vec(&ping_core::ReceiverInstruction::UpdateSources {
|
||||
sources: vec![(src_zone, programs::ping_sender().id())],
|
||||
})
|
||||
.expect("serialize"),
|
||||
"must be the receiver config PDA",
|
||||
),
|
||||
(
|
||||
risc0_zkvm::serde::to_vec(&ping_core::ReceiverInstruction::RenounceAuthority)
|
||||
.expect("serialize"),
|
||||
"must be the receiver config PDA",
|
||||
),
|
||||
] {
|
||||
rejects(
|
||||
&state,
|
||||
&signed(
|
||||
receiver_id,
|
||||
vec![ping_record_pda(receiver_id), authority],
|
||||
words,
|
||||
),
|
||||
expected,
|
||||
);
|
||||
}
|
||||
|
||||
// Reached through the inbox rather than top-level, for the three caller pins
|
||||
// that had no chained test.
|
||||
for (target, config_id, words) in [
|
||||
(
|
||||
wrapped_token_id,
|
||||
wrapped_token_core::config_account_id(wrapped_token_id),
|
||||
risc0_zkvm::serde::to_vec(&wrapped_token_core::Instruction::RenounceAuthority)
|
||||
.expect("serialize"),
|
||||
),
|
||||
(
|
||||
receiver_id,
|
||||
receiver_config_account_id(receiver_id),
|
||||
risc0_zkvm::serde::to_vec(&ping_core::ReceiverInstruction::RenounceAuthority)
|
||||
.expect("serialize"),
|
||||
),
|
||||
(
|
||||
receiver_id,
|
||||
receiver_config_account_id(receiver_id),
|
||||
risc0_zkvm::serde::to_vec(&ping_core::ReceiverInstruction::UpdateSources {
|
||||
sources: vec![(src_zone, programs::ping_sender().id())],
|
||||
})
|
||||
.expect("serialize"),
|
||||
),
|
||||
] {
|
||||
let msg = CrossZoneMessage {
|
||||
src_zone,
|
||||
src_block_id: 5,
|
||||
src_block_hash: SRC_BLOCK_HASH,
|
||||
src_tx_index: 0,
|
||||
src_program_id: programs::bridge_lock().id(),
|
||||
target_program_id: target,
|
||||
payload: words.iter().flat_map(|word| word.to_le_bytes()).collect(),
|
||||
l1_inclusion_witness: None,
|
||||
};
|
||||
let message = Message::try_new(
|
||||
inbox_id,
|
||||
dispatch_accounts(inbox_id, &msg, vec![config_id, authority]),
|
||||
vec![],
|
||||
InboxInstruction::Dispatch(msg),
|
||||
)
|
||||
.expect("build dispatch message");
|
||||
let tx = PublicTransaction::new(message, WitnessSet::from_raw_parts(vec![]));
|
||||
rejects(&state, &tx, "only invoked as a top-level transaction");
|
||||
}
|
||||
|
||||
// The token's already-renounced branch, which only the receiver covered.
|
||||
let mut unset = base_state();
|
||||
seed_wrapped_config(&mut unset, None, vec![]);
|
||||
rejects(
|
||||
&unset,
|
||||
&signed(
|
||||
wrapped_token_id,
|
||||
vec![
|
||||
wrapped_token_core::config_account_id(wrapped_token_id),
|
||||
authority,
|
||||
],
|
||||
risc0_zkvm::serde::to_vec(&wrapped_token_core::Instruction::RenounceAuthority)
|
||||
.expect("serialize"),
|
||||
),
|
||||
"already renounced",
|
||||
);
|
||||
}
|
||||
|
||||
/// A token that authorizes nothing mints for nobody. The state a zone reaches with
|
||||
/// no peers configured, where the config is still seeded so its PDA cannot be
|
||||
/// claimed by a first initializer.
|
||||
|
||||
+19
-15
@@ -204,15 +204,17 @@ pub fn build_holding_account(holder: AccountId, amount: Balance) -> (AccountId,
|
||||
/// The `(src_zone, src_program_id)` pairs the operator's routes name for one
|
||||
/// target.
|
||||
///
|
||||
/// Panics on a route naming a target this zone does not host. Nothing downstream
|
||||
/// would notice otherwise: the route is dropped here, the watcher no longer
|
||||
/// filters targets, and every delivery would fail at a program that does not
|
||||
/// exist and dead-letter after three attempts. A typo in a config file should not
|
||||
/// cost a channel silently.
|
||||
/// Panics on a route naming a program that does not authorize cross-zone sources.
|
||||
/// Nothing downstream would notice otherwise: the route is dropped here, the
|
||||
/// watcher no longer filters targets, and every delivery would be refused by a
|
||||
/// program that never opted in, dead-lettering after three attempts. A typo in a
|
||||
/// config file should not cost a channel silently.
|
||||
///
|
||||
/// Only the sequencer builds genesis, so an indexer handed the same typo starts
|
||||
/// normally and only the sequencer refuses to boot.
|
||||
fn sources_for_target(
|
||||
cross_zone: Option<&CrossZoneConfig>,
|
||||
target_program_id: ProgramId,
|
||||
hosted: &[ProgramId],
|
||||
) -> Vec<(ZoneId, ProgramId)> {
|
||||
let Some(cross_zone) = cross_zone else {
|
||||
return Vec::new();
|
||||
@@ -221,8 +223,9 @@ fn sources_for_target(
|
||||
for peer in &cross_zone.peers {
|
||||
for route in &peer.allowed_routes {
|
||||
assert!(
|
||||
hosted.contains(&route.target_program_id),
|
||||
"cross-zone route names a target this zone does not host"
|
||||
cross_zone_targets().contains(&route.target_program_id),
|
||||
"cross-zone route names {:?}, which does not authorize cross-zone sources",
|
||||
route.target_program_id
|
||||
);
|
||||
if route.target_program_id == target_program_id {
|
||||
sources.push((peer.channel_id, route.src_program_id));
|
||||
@@ -254,7 +257,7 @@ pub fn build_wrapped_token_init_config_tx(
|
||||
cross_zone: Option<&CrossZoneConfig>,
|
||||
) -> lee::PublicTransaction {
|
||||
let wrapped_token_id = programs::wrapped_token().id();
|
||||
let sources = sources_for_target(cross_zone, wrapped_token_id, &cross_zone_targets());
|
||||
let sources = sources_for_target(cross_zone, wrapped_token_id);
|
||||
genesis_public_tx(
|
||||
wrapped_token_id,
|
||||
vec![wrapped_token_core::config_account_id(wrapped_token_id)],
|
||||
@@ -303,7 +306,7 @@ pub fn build_ping_receiver_init_config_tx(
|
||||
cross_zone: Option<&CrossZoneConfig>,
|
||||
) -> lee::PublicTransaction {
|
||||
let receiver_id = programs::ping_receiver().id();
|
||||
let sources = sources_for_target(cross_zone, receiver_id, &cross_zone_targets());
|
||||
let sources = sources_for_target(cross_zone, receiver_id);
|
||||
genesis_public_tx(
|
||||
receiver_id,
|
||||
vec![ping_core::receiver_config_account_id(receiver_id)],
|
||||
@@ -335,12 +338,13 @@ fn genesis_public_tx<I: Serialize>(
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// A route naming a target this zone does not host is an operator typo that
|
||||
/// nothing downstream would report: dropped here, unfiltered by the watcher,
|
||||
/// and dead-lettered at a program that does not exist.
|
||||
/// A route naming a program that never opted into cross-zone sources is an
|
||||
/// operator typo that nothing downstream would report: the fan-out would drop
|
||||
/// it, the watcher no longer filters targets, and every delivery would be
|
||||
/// refused by the target and dead-lettered.
|
||||
#[test]
|
||||
#[should_panic(expected = "does not host")]
|
||||
fn a_route_to_an_unhosted_target_is_refused_at_genesis() {
|
||||
#[should_panic(expected = "does not authorize cross-zone sources")]
|
||||
fn a_route_to_a_program_that_does_not_authorize_sources_is_refused() {
|
||||
let cross_zone = CrossZoneConfig {
|
||||
peers: vec![CrossZonePeer {
|
||||
channel_id: [2; 32],
|
||||
|
||||
@@ -49,8 +49,8 @@ pub struct CrossZonePeer {
|
||||
/// No longer enforced in transit: the inbox delivers to whatever a message
|
||||
/// names, and the target refuses a source it did not authorize. This is the
|
||||
/// operator's statement of intent, fanned out at genesis into each target's
|
||||
/// own config. A route naming a target this zone does not host is dropped
|
||||
/// there, silently.
|
||||
/// own config. A route naming a program that does not authorize cross-zone
|
||||
/// sources is refused there, at genesis.
|
||||
pub allowed_routes: Vec<CrossZoneRoute>,
|
||||
/// The peer's block-signing public key, pinned to reject blocks inscribed by
|
||||
/// anyone other than that zone's sequencer. `None` skips the check (the
|
||||
@@ -74,6 +74,13 @@ pub struct CrossZoneConfig {
|
||||
/// needs a config change and a restart on both the sequencer and the indexer,
|
||||
/// and this field itself can only ever be set at genesis.
|
||||
///
|
||||
/// Must be a fresh, never-used account. The target claims it on first use,
|
||||
/// because the state machine refuses an unowned post state whose pre-state is
|
||||
/// not exactly default, and neither target has any instruction that moves this
|
||||
/// account's balance afterwards, so anything sent to it is frozen for good and
|
||||
/// no other program can ever claim it. Renouncing seizes it the same way, and
|
||||
/// whichever target is used first is the one that ends up owning it.
|
||||
///
|
||||
/// It is a value-authorizing key: whoever holds it can authorize a source, and
|
||||
/// a source can mint, so its compromise is theft rather than delay. One value
|
||||
/// seeds every target, so setting it for one program grants it over all of
|
||||
|
||||
@@ -56,10 +56,14 @@ fn main() {
|
||||
/// instruction bytes and account ids the peer chose. So a program meant to be
|
||||
/// reachable across zones MUST check the marker at position 0 against sources it
|
||||
/// authorized itself, the way `wrapped_token` and `ping_receiver` do. A program
|
||||
/// not meant to be reachable is protected only by its own asserts; several
|
||||
/// builtins currently survive incidentally, through a claim check or an owner
|
||||
/// check written for another reason, which is not the same as being safe by
|
||||
/// design.
|
||||
/// not meant to be reachable has only whatever its own code happens to do. Today
|
||||
/// every other builtin refuses, but by three different accidents: four assert
|
||||
/// `caller_program_id` is none; several run to completion and are stopped by the
|
||||
/// host, either because they try to claim the marker without its authorization or
|
||||
/// because they chain into its zero program id; and the rest are saved by an
|
||||
/// address assert on a PDA. None of that was written with cross-zone delivery in
|
||||
/// mind. User-deployed programs are reachable too, and were written with no
|
||||
/// expectation of an inbox caller at all.
|
||||
fn dispatch(
|
||||
self_program_id: ProgramId,
|
||||
caller_program_id: Option<ProgramId>,
|
||||
|
||||
@@ -42,11 +42,9 @@ pub fn inbox_source_marker_account_id(
|
||||
)
|
||||
}
|
||||
|
||||
/// Seed of the source marker. Targets compare addresses rather than seeds, so
|
||||
/// they use [`inbox_source_marker_account_id`]; this is exposed for callers that
|
||||
/// need the seed itself.
|
||||
#[must_use]
|
||||
pub fn inbox_source_marker_seed(src_zone: &ZoneId, src_program_id: ProgramId) -> PdaSeed {
|
||||
/// Seed of the source marker. Private: nothing claims this account, so no caller
|
||||
/// needs the seed, only the address.
|
||||
fn inbox_source_marker_seed(src_zone: &ZoneId, src_program_id: ProgramId) -> PdaSeed {
|
||||
use risc0_zkvm::sha::{Impl, Sha256 as _};
|
||||
|
||||
let mut bytes = [0_u8; 96];
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
use cross_zone_marker_core::inbox_source_marker_account_id;
|
||||
use lee_core::{
|
||||
account::{Account, AccountWithMetadata},
|
||||
program::{AccountPostState, Claim, ProgramId, ProgramInput, ProgramOutput, read_lee_inputs},
|
||||
program::{
|
||||
AccountPostState, Claim, DEFAULT_PROGRAM_ID, ProgramId, ProgramInput, ProgramOutput,
|
||||
read_lee_inputs,
|
||||
},
|
||||
};
|
||||
use ping_core::{
|
||||
ReceiverConfig, ReceiverInstruction, ping_record_pda, ping_record_seed,
|
||||
@@ -64,7 +67,7 @@ fn record(
|
||||
assert_eq!(
|
||||
config.account_id,
|
||||
receiver_config_account_id(self_program_id),
|
||||
"Second account must be the receiver config PDA"
|
||||
"second account must be the receiver config PDA"
|
||||
);
|
||||
let cfg = ReceiverConfig::from_bytes(&config.account.data.clone().into_inner())
|
||||
.expect("config account holds a receiver config");
|
||||
@@ -86,7 +89,7 @@ fn record(
|
||||
assert_eq!(
|
||||
record.account_id,
|
||||
ping_record_pda(self_program_id),
|
||||
"Third account must be the ping record PDA"
|
||||
"third account must be the ping record PDA"
|
||||
);
|
||||
|
||||
let mut post_account = record.account.clone();
|
||||
@@ -138,6 +141,16 @@ fn renounce_authority(
|
||||
authority.account_id, expected,
|
||||
"second account must be the configured authority"
|
||||
);
|
||||
// The program claims this account, and a claim cannot rescue a first use: the
|
||||
// state machine checks post states before it applies claims, so an unowned
|
||||
// account that is not exactly default is refused and, since renouncing is
|
||||
// refused for the same reason, the receiver source list would be frozen for the
|
||||
// life of the zone. Say so here rather than let it surface as a rule number.
|
||||
assert!(
|
||||
authority.account == Account::default()
|
||||
|| authority.account.program_owner != DEFAULT_PROGRAM_ID,
|
||||
"the authority account must be untouched before its first use as one"
|
||||
);
|
||||
assert!(
|
||||
authority.is_authorized,
|
||||
"the configured authority must authorize renouncing it"
|
||||
@@ -157,7 +170,11 @@ fn renounce_authority(
|
||||
vec![config, authority.clone()],
|
||||
vec![
|
||||
AccountPostState::new(config_account),
|
||||
AccountPostState::new(authority.account),
|
||||
// Claimed on first use, not merely echoed: an unowned account whose
|
||||
// pre-state is not exactly default cannot be returned with a default
|
||||
// owner (state-machine rule 7), and the authority's own signature
|
||||
// bumps its nonce, so echoing it works once and never again.
|
||||
AccountPostState::new_claimed_if_default(authority.account, Claim::Authorized),
|
||||
],
|
||||
)
|
||||
.write();
|
||||
@@ -200,6 +217,16 @@ fn update_sources(
|
||||
authority.account_id, expected,
|
||||
"second account must be the configured authority"
|
||||
);
|
||||
// The program claims this account, and a claim cannot rescue a first use: the
|
||||
// state machine checks post states before it applies claims, so an unowned
|
||||
// account that is not exactly default is refused and, since renouncing is
|
||||
// refused for the same reason, the receiver source list would be frozen for the
|
||||
// life of the zone. Say so here rather than let it surface as a rule number.
|
||||
assert!(
|
||||
authority.account == Account::default()
|
||||
|| authority.account.program_owner != DEFAULT_PROGRAM_ID,
|
||||
"the authority account must be untouched before its first use as one"
|
||||
);
|
||||
assert!(
|
||||
authority.is_authorized,
|
||||
"the configured authority must authorize a source change"
|
||||
@@ -219,7 +246,11 @@ fn update_sources(
|
||||
vec![config, authority.clone()],
|
||||
vec![
|
||||
AccountPostState::new(config_account),
|
||||
AccountPostState::new(authority.account),
|
||||
// Claimed on first use, not merely echoed: an unowned account whose
|
||||
// pre-state is not exactly default cannot be returned with a default
|
||||
// owner (state-machine rule 7), and the authority's own signature
|
||||
// bumps its nonce, so echoing it works once and never again.
|
||||
AccountPostState::new_claimed_if_default(authority.account, Claim::Authorized),
|
||||
],
|
||||
)
|
||||
.write();
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
use cross_zone_marker_core::inbox_source_marker_account_id;
|
||||
use lee_core::{
|
||||
account::{Account, AccountWithMetadata},
|
||||
program::{AccountPostState, Claim, ProgramInput, ProgramOutput, read_lee_inputs},
|
||||
program::{
|
||||
AccountPostState, Claim, DEFAULT_PROGRAM_ID, ProgramInput, ProgramOutput, read_lee_inputs,
|
||||
},
|
||||
};
|
||||
use wrapped_token_core::{
|
||||
Instruction, MAX_MINT_AMOUNT, WrappedTokenConfig, balance_bytes, config_account_id,
|
||||
@@ -158,6 +160,16 @@ fn renounce_authority(
|
||||
authority.account_id, expected,
|
||||
"second account must be the configured authority"
|
||||
);
|
||||
// The program claims this account, and a claim cannot rescue a first use: the
|
||||
// state machine checks post states before it applies claims, so an unowned
|
||||
// account that is not exactly default is refused and, since renouncing is
|
||||
// refused for the same reason, the wrapped-token source list would be frozen for the
|
||||
// life of the zone. Say so here rather than let it surface as a rule number.
|
||||
assert!(
|
||||
authority.account == Account::default()
|
||||
|| authority.account.program_owner != DEFAULT_PROGRAM_ID,
|
||||
"the authority account must be untouched before its first use as one"
|
||||
);
|
||||
assert!(
|
||||
authority.is_authorized,
|
||||
"the configured authority must authorize renouncing it"
|
||||
@@ -177,7 +189,11 @@ fn renounce_authority(
|
||||
vec![config, authority.clone()],
|
||||
vec![
|
||||
AccountPostState::new(config_account),
|
||||
AccountPostState::new(authority.account),
|
||||
// Claimed on first use, not merely echoed: an unowned account whose
|
||||
// pre-state is not exactly default cannot be returned with a default
|
||||
// owner (state-machine rule 7), and the authority's own signature
|
||||
// bumps its nonce, so echoing it works once and never again.
|
||||
AccountPostState::new_claimed_if_default(authority.account, Claim::Authorized),
|
||||
],
|
||||
)
|
||||
.write();
|
||||
@@ -222,6 +238,16 @@ fn update_sources(
|
||||
);
|
||||
// Authorized rather than merely named, so a PDA held by a governance program
|
||||
// works through the same delegation any signer would use.
|
||||
// The program claims this account, and a claim cannot rescue a first use: the
|
||||
// state machine checks post states before it applies claims, so an unowned
|
||||
// account that is not exactly default is refused and, since renouncing is
|
||||
// refused for the same reason, the wrapped-token source list would be frozen for the
|
||||
// life of the zone. Say so here rather than let it surface as a rule number.
|
||||
assert!(
|
||||
authority.account == Account::default()
|
||||
|| authority.account.program_owner != DEFAULT_PROGRAM_ID,
|
||||
"the authority account must be untouched before its first use as one"
|
||||
);
|
||||
assert!(
|
||||
authority.is_authorized,
|
||||
"the configured authority must authorize a source change"
|
||||
@@ -241,7 +267,11 @@ fn update_sources(
|
||||
vec![config, authority.clone()],
|
||||
vec![
|
||||
AccountPostState::new(config_account),
|
||||
AccountPostState::new(authority.account),
|
||||
// Claimed on first use, not merely echoed: an unowned account whose
|
||||
// pre-state is not exactly default cannot be returned with a default
|
||||
// owner (state-machine rule 7), and the authority's own signature
|
||||
// bumps its nonce, so echoing it works once and never again.
|
||||
AccountPostState::new_claimed_if_default(authority.account, Claim::Authorized),
|
||||
],
|
||||
)
|
||||
.write();
|
||||
|
||||
Reference in New Issue
Block a user