Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions contracts/finance/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,16 @@ If `release` instead required the witness `S`, this would be impossible: a wrapp
that doesn't own `S` could not call it, so it would have to expose `&mut inner` and
lose all control over deposits and releases.

**Funding a wrapped wallet.** `release` is the only *outflow*, so it is the only entry
point a wrapper must gate. The inflow entry points - `deposit`, `receive_and_deposit`,
and `sweep_settled` - are permissionless, witness-free, and can only add funds for the
beneficiary, so a wrapper can safely re-expose them ungated, each just delegating to the
private `&mut inner`. Do so whenever the wrapper can be funded after wrapping, and
especially by address: anyone can `public_transfer` a `Coin<C>` or settle a `Balance<C>`
to the inner wallet's object address without the wrapper's cooperation, yet only
`&mut inner` can draw it in (`receive_and_deposit` / `sweep_settled`). A wrapper that omits
those passthroughs leaves such funds unclaimable until the wallet is unwrapped.

**Constructing the wallet.** The wrapper either accepts an already-built
`VestingWallet<S, P, C>` from the caller, or builds one itself. To build it without
depending on a curve's `new`, take a validated `P` from the curve module's `params`
Expand Down Expand Up @@ -293,6 +303,14 @@ one per integration boundary described above:
coin would push the lifetime total (`balance + released`) past `u64::MAX` the call
aborts, leaving the already-transferred coin parked at the wallet address. High
volume emitters should track headroom before transferring.
- **A wrapper must pass through inflow to accept address-targeted funding.** A
curve-agnostic wrapper that nests a wallet and keeps `&mut inner` private can only claim
funds sent to the inner wallet's address - `receive_and_deposit` for a `Coin<C>`,
`sweep_settled` for a settled `Balance<C>` - if it re-exposes those entry points.
Without them, funds a third party sends to the address stay unclaimable until the wallet
is unwrapped (claim before teardown; funds left at a destroyed wallet's address are
lost). The inflow passthroughs are safe to expose - they only add funds - so a wrapper
that can be funded by address should include them.

## Learn More

Expand Down
18 changes: 15 additions & 3 deletions contracts/finance/examples/vesting_wallet/pausable_grant.move
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,21 @@
/// witness-gated `destroy`) against it. See the tests for the end-to-end flow.
///
/// The wallet must be funded *before* it is wrapped, since `new` consumes it and the
/// wrapper never re-exposes `&mut inner`. Re-enabling top-ups would mean adding a
/// `deposit` passthrough (safe, as deposit is permissionless); it is left out here to
/// keep the surface minimal.
/// wrapper never re-exposes `&mut inner`. Re-enabling funding would mean adding
/// passthroughs for the wallet's inflow entry points (`deposit`, `receive_and_deposit`,
/// `sweep_settled`); all three are safe to expose ungated - permissionless, witness-free,
/// and inflow-only, so they can only add funds for the beneficiary. They are left out
/// here to keep the surface minimal, which is safe only because this grant is funded once
/// up front and is not meant to be funded by address.
///
/// Note the asymmetry: omitting `deposit` merely blocks top-ups (a would-be funder just
/// keeps their coin), but omitting `receive_and_deposit`/`sweep_settled` is a liveness
/// hazard. Anyone can `public_transfer` a `Coin<C>` (or settle a `Balance<C>`) to the
/// inner wallet's object address without the wrapper's cooperation, and only `&mut inner`
/// can claim it - so with no passthrough those funds are unclaimable until `unwrap`
/// restores `&mut` (claim them *before* any teardown; a coin left at a destroyed wallet's
/// address is stranded for good). A wrapper that expects address-targeted funding should
/// expose these passthroughs.
///
/// # Disclaimer
///
Expand Down
8 changes: 8 additions & 0 deletions contracts/finance/sources/vesting_wallet.move
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,10 @@ public fun deposit<S: drop, P: copy + drop + store, C>(
/// A wallet with no settled funds at its address is a no-op: nothing is swept and
/// no `Swept` event is emitted.
///
/// Requires `&mut wallet`: a wrapper that nests the wallet and keeps `&mut inner` private
/// must re-expose this entry point, or settled funds parked at the address stay unswept
/// until the wallet is unwrapped.
///
/// #### Parameters
/// - `wallet`: The wallet to sweep into.
/// - `root`: The shared `AccumulatorRoot`, read to find the wallet's settled funds.
Expand All @@ -396,6 +400,10 @@ public fun sweep_settled<S: drop, P: copy + drop + store, C>(
/// object address, then add it to the balance. Used by emission schedules and
/// payroll robots that don't hold a wallet reference.
///
/// Requires `&mut wallet`: a wrapper that nests the wallet and keeps `&mut inner` private
/// must re-expose this entry point, or coins parked at the address stay unclaimable until
/// the wallet is unwrapped.
///
/// A claim of a zero-value coin is a no-op: the (empty) balance is consumed but
/// nothing changes and no `Received` event is emitted.
///
Expand Down
Loading