@layerzerolabs/oapp-stellar-contracts 0.2.122

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.
@@ -0,0 +1,246 @@
1
+ use crate::{
2
+ self as oapp,
3
+ errors::OAppError,
4
+ oapp_core::OAppCore,
5
+ oapp_options_type3::{EnforcedOptionParam, EnforcedOptionSet},
6
+ oapp_receiver::{LzReceiveInternal, OAppReceiver},
7
+ };
8
+ use common_macros::contract_impl;
9
+ use soroban_sdk::{
10
+ contract, contractimpl,
11
+ testutils::{Address as _, MockAuth, MockAuthInvoke},
12
+ vec, Address, Bytes, BytesN, Env, IntoVal,
13
+ };
14
+ use utils::testing_utils::assert_eq_event;
15
+
16
+ const OPTION_TYPE_3: u32 = 3;
17
+ const REMOTE_EID_1: u32 = 100;
18
+ const REMOTE_EID_2: u32 = 200;
19
+ const MSG_TYPE_SEND: u32 = 1;
20
+ const MSG_TYPE_RECEIVE: u32 = 2;
21
+
22
+ #[contract]
23
+ pub struct DummyEndpoint;
24
+
25
+ #[contractimpl]
26
+ impl DummyEndpoint {
27
+ pub fn set_delegate(_env: Env, _oapp: &Address, _delegate: &Option<Address>) {
28
+ // do nothing
29
+ }
30
+ }
31
+
32
+ #[oapp_macros::oapp(custom = [core, sender, receiver])]
33
+ #[common_macros::lz_contract]
34
+ pub struct DummyOAppOptionsType3;
35
+
36
+ #[contract_impl(contracttrait)]
37
+ impl utils::rbac::RoleBasedAccessControl for DummyOAppOptionsType3 {}
38
+
39
+ #[contract_impl(contracttrait)]
40
+ impl OAppCore for DummyOAppOptionsType3 {}
41
+
42
+ impl LzReceiveInternal for DummyOAppOptionsType3 {
43
+ fn __lz_receive(
44
+ _env: &Env,
45
+ _origin: &endpoint_v2::Origin,
46
+ _guid: &BytesN<32>,
47
+ _message: &Bytes,
48
+ _extra_data: &Bytes,
49
+ _executor: &Address,
50
+ _value: i128,
51
+ ) {
52
+ // Dummy implementation for testing
53
+ }
54
+ }
55
+
56
+ #[contract_impl(contracttrait)]
57
+ impl OAppReceiver for DummyOAppOptionsType3 {}
58
+
59
+ #[contract_impl]
60
+ impl DummyOAppOptionsType3 {
61
+ pub fn __constructor(env: &Env, owner: &Address, endpoint: &Address, delegate: &Address) {
62
+ oapp::oapp_core::init_ownable_oapp::<Self>(env, owner, endpoint, delegate);
63
+ }
64
+ }
65
+
66
+ struct TestSetup<'a> {
67
+ env: Env,
68
+ owner: Address,
69
+ oapp_client: DummyOAppOptionsType3Client<'a>,
70
+ }
71
+
72
+ fn setup<'a>() -> TestSetup<'a> {
73
+ let env = Env::default();
74
+
75
+ let owner = Address::generate(&env);
76
+ let endpoint = env.register(DummyEndpoint, ());
77
+ let delegate = owner.clone();
78
+ let oapp = env.register(DummyOAppOptionsType3, (&owner, &endpoint, &delegate));
79
+ let oapp_client = DummyOAppOptionsType3Client::new(&env, &oapp);
80
+
81
+ TestSetup { env, owner, oapp_client }
82
+ }
83
+
84
+ fn create_valid_options(env: &Env, data: &[u8]) -> Bytes {
85
+ let mut buffer = Bytes::from_array(env, &(OPTION_TYPE_3 as u16).to_be_bytes());
86
+ buffer.extend_from_slice(data);
87
+ buffer
88
+ }
89
+
90
+ fn set_enforced_options_with_auth(
91
+ env: &Env,
92
+ signer: &Address,
93
+ oapp_client: &DummyOAppOptionsType3Client<'_>,
94
+ enforced_params: &soroban_sdk::Vec<EnforcedOptionParam>,
95
+ ) {
96
+ env.mock_auths(&[MockAuth {
97
+ address: signer,
98
+ invoke: &MockAuthInvoke {
99
+ contract: &oapp_client.address,
100
+ fn_name: "set_enforced_options",
101
+ args: (enforced_params, signer).into_val(env),
102
+ sub_invokes: &[],
103
+ },
104
+ }]);
105
+ oapp_client.set_enforced_options(enforced_params, signer);
106
+ }
107
+
108
+ #[test]
109
+ fn test_enforced_options_lifecycle() {
110
+ let TestSetup { env, owner, oapp_client, .. } = setup();
111
+
112
+ // Unset returns None
113
+ assert_eq!(oapp_client.enforced_options(&999, &999), None);
114
+
115
+ // Set enforced options for different eid/msg_type combinations
116
+ let enforced1 = create_valid_options(&env, &[1, 2, 3, 4]);
117
+ let enforced2 = create_valid_options(&env, &[5, 6, 7, 8]);
118
+ let enforced_params = vec![
119
+ &env,
120
+ EnforcedOptionParam { eid: REMOTE_EID_1, msg_type: MSG_TYPE_SEND, options: Some(enforced1.clone()) },
121
+ EnforcedOptionParam { eid: REMOTE_EID_2, msg_type: MSG_TYPE_RECEIVE, options: Some(enforced2.clone()) },
122
+ ];
123
+ set_enforced_options_with_auth(&env, &owner, &oapp_client, &enforced_params);
124
+
125
+ // assert events
126
+ assert_eq_event(&env, &oapp_client.address, EnforcedOptionSet { enforced_options: enforced_params.clone() });
127
+
128
+ // Verify options were set correctly
129
+ assert_eq!(oapp_client.enforced_options(&REMOTE_EID_1, &MSG_TYPE_SEND), Some(enforced1.clone()));
130
+ assert_eq!(oapp_client.enforced_options(&REMOTE_EID_2, &MSG_TYPE_RECEIVE), Some(enforced2.clone()));
131
+
132
+ // Update enforced options for one combination
133
+ let updated = create_valid_options(&env, &[9, 8, 7, 6, 5]);
134
+ let update_params =
135
+ vec![&env, EnforcedOptionParam { eid: REMOTE_EID_1, msg_type: MSG_TYPE_SEND, options: Some(updated.clone()) }];
136
+ set_enforced_options_with_auth(&env, &owner, &oapp_client, &update_params);
137
+ assert_eq_event(&env, &oapp_client.address, EnforcedOptionSet { enforced_options: update_params.clone() });
138
+ assert_eq!(oapp_client.enforced_options(&REMOTE_EID_1, &MSG_TYPE_SEND), Some(updated));
139
+ }
140
+
141
+ #[test]
142
+ fn test_combine_options() {
143
+ let TestSetup { env, owner, oapp_client, .. } = setup();
144
+
145
+ // combine_options: no enforced -> returns extra (including empty)
146
+ let extra_only = create_valid_options(&env, &[9, 10, 11]);
147
+ assert_eq!(oapp_client.combine_options(&REMOTE_EID_1, &MSG_TYPE_SEND, &extra_only), extra_only);
148
+
149
+ let empty_extra = Bytes::new(&env);
150
+ assert_eq!(oapp_client.combine_options(&REMOTE_EID_1, &MSG_TYPE_SEND, &empty_extra), empty_extra);
151
+
152
+ // Set enforced options for the combine tests
153
+ let enforced = create_valid_options(&env, &[1, 2, 3, 4]);
154
+ let enforced_params =
155
+ vec![&env, EnforcedOptionParam { eid: REMOTE_EID_1, msg_type: MSG_TYPE_SEND, options: Some(enforced.clone()) }];
156
+ set_enforced_options_with_auth(&env, &owner, &oapp_client, &enforced_params);
157
+
158
+ // combine_options: enforced present -> empty extra returns enforced
159
+ assert_eq!(oapp_client.combine_options(&REMOTE_EID_1, &MSG_TYPE_SEND, &Bytes::new(&env)), enforced.clone());
160
+
161
+ // combine_options: both present -> enforced + extra(without its 2-byte header)
162
+ let extra: Bytes = create_valid_options(&env, &[4, 5, 6]);
163
+ let combined = oapp_client.combine_options(&REMOTE_EID_1, &MSG_TYPE_SEND, &extra);
164
+ let expected_combined = create_valid_options(&env, &[1, 2, 3, 4, 4, 5, 6]);
165
+ assert_eq!(combined, expected_combined);
166
+ }
167
+
168
+ #[test]
169
+ #[should_panic(expected = "HostError: Error(Auth, InvalidAction)")]
170
+ fn test_set_enforced_options_unauthorized() {
171
+ let TestSetup { env, owner, oapp_client, .. } = setup();
172
+
173
+ let options = create_valid_options(&env, &[1, 2, 3, 4]);
174
+ let enforced_params =
175
+ vec![&env, EnforcedOptionParam { eid: REMOTE_EID_1, msg_type: MSG_TYPE_SEND, options: Some(options.clone()) }];
176
+ oapp_client.set_enforced_options(&enforced_params, &owner);
177
+ }
178
+
179
+ #[test]
180
+ #[should_panic(expected = "Error(Contract, #1086)")] // RbacError::Unauthorized
181
+ fn test_set_enforced_options_non_owner_authorized() {
182
+ let TestSetup { env, owner, oapp_client, .. } = setup();
183
+ let non_owner = Address::generate(&env);
184
+ assert!(non_owner != owner);
185
+
186
+ let options = create_valid_options(&env, &[1, 2, 3, 4]);
187
+ let enforced_params =
188
+ vec![&env, EnforcedOptionParam { eid: REMOTE_EID_1, msg_type: MSG_TYPE_SEND, options: Some(options.clone()) }];
189
+
190
+ set_enforced_options_with_auth(&env, &non_owner, &oapp_client, &enforced_params);
191
+ }
192
+
193
+ #[test]
194
+ fn test_set_enforced_options_invalid_options_returns_error() {
195
+ let TestSetup { env, owner, oapp_client, .. } = setup();
196
+
197
+ // wrong option type (not 3)
198
+ let mut invalid = Bytes::from_array(&env, &(4u16).to_be_bytes());
199
+ invalid.extend_from_slice(&[1, 2, 3]);
200
+
201
+ let enforced_params =
202
+ vec![&env, EnforcedOptionParam { eid: REMOTE_EID_1, msg_type: MSG_TYPE_SEND, options: Some(invalid) }];
203
+
204
+ env.mock_auths(&[MockAuth {
205
+ address: &owner,
206
+ invoke: &MockAuthInvoke {
207
+ contract: &oapp_client.address,
208
+ fn_name: "set_enforced_options",
209
+ args: (&enforced_params, &owner).into_val(&env),
210
+ sub_invokes: &[],
211
+ },
212
+ }]);
213
+ let result = oapp_client.try_set_enforced_options(&enforced_params, &owner);
214
+ assert_eq!(result.err().unwrap().ok().unwrap(), OAppError::InvalidOptions.into());
215
+ }
216
+
217
+ #[test]
218
+ fn test_combine_options_extra_invalid_type_returns_error_when_enforced_present() {
219
+ let TestSetup { env, owner, oapp_client, .. } = setup();
220
+
221
+ let enforced = create_valid_options(&env, &[1, 2, 3]);
222
+ let params =
223
+ vec![&env, EnforcedOptionParam { eid: REMOTE_EID_1, msg_type: MSG_TYPE_SEND, options: Some(enforced) }];
224
+ set_enforced_options_with_auth(&env, &owner, &oapp_client, &params);
225
+
226
+ // extra has wrong option type (not 3) but len >= 2 -> validated and should panic
227
+ let mut extra_invalid = Bytes::from_array(&env, &(4u16).to_be_bytes());
228
+ extra_invalid.extend_from_slice(&[9, 9, 9]);
229
+ let result = oapp_client.try_combine_options(&REMOTE_EID_1, &MSG_TYPE_SEND, &extra_invalid);
230
+ assert_eq!(result.err().unwrap().ok().unwrap(), OAppError::InvalidOptions.into());
231
+ }
232
+
233
+ #[test]
234
+ fn test_combine_options_extra_too_short_returns_error_when_enforced_present() {
235
+ let TestSetup { env, owner, oapp_client, .. } = setup();
236
+
237
+ let enforced = create_valid_options(&env, &[1, 2, 3]);
238
+ let params =
239
+ vec![&env, EnforcedOptionParam { eid: REMOTE_EID_1, msg_type: MSG_TYPE_SEND, options: Some(enforced) }];
240
+ set_enforced_options_with_auth(&env, &owner, &oapp_client, &params);
241
+
242
+ // extra is non-empty but len < 2 -> should panic
243
+ let extra_too_short = Bytes::from_array(&env, &[1u8]);
244
+ let result = oapp_client.try_combine_options(&REMOTE_EID_1, &MSG_TYPE_SEND, &extra_too_short);
245
+ assert_eq!(result.err().unwrap().ok().unwrap(), OAppError::InvalidOptions.into());
246
+ }
@@ -0,0 +1,383 @@
1
+ use crate::{self as oapp, errors::OAppError, oapp_receiver::LzReceiveInternal};
2
+ use endpoint_v2::Origin;
3
+ use soroban_sdk::{
4
+ contract, contractimpl, symbol_short,
5
+ testutils::{Address as _, MockAuth, MockAuthInvoke},
6
+ token::StellarAssetClient,
7
+ Address, Bytes, BytesN, Env, IntoVal,
8
+ };
9
+
10
+ #[oapp_macros::oapp]
11
+ #[common_macros::lz_contract]
12
+ pub struct DummyOAppReceiver;
13
+
14
+ impl LzReceiveInternal for DummyOAppReceiver {
15
+ fn __lz_receive(
16
+ _env: &Env,
17
+ _origin: &Origin,
18
+ _guid: &BytesN<32>,
19
+ _message: &Bytes,
20
+ _extra_data: &Bytes,
21
+ _executor: &Address,
22
+ _value: i128,
23
+ ) {
24
+ // do nothing
25
+ }
26
+ }
27
+
28
+ #[contractimpl]
29
+ impl DummyOAppReceiver {
30
+ pub fn __constructor(env: &Env, owner: &Address, endpoint: &Address) {
31
+ oapp::oapp_core::init_ownable_oapp::<Self>(env, owner, endpoint, owner);
32
+ }
33
+ }
34
+
35
+ const REMOTE_EID: u32 = 100;
36
+ const UNSET_EID: u32 = 999;
37
+
38
+ // Mock Endpoint (minimal subset used by oapp_receiver::verify_and_clear_payload)
39
+
40
+ #[contract]
41
+ pub struct MockEndpoint;
42
+
43
+ #[contractimpl]
44
+ impl MockEndpoint {
45
+ pub fn __constructor(env: Env, native_token: Address) {
46
+ env.storage().instance().set(&symbol_short!("ntk"), &native_token);
47
+ }
48
+
49
+ pub fn set_delegate(_env: Env, _oapp: &Address, _delegate: &Option<Address>) {
50
+ // do nothing
51
+ }
52
+
53
+ pub fn native_token(env: Env) -> Address {
54
+ env.storage().instance().get(&symbol_short!("ntk")).unwrap()
55
+ }
56
+
57
+ pub fn clear(env: Env, caller: Address, origin: Origin, receiver: Address, guid: BytesN<32>, message: Bytes) {
58
+ // Record last clear for assertions
59
+ env.storage().instance().set(&symbol_short!("clr_c"), &caller);
60
+ env.storage().instance().set(&symbol_short!("clr_o"), &origin);
61
+ env.storage().instance().set(&symbol_short!("clr_r"), &receiver);
62
+ env.storage().instance().set(&symbol_short!("clr_g"), &guid);
63
+ env.storage().instance().set(&symbol_short!("clr_m"), &message);
64
+ }
65
+
66
+ pub fn last_clear(env: Env) -> (Address, Origin, Address, BytesN<32>, Bytes) {
67
+ (
68
+ env.storage().instance().get(&symbol_short!("clr_c")).unwrap(),
69
+ env.storage().instance().get(&symbol_short!("clr_o")).unwrap(),
70
+ env.storage().instance().get(&symbol_short!("clr_r")).unwrap(),
71
+ env.storage().instance().get(&symbol_short!("clr_g")).unwrap(),
72
+ env.storage().instance().get(&symbol_short!("clr_m")).unwrap(),
73
+ )
74
+ }
75
+ }
76
+
77
+ struct TestSetup<'a> {
78
+ env: Env,
79
+ owner: Address,
80
+ endpoint: Address,
81
+ token_admin: Address,
82
+ native_token: Address,
83
+ native_token_admin_client: StellarAssetClient<'a>,
84
+ oapp_client: DummyOAppReceiverClient<'a>,
85
+ }
86
+
87
+ fn setup<'a>() -> TestSetup<'a> {
88
+ let env = Env::default();
89
+
90
+ let owner = Address::generate(&env);
91
+ let token_admin = Address::generate(&env);
92
+ let native_token_sac = env.register_stellar_asset_contract_v2(token_admin.clone());
93
+ let native_token = native_token_sac.address();
94
+ let native_token_admin_client = StellarAssetClient::new(&env, &native_token);
95
+
96
+ let endpoint = env.register(MockEndpoint, (&native_token,));
97
+ let oapp = env.register(DummyOAppReceiver, (&owner, &endpoint));
98
+ let oapp_client = DummyOAppReceiverClient::new(&env, &oapp);
99
+
100
+ TestSetup { env, owner, endpoint, token_admin, native_token, native_token_admin_client, oapp_client }
101
+ }
102
+
103
+ fn set_peer(env: &Env, owner: &Address, oapp_client: &DummyOAppReceiverClient<'_>, eid: u32, peer: &BytesN<32>) {
104
+ let peer_option = Some(peer.clone());
105
+ env.mock_auths(&[MockAuth {
106
+ address: owner,
107
+ invoke: &MockAuthInvoke {
108
+ contract: &oapp_client.address,
109
+ fn_name: "set_peer",
110
+ args: (&eid, &peer_option, owner).into_val(env),
111
+ sub_invokes: &[],
112
+ },
113
+ }]);
114
+ oapp_client.set_peer(&eid, &peer_option, owner);
115
+ }
116
+
117
+ fn lz_receive(
118
+ env: &Env,
119
+ oapp_client: &DummyOAppReceiverClient<'_>,
120
+ executor: &Address,
121
+ origin: &Origin,
122
+ guid: &BytesN<32>,
123
+ message: &Bytes,
124
+ extra_data: &Bytes,
125
+ value: i128,
126
+ sub_invokes: &[MockAuthInvoke<'_>],
127
+ ) {
128
+ env.mock_auths(&[MockAuth {
129
+ address: executor,
130
+ invoke: &MockAuthInvoke {
131
+ contract: &oapp_client.address,
132
+ fn_name: "lz_receive",
133
+ args: (executor, origin, guid, message, extra_data, value).into_val(env),
134
+ sub_invokes,
135
+ },
136
+ }]);
137
+ oapp_client.lz_receive(executor, origin, guid, message, extra_data, &value);
138
+ }
139
+
140
+ #[test]
141
+ fn test_is_compose_msg_sender() {
142
+ let TestSetup { env, oapp_client, .. } = setup();
143
+
144
+ let origin = Origin { src_eid: REMOTE_EID, sender: BytesN::from_array(&env, &[1; 32]), nonce: 1 };
145
+ let message = Bytes::from_array(&env, &[1, 2, 3, 4]);
146
+
147
+ // Test with same contract address (should return true)
148
+ let result = oapp_client.is_compose_msg_sender(&origin, &message, &oapp_client.address);
149
+ assert_eq!(result, true);
150
+ let different_sender = Address::generate(&env);
151
+
152
+ // Test with different sender address (should return false)
153
+ assert_eq!(oapp_client.is_compose_msg_sender(&origin, &message, &different_sender), false);
154
+ }
155
+
156
+ #[test]
157
+ fn test_allow_initialize_path_cases() {
158
+ let TestSetup { env, owner, oapp_client, .. } = setup();
159
+
160
+ // no peer set -> false
161
+ let origin_no_peer = Origin { src_eid: UNSET_EID, sender: BytesN::from_array(&env, &[5; 32]), nonce: 1 };
162
+ assert_eq!(oapp_client.allow_initialize_path(&origin_no_peer), false);
163
+
164
+ // set peer for REMOTE_EID
165
+ let peer_bytes: BytesN<32> = BytesN::from_array(&env, &[5; 32]);
166
+ set_peer(&env, &owner, &oapp_client, REMOTE_EID, &peer_bytes);
167
+
168
+ // matching -> true
169
+ let origin_match = Origin { src_eid: REMOTE_EID, sender: peer_bytes.clone(), nonce: 1 };
170
+ assert_eq!(oapp_client.allow_initialize_path(&origin_match), true);
171
+
172
+ // non-matching -> false
173
+ let origin_non_match = Origin { src_eid: REMOTE_EID, sender: BytesN::from_array(&env, &[6; 32]), nonce: 1 };
174
+ assert_eq!(oapp_client.allow_initialize_path(&origin_non_match), false);
175
+ }
176
+
177
+ #[test]
178
+ fn test_lz_receive_verifies_peer_and_calls_clear_value_zero() {
179
+ let TestSetup { env, owner, endpoint, oapp_client, .. } = setup();
180
+
181
+ // Configure peer
182
+ let peer: BytesN<32> = BytesN::from_array(&env, &[7; 32]);
183
+ set_peer(&env, &owner, &oapp_client, REMOTE_EID, &peer);
184
+
185
+ let executor = Address::generate(&env);
186
+ let origin = Origin { src_eid: REMOTE_EID, sender: peer, nonce: 1 };
187
+ let guid = BytesN::from_array(&env, &[2u8; 32]);
188
+ let message = Bytes::from_array(&env, &[1, 2, 3]);
189
+ let extra_data = Bytes::new(&env);
190
+ lz_receive(&env, &oapp_client, &executor, &origin, &guid, &message, &extra_data, 0, &[]);
191
+
192
+ let endpoint_client = MockEndpointClient::new(&env, &endpoint);
193
+ let (caller, cleared_origin, receiver, cleared_guid, cleared_message) = endpoint_client.last_clear();
194
+ assert_eq!(caller, oapp_client.address);
195
+ assert_eq!(cleared_origin, origin);
196
+ assert_eq!(receiver, oapp_client.address);
197
+ assert_eq!(cleared_guid, guid);
198
+ assert_eq!(cleared_message, message);
199
+ }
200
+
201
+ #[test]
202
+ fn test_lz_receive_transfers_native_token_when_value_positive() {
203
+ let TestSetup { env, owner, endpoint, native_token, native_token_admin_client, token_admin, oapp_client, .. } =
204
+ setup();
205
+
206
+ // Configure peer
207
+ let peer: BytesN<32> = BytesN::from_array(&env, &[9; 32]);
208
+ set_peer(&env, &owner, &oapp_client, REMOTE_EID, &peer);
209
+
210
+ let executor = Address::generate(&env);
211
+ let origin = Origin { src_eid: REMOTE_EID, sender: peer, nonce: 1 };
212
+ let guid = BytesN::from_array(&env, &[3u8; 32]);
213
+ let message = Bytes::from_array(&env, &[4, 5, 6]);
214
+ let extra_data = Bytes::new(&env);
215
+ let value: i128 = 123;
216
+
217
+ // Mint native token to executor (admin auth)
218
+ env.mock_auths(&[MockAuth {
219
+ address: &token_admin,
220
+ invoke: &MockAuthInvoke {
221
+ contract: &native_token,
222
+ fn_name: "mint",
223
+ args: (&executor, &value).into_val(&env),
224
+ sub_invokes: &[],
225
+ },
226
+ }]);
227
+ native_token_admin_client.mint(&executor, &value);
228
+
229
+ // lz_receive should transfer tokens from executor -> oapp
230
+ let transfer_invoke = MockAuthInvoke {
231
+ contract: &native_token,
232
+ fn_name: "transfer",
233
+ args: (&executor, &oapp_client.address, &value).into_val(&env),
234
+ sub_invokes: &[],
235
+ };
236
+ let sub_invokes = [transfer_invoke];
237
+ lz_receive(&env, &oapp_client, &executor, &origin, &guid, &message, &extra_data, value, &sub_invokes);
238
+
239
+ let endpoint_client = MockEndpointClient::new(&env, &endpoint);
240
+ // Asserts clear() was called (last_clear() will panic if not)
241
+ endpoint_client.last_clear();
242
+
243
+ let token_client = soroban_sdk::token::TokenClient::new(&env, &native_token);
244
+ assert_eq!(token_client.balance(&executor), 0);
245
+ assert_eq!(token_client.balance(&oapp_client.address), value);
246
+ }
247
+
248
+ #[test]
249
+ fn test_lz_receive_rejects_negative_value_and_does_not_clear() {
250
+ let TestSetup { env, owner, endpoint, native_token, native_token_admin_client, token_admin, oapp_client, .. } =
251
+ setup();
252
+
253
+ // Configure peer
254
+ let peer: BytesN<32> = BytesN::from_array(&env, &[9; 32]);
255
+ set_peer(&env, &owner, &oapp_client, REMOTE_EID, &peer);
256
+
257
+ let executor = Address::generate(&env);
258
+ let origin = Origin { src_eid: REMOTE_EID, sender: peer, nonce: 1 };
259
+ let guid = BytesN::from_array(&env, &[3u8; 32]);
260
+ let message = Bytes::from_array(&env, &[4, 5, 6]);
261
+ let extra_data = Bytes::new(&env);
262
+ let value: i128 = -123;
263
+
264
+ // Fund executor with a positive balance so the only failure is the negative amount.
265
+ env.mock_auths(&[MockAuth {
266
+ address: &token_admin,
267
+ invoke: &MockAuthInvoke {
268
+ contract: &native_token,
269
+ fn_name: "mint",
270
+ args: (&executor, &(-value)).into_val(&env),
271
+ sub_invokes: &[],
272
+ },
273
+ }]);
274
+ native_token_admin_client.mint(&executor, &(-value));
275
+
276
+ // lz_receive should attempt transfer with a negative value and fail.
277
+ let transfer_invoke = MockAuthInvoke {
278
+ contract: &native_token,
279
+ fn_name: "transfer",
280
+ args: (&executor, &oapp_client.address, &value).into_val(&env),
281
+ sub_invokes: &[],
282
+ };
283
+ let sub_invokes = [transfer_invoke];
284
+ env.mock_auths(&[MockAuth {
285
+ address: &executor,
286
+ invoke: &MockAuthInvoke {
287
+ contract: &oapp_client.address,
288
+ fn_name: "lz_receive",
289
+ args: (&executor, &origin, &guid, &message, &extra_data, &value).into_val(&env),
290
+ sub_invokes: &sub_invokes,
291
+ },
292
+ }]);
293
+
294
+ let result = oapp_client.try_lz_receive(&executor, &origin, &guid, &message, &extra_data, &value);
295
+ assert_eq!(result.err().unwrap().ok().unwrap(), soroban_sdk::Error::from_contract_error(8)); //negative amount is not allowed
296
+
297
+ // No balances changed.
298
+ let token_client = soroban_sdk::token::TokenClient::new(&env, &native_token);
299
+ assert_eq!(token_client.balance(&executor), -value);
300
+ assert_eq!(token_client.balance(&oapp_client.address), 0);
301
+
302
+ // Payload should not have been cleared.
303
+ let endpoint_client = MockEndpointClient::new(&env, &endpoint);
304
+ assert!(endpoint_client.try_last_clear().is_err());
305
+ }
306
+
307
+ #[test]
308
+ fn test_next_nonce_defaults_to_zero() {
309
+ let TestSetup { env, oapp_client, .. } = setup();
310
+ let sender = BytesN::from_array(&env, &[1u8; 32]);
311
+ assert_eq!(oapp_client.next_nonce(&REMOTE_EID, &sender), 0);
312
+ }
313
+
314
+ #[test]
315
+ #[should_panic(expected = "HostError: Error(Auth, InvalidAction)")]
316
+ fn test_lz_receive_requires_executor_auth() {
317
+ let TestSetup { env, owner, oapp_client, .. } = setup();
318
+
319
+ let peer: BytesN<32> = BytesN::from_array(&env, &[7; 32]);
320
+ set_peer(&env, &owner, &oapp_client, REMOTE_EID, &peer);
321
+
322
+ // Call without executor auth mocked -> should fail at executor.require_auth()
323
+ let executor = Address::generate(&env);
324
+ let origin = Origin { src_eid: REMOTE_EID, sender: peer, nonce: 1 };
325
+ let guid = BytesN::from_array(&env, &[2u8; 32]);
326
+ let message = Bytes::from_array(&env, &[1, 2, 3]);
327
+ let extra_data = Bytes::new(&env);
328
+ oapp_client.lz_receive(&executor, &origin, &guid, &message, &extra_data, &0);
329
+ }
330
+
331
+ #[test]
332
+ fn test_lz_receive_wrong_peer_returns_only_peer_error() {
333
+ let TestSetup { env, owner, oapp_client, .. } = setup();
334
+
335
+ let configured_peer: BytesN<32> = BytesN::from_array(&env, &[7; 32]);
336
+ set_peer(&env, &owner, &oapp_client, REMOTE_EID, &configured_peer);
337
+
338
+ let executor = Address::generate(&env);
339
+ let wrong_sender: BytesN<32> = BytesN::from_array(&env, &[8; 32]);
340
+ let origin = Origin { src_eid: REMOTE_EID, sender: wrong_sender, nonce: 1 };
341
+ let guid = BytesN::from_array(&env, &[2u8; 32]);
342
+ let message = Bytes::from_array(&env, &[1, 2, 3]);
343
+ let extra_data = Bytes::new(&env);
344
+ let value: i128 = 0;
345
+
346
+ env.mock_auths(&[MockAuth {
347
+ address: &executor,
348
+ invoke: &MockAuthInvoke {
349
+ contract: &oapp_client.address,
350
+ fn_name: "lz_receive",
351
+ args: (&executor, &origin, &guid, &message, &extra_data, &value).into_val(&env),
352
+ sub_invokes: &[],
353
+ },
354
+ }]);
355
+ let result = oapp_client.try_lz_receive(&executor, &origin, &guid, &message, &extra_data, &value);
356
+ assert_eq!(result.err().unwrap().ok().unwrap(), OAppError::OnlyPeer.into());
357
+ }
358
+
359
+ #[test]
360
+ fn test_lz_receive_no_peer_returns_no_peer_error() {
361
+ let TestSetup { env, oapp_client, .. } = setup();
362
+
363
+ let executor = Address::generate(&env);
364
+ let origin = Origin { src_eid: REMOTE_EID, sender: BytesN::from_array(&env, &[1; 32]), nonce: 1 };
365
+ let guid = BytesN::from_array(&env, &[2u8; 32]);
366
+ let message = Bytes::from_array(&env, &[1, 2, 3]);
367
+ let extra_data = Bytes::new(&env);
368
+ let value: i128 = 0;
369
+
370
+ env.mock_auths(&[MockAuth {
371
+ address: &executor,
372
+ invoke: &MockAuthInvoke {
373
+ contract: &oapp_client.address,
374
+ fn_name: "lz_receive",
375
+ args: (&executor, &origin, &guid, &message, &extra_data, &value).into_val(&env),
376
+ sub_invokes: &[],
377
+ },
378
+ }]);
379
+
380
+ // No peer configured -> should fail with NoPeer since no peer is set for the source eid
381
+ let result = oapp_client.try_lz_receive(&executor, &origin, &guid, &message, &extra_data, &value);
382
+ assert_eq!(result.err().unwrap().ok().unwrap(), OAppError::NoPeer.into());
383
+ }