Skip to content

Commit 263af93

Browse files
Merge pull request #521 from newmattock/issue-484-yield-deposit
Add revenue pool yield deposit entrypoint
2 parents d27b1a0 + e0f133c commit 263af93

4 files changed

Lines changed: 213 additions & 10 deletions

File tree

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Revenue Pool Yield Deposits
2+
3+
`deposit_yield(treasury, amount, source)` lets the current revenue-pool admin
4+
deposit accumulated protocol earnings into the pool through one audited
5+
entrypoint.
6+
7+
## Behavior
8+
9+
- `treasury` must be the current admin and must authorize the call.
10+
- `amount` must be positive and is transferred from `treasury` to the revenue
11+
pool contract using the configured USDC token contract.
12+
- `source` is a short Soroban `Symbol` label for indexers, such as `fees` or
13+
`yield`.
14+
- `get_cumulative_yield_deposited()` returns the total amount deposited through
15+
this entrypoint.
16+
17+
## Event
18+
19+
Each successful deposit emits:
20+
21+
```text
22+
topics: ["yield_deposited", treasury]
23+
data: (amount, source, cumulative_yield_deposited)
24+
```
25+
26+
The metric update, token transfer, and event emission are part of the same
27+
Soroban transaction. If the transfer fails, the metric and event are reverted
28+
with the rest of the transaction.

contracts/revenue_pool/src/events.rs

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,14 @@ pub fn event_receive_payment(env: &Env) -> Symbol {
6060
Symbol::new(env, "receive_payment")
6161
}
6262

63+
/// Returns the Symbol for the `"yield_deposited"` event topic.
64+
///
65+
/// Emitted when the treasury deposits accumulated protocol yield into the
66+
/// revenue pool via `deposit_yield`.
67+
pub fn event_yield_deposited(env: &Env) -> Symbol {
68+
Symbol::new(env, "yield_deposited")
69+
}
70+
6371
/// Returns the Symbol for the `"set_max_distribute"` event topic.
6472
///
6573
/// Emitted when the admin updates the per-leg maximum distribute cap.
@@ -112,7 +120,10 @@ mod tests {
112120
#[test]
113121
fn test_event_admin_changed_bytes() {
114122
let env = Env::default();
115-
assert_eq!(event_admin_changed(&env), Symbol::new(&env, "admin_changed"));
123+
assert_eq!(
124+
event_admin_changed(&env),
125+
Symbol::new(&env, "admin_changed")
126+
);
116127
}
117128

118129
/// Snapshot: proves event_admin_transfer_started still maps to exactly the bytes for "admin_transfer_started".
@@ -156,14 +167,30 @@ mod tests {
156167
#[test]
157168
fn test_event_receive_payment_bytes() {
158169
let env = Env::default();
159-
assert_eq!(event_receive_payment(&env), Symbol::new(&env, "receive_payment"));
170+
assert_eq!(
171+
event_receive_payment(&env),
172+
Symbol::new(&env, "receive_payment")
173+
);
174+
}
175+
176+
/// Snapshot: proves event_yield_deposited still maps to exactly the bytes for "yield_deposited".
177+
#[test]
178+
fn test_event_yield_deposited_bytes() {
179+
let env = Env::default();
180+
assert_eq!(
181+
event_yield_deposited(&env),
182+
Symbol::new(&env, "yield_deposited")
183+
);
160184
}
161185

162186
/// Snapshot: proves event_set_max_distribute still maps to exactly the bytes for "set_max_distribute".
163187
#[test]
164188
fn test_event_set_max_distribute_bytes() {
165189
let env = Env::default();
166-
assert_eq!(event_set_max_distribute(&env), Symbol::new(&env, "set_max_distribute"));
190+
assert_eq!(
191+
event_set_max_distribute(&env),
192+
Symbol::new(&env, "set_max_distribute")
193+
);
167194
}
168195

169196
/// Snapshot: proves event_distribute still maps to exactly the bytes for "distribute".
@@ -177,7 +204,10 @@ mod tests {
177204
#[test]
178205
fn test_event_batch_distribute_bytes() {
179206
let env = Env::default();
180-
assert_eq!(event_batch_distribute(&env), Symbol::new(&env, "batch_distribute"));
207+
assert_eq!(
208+
event_batch_distribute(&env),
209+
Symbol::new(&env, "batch_distribute")
210+
);
181211
}
182212

183213
/// Snapshot: proves event_upgraded still maps to exactly the bytes for "upgraded".

contracts/revenue_pool/src/lib.rs

Lines changed: 73 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ const ADMIN_KEY: &str = "admin";
1919
const PENDING_ADMIN_KEY: &str = "pending_admin";
2020
const USDC_KEY: &str = "usdc";
2121
const MAX_DISTRIBUTE_KEY: &str = "max_distribute";
22+
const CUMULATIVE_YIELD_DEPOSITED_KEY: &str = "cumulative_yield_deposited";
2223
const ERR_AMOUNT_NOT_POSITIVE: &str = "amount must be positive";
2324
const ERR_AMOUNT_EXCEEDS_MAX_DISTRIBUTE: &str = "amount exceeds max_distribute";
2425
const ERR_UNAUTHORIZED: &str = "unauthorized: caller is not admin";
@@ -247,10 +248,8 @@ impl RevenuePool {
247248
inst.remove(&Symbol::new(&env, PENDING_ADMIN_KEY));
248249
inst.extend_ttl(LIFETIME_THRESHOLD, BUMP_AMOUNT);
249250

250-
env.events().publish(
251-
(events::event_admin_cancelled(&env), current, pending),
252-
(),
253-
);
251+
env.events()
252+
.publish((events::event_admin_cancelled(&env), current, pending), ());
254253
}
255254

256255
/// Return the pending admin address, or `None` if no two-step admin transfer is in progress.
@@ -369,6 +368,76 @@ impl RevenuePool {
369368
);
370369
}
371370

371+
/// Deposit accumulated protocol yield into the revenue pool.
372+
///
373+
/// The current admin acts as the treasury authority. The treasury must
374+
/// authorize the call, and USDC is transferred from that treasury address to
375+
/// this revenue-pool contract. The cumulative deposited-yield metric is
376+
/// updated atomically with the transfer and event emission.
377+
///
378+
/// # Arguments
379+
/// * `env` - The environment running the contract.
380+
/// * `treasury` - Must be the current admin and must authorize the call.
381+
/// * `amount` - USDC amount in base units. Must be positive.
382+
/// * `source` - Short source label for indexers, e.g. `fees` or `yield`.
383+
///
384+
/// # Panics
385+
/// * If `treasury` is not the current admin (`"unauthorized: caller is not admin"`).
386+
/// * If `amount` is zero or negative (`"amount must be positive"`).
387+
/// * If the cumulative metric would overflow (`"cumulative yield overflow"`).
388+
/// * If the revenue pool has not been initialized.
389+
///
390+
/// # Events
391+
/// Emits `yield_deposited` with `treasury` as topic and
392+
/// `(amount, source, cumulative_yield_deposited)` as data.
393+
pub fn deposit_yield(env: Env, treasury: Address, amount: i128, source: Symbol) {
394+
treasury.require_auth();
395+
let admin = Self::get_admin(env.clone());
396+
if treasury != admin {
397+
panic!("{}", ERR_UNAUTHORIZED);
398+
}
399+
if amount <= 0 {
400+
panic!("{}", ERR_AMOUNT_NOT_POSITIVE);
401+
}
402+
403+
let previous_total = Self::get_cumulative_yield_deposited(env.clone());
404+
let new_total = match previous_total.checked_add(amount) {
405+
Some(total) => total,
406+
None => panic!("cumulative yield overflow"),
407+
};
408+
409+
let usdc_address: Address = env
410+
.storage()
411+
.instance()
412+
.get(&Symbol::new(&env, USDC_KEY))
413+
.expect(ERR_NOT_INITIALIZED);
414+
let usdc = token::Client::new(&env, &usdc_address);
415+
let contract_address = env.current_contract_address();
416+
417+
let inst = env.storage().instance();
418+
inst.set(
419+
&Symbol::new(&env, CUMULATIVE_YIELD_DEPOSITED_KEY),
420+
&new_total,
421+
);
422+
inst.extend_ttl(LIFETIME_THRESHOLD, BUMP_AMOUNT);
423+
424+
usdc.transfer(&treasury, &contract_address, &amount);
425+
env.events().publish(
426+
(events::event_yield_deposited(&env), treasury),
427+
(amount, source, new_total),
428+
);
429+
}
430+
431+
/// Return the cumulative USDC yield deposited through [`Self::deposit_yield`].
432+
///
433+
/// Defaults to zero before the first yield deposit.
434+
pub fn get_cumulative_yield_deposited(env: Env) -> i128 {
435+
env.storage()
436+
.instance()
437+
.get(&Symbol::new(&env, CUMULATIVE_YIELD_DEPOSITED_KEY))
438+
.unwrap_or(0)
439+
}
440+
372441
/// Get the current per-leg distribution cap.
373442
/// Defaults to `i128::MAX` when unset.
374443
pub fn get_max_distribute(env: Env) -> i128 {

contracts/revenue_pool/src/test.rs

Lines changed: 78 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,9 @@ fn create_usdc<'a>(
158158
client.pause(&admin);
159159
assert!(client.is_paused());
160160

161-
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| client.distribute(&admin, &developer, &100)));
161+
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
162+
client.distribute(&admin, &developer, &100)
163+
}));
162164
assert!(result.is_err());
163165
}
164166

@@ -2121,7 +2123,7 @@ fn chunk_iter_preserves_order_and_amounts() {
21212123
let env = Env::default();
21222124
let payments = make_payments(&env, 7); // amounts 1..=7
21232125
let chunks = crate::chunk_iter(&env, payments, 3); // [3, 3, 1]
2124-
// Flatten and assert amounts return in order 1,2,3,4,5,6,7.
2126+
// Flatten and assert amounts return in order 1,2,3,4,5,6,7.
21252127
let mut expected: i128 = 1;
21262128
for chunk in chunks.iter() {
21272129
for (_, amount) in chunk.iter() {
@@ -2506,3 +2508,77 @@ fn batch_distribute_duplicate_detected_before_balance_check() {
25062508

25072509
client.batch_distribute(&admin, &payments);
25082510
}
2511+
2512+
#[test]
2513+
fn deposit_yield_transfers_from_treasury_and_updates_metric() {
2514+
let env = Env::default();
2515+
env.mock_all_auths();
2516+
let admin = Address::generate(&env);
2517+
let (pool_addr, client) = create_pool(&env);
2518+
let (usdc_address, usdc_client, usdc_admin) = create_usdc(&env, &admin);
2519+
let source = Symbol::new(&env, "fees");
2520+
2521+
client.init(&admin, &usdc_address);
2522+
usdc_admin.mint(&admin, &1_000);
2523+
2524+
client.deposit_yield(&admin, &400, &source);
2525+
2526+
assert_eq!(usdc_client.balance(&admin), 600);
2527+
assert_eq!(usdc_client.balance(&pool_addr), 400);
2528+
assert_eq!(client.get_cumulative_yield_deposited(), 400);
2529+
2530+
let events = env.events().all();
2531+
let deposit_event = events.last().unwrap();
2532+
let event_name = Symbol::try_from_val(&env, &deposit_event.1.get(0).unwrap()).unwrap();
2533+
assert_eq!(event_name, Symbol::new(&env, "yield_deposited"));
2534+
2535+
let data: (i128, Symbol, i128) =
2536+
<(i128, Symbol, i128)>::try_from_val(&env, &deposit_event.2).unwrap();
2537+
assert_eq!(data, (400, source, 400));
2538+
}
2539+
2540+
#[test]
2541+
fn deposit_yield_accumulates_multiple_sources() {
2542+
let env = Env::default();
2543+
env.mock_all_auths();
2544+
let admin = Address::generate(&env);
2545+
let (pool_addr, client) = create_pool(&env);
2546+
let (usdc_address, usdc_client, usdc_admin) = create_usdc(&env, &admin);
2547+
2548+
client.init(&admin, &usdc_address);
2549+
usdc_admin.mint(&admin, &1_000);
2550+
2551+
client.deposit_yield(&admin, &250, &Symbol::new(&env, "fees"));
2552+
client.deposit_yield(&admin, &150, &Symbol::new(&env, "yield"));
2553+
2554+
assert_eq!(client.get_cumulative_yield_deposited(), 400);
2555+
assert_eq!(usdc_client.balance(&pool_addr), 400);
2556+
assert_eq!(usdc_client.balance(&admin), 600);
2557+
}
2558+
2559+
#[test]
2560+
#[should_panic(expected = "unauthorized: caller is not admin")]
2561+
fn deposit_yield_rejects_non_treasury() {
2562+
let env = Env::default();
2563+
env.mock_all_auths();
2564+
let admin = Address::generate(&env);
2565+
let attacker = Address::generate(&env);
2566+
let (_, client) = create_pool(&env);
2567+
let (usdc_address, _, _) = create_usdc(&env, &admin);
2568+
2569+
client.init(&admin, &usdc_address);
2570+
client.deposit_yield(&attacker, &100, &Symbol::new(&env, "fees"));
2571+
}
2572+
2573+
#[test]
2574+
#[should_panic(expected = "amount must be positive")]
2575+
fn deposit_yield_rejects_zero_amount() {
2576+
let env = Env::default();
2577+
env.mock_all_auths();
2578+
let admin = Address::generate(&env);
2579+
let (_, client) = create_pool(&env);
2580+
let (usdc_address, _, _) = create_usdc(&env, &admin);
2581+
2582+
client.init(&admin, &usdc_address);
2583+
client.deposit_yield(&admin, &0, &Symbol::new(&env, "fees"));
2584+
}

0 commit comments

Comments
 (0)