@@ -7,22 +7,18 @@ mod storage;
77#[ cfg( test) ]
88mod test;
99
10- use soroban_sdk:: { contract, contractimpl, xdr:: ToXdr , Address , BytesN , Env , Vec } ;
11-
1210pub use bridgelet_shared:: { AccountInfo , AccountStatus , Payment } ;
1311pub use errors:: Error ;
1412pub use events:: {
1513 AccountCreated , AccountExpired , MultiPaymentReceived , PaymentReceived , ReserveReclaimed ,
1614 SweepExecutedMulti ,
1715} ;
16+ use soroban_sdk:: { contract, contractimpl, Address , BytesN , Env , Vec } ;
1817pub use storage:: DataKey ;
19-
2018const BASE_RESERVE_STROOPS : i128 = 1_000_000_000 ;
2119const CONTRACT_VERSION : u32 = 1 ;
22-
2320#[ contract]
2421pub struct EphemeralAccountContract ;
25-
2622#[ contractimpl]
2723impl EphemeralAccountContract {
2824 /// Initialize the ephemeral account with restrictions
@@ -49,10 +45,8 @@ impl EphemeralAccountContract {
4945 if storage:: is_initialized ( & env) {
5046 return Err ( Error :: AlreadyInitialized ) ;
5147 }
52-
5348 // Verify creator authorization
5449 creator. require_auth ( ) ;
55-
5650 // Validate expiry is in future
5751 let current_ledger = env. ledger ( ) . sequence ( ) ;
5852 if expiry_ledger <= current_ledger {
@@ -76,39 +70,37 @@ impl EphemeralAccountContract {
7670 storage:: set_min_payment_amount ( & env, min_amount) ;
7771 storage:: init_reserve_tracking ( & env, BASE_RESERVE_STROOPS ) ;
7872 storage:: set_contract_version ( & env, CONTRACT_VERSION ) ;
79-
8073 // Emit event
8174 events:: emit_account_created ( & env, creator, expiry_ledger) ;
82-
8375 Ok ( ( ) )
8476 }
8577
86- /// Record an inbound payment to this ephemeral account.
87- /// Only the registered relayer address may call this function.
88- /// Multiple payments with different assets are supported.
78+ /// Record a single inbound payment to this ephemeral account.
79+ ///
80+ /// Each account is restricted to **one** payment. A second call will revert
81+ /// with [`Error::PaymentAlreadyReceived`] regardless of the asset.
8982 ///
9083 /// # Arguments
91- /// * `amount` - Payment amount
92- /// * `asset` - Asset address
84+ /// * `amount` - Payment amount (must be positive)
85+ /// * `asset` - Asset contract address
9386 ///
9487 /// # Errors
95- /// Returns Error::Unauthorized if caller is not the registered relayer
96- /// Returns Error::InvalidAmount if amount is not positive
97- /// Returns Error::DuplicateAsset if asset already has a payment
88+ /// * [` Error::NotInitialized`] – account not yet initialised
89+ /// * [` Error::InvalidAmount`] – amount is not positive
90+ /// * [` Error::PaymentAlreadyReceived`] – a payment was already recorded
9891 pub fn record_payment ( env : Env , amount : i128 , asset : Address ) -> Result < ( ) , Error > {
9992 // Check initialized
10093 if !storage:: is_initialized ( & env) {
10194 return Err ( Error :: NotInitialized ) ;
10295 }
103-
104- // Only the registered relayer may record payments
105- let relayer = storage:: get_relayer ( & env) . ok_or ( Error :: Unauthorized ) ?;
106- relayer. require_auth ( ) ;
107-
10896 // Validate amount
10997 if amount <= 0 {
11098 return Err ( Error :: InvalidAmount ) ;
11199 }
100+ // Enforce single inbound payment restriction
101+ if storage:: has_payment_received ( & env) {
102+ return Err ( Error :: PaymentAlreadyReceived ) ;
103+ }
112104
113105 // Check minimum payment amount
114106 let min_amount = storage:: get_min_payment_amount ( & env) ;
@@ -120,35 +112,18 @@ impl EphemeralAccountContract {
120112 if storage:: get_payment ( & env, & asset) . is_some ( ) {
121113 return Err ( Error :: DuplicateAsset ) ;
122114 }
123-
124- // Check payment limit to prevent gas issues (max 10 assets)
125- let payment_count = storage:: get_total_payments ( & env) ;
126- if payment_count >= 10 {
127- return Err ( Error :: TooManyPayments ) ;
128- }
129-
130115 // Create payment with current timestamp
131116 let payment = Payment {
132117 asset : asset. clone ( ) ,
133118 amount,
134119 timestamp : env. ledger ( ) . timestamp ( ) ,
135120 } ;
136-
137121 // Add payment
138122 storage:: add_payment ( & env, payment) ;
139-
140- // Update status only on first payment
141- if payment_count == 0 {
142- storage:: set_status ( & env, AccountStatus :: PaymentReceived ) ;
143- }
144-
145- // Emit appropriate event
146- if payment_count == 0 {
147- events:: emit_payment_received ( & env, amount, asset) ;
148- } else {
149- events:: emit_multi_payment_received ( & env, asset, amount) ;
150- }
151-
123+ // Update status
124+ storage:: set_status ( & env, AccountStatus :: PaymentReceived ) ;
125+ // Emit event
126+ events:: emit_payment_received ( & env, amount, asset) ;
152127 Ok ( ( ) )
153128 }
154129
@@ -167,32 +142,26 @@ impl EphemeralAccountContract {
167142 if !storage:: is_initialized ( & env) {
168143 return Err ( Error :: NotInitialized ) ;
169144 }
170-
171145 // Check not already swept
172146 if storage:: get_status ( & env) == AccountStatus :: Swept {
173147 return Err ( Error :: AlreadySwept ) ;
174148 }
175-
176149 // Check payment received
177150 if !storage:: has_payment_received ( & env) {
178151 return Err ( Error :: NoPaymentReceived ) ;
179152 }
180-
181153 // Check not expired
182154 if Self :: is_expired ( env. clone ( ) ) {
183155 return Err ( Error :: AccountExpired ) ;
184156 }
185-
186157 // Verify authorization signature
187158 Self :: verify_sweep_authorization ( & env, & destination, & auth_signature) ?;
188-
189159 // Get all payments
190160 let payments = storage:: get_all_payments ( & env) ;
191161 let mut payments_vec = Vec :: new ( & env) ;
192162 for payment in payments. values ( ) {
193163 payments_vec. push_back ( payment) ;
194164 }
195-
196165 // Update status before transfer to prevent reentrancy
197166 storage:: set_status ( & env, AccountStatus :: Swept ) ;
198167 storage:: set_swept_to ( & env, & destination) ;
@@ -201,16 +170,24 @@ impl EphemeralAccountContract {
201170 // This contract enforces authorization/state transitions and reserve lifecycle.
202171 let sweep_id = env. ledger ( ) . sequence ( ) as u64 ;
203172 storage:: set_last_sweep_id ( & env, sweep_id) ;
204-
205173 // Emit sweep event once transfer authorization/state update succeeds.
206174 events:: emit_sweep_executed_multi ( & env, destination. clone ( ) , & payments_vec) ;
207-
208175 // Reclaim base reserve only after successful sweep state transition.
209176 Self :: reclaim_reserve_to ( & env, & destination, sweep_id) ?;
210-
211177 Ok ( ( ) )
212178 }
213179
180+ /// Check if the account has expired.
181+ ///
182+ /// Expiry is determined by comparing the current **ledger sequence number**
183+ /// (`env.ledger().sequence()`) against the `expiry_ledger` set at
184+ /// initialization. Soroban contracts do not have access to wall-clock time
185+ /// (UNIX timestamps) for consensus-safe comparisons; ledger sequence is the
186+ /// canonical on-chain time source. Each ledger closes roughly every 5 s on
187+ /// Stellar mainnet, so `expiry_ledger` effectively encodes a duration in
188+ /// ledger ticks rather than seconds.
189+ ///
190+ /// Returns `false` if the account has not been initialized.
214191 /// Check whether this ephemeral account has expired.
215192 ///
216193 /// ## Ledger time vs wall-clock time
@@ -236,10 +213,8 @@ impl EphemeralAccountContract {
236213 if !storage:: is_initialized ( & env) {
237214 return false ;
238215 }
239-
240216 let expiry_ledger = storage:: get_expiry_ledger ( & env) ;
241217 let current_ledger = env. ledger ( ) . sequence ( ) ;
242-
243218 current_ledger >= expiry_ledger
244219 }
245220
@@ -253,7 +228,6 @@ impl EphemeralAccountContract {
253228 if !storage:: is_initialized ( & env) {
254229 return AccountStatus :: Active ;
255230 }
256-
257231 storage:: get_status ( & env)
258232 }
259233
@@ -267,25 +241,20 @@ impl EphemeralAccountContract {
267241 if !storage:: is_initialized ( & env) {
268242 return Err ( Error :: NotInitialized ) ;
269243 }
270-
271244 // Check not already swept or expired
272245 let status = storage:: get_status ( & env) ;
273246 if status == AccountStatus :: Swept || status == AccountStatus :: Expired {
274247 return Err ( Error :: InvalidStatus ) ;
275248 }
276-
277249 // Check if expired
278250 if !Self :: is_expired ( env. clone ( ) ) {
279251 return Err ( Error :: NotExpired ) ;
280252 }
281-
282253 // Get recovery address
283254 let recovery_address = storage:: get_recovery_address ( & env) ;
284-
285255 // Update status
286256 storage:: set_status ( & env, AccountStatus :: Expired ) ;
287257 storage:: set_swept_to ( & env, & recovery_address) ;
288-
289258 // Get total amount from all payments if any payments were received
290259 let total_amount = if storage:: has_payment_received ( & env) {
291260 let payments = storage:: get_all_payments ( & env) ;
@@ -299,16 +268,12 @@ impl EphemeralAccountContract {
299268 } else {
300269 0
301270 } ;
302-
303271 let sweep_id = env. ledger ( ) . sequence ( ) as u64 ;
304272 storage:: set_last_sweep_id ( & env, sweep_id) ;
305-
306273 // Reclaim reserve to recovery destination.
307274 let reclaimed_reserve = Self :: reclaim_reserve_to ( & env, & recovery_address, sweep_id) ?;
308-
309275 // Emit expiration event with reserve amount reclaimed in this call.
310276 events:: emit_account_expired ( & env, recovery_address, total_amount, reclaimed_reserve) ;
311-
312277 Ok ( ( ) )
313278 }
314279
@@ -318,15 +283,12 @@ impl EphemeralAccountContract {
318283 if !storage:: is_initialized ( & env) {
319284 return Err ( Error :: NotInitialized ) ;
320285 }
321-
322286 let status = storage:: get_status ( & env) ;
323287 if status != AccountStatus :: Swept && status != AccountStatus :: Expired {
324288 return Err ( Error :: InvalidStatus ) ;
325289 }
326-
327290 let destination = storage:: get_swept_to ( & env) . ok_or ( Error :: InvalidStatus ) ?;
328291 let sweep_id = storage:: get_last_sweep_id ( & env) ;
329-
330292 Self :: reclaim_reserve_to ( & env, & destination, sweep_id)
331293 }
332294
@@ -335,7 +297,6 @@ impl EphemeralAccountContract {
335297 if !storage:: is_initialized ( & env) {
336298 return 0 ;
337299 }
338-
339300 storage:: get_base_reserve_remaining ( & env)
340301 }
341302
@@ -344,7 +305,6 @@ impl EphemeralAccountContract {
344305 if !storage:: is_initialized ( & env) {
345306 return 0 ;
346307 }
347-
348308 storage:: get_available_reserve ( & env)
349309 }
350310
@@ -353,7 +313,6 @@ impl EphemeralAccountContract {
353313 if !storage:: is_initialized ( & env) {
354314 return false ;
355315 }
356-
357316 storage:: is_reserve_reclaimed ( & env)
358317 }
359318
@@ -362,7 +321,6 @@ impl EphemeralAccountContract {
362321 if !storage:: is_initialized ( & env) {
363322 return None ;
364323 }
365-
366324 storage:: get_last_reserve_event ( & env)
367325 }
368326
@@ -371,7 +329,6 @@ impl EphemeralAccountContract {
371329 if !storage:: is_initialized ( & env) {
372330 return 0 ;
373331 }
374-
375332 storage:: get_reserve_event_count ( & env)
376333 }
377334
@@ -380,10 +337,8 @@ impl EphemeralAccountContract {
380337 if !storage:: is_initialized ( & env) {
381338 return Err ( Error :: NotInitialized ) ;
382339 }
383-
384340 let payments = storage:: get_all_payments ( & env) ;
385341 let payment_count = payments. len ( ) ;
386-
387342 Ok ( AccountInfo {
388343 creator : storage:: get_creator ( & env) ,
389344 status : storage:: get_status ( & env) ,
@@ -403,13 +358,6 @@ impl EphemeralAccountContract {
403358 }
404359
405360 // Private helper functions
406-
407- /// Verify sweep authorization via Ed25519 signature.
408- ///
409- /// Message = SHA-256(destination_xdr || contract_id_xdr)
410- ///
411- /// The off-chain signer signs this message with the private key
412- /// corresponding to the `authorized_signer` stored at initialization.
413361 fn verify_sweep_authorization (
414362 env : & Env ,
415363 destination : & Address ,
@@ -434,11 +382,9 @@ impl EphemeralAccountContract {
434382 fn reclaim_reserve_to ( env : & Env , destination : & Address , sweep_id : u64 ) -> Result < i128 , Error > {
435383 let reserve_remaining = storage:: get_base_reserve_remaining ( env) ;
436384 let reserve_available = storage:: get_available_reserve ( env) ;
437-
438385 if reserve_remaining < 0 || reserve_available < 0 {
439386 return Err ( Error :: InvalidAmount ) ;
440387 }
441-
442388 if reserve_remaining == 0 {
443389 storage:: set_reserve_reclaimed ( env, true ) ;
444390 let event = ReserveReclaimed {
@@ -451,24 +397,20 @@ impl EphemeralAccountContract {
451397 Self :: emit_and_store_reserve_event ( env, event) ?;
452398 return Ok ( 0 ) ;
453399 }
454-
455400 let reclaim_amount = if reserve_available < reserve_remaining {
456401 reserve_available
457402 } else {
458403 reserve_remaining
459404 } ;
460-
461405 let new_available = reserve_available
462406 . checked_sub ( reclaim_amount)
463407 . ok_or ( Error :: InvalidAmount ) ?;
464408 let new_remaining = reserve_remaining
465409 . checked_sub ( reclaim_amount)
466410 . ok_or ( Error :: InvalidAmount ) ?;
467-
468411 storage:: set_available_reserve ( env, new_available) ;
469412 storage:: set_base_reserve_remaining ( env, new_remaining) ;
470413 storage:: set_reserve_reclaimed ( env, new_remaining == 0 ) ;
471-
472414 let event = ReserveReclaimed {
473415 destination : destination. clone ( ) ,
474416 amount : reclaim_amount,
@@ -477,7 +419,6 @@ impl EphemeralAccountContract {
477419 remaining_reserve : new_remaining,
478420 } ;
479421 Self :: emit_and_store_reserve_event ( env, event) ?;
480-
481422 Ok ( reclaim_amount)
482423 }
483424
@@ -490,12 +431,10 @@ impl EphemeralAccountContract {
490431 event. fully_reclaimed ,
491432 event. remaining_reserve ,
492433 ) ;
493-
494434 let event_count = storage:: get_reserve_event_count ( env) ;
495435 let next_count = event_count. checked_add ( 1 ) . ok_or ( Error :: InvalidAmount ) ?;
496436 storage:: set_last_reserve_event ( env, & event) ;
497437 storage:: set_reserve_event_count ( env, next_count) ;
498-
499438 Ok ( ( ) )
500439 }
501440}
0 commit comments