-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathtrade_escrow.rs
More file actions
330 lines (270 loc) · 9.91 KB
/
trade_escrow.rs
File metadata and controls
330 lines (270 loc) · 9.91 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
// Arbitrum Pulse: Trade Escrow
// Purpose: Secure B2B trade payments for Ethiopian exporters
// Author: Umojaverse (Griffins Oduol)
// License: MIT
#![no_std]
use stylus_sdk::{
alloy_primitives::{Address, U256},
msg, prelude::*,
stylus_proc::*,
};
// Trade escrow contract storage
#[derive(StorageField)]
struct TradeEscrowStorage {
// Maps trade ID to Trade struct
#[selector(0x0)]
trades: StorageMap<U256, Trade>,
// Current trade counter
#[selector(0x1)]
trade_counter: StorageU256,
// Platform fee percentage (in basis points, e.g., 25 = 0.25%)
#[selector(0x2)]
fee_basis_points: StorageU256,
// Platform admin address
#[selector(0x3)]
admin: StorageAddress,
// Platform fee collector address
#[selector(0x4)]
fee_collector: StorageAddress,
}
// Trade struct to store each transaction
#[derive(PartialEq, Clone)]
struct Trade {
// Exporter's address
exporter: Address,
// Importer's address
importer: Address,
// Amount in escrow
amount: U256,
// Status of the trade (0=created, 1=funded, 2=delivered, 3=completed, 4=refunded, 5=disputed)
status: u8,
// Trade description (e.g., "10 bags of Ethiopian coffee")
description_hash: [u8; 32],
// Trade creation timestamp
created_at: U256,
// Trade expiration timestamp
expires_at: U256,
}
// Main contract implementation
#[external]
impl TradeEscrow {
// Initialize the escrow contract with admin and fee settings
#[payable(false)]
pub fn initialize(&mut self, fee_basis_points: U256, fee_collector: Address) -> Result<(), Vec<u8>> {
// Ensure contract is being initialized
if self.admin.get() != Address::ZERO {
return Err(b"Already initialized".to_vec());
}
// Set admin as the deployer
self.admin.set(msg::sender());
// Set the fee basis points (limit to max 1000 basis points = 10%)
if fee_basis_points > U256::from(1000u32) {
return Err(b"Fee too high".to_vec());
}
self.fee_basis_points.set(fee_basis_points);
// Set fee collector address
self.fee_collector.set(fee_collector);
// Initialize trade counter
self.trade_counter.set(U256::ZERO);
Ok(())
}
// Create a new trade escrow
#[payable(false)]
pub fn create_trade(
&mut self,
importer: Address,
description_hash: [u8; 32],
expiry_duration: U256,
) -> Result<U256, Vec<u8>> {
// Get exporter address (trade creator)
let exporter = msg::sender();
// Ensure not sending to self
if exporter == importer {
return Err(b"Cannot trade with self".to_vec());
}
// Get current trade ID and increment counter
let trade_id = self.trade_counter.get();
self.trade_counter.set(trade_id + U256::from(1u32));
// Calculate expiration timestamp
let current_time = block_timestamp();
let expires_at = current_time + expiry_duration;
// Create new trade
let trade = Trade {
exporter,
importer,
amount: U256::ZERO,
status: 0, // Created
description_hash,
created_at: current_time,
expires_at,
};
// Save trade in storage
self.trades.insert(trade_id, trade);
// Return the trade ID
Ok(trade_id)
}
// Fund a trade by the importer
#[payable(true)]
pub fn fund_trade(&mut self, trade_id: U256) -> Result<(), Vec<u8>> {
// Get trade from storage
let mut trade = self.get_trade(trade_id)?;
// Ensure sender is the importer
if msg::sender() != trade.importer {
return Err(b"Not the importer".to_vec());
}
// Ensure trade is in created status
if trade.status != 0 {
return Err(b"Invalid trade status".to_vec());
}
// Ensure not expired
let current_time = block_timestamp();
if current_time > trade.expires_at {
return Err(b"Trade expired".to_vec());
}
// Set trade amount and update status
trade.amount = msg::value();
trade.status = 1; // Funded
// Update trade in storage
self.trades.insert(trade_id, trade);
Ok(())
}
// Confirm delivery by the importer, releasing funds to exporter
#[payable(false)]
pub fn confirm_delivery(&mut self, trade_id: U256) -> Result<(), Vec<u8>> {
// Get trade from storage
let mut trade = self.get_trade(trade_id)?;
// Ensure sender is the importer
if msg::sender() != trade.importer {
return Err(b"Not the importer".to_vec());
}
// Ensure trade is in funded status
if trade.status != 1 {
return Err(b"Trade not funded".to_vec());
}
// Update trade status
trade.status = 3; // Completed
// Calculate fee
let fee_basis_points = self.fee_basis_points.get();
let fee = (trade.amount * fee_basis_points) / U256::from(10000u32);
let payment_amount = trade.amount - fee;
// Update trade in storage
self.trades.insert(trade_id, trade.clone());
// Transfer fee to fee collector if fee is non-zero
if fee > U256::ZERO {
let fee_collector = self.fee_collector.get();
if !fee_collector.transfer(fee) {
return Err(b"Fee transfer failed".to_vec());
}
}
// Transfer payment to exporter
if !trade.exporter.transfer(payment_amount) {
return Err(b"Payment transfer failed".to_vec());
}
Ok(())
}
// Refund if delivery not confirmed before expiry
#[payable(false)]
pub fn claim_refund(&mut self, trade_id: U256) -> Result<(), Vec<u8>> {
// Get trade from storage
let mut trade = self.get_trade(trade_id)?;
// Ensure trade is in funded status
if trade.status != 1 {
return Err(b"Trade not funded".to_vec());
}
// Ensure trade has expired
let current_time = block_timestamp();
if current_time <= trade.expires_at {
return Err(b"Trade not expired".to_vec());
}
// Update trade status
trade.status = 4; // Refunded
// Update trade in storage
self.trades.insert(trade_id, trade.clone());
// Transfer full amount back to importer
if !trade.importer.transfer(trade.amount) {
return Err(b"Refund transfer failed".to_vec());
}
Ok(())
}
// Mark trade as disputed, only admin can resolve
#[payable(false)]
pub fn dispute_trade(&mut self, trade_id: U256) -> Result<(), Vec<u8>> {
// Get trade from storage
let mut trade = self.get_trade(trade_id)?;
// Ensure sender is either importer or exporter
let sender = msg::sender();
if sender != trade.importer && sender != trade.exporter {
return Err(b"Not a trade party".to_vec());
}
// Ensure trade is in funded status
if trade.status != 1 {
return Err(b"Trade not funded".to_vec());
}
// Update trade status
trade.status = 5; // Disputed
// Update trade in storage
self.trades.insert(trade_id, trade);
Ok(())
}
// Admin resolves dispute by deciding where funds go
#[payable(false)]
pub fn resolve_dispute(
&mut self,
trade_id: U256,
exporter_percent: U256,
) -> Result<(), Vec<u8>> {
// Ensure sender is admin
if msg::sender() != self.admin.get() {
return Err(b"Not admin".to_vec());
}
// Get trade from storage
let mut trade = self.get_trade(trade_id)?;
// Ensure trade is in disputed status
if trade.status != 5 {
return Err(b"Trade not disputed".to_vec());
}
// Ensure exporter_percent is valid (0-100%)
if exporter_percent > U256::from(100u32) {
return Err(b"Invalid percentage".to_vec());
}
// Calculate amounts
let exporter_amount = (trade.amount * exporter_percent) / U256::from(100u32);
let importer_amount = trade.amount - exporter_amount;
// Update trade status
trade.status = 3; // Completed
// Update trade in storage
self.trades.insert(trade_id, trade.clone());
// Transfer funds according to resolution
if exporter_amount > U256::ZERO {
if !trade.exporter.transfer(exporter_amount) {
return Err(b"Exporter transfer failed".to_vec());
}
}
if importer_amount > U256::ZERO {
if !trade.importer.transfer(importer_amount) {
return Err(b"Importer transfer failed".to_vec());
}
}
Ok(())
}
// Get trade details by ID
#[payable(false)]
pub fn get_trade(&self, trade_id: U256) -> Result<Trade, Vec<u8>> {
let trade = self.trades.get(trade_id);
// Ensure trade exists (check if exporter is non-zero)
if trade.exporter == Address::ZERO {
return Err(b"Trade not found".to_vec());
}
Ok(trade)
}
// Get trade status by ID
#[payable(false)]
pub fn get_trade_status(&self, trade_id: U256) -> Result<u8, Vec<u8>> {
let trade = self.get_trade(trade_id)?;
Ok(trade.status)
}
}
// Helper function to get current block timestamp
fn block_timestamp() -> U256 {
U256::from(stylus_sdk::block::timestamp())
}