@layerzerolabs/protocol-stellar-v2 0.2.60 → 0.2.61

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,10 +2,9 @@ use super::*;
2
2
  use crate::{Sender, TransactionAuthData};
3
3
  use soroban_sdk::{
4
4
  address_payload::AddressPayload,
5
- assert_with_error,
6
5
  auth::{Context, CustomAccountInterface},
7
6
  crypto::Hash,
8
- Bytes, Symbol,
7
+ vec, Symbol,
9
8
  };
10
9
 
11
10
  // ============================================================================
@@ -24,12 +23,9 @@ impl CustomAccountInterface for LzDVN {
24
23
  auth_data: Self::Signature,
25
24
  auth_contexts: Vec<Context>,
26
25
  ) -> Result<(), Self::Error> {
27
- let TransactionAuthData { vid, expiration, signatures, sender, preimage } = auth_data;
26
+ let TransactionAuthData { vid, expiration, signatures, sender } = auth_data;
28
27
 
29
- // 1. Verify preimage matches signature_payload
30
- let invocation = Self::verify_preimage(&env, &signature_payload, &preimage);
31
-
32
- // 2. Check VID and expiration
28
+ // 1. Check VID and expiration
33
29
  if vid != Self::vid(&env) {
34
30
  return Err(DvnError::InvalidVid);
35
31
  }
@@ -37,14 +33,24 @@ impl CustomAccountInterface for LzDVN {
37
33
  return Err(DvnError::AuthDataExpired);
38
34
  }
39
35
 
40
- // 3. Admin verification (`set_admin` bypasses this - allows quorum-only admin changes)
41
- let requires_admin = !Self::is_invoking_worker_set_admin(&env, &auth_contexts);
42
- if requires_admin {
36
+ // 2. Extract and validate calls from auth contexts
37
+ let (calls, is_set_admin) = match auth_contexts.len() {
38
+ 1 => {
39
+ let call = Self::extract_single_self_call(&env, &auth_contexts)?;
40
+ let is_set_admin = call.func == Symbol::new(&env, "set_admin");
41
+ (vec![&env, call], is_set_admin)
42
+ }
43
+ 3 => (Self::extract_upgrade_calls(&env, &auth_contexts)?, false),
44
+ _ => return Err(DvnError::InvalidAuthContext),
45
+ };
46
+
47
+ // 3. Admin verification (set_admin bypasses)
48
+ if !is_set_admin {
43
49
  Self::verify_admin_signature(&env, &sender, &signature_payload)?;
44
50
  }
45
51
 
46
52
  // 4. Replay protection
47
- let hash = Self::hash_call_data(&env, vid, expiration, &invocation);
53
+ let hash = Self::hash_call_data(&env, vid, expiration, &calls);
48
54
  if DvnStorage::used_hash(&env, &hash) {
49
55
  return Err(DvnError::HashAlreadyUsed);
50
56
  }
@@ -62,17 +68,6 @@ impl CustomAccountInterface for LzDVN {
62
68
  // ============================================================================
63
69
 
64
70
  impl LzDVN {
65
- /// Verifies the preimage matches the host-provided `signature_payload`
66
- /// and returns the invocation bytes extracted from it.
67
- fn verify_preimage(env: &Env, signature_payload: &Hash<32>, preimage: &Bytes) -> Bytes {
68
- let computed = env.crypto().sha256(preimage);
69
- assert_with_error!(env, computed.to_bytes() == signature_payload.to_bytes(), DvnError::InvalidPreimage);
70
-
71
- // Skip the 48-byte fixed prefix:
72
- // envelope_type (4) + network_id (32) + nonce (8) + expiry_ledger (4) = 48
73
- preimage.slice(48..)
74
- }
75
-
76
71
  /// Verifies that the sender is an admin with a valid signature.
77
72
  ///
78
73
  /// # Errors
@@ -92,14 +87,56 @@ impl LzDVN {
92
87
  Ok(())
93
88
  }
94
89
 
95
- /// Checks if the first auth context is a call to `set_admin` on this contract.
90
+ /// Extracts a single self-targeting contract call from auth_contexts.
91
+ fn extract_single_self_call(env: &Env, auth_contexts: &Vec<Context>) -> Result<Call, DvnError> {
92
+ let Context::Contract(ctx) = auth_contexts.get(0).unwrap() else {
93
+ return Err(DvnError::NonContractInvoke);
94
+ };
95
+ if ctx.contract != env.current_contract_address() {
96
+ return Err(DvnError::InvalidAuthContext);
97
+ }
98
+ Ok(Call { to: ctx.contract, func: ctx.fn_name, args: ctx.args })
99
+ }
100
+
101
+ /// Extracts and validates upgrade auth contexts (3 entries, positional).
96
102
  ///
97
- /// Used to allow quorum-only admin changes without requiring existing admin authorization.
98
- fn is_invoking_worker_set_admin(env: &Env, auth_contexts: &Vec<Context>) -> bool {
99
- auth_contexts.len() == 1
100
- && auth_contexts.first().is_some_and(|ctx| {
101
- let Context::Contract(c) = ctx else { return false };
102
- c.contract == env.current_contract_address() && c.fn_name == Symbol::new(env, "set_admin")
103
- })
103
+ /// Expected order:
104
+ /// - `[0]`: Upgrader contract call (must target the registered upgrader)
105
+ /// - `[1]`: `upgrade` self-call
106
+ /// - `[2]`: `migrate` self-call
107
+ fn extract_upgrade_calls(env: &Env, auth_contexts: &Vec<Context>) -> Result<Vec<Call>, DvnError> {
108
+ let self_addr = env.current_contract_address();
109
+ let upgrader_addr = Self::upgrader(env).ok_or(DvnError::UpgraderNotSet)?;
110
+
111
+ // [0]: Upgrader contract call
112
+ let Context::Contract(ctx0) = auth_contexts.get(0).unwrap() else {
113
+ return Err(DvnError::NonContractInvoke);
114
+ };
115
+ if ctx0.contract != upgrader_addr {
116
+ return Err(DvnError::InvalidUpgradeContext);
117
+ }
118
+
119
+ // [1]: upgrade self-call
120
+ let Context::Contract(ctx1) = auth_contexts.get(1).unwrap() else {
121
+ return Err(DvnError::NonContractInvoke);
122
+ };
123
+ if ctx1.contract != self_addr || ctx1.fn_name != Symbol::new(env, "upgrade") {
124
+ return Err(DvnError::InvalidUpgradeContext);
125
+ }
126
+
127
+ // [2]: migrate self-call
128
+ let Context::Contract(ctx2) = auth_contexts.get(2).unwrap() else {
129
+ return Err(DvnError::NonContractInvoke);
130
+ };
131
+ if ctx2.contract != self_addr || ctx2.fn_name != Symbol::new(env, "migrate") {
132
+ return Err(DvnError::InvalidUpgradeContext);
133
+ }
134
+
135
+ Ok(vec![
136
+ env,
137
+ Call { to: ctx0.contract, func: ctx0.fn_name, args: ctx0.args },
138
+ Call { to: ctx1.contract, func: ctx1.fn_name, args: ctx1.args },
139
+ Call { to: ctx2.contract, func: ctx2.fn_name, args: ctx2.args },
140
+ ])
104
141
  }
105
142
  }
@@ -5,12 +5,17 @@
5
5
  //! signatures for authorization and implements Soroban's custom account
6
6
  //! interface for transaction signing.
7
7
 
8
- use crate::{errors::DvnError, events::SetDstConfig, storage::DvnStorage, DstConfig, DstConfigParam, IDVN};
9
- use common_macros::{contract_impl, lz_contract};
8
+ use crate::{
9
+ errors::DvnError,
10
+ events::{SetDstConfig, SetUpgrader},
11
+ storage::DvnStorage,
12
+ Call, DstConfig, DstConfigParam, IDVN,
13
+ };
14
+ use common_macros::{contract_impl, lz_contract, only_auth};
10
15
  use endpoint_v2::FeeRecipient;
11
16
  use fee_lib_interfaces::{DvnFeeLibClient, DvnFeeParams};
12
17
  use message_lib_common::interfaces::ILayerZeroDVN;
13
- use soroban_sdk::{Address, Bytes, BytesN, Env, Vec};
18
+ use soroban_sdk::{xdr::ToXdr, Address, Bytes, BytesN, Env, Val, Vec};
14
19
  use utils::{buffer_writer::BufferWriter, multisig, option_ext::OptionExt};
15
20
  use worker::{
16
21
  assert_acl, assert_not_paused, assert_supported_message_lib, init_worker, require_admin_auth, set_admin_by_admin,
@@ -88,6 +93,26 @@ impl LzDVN {
88
93
 
89
94
  #[contract_impl]
90
95
  impl IDVN for LzDVN {
96
+ /// Dispatches external contract calls.
97
+ #[only_auth]
98
+ fn execute_transaction(env: &Env, calls: &Vec<Call>) {
99
+ for call in calls.iter() {
100
+ env.invoke_contract::<Val>(&call.to, &call.func, call.args);
101
+ }
102
+ }
103
+
104
+ /// Sets the upgrader contract address. Requires self authorization.
105
+ #[only_auth]
106
+ fn set_upgrader(env: &Env, upgrader: &Option<Address>) {
107
+ DvnStorage::set_or_remove_upgrader(env, upgrader);
108
+ SetUpgrader { upgrader: upgrader.clone() }.publish(env);
109
+ }
110
+
111
+ /// Returns the current upgrader contract address, if set.
112
+ fn upgrader(env: &Env) -> Option<Address> {
113
+ DvnStorage::upgrader(env)
114
+ }
115
+
91
116
  /// Sets destination chain configurations. Requires admin authorization.
92
117
  fn set_dst_config(env: &Env, admin: &Address, params: &Vec<DstConfigParam>) {
93
118
  require_admin_auth::<Self>(env, admin);
@@ -107,12 +132,12 @@ impl IDVN for LzDVN {
107
132
  DvnStorage::vid(env).unwrap()
108
133
  }
109
134
 
110
- /// Computes the hash of invocation data for multisig signing.
135
+ /// Computes the hash of call data for multisig signing.
111
136
  ///
112
137
  /// Off-chain signers use this to compute the hash they need to sign.
113
- fn hash_call_data(env: &Env, vid: u32, expiration: u64, invocation: &Bytes) -> BytesN<32> {
138
+ fn hash_call_data(env: &Env, vid: u32, expiration: u64, calls: &Vec<Call>) -> BytesN<32> {
114
139
  let mut writer = BufferWriter::new(env);
115
- let data = writer.write_u32(vid).write_u64(expiration).write_bytes(invocation).to_bytes();
140
+ let data = writer.write_u32(vid).write_u64(expiration).write_bytes(&calls.to_xdr(env)).to_bytes();
116
141
  env.crypto().keccak256(&data).into()
117
142
  }
118
143
  }
@@ -5,7 +5,10 @@ pub enum DvnError {
5
5
  AuthDataExpired,
6
6
  EidNotSupported,
7
7
  HashAlreadyUsed,
8
- InvalidPreimage,
8
+ InvalidAuthContext,
9
+ InvalidUpgradeContext,
9
10
  InvalidVid,
11
+ NonContractInvoke,
10
12
  OnlyAdmin,
13
+ UpgraderNotSet,
11
14
  }
@@ -1,8 +1,14 @@
1
1
  use crate::DstConfigParam;
2
- use soroban_sdk::{contractevent, Vec};
2
+ use soroban_sdk::{contractevent, Address, Vec};
3
3
 
4
4
  #[contractevent]
5
5
  #[derive(Clone, Debug, Eq, PartialEq)]
6
6
  pub struct SetDstConfig {
7
7
  pub params: Vec<DstConfigParam>,
8
8
  }
9
+
10
+ #[contractevent]
11
+ #[derive(Clone, Debug, Eq, PartialEq)]
12
+ pub struct SetUpgrader {
13
+ pub upgrader: Option<Address>,
14
+ }
@@ -1,5 +1,5 @@
1
1
  use message_lib_common::interfaces::ILayerZeroDVN;
2
- use soroban_sdk::{auth::CustomAccountInterface, contractclient, contracttype, Address, Bytes, BytesN, Env, Vec};
2
+ use soroban_sdk::{auth::CustomAccountInterface, contractclient, contracttype, Address, BytesN, Env, Symbol, Val, Vec};
3
3
  use utils::multisig::MultiSig;
4
4
  use worker::Worker;
5
5
 
@@ -32,9 +32,6 @@ pub struct TransactionAuthData {
32
32
  pub signatures: Vec<BytesN<65>>,
33
33
  /// Entity submitting the transaction (admin, or permissionless).
34
34
  pub sender: Sender,
35
- /// XDR-encoded `HashIdPreimage::SorobanAuthorization` from the auth entry.
36
- /// Layout: envelope_type (4) || network_id (32) || nonce (8) || expiry_ledger (4) || invocation (variable).
37
- pub preimage: Bytes,
38
35
  }
39
36
 
40
37
  // ============================================================================
@@ -66,12 +63,32 @@ pub struct DstConfigParam {
66
63
  pub config: DstConfig,
67
64
  }
68
65
 
66
+ /// Represents a single contract invocation for multisig authorization.
67
+ ///
68
+ /// Used in `hash_call_data` to compute the hash that signers sign over.
69
+ #[contracttype]
70
+ #[derive(Clone, Debug, Eq, PartialEq)]
71
+ pub struct Call {
72
+ /// Target contract address.
73
+ pub to: Address,
74
+ /// Function name to invoke.
75
+ pub func: Symbol,
76
+ /// Function arguments.
77
+ pub args: Vec<Val>,
78
+ }
79
+
69
80
  /// DVN (Decentralized Verifier Network) contract interface.
70
81
  ///
71
82
  /// Extends the LayerZero DVN interface with destination configuration management
72
83
  /// and multisig capabilities for secure cross-chain message verification.
73
84
  #[contractclient(name = "DVNClient")]
74
85
  pub trait IDVN: ILayerZeroDVN + Worker + MultiSig + CustomAccountInterface {
86
+ /// Dispatches a list of external contract calls.
87
+ ///
88
+ /// # Arguments
89
+ /// * `calls` - List of calls to execute atomically
90
+ fn execute_transaction(env: &Env, calls: &Vec<Call>);
91
+
75
92
  /// Sets the configuration for one or more destination chains.
76
93
  ///
77
94
  /// # Arguments
@@ -93,17 +110,26 @@ pub trait IDVN: ILayerZeroDVN + Worker + MultiSig + CustomAccountInterface {
93
110
  /// The VID is a unique identifier used in multisig authentication.
94
111
  fn vid(env: &Env) -> u32;
95
112
 
96
- /// Computes the hash of invocation data for multisig signing.
113
+ /// Sets or clears the registered upgrader contract address.
114
+ ///
115
+ /// Protected by `#[only_auth]` (multisig quorum required).
116
+ /// Pass `None` to remove the upgrader.
117
+ fn set_upgrader(env: &Env, upgrader: &Option<Address>);
118
+
119
+ /// Returns the registered upgrader contract address, if any.
120
+ fn upgrader(env: &Env) -> Option<Address>;
121
+
122
+ /// Computes the hash of call data for multisig signing.
97
123
  ///
98
124
  /// Off-chain signers use this to compute the hash they need to sign.
99
- /// The hash includes the VID, expiration, and the raw invocation XDR.
125
+ /// The hash includes the VID, expiration, and the calls being authorized.
100
126
  ///
101
127
  /// # Arguments
102
128
  /// * `vid` - Verifier ID (must match contract's VID)
103
129
  /// * `expiration` - Expiration timestamp for the authorization
104
- /// * `invocation` - XDR-encoded `SorobanAuthorizedInvocation` from the auth entry
130
+ /// * `calls` - The contract calls being authorized
105
131
  ///
106
132
  /// # Returns
107
133
  /// A 32-byte keccak256 hash to be signed by the multisig quorum
108
- fn hash_call_data(env: &Env, vid: u32, expiration: u64, invocation: &Bytes) -> BytesN<32>;
134
+ fn hash_call_data(env: &Env, vid: u32, expiration: u64, calls: &Vec<Call>) -> BytesN<32>;
109
135
  }
@@ -1,6 +1,6 @@
1
1
  use crate::DstConfig;
2
2
  use common_macros::storage;
3
- use soroban_sdk::BytesN;
3
+ use soroban_sdk::{Address, BytesN};
4
4
 
5
5
  /// DVN contract storage keys.
6
6
  ///
@@ -11,6 +11,10 @@ pub enum DvnStorage {
11
11
  #[instance(u32)]
12
12
  Vid,
13
13
 
14
+ /// Registered upgrader contract address for upgrade operations.
15
+ #[instance(Address)]
16
+ Upgrader,
17
+
14
18
  /// Destination chain configuration, keyed by endpoint ID.
15
19
  #[persistent(DstConfig)]
16
20
  DstConfig { dst_eid: u32 },