Skip to content

feat: Treasury yield strategy — earn on idle scholarship funds under governance-set risk caps #1086

Description

@Anuoluwapo25

Summary

Build a treasury yield strategy — a governed way for idle scholarship funds to earn a return between disbursements, behind a pluggable strategy adapter, a governance-set allocation cap, and a liquidity buffer that guarantees approved scholarships are never blocked by an investment. Today scholarship_treasury can only deposit, disburse, and report get_balance (contracts/scholarship_treasury/src/lib.rs:410, :506, :690) — every donated USDC sits completely idle from the moment it arrives until the moment it is paid out.

Background

Donations arrive in lumps and go out in tranches over months. The gap between those two facts is dead capital, and for a scholarship treasury that capital is measured in learners not funded.

The DAO already has the governance machinery to make this decision safely — proposals, quorum, approval thresholds, a timelock vault, and a pause switch are all in the treasury contract already. What is missing is anything for that machinery to govern beyond disbursement. This issue adds the smallest responsible version of that: an adapter interface, hard caps, and an exit.

This one deserves argument before code. A scholarship treasury taking on protocol risk is a real policy decision, not a technical nicety. If you think the answer is that LearnVault should hold only cash, say so in this issue — a well-argued "no" closes it, and that is a good outcome.

Scope

Smart contract, backend, and frontend. The contract is the substance; the API and dashboard exist so the DAO can see what it has done. Integrating a specific lending protocol is deliberately behind an adapter — ship the interface plus one adapter, and a mock adapter for tests.

What to build

1. Strategy adapter interface

  • A trait/interface contract defining deposit, withdraw, balance_of, and max_withdrawable, so the treasury is never hardwired to one venue and a bad venue can be swapped without a treasury upgrade.
  • One real adapter against an established Soroban lending market, plus a mock adapter used by the entire test suite. The mock must be able to simulate partial withdrawal, withdrawal failure, and loss — a strategy layer that has only ever been tested against a cooperative counterparty is untested.

2. Treasury allocation controls (contracts/scholarship_treasury/)

  • allocate(strategy, amount) and deallocate(strategy, amount), callable only through an executed governance proposal, subject to the existing timelock. No admin shortcut.
  • max_allocation_bps — a governance-set ceiling on the share of treasury value that may ever be at risk. Enforced on every allocation, in basis points, defaulting conservatively.
  • A liquidity buffer floor: allocation must leave enough idle balance to cover all approved-but-unpaid proposals plus a configured margin. A scholarship that cannot be paid because the money is earning yield is a total failure of this feature, and it must be structurally impossible rather than a matter of care.
  • emergency_withdraw(strategy) — pulls everything recoverable, callable by the existing pause authority without waiting for the timelock. Losing yield is acceptable; being unable to exit is not.
  • Every allocation, deallocation, harvest, and emergency exit emits an event.

3. Accounting

  • Track principal allocated and yield accrued separately. get_balance must keep meaning what callers already assume it means; add explicit accessors for idle, allocated, and total value rather than quietly redefining the existing one.
  • Handle the case where a strategy returns less than was put in. Loss must be representable in the accounting, not an arithmetic panic — the contract's behaviour when the number goes down is the whole safety story.
  • Yield accrues to the treasury and is disbursable like any other balance.

4. Backend and frontend

  • Index the new events through the existing indexer (server/src/services/event-indexer.service.ts, server/src/lib/event-config.ts) into a migration-backed table, and expose allocation history and current position over the API.
  • Extend the treasury dashboard (src/pages/Treasury.tsx, src/components/treasury/) with idle vs. allocated split, yield earned to date, current strategy and cap, and the liquidity buffer status.
  • Surface the risk honestly to donors — which protocol holds the funds, how much is exposed, and what the DAO voted to allow. A donor funding scholarships is entitled to know their donation is in a lending market.

5. Tests

  • Allocation beyond max_allocation_bps is rejected.
  • Allocation that would breach the liquidity buffer is rejected, including when a proposal is approved after the allocation.
  • disburse succeeds for any approved proposal at any allocation level — assert this against a fully-capped treasury.
  • Allocation without an executed proposal, or before the timelock elapses, is rejected.
  • Emergency withdraw works while paused, and works when the strategy can only return part of the balance.
  • A strategy reporting a loss is accounted for without panicking, and reduces total value correctly.
  • Backend: every event maps to the right row; the dashboard reflects a mocked position.

Acceptance criteria

  • Adapter interface with one real adapter and a fully adversarial mock
  • Allocation only via executed governance proposal, through the existing timelock
  • Governance-set allocation ceiling enforced on every allocation
  • Liquidity buffer makes an unpayable approved scholarship structurally impossible
  • Emergency withdraw available to the pause authority without timelock delay
  • Principal and yield accounted separately; losses representable without panic
  • All allocation events emitted, indexed, and exposed over the API
  • Treasury dashboard shows idle/allocated/yield and names the venue holding the funds
  • cargo test --workspace passes, including loss and partial-withdrawal cases

Notes for contributors

  • Start in the issue thread, not the editor. Propose the default cap, the buffer margin, and the venue, and get maintainer agreement before writing the contract. These are policy, and a technically perfect PR with parameters nobody agreed to will not merge.
  • Do not add a "just this once" admin allocation path. The moment one exists, it is the only path anyone uses, and the governance in this design becomes decoration.
  • Integer maths on money: state your rounding direction and make it always favour the treasury. Write the test that proves repeated allocate/deallocate cycles cannot bleed value.
  • Read contracts/upgrade_timelock_vault/ and the existing pause implementation before designing the authorisation paths — reuse them rather than inventing a parallel mechanism.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

GrantFox OSSIssue tracked in GrantFox OSSMaybe RewardedIssue may be eligible for a GrantFox rewardOfficial Campaign | FWC26Campaign: Official Campaign | FWC26Third CampaignCampaign: Third CampaignbackendAPI/database/infrastructureenhancementNew feature or requestfeatureNew feature implementationfrontendReact/TypeScript UI worksecuritySecurity, audits, hardeningsmart-contractSoroban/Rust contract work

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions