Skip to content

Commit 7511fa0

Browse files
authored
Merge pull request #197 from Tyler7x/fix/105-lock-sweep-destination
feat(#105): lock sweep destination to one address per account
2 parents 9b864c3 + aa192ba commit 7511fa0

6 files changed

Lines changed: 252 additions & 269 deletions

File tree

contracts/ephemeral_account/src/errors.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,5 @@ pub enum Error {
1919
DuplicateAsset = 13,
2020
TooManyPayments = 14,
2121
PaymentBelowMinimum = 15,
22+
SweepDestinationLocked = 16,
2223
}

contracts/ephemeral_account/src/lib.rs

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -127,16 +127,20 @@ impl EphemeralAccountContract {
127127
Ok(())
128128
}
129129

130-
/// Execute sweep to destination wallet
131-
/// Transfers all funds from all assets to the specified destination atomically
130+
/// Execute sweep to destination wallet.
131+
///
132+
/// On the first successful call the `destination` is persisted as the locked
133+
/// sweep destination for this account. Any subsequent call with a different
134+
/// destination is rejected with [`Error::SweepDestinationLocked`].
132135
///
133136
/// # Arguments
134137
/// * `destination` - Recipient wallet address
135138
/// * `auth_signature` - Authorization signature from off-chain system
136139
///
137140
/// # Errors
138-
/// Returns Error::Unauthorized if authorization fails
139-
/// Returns Error::AlreadySwept if sweep already executed
141+
/// * [`Error::AlreadySwept`] — sweep has already been executed
142+
/// * [`Error::SweepDestinationLocked`] — destination differs from the locked address
143+
/// * [`Error::Unauthorized`] — authorization check failed
140144
pub fn sweep(env: Env, destination: Address, auth_signature: BytesN<64>) -> Result<(), Error> {
141145
// Check initialized
142146
if !storage::is_initialized(&env) {
@@ -155,6 +159,16 @@ impl EphemeralAccountContract {
155159
return Err(Error::AccountExpired);
156160
}
157161
// Verify authorization signature
162+
// Note: In production, implement proper signature verification
163+
// For MVP, we trust the SDK to only call with valid signatures
164+
// Enforce single-destination lock.
165+
if let Some(locked) = storage::get_sweep_destination(&env) {
166+
if locked != destination {
167+
return Err(Error::SweepDestinationLocked);
168+
}
169+
} else {
170+
storage::set_sweep_destination(&env, &destination);
171+
}
158172
Self::verify_sweep_authorization(&env, &destination, &auth_signature)?;
159173
// Get all payments
160174
let payments = storage::get_all_payments(&env);

contracts/ephemeral_account/src/storage.rs

Lines changed: 15 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ pub enum DataKey {
1111
Payments,
1212
Status,
1313
SweptTo,
14+
/// Locked destination set on the first successful sweep call.
15+
/// Any subsequent sweep attempt with a different address is rejected.
16+
SweepDestination,
1417
BaseReserveRemaining,
1518
AvailableReserve,
1619
ReserveReclaimed,
@@ -29,7 +32,6 @@ pub enum DataKey {
2932
pub fn is_initialized(env: &Env) -> bool {
3033
env.storage().instance().has(&DataKey::Initialized)
3134
}
32-
3335
pub fn set_initialized(env: &Env, value: bool) {
3436
env.storage().instance().set(&DataKey::Initialized, &value);
3537
}
@@ -38,7 +40,6 @@ pub fn set_initialized(env: &Env, value: bool) {
3840
pub fn set_creator(env: &Env, creator: &Address) {
3941
env.storage().instance().set(&DataKey::Creator, creator);
4042
}
41-
4243
pub fn get_creator(env: &Env) -> Address {
4344
env.storage().instance().get(&DataKey::Creator).unwrap()
4445
}
@@ -49,7 +50,6 @@ pub fn set_expiry_ledger(env: &Env, ledger: u32) {
4950
.instance()
5051
.set(&DataKey::ExpiryLedger, &ledger);
5152
}
52-
5353
pub fn get_expiry_ledger(env: &Env) -> u32 {
5454
env.storage()
5555
.instance()
@@ -63,7 +63,6 @@ pub fn set_recovery_address(env: &Env, address: &Address) {
6363
.instance()
6464
.set(&DataKey::RecoveryAddress, address);
6565
}
66-
6766
pub fn get_recovery_address(env: &Env) -> Address {
6867
env.storage()
6968
.instance()
@@ -75,33 +74,27 @@ pub fn get_recovery_address(env: &Env) -> Address {
7574
pub fn has_payments(env: &Env) -> bool {
7675
env.storage().instance().has(&DataKey::Payments)
7776
}
78-
7977
pub fn get_all_payments(env: &Env) -> Map<Address, Payment> {
8078
env.storage()
8179
.instance()
8280
.get(&DataKey::Payments)
8381
.unwrap_or_else(|| Map::new(env))
8482
}
85-
8683
pub fn set_all_payments(env: &Env, payments: &Map<Address, Payment>) {
8784
env.storage().instance().set(&DataKey::Payments, payments);
8885
}
89-
9086
pub fn add_payment(env: &Env, payment: Payment) {
9187
let mut payments = get_all_payments(env);
9288
payments.set(payment.asset.clone(), payment);
9389
set_all_payments(env, &payments);
9490
}
95-
9691
pub fn get_payment(env: &Env, asset: &Address) -> Option<Payment> {
9792
let payments = get_all_payments(env);
9893
payments.get(asset.clone())
9994
}
100-
10195
pub fn get_total_payments(env: &Env) -> u32 {
10296
get_all_payments(env).len()
10397
}
104-
10598
pub fn has_payment_received(env: &Env) -> bool {
10699
has_payments(env)
107100
}
@@ -110,7 +103,6 @@ pub fn has_payment_received(env: &Env) -> bool {
110103
pub fn set_status(env: &Env, status: AccountStatus) {
111104
env.storage().instance().set(&DataKey::Status, &status);
112105
}
113-
114106
pub fn get_status(env: &Env) -> AccountStatus {
115107
env.storage()
116108
.instance()
@@ -122,11 +114,20 @@ pub fn get_status(env: &Env) -> AccountStatus {
122114
pub fn set_swept_to(env: &Env, address: &Address) {
123115
env.storage().instance().set(&DataKey::SweptTo, address);
124116
}
125-
126117
pub fn get_swept_to(env: &Env) -> Option<Address> {
127118
env.storage().instance().get(&DataKey::SweptTo)
128119
}
129120

121+
// Locked sweep destination (set once; immutable after first sweep)
122+
pub fn set_sweep_destination(env: &Env, address: &Address) {
123+
env.storage()
124+
.instance()
125+
.set(&DataKey::SweepDestination, address);
126+
}
127+
pub fn get_sweep_destination(env: &Env) -> Option<Address> {
128+
env.storage().instance().get(&DataKey::SweepDestination)
129+
}
130+
130131
// Reserve lifecycle
131132
pub fn init_reserve_tracking(env: &Env, base_reserve: i128) {
132133
set_base_reserve_remaining(env, base_reserve);
@@ -135,78 +136,66 @@ pub fn init_reserve_tracking(env: &Env, base_reserve: i128) {
135136
set_last_sweep_id(env, 0);
136137
set_reserve_event_count(env, 0);
137138
}
138-
139139
pub fn set_base_reserve_remaining(env: &Env, amount: i128) {
140140
env.storage()
141141
.instance()
142142
.set(&DataKey::BaseReserveRemaining, &amount);
143143
}
144-
145144
pub fn get_base_reserve_remaining(env: &Env) -> i128 {
146145
env.storage()
147146
.instance()
148147
.get(&DataKey::BaseReserveRemaining)
149148
.unwrap_or(0)
150149
}
151-
152150
pub fn set_available_reserve(env: &Env, amount: i128) {
153151
env.storage()
154152
.instance()
155153
.set(&DataKey::AvailableReserve, &amount);
156154
}
157-
158155
pub fn get_available_reserve(env: &Env) -> i128 {
159156
env.storage()
160157
.instance()
161158
.get(&DataKey::AvailableReserve)
162159
.unwrap_or(0)
163160
}
164-
165161
pub fn set_reserve_reclaimed(env: &Env, reclaimed: bool) {
166162
env.storage()
167163
.instance()
168164
.set(&DataKey::ReserveReclaimed, &reclaimed);
169165
}
170-
171166
pub fn is_reserve_reclaimed(env: &Env) -> bool {
172167
env.storage()
173168
.instance()
174169
.get(&DataKey::ReserveReclaimed)
175170
.unwrap_or(false)
176171
}
177-
178172
pub fn set_last_sweep_id(env: &Env, sweep_id: u64) {
179173
env.storage()
180174
.instance()
181175
.set(&DataKey::LastSweepId, &sweep_id);
182176
}
183-
184177
pub fn get_last_sweep_id(env: &Env) -> u64 {
185178
env.storage()
186179
.instance()
187180
.get(&DataKey::LastSweepId)
188181
.unwrap_or(0)
189182
}
190-
191183
pub fn set_reserve_event_count(env: &Env, count: u32) {
192184
env.storage()
193185
.instance()
194186
.set(&DataKey::ReserveEventCount, &count);
195187
}
196-
197188
pub fn get_reserve_event_count(env: &Env) -> u32 {
198189
env.storage()
199190
.instance()
200191
.get(&DataKey::ReserveEventCount)
201192
.unwrap_or(0)
202193
}
203-
204194
pub fn set_last_reserve_event(env: &Env, event: &ReserveReclaimed) {
205195
env.storage()
206196
.instance()
207197
.set(&DataKey::LastReserveEvent, event);
208198
}
209-
210199
pub fn get_last_reserve_event(env: &Env) -> Option<ReserveReclaimed> {
211200
env.storage().instance().get(&DataKey::LastReserveEvent)
212201
}
@@ -217,7 +206,6 @@ pub fn set_contract_version(env: &Env, version: u32) {
217206
.instance()
218207
.set(&DataKey::ContractVersion, &version);
219208
}
220-
221209
pub fn get_contract_version(env: &Env) -> u32 {
222210
env.storage()
223211
.instance()
@@ -245,9 +233,8 @@ pub fn set_authorized_signer(env: &Env, signer: &BytesN<32>) {
245233
.instance()
246234
.set(&DataKey::AuthorizedSigner, signer);
247235
}
248-
249-
pub fn get_authorized_signer(env: &Env) -> Option<BytesN<32>> {
250-
env.storage().instance().get(&DataKey::AuthorizedSigner)
236+
pub fn get_authorized_controller(env: &Env) -> Option<Address> {
237+
env.storage().instance().get(&DataKey::AuthorizedController)
251238
}
252239

253240
// Relayer — the only address authorized to call record_payment()

0 commit comments

Comments
 (0)