A non-custodial, deterministic capital formation primitive for Sui.
Tide enables projects to raise capital after product-market fit without selling tokens, issuing equity, or introducing discretionary fund control. Capital is released on a fixed schedule while backers earn rewards from real protocol revenue and native staking yield.
| Traditional Funding | Tide |
|---|---|
| Token sales dilute holders | No token issuance |
| VCs want control | Non-custodial, on-chain logic only |
| Revenue sharing is discretionary | Enforceable on-chain routing |
| Complex legal structures | Deterministic smart contracts |
| Locked capital is idle | Native staking while locked |
- Backers — Contribute SUI, receive a transferable economic position (SupporterPass), claim rewards
- Issuer (Protocol Operator) — Manages listing infrastructure, holds RouteCapability/ListingCap, routes protocol revenue
- Release Recipient (Artist/Creator) — Receives released capital on schedule from the CapitalVault
- Listing Council — 3-5 key multisig that gates listing creation, activation, and pause
- TreasuryVault — On-chain vault collecting protocol fees (1% raise fee + 20% of staking rewards), admin-gated withdrawals
Note: The
issuerandrelease_recipientare separate addresses. This allows the protocol operator (Tide) to manage adapter deployment and revenue routing while the artist receives their capital tranches directly.
| Object | Type | Purpose |
|---|---|---|
Tide |
Shared | Global configuration, pause flag, version |
TreasuryVault |
Shared | Protocol fee collection (raise fees + staking splits) |
ListingRegistry |
Shared | Registry of all listings, council-gated creation |
CouncilCap |
Owned | Capability for council-gated operations |
Listing |
Shared | Capital raise parameters, lifecycle state, release schedule |
CapitalVault |
Shared | Holds contributed SUI, manages tranche releases |
RewardVault |
Shared | Holds rewards, maintains cumulative index for fair distribution |
StakingAdapter |
Shared | Manages native Sui staking for locked capital |
SupporterPass |
Owned (NFT) | Backer's transferable NFT with shares, claim cursor, pass number, original backer, and lifetime earnings |
┌─────────────────────────────────────────────────────────────────────────┐
│ TIDE PROTOCOL │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────┐ ┌─────────────────┐ ┌──────────────┐ │
│ │ Backer │ ──SUI──▶│ CapitalVault │ ─────── │ Staking │ │
│ └──────────┘ │ (locked SUI) │ ◀────── │ Adapter │ │
│ │ └────────┬────────┘ └──────────────┘ │
│ │ │ │ │
│ │ receives │ releases on │ yields │
│ ▼ │ schedule ▼ │
│ ┌──────────────┐ │ ┌─────────────┐ │
│ │ SupporterPass│ ▼ │ RewardVault │ │
│ │ (NFT) │ ┌─────────┐ │ (SUI) │ │
│ └──────────────┘ │ Issuer │ └──────┬──────┘ │
│ │ └─────────┘ │ │
│ │ │ │ │
│ │ │ routes revenue │ │
│ │ └─────────────────────────┘ │
│ │ │ │
│ └─────────────────── claims ──────────────────────▶│ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
- Protocol Revenue — Fixed percentage routed from issuer (e.g., 10% of FAITH fees)
- Staking Yield — Native Sui staking rewards on locked capital
sequenceDiagram
participant B as Backer
participant L as Listing
participant CV as CapitalVault
participant SP as SupporterPass
B->>L: deposit(coin<SUI>)
L->>L: assert state == Active
L->>L: assert !paused
L->>CV: accept_deposit(coin)
CV->>CV: update total_principal
CV->>CV: calculate shares (fixed-point)
CV->>SP: mint SupporterPass
SP->>B: transfer pass
L->>L: emit Deposited event
- Backer calls
deposit()with SUI coin (minimum 1 SUI) - Listing validates state — Must be
Active, not paused - CapitalVault accepts deposit — Validates minimum, adds to total principal
- Shares calculated —
shares = deposit_amount * PRECISION / total_principal_at_deposit - SupporterPass minted — NFT with:
- Fixed shares (proportional claim on rewards)
- Claim cursor at current index
- Sequential pass number (e.g., "Backer #42")
- Original backer address (provenance)
- Lifetime claimed tracker (starts at 0)
- Pass transferred to backer — Fully owned, transferable
sequenceDiagram
participant B as Backer
participant SP as SupporterPass
participant RV as RewardVault
participant L as Listing
B->>L: claim(supporter_pass)
L->>RV: get global_index
L->>SP: get pass_index, shares
L->>L: claimable = shares × (global_index - pass_index)
alt claimable > 0
RV->>RV: withdraw(claimable)
RV->>B: transfer SUI
SP->>SP: update pass_index = global_index
L->>L: emit Claimed event
else claimable == 0
L->>L: no-op (or abort)
end
- Backer calls
claim()with their SupporterPass - Fetch global reward index from RewardVault
- Calculate claimable amount —
shares × (global_index - pass_index) - Withdraw from RewardVault — Deduct from vault balance
- Transfer SUI to backer — Direct transfer
- Update pass cursor — Set
pass_index = global_indexto prevent double-claim
Backers with multiple SupporterPasses can claim all rewards in a single transaction using claim_many():
/// Claim rewards from multiple passes at once
public fun claim_many(
listing: &Listing,
tide: &Tide,
reward_vault: &mut RewardVault,
passes: &mut vector<SupporterPass>,
ctx: &mut TxContext,
): Coin<SUI>Benefits:
- Single transaction for all passes = lower gas
- Automatically skips passes with nothing to claim (no errors)
- Returns merged coin with total claimed amount
- Individual
Claimedevents still emitted for each pass (for indexing) - Summary
BatchClaimedevent emitted with totals
SupporterPass holders can claim rewards even when their pass is listed on a Kiosk-based NFT marketplace (BlueMove, Clutchy, etc.):
/// Claim from a pass in your Kiosk
public fun claim_from_kiosk(
listing: &Listing,
tide: &Tide,
reward_vault: &mut RewardVault,
kiosk: &mut Kiosk,
kiosk_cap: &KioskOwnerCap,
pass_id: ID,
ctx: &mut TxContext,
): Coin<SUI>
/// Batch claim from multiple passes in your Kiosk
public fun claim_many_from_kiosk(
listing: &Listing,
tide: &Tide,
reward_vault: &mut RewardVault,
kiosk: &mut Kiosk,
kiosk_cap: &KioskOwnerCap,
pass_ids: vector<ID>,
ctx: &mut TxContext,
): Coin<SUI>How it works:
- Pass stays in Kiosk (listing not affected)
- Owner uses their KioskOwnerCap to claim
- Rewards withdrawn, pass returns to Kiosk
- All in a single transaction
When a SupporterPass is transferred:
- New owner inherits the current
pass_index - They can only claim rewards accrued after the last claim
- Previous owner cannot claim again (no longer owns the pass)
- No coordination or approval needed
The SupporterPass is a dynamic NFT with economic and provenance data:
| Field | Type | Purpose | Mutable |
|---|---|---|---|
pass_number |
u64 | Sequential backer number ("Backer #42") | ❌ |
original_backer |
address | Who first minted (provenance for resale) | ❌ |
shares |
u128 | Proportional claim on rewards | ❌ |
claim_index |
u128 | Last claimed reward index | ✅ (on claim) |
total_claimed |
u64 | Lifetime rewards claimed through this pass | ✅ (on claim) |
created_epoch |
u64 | Sui epoch when minted | ❌ |
Secondary Market Benefits:
pass_numberprovides collectibility ("I was an early backer!")original_backerprovides provenance (even after transfer)total_claimedshows earning history (dynamic, living NFT)
Capital is released on a fixed, immutable schedule computed at listing finalization:
| Phase | Timing | Amount |
|---|---|---|
| Initial | At finalization | 20% of net capital (after 1% fee) |
| Month 1-12 | 30 days apart | 6.67% each (80% ÷ 12) |
Key Properties:
- Pull-based — Anyone can call
release_tranche()once time passes - Non-discretionary — Issuer cannot accelerate, delay, or reorder
- Immutable — Schedule locked at finalization
- Independent — Revenue or performance doesn't affect releases
sequenceDiagram
participant T as Time/Keeper
participant L as Listing
participant CV as CapitalVault
participant SA as StakingAdapter
participant I as Issuer
T->>L: release_tranche()
L->>L: assert tranche is releasable
L->>CV: get tranche amount
alt capital is staked
CV->>SA: request_unstake(amount)
SA->>SA: queue for epoch boundary
Note over SA: Wait for unstake completion
SA->>CV: return unstaked SUI
end
CV->>CV: deduct from principal
CV->>I: transfer released SUI
L->>L: emit TrancheReleased event
L->>L: update release state
- Keeper/crank calls
release_tranche() - Validate release conditions — Timestamp, tranche not already released
- Check staking status — If capital is staked, initiate unstake
- Wait for unstake — Epoch boundary for Sui staking (may require separate tx)
- Transfer to issuer — Principal flows to issuer address
- Update tranche state — Mark released, no further rewards accrue
sequenceDiagram
participant P as Protocol (FAITH)
participant A as Adapter
participant RV as RewardVault
P->>A: route_revenue(coin<SUI>)
A->>A: validate source
A->>RV: deposit_rewards(coin)
RV->>RV: add to balance
RV->>RV: update global_index
RV->>RV: emit RouteIn event
- Protocol calls adapter with revenue SUI
- Adapter validates — Checks capability/authorization
- Deposit to RewardVault — Adds to reward pool
- Update global index —
new_index = old_index + (amount / total_shares) - Emit RouteIn event — Standardized event for tracking
claimable = shares × (global_index - pass_index)
Where:
shares— Fixed at deposit time, stored in SupporterPassglobal_index— Cumulative reward-per-share in RewardVaultpass_index— Last claimed index, stored in SupporterPass
| Property | Guarantee |
|---|---|
| Transfer-safe | Ownership = full entitlement |
| No double-claim | Cursor updates atomically |
| Deterministic gas | O(1) claim regardless of history |
| Late joiner fair | Only earn from rewards after deposit |
┌─────────┐ activate() ┌────────┐ finalize() ┌───────────┐ complete() ┌───────────┐
│ Draft │ ─────────────────▶ │ Active │ ─────────────────▶ │ Finalized │ ─────────────────▶ │ Completed │
└─────────┘ └────────┘ └───────────┘ └───────────┘
│ │ │ │
│ Config editable │ Deposits accepted │ No new deposits │ Terminal
│ No deposits │ Releases scheduled │ Releases continue │ All released
│ │ │ Revenue routing │ Claims only
│ │
│ cancel_listing() │ cancel_listing()
└─────────────┬────────────────┘
▼
┌───────────┐
│ Cancelled │
└───────────┘
│
│ Refunds enabled
│ Pass burned on claim
If a listing needs to be cancelled (raise failed, issuer backed out, etc.), the council can cancel and backers can claim refunds:
public fun cancel_listing(
listing: &mut Listing,
tide: &Tide,
council_cap: &CouncilCap,
capital_vault: &CapitalVault,
staking_adapter: &StakingAdapter,
ctx: &mut TxContext,
)Requirements:
- Listing must be in Draft or Active state
- All staked capital must be unstaked first (call
unstake_all()before cancelling) - Function will abort with
EStakedCapitalif staking_adapter has pending balance - Council-gated (requires CouncilCap)
Process:
- Council calls
unstake_all()to initiate unstaking (may require epoch wait) - Wait for pending balance to clear (Sui epoch boundary)
- Call
cancel_listing()to transition to Cancelled state - Backers can now call
claim_refund()orclaim_refunds()
/// Single refund claim
public fun claim_refund(
listing: &Listing,
capital_vault: &mut CapitalVault,
pass: SupporterPass, // Consumed - pass is burned
ctx: &mut TxContext,
): Coin<SUI>
/// Batch refund claim (multiple passes)
public fun claim_refunds(
listing: &Listing,
capital_vault: &mut CapitalVault,
passes: vector<SupporterPass>, // All consumed
ctx: &mut TxContext,
): Coin<SUI>How it works:
- Refund is proportional:
(pass.shares / total_shares) * vault_balance - Pass is burned on refund (prevents double-claim)
- Fair distribution even if some capital was released
- Batch function for users with multiple passes (single transaction)
| Fee Type | Rate | Destination | Timing |
|---|---|---|---|
| Raise Fee | 1% of total raised | TreasuryVault | Before first tranche release |
| Staking Split | 20% of staking rewards | TreasuryVault | On reward harvest |
| Revenue Skim | 0% (no fee) | N/A | N/A |
Note: All fee parameters are immutable and disclosed in the listing config hash.
The TreasuryVault is a shared object that accumulates protocol fees. Admin-gated operations:
| Operation | Function | Description |
|---|---|---|
| Withdraw to Admin | withdraw_from_treasury() |
Withdraw amount to configured admin wallet |
| Withdraw All | withdraw_all_from_treasury() |
Withdraw entire balance to admin wallet |
| Withdraw to Custom | withdraw_treasury_to() |
Withdraw amount to custom recipient |
| Update Admin Wallet | set_admin_wallet() |
Change recipient address for withdrawals |
Security: Only AdminCap holders can withdraw from TreasuryVault.
When paused:
- ❌ Capital releases STOP
- ❌ New deposits MAY be halted
- ✅ Staking MAY continue
- ✅ Revenue routing MAY continue
- ✅ Reward claims ENABLED
Invariant: Pause MUST NOT allow capital or reward redirection.
- Principal isolation — Capital MUST ONLY flow to issuer via release schedule
- Principal/reward separation — Principal MUST NEVER enter RewardVault
- Monotonic index — Reward index MUST only increase
- Economics immutable — Parameters MUST NOT change after activation
- Non-custodial — No admin can redirect funds
Tide v1 is intentionally minimal with a registry-first architecture:
| Feature | v1 Status |
|---|---|
| Architecture | Registry-first (future-proof) |
| Listings | Only FAITH configured & surfaced (Listing #1) |
| Governance | Minimal council gating (3-5 key multisig) |
| Raise Fee | 1% of total raised (collected before first release) |
| Staking Split | 80% backers / 20% treasury |
| Min Deposit | 1 SUI per backer |
| Assets | SUI only |
| Staking | Native Sui staking (fully implemented) |
| Marketplace | Native SupporterPass marketplace (5% seller fee) |
| Refunds | Cancellation + proportional refund claims |
| Self-Paying Loans | Borrow against SupporterPass, rewards auto-repay |
Council MAY: Create listings, activate/finalize, pause/resume
Council MUST NOT: Seize capital, redirect rewards, change live economics
tide-protocol/
├── CLAUDE.md # AI/development guidelines
├── README.md # This file
│
├── contracts/
│ ├── core/ # Tide Core package
│ │ ├── Move.toml
│ │ ├── sources/
│ │ │ ├── tide.move # Global config + pause
│ │ │ ├── treasury_vault.move # Protocol fee collection vault
│ │ │ ├── registry.move # Listing registry (council-gated)
│ │ │ ├── council.move # Council capability
│ │ │ ├── listing.move # Listing lifecycle
│ │ │ ├── capital_vault.move # Principal custody
│ │ │ ├── reward_vault.move # Reward distribution
│ │ │ ├── staking_adapter.move # Native Sui staking
│ │ │ ├── supporter_pass.move # Backer NFT position (economics only)
│ │ │ ├── kiosk_ext.move # Kiosk extension (claim while listed)
│ │ │ ├── display.move # Display metadata (sui::display)
│ │ │ ├── math.move # Fixed-point arithmetic
│ │ │ ├── admin.move # Capability-gated admin
│ │ │ ├── constants.move # Shared constants
│ │ │ ├── errors.move # Error codes
│ │ │ └── events.move # Event definitions
│ │ └── tests/
│ │ └── *.move
│ │
│ ├── adapters/
│ │ └── faith_router/ # FAITH revenue adapter
│ │ ├── Move.toml
│ │ ├── sources/
│ │ │ └── faith_router.move
│ │ └── tests/
│ │
│ ├── marketplace/ # Tide Marketplace
│ │ ├── Move.toml
│ │ ├── sources/
│ │ │ └── marketplace.move
│ │ └── tests/
│ │
│ └── loans/ # Self-Paying Loans
│ ├── Move.toml
│ ├── sources/
│ │ └── loan_vault.move
│ └── tests/
│
├── spec/
│ ├── tide-core-v1.md # Locked specification
│ ├── marketplace-v1.md # Marketplace specification
│ ├── self-paying-loans-v2.md # Self-paying loans design
│ ├── frontend-spec.md # Frontend/API specification
│ ├── invariants.md # Audit-ready invariant list
│ └── legal.md # Legal considerations
│
└── LICENSE
# Build core contracts
cd contracts/core
sui move build
sui move test
# Build adapter
cd ../adapters/faith_router
sui move build
sui move test
# Build marketplace
cd ../marketplace
sui move build
sui move test
# Build loans
cd ../loans
sui move build
sui move testSee DEPLOYMENT.md for:
- Environment strategy (testnet → mainnet)
- Wallet and multisig setup
- Step-by-step deployment guide
- Emergency procedures
See MARKETPLACE.md for:
- Native SupporterPass trading
- 5% seller fee structure
- List/buy/delist operations
- Integration with TreasuryVault
See LOANS.md for:
- Borrow against SupporterPass NFTs
- Automatic reward-based repayment
- 50% LTV, 5% APR, 1% origination fee
- Keeper model for harvesting
- Liquidation mechanics
DeepBook integration is deferred until the protocol reaches scale.
Current approach (v1):
- Treasury-funded loans (sufficient for FAITH + 1-2 issuers)
- Fixed 5% APR (simple, predictable)
When to revisit:
- Treasury 50%+ utilized consistently
- 3+ issuers onboarded
- User demand for dynamic rates
Want to integrate your protocol with Tide? See our partner guides:
| Guide | Purpose |
|---|---|
| INTEGRATION.md | Partner onboarding guide — Complete walkthrough from partnership to live revenue routing |
| ADAPTERS.md | Technical adapter architecture — How adapters work, template code, best practices |
- Partnership Setup — Define terms with Tide (revenue %, addresses)
- Listing Creation — Tide council creates your listing, you receive
RouteCapability - Deploy Adapter — Deploy your router package (use template)
- Create Router — Consume
RouteCapabilityto create router instance - Activate & Route — Start accepting deposits and routing revenue
# Route revenue to backers (after setup)
sui client ptb \
--split-coins gas "[100000000]" \
--assign revenue \
--move-call "pkg::router::route" router reward_vault "revenue.0" \
--gas-budget 50000000For detailed deployment steps, see DEPLOYMENT.md.
[TBD]