Skip to content

Commit 501e321

Browse files
authored
Merge pull request #150 from dannyy2000/feat/132-protocol-fees-treasury
feat: implement global protocol fees and treasury loses #132
2 parents 6bd65a0 + fbd26b0 commit 501e321

19 files changed

Lines changed: 10421 additions & 6 deletions

contracts/stream_contract/src/lib.rs

Lines changed: 156 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ use soroban_sdk::{
1010
pub enum DataKey {
1111
Stream(u64),
1212
StreamCounter,
13+
ProtocolConfig,
1314
}
1415

1516
#[derive(Clone, Debug, Eq, PartialEq)]
@@ -26,13 +27,25 @@ pub struct Stream {
2627
pub is_active: bool,
2728
}
2829

30+
#[contracttype]
31+
#[derive(Clone, Debug, Eq, PartialEq)]
32+
pub struct ProtocolConfig {
33+
pub admin: Address,
34+
pub treasury: Address,
35+
pub fee_rate_bps: u32,
36+
}
37+
2938
#[contracterror]
3039
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
3140
pub enum StreamError {
3241
InvalidAmount = 1,
3342
StreamNotFound = 2,
3443
Unauthorized = 3,
3544
StreamInactive = 4,
45+
AlreadyInitialized = 5,
46+
NotAdmin = 6,
47+
InvalidFeeRate = 7,
48+
NotInitialized = 8,
3649
}
3750

3851
#[contracttype]
@@ -73,11 +86,142 @@ pub struct StreamToppedUpEvent {
7386
pub new_deposited_amount: i128,
7487
}
7588

89+
#[contracttype]
90+
#[derive(Clone, Debug, Eq, PartialEq)]
91+
pub struct FeeCollectedEvent {
92+
pub stream_id: u64,
93+
pub treasury: Address,
94+
pub fee_amount: i128,
95+
pub token: Address,
96+
}
97+
7698
#[contract]
7799
pub struct StreamContract;
78100

101+
/// Maximum fee rate: 1000 basis points = 10%
102+
const MAX_FEE_RATE_BPS: u32 = 1_000;
103+
79104
#[contractimpl]
80105
impl StreamContract {
106+
// ─── Admin: Protocol Fee Configuration ───────────────────────────
107+
108+
/// One-time initialization of the protocol fee config.
109+
/// Sets the admin, treasury address, and fee rate (in basis points).
110+
pub fn initialize(
111+
env: Env,
112+
admin: Address,
113+
treasury: Address,
114+
fee_rate_bps: u32,
115+
) -> Result<(), StreamError> {
116+
admin.require_auth();
117+
118+
if env
119+
.storage()
120+
.instance()
121+
.has(&DataKey::ProtocolConfig)
122+
{
123+
return Err(StreamError::AlreadyInitialized);
124+
}
125+
126+
if fee_rate_bps > MAX_FEE_RATE_BPS {
127+
return Err(StreamError::InvalidFeeRate);
128+
}
129+
130+
let config = ProtocolConfig {
131+
admin,
132+
treasury,
133+
fee_rate_bps,
134+
};
135+
env.storage()
136+
.instance()
137+
.set(&DataKey::ProtocolConfig, &config);
138+
139+
Ok(())
140+
}
141+
142+
/// Update the treasury address and/or fee rate. Admin-only.
143+
pub fn update_fee_config(
144+
env: Env,
145+
admin: Address,
146+
treasury: Address,
147+
fee_rate_bps: u32,
148+
) -> Result<(), StreamError> {
149+
admin.require_auth();
150+
151+
let config: ProtocolConfig = env
152+
.storage()
153+
.instance()
154+
.get(&DataKey::ProtocolConfig)
155+
.ok_or(StreamError::NotInitialized)?;
156+
157+
if config.admin != admin {
158+
return Err(StreamError::NotAdmin);
159+
}
160+
161+
if fee_rate_bps > MAX_FEE_RATE_BPS {
162+
return Err(StreamError::InvalidFeeRate);
163+
}
164+
165+
let new_config = ProtocolConfig {
166+
admin: config.admin,
167+
treasury,
168+
fee_rate_bps,
169+
};
170+
env.storage()
171+
.instance()
172+
.set(&DataKey::ProtocolConfig, &new_config);
173+
174+
Ok(())
175+
}
176+
177+
/// Read the current protocol fee configuration (returns None if not initialized).
178+
pub fn get_fee_config(env: Env) -> Option<ProtocolConfig> {
179+
env.storage()
180+
.instance()
181+
.get(&DataKey::ProtocolConfig)
182+
}
183+
184+
// ─── Fee Collection ──────────────────────────────────────────────
185+
186+
/// Deducts protocol fee from `amount` and transfers it to the treasury.
187+
/// Returns the net amount (amount - fee). If no config or fee is 0, returns `amount` unchanged.
188+
fn collect_fee(
189+
env: &Env,
190+
token_address: &Address,
191+
amount: i128,
192+
stream_id: u64,
193+
) -> i128 {
194+
let config: Option<ProtocolConfig> = env
195+
.storage()
196+
.instance()
197+
.get(&DataKey::ProtocolConfig);
198+
199+
match config {
200+
Some(cfg) if cfg.fee_rate_bps > 0 => {
201+
let fee = amount * (cfg.fee_rate_bps as i128) / 10_000;
202+
if fee > 0 {
203+
let token_client = token::Client::new(env, token_address);
204+
let contract_address = env.current_contract_address();
205+
token_client.transfer(&contract_address, &cfg.treasury, &fee);
206+
207+
env.events().publish(
208+
(Symbol::new(env, "fee_collected"), stream_id),
209+
FeeCollectedEvent {
210+
stream_id,
211+
treasury: cfg.treasury,
212+
fee_amount: fee,
213+
token: token_address.clone(),
214+
},
215+
);
216+
}
217+
amount - fee
218+
}
219+
_ => amount,
220+
}
221+
}
222+
223+
// ─── Stream Operations ───────────────────────────────────────────
224+
81225
pub fn create_stream(
82226
env: Env,
83227
sender: Address,
@@ -94,18 +238,22 @@ impl StreamContract {
94238

95239
let stream_id = Self::get_next_stream_id(&env);
96240
let start_time = env.ledger().timestamp();
97-
let rate_per_second = amount / (duration as i128);
98241

242+
// Transfer full amount from sender to contract
99243
let token_client = token::Client::new(&env, &token_address);
100244
let contract_address = env.current_contract_address();
101245
token_client.transfer(&sender, &contract_address, &amount);
102246

247+
// Deduct protocol fee (if configured) and get net amount for the stream
248+
let net_amount = Self::collect_fee(&env, &token_address, amount, stream_id);
249+
let rate_per_second = net_amount / (duration as i128);
250+
103251
let stream = Stream {
104252
sender: sender.clone(),
105253
recipient: recipient.clone(),
106254
token_address: token_address.clone(),
107255
rate_per_second,
108-
deposited_amount: amount,
256+
deposited_amount: net_amount,
109257
withdrawn_amount: 0,
110258
start_time,
111259
last_update_time: start_time,
@@ -258,11 +406,15 @@ impl StreamContract {
258406
return Err(StreamError::StreamInactive);
259407
}
260408

409+
// Transfer full amount from sender to contract
261410
let token_client = token::Client::new(&env, &stream.token_address);
262411
let contract_address = env.current_contract_address();
263412
token_client.transfer(&sender, &contract_address, &amount);
264413

265-
stream.deposited_amount += amount;
414+
// Deduct protocol fee (if configured) and add net amount to stream
415+
let net_amount = Self::collect_fee(&env, &stream.token_address, amount, stream_id);
416+
417+
stream.deposited_amount += net_amount;
266418
stream.last_update_time = env.ledger().timestamp();
267419

268420
storage.set(&stream_key, &stream);
@@ -272,7 +424,7 @@ impl StreamContract {
272424
StreamToppedUpEvent {
273425
stream_id,
274426
sender,
275-
amount,
427+
amount: net_amount,
276428
new_deposited_amount: stream.deposited_amount,
277429
},
278430
);

0 commit comments

Comments
 (0)