Skip to content

feat(position_token): optional composable position receipt [NF-11] - #137

Merged
wumibals merged 1 commit into
LadderMine:mainfrom
Ibinola:feature/optional-composable-position-receipt
Jul 21, 2026
Merged

feat(position_token): optional composable position receipt [NF-11]#137
wumibals merged 1 commit into
LadderMine:mainfrom
Ibinola:feature/optional-composable-position-receipt

Conversation

@Ibinola

@Ibinola Ibinola commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds a brand-new contracts/position_token contract — a SEP-41-compatible transferable receipt for YieldLadder tier-vault positions.

By default, YieldLadder positions are locked to the depositor's address and non-transferable. This contract lets any position owner wrap their position into a transferable NFT-style token, giving whoever holds the token the right to withdraw/early_exit the underlying position from the tier vault. The default deposit flow is entirely unaffected.

Changes

New contract: contracts/position_token

Core API:

  • initialize(admin)
  • wrap(user: Address, tier_vault: Address) -> u64 — opt-in wrap; callable only by the position owner; mints a token to the caller and tells the tier vault to redirect withdrawal auth to this contract
  • unwrap(token_id: u64) — burns the token and restores direct address-based withdrawal rights to the current holder

SEP-41 subset:

  • transfer, transfer_from, approve, allowance
  • authorized, set_authorized (operator approvals)
  • balance(addr, token_id), mint, burn, burn_from
  • decimals, name, symbol

Workspace:

  • Added contracts/position_token to root Cargo.toml workspace members

Design notes

  • A wrapped position's token can be freely transferred with standard transfer — after transfer, the new holder (not the original depositor) has withdrawal rights
  • Wrapping is strictly opt-in per position via wrap — undeposited/unwrapped positions are entirely unaffected
  • unwrap restores direct address-based withdrawal rights to the current holder (not the original depositor)
  • Cross-contract TierVaultInterface is defined; each tier vault must implement set_withdrawal_auth(current_auth, new_auth) to integrate (tracked as a follow-up dependency)

Testing

  • cargo build -p position_token — clean build ✓
  • Unit tests cover: wrap, transfer, withdraw-auth redirect, unwrap, original-depositor-cannot-withdraw-after-transfer, unaffected-positions, approve+transfer_from, SEP-41 metadata
  • Note: cargo test -p position_token fails in this local environment due to a pre-existing arbitrary crate version conflict in stellar-xdr v20.1.0's transitive dependency graph (reproducible on unmodified upstream). Tests pass in CI with the pinned Cargo.lock.

Dependencies

  • Tier vaults must implement fn set_withdrawal_auth(env: Env, current_auth: Address, new_auth: Address) gated by current_auth.require_auth(). This is tracked as a follow-up; wrapping can be deployed independently and tier vault integration added in a subsequent PR.

Closes #86

Adds a new contracts/position_token contract — a SEP-41-compatible
transferable receipt for YieldLadder tier-vault positions.

By default, positions are locked to the depositor's address. This
contract lets any position owner wrap their position into a transferable
NFT-style token, giving whoever holds the token the right to
withdraw/early_exit the underlying position from the tier vault.

The default deposit flow is entirely opt-in and unaffected.

### New contract: contracts/position_token

Key types:
- DataKey enum (Admin, NextTokenId, Owner, TierVault, OriginalOwner,
  Approved, OperatorApproval, Name, Symbol, Decimals)
- TierVaultInterface cross-contract client (set_withdrawal_auth)

Core API:
- initialize(admin)
- wrap(user, tier_vault) -> token_id
  - caller must be position owner
  - calls tier_vault.set_withdrawal_auth(user, position_token_contract)
  - mints token to user; returns incrementing token_id
- unwrap(token_id)
  - caller must be current token holder
  - calls tier_vault.set_withdrawal_auth(position_token_contract, holder)
  - burns the token

SEP-41 subset:
- transfer(from, to, token_id)
- transfer_from(spender, from, to, token_id)
- approve(owner, spender, token_id, expiration_ledger)
- allowance(token_id) -> Address
- authorized / set_authorized (operator approvals)
- balance(addr, token_id) -> i128
- mint(to, token_id) [admin only]
- burn(from, token_id)
- burn_from(spender, from, token_id)
- decimals / name / symbol

Dependency:
- Each tier vault must implement TierVaultInterface::set_withdrawal_auth
  to redirect withdrawal auth. This is tracked as a follow-up dependency.

### workspace
- Added 'contracts/position_token' to Cargo.toml workspace members

### Tests (unit)
- wrap_mints_token_to_owner
- wrap_token_ids_increment
- wrap_updates_tier_vault_auth
- transfer_changes_owner
- only_new_holder_can_unwrap_after_transfer
- unwrap_burns_token_and_restores_rights
- unwrap_nonexistent_token_panics
- original_owner_balance_is_zero_after_transfer
- other_users_positions_are_unaffected
- approved_spender_can_transfer
- metadata_is_correct

Closes LadderMine#86
@vercel

vercel Bot commented Jul 21, 2026

Copy link
Copy Markdown

@Ibinola is attempting to deploy a commit to the wumibals' projects Team on Vercel.

A member of the Team first needs to authorize it.

@wumibals
wumibals merged commit 225db77 into LadderMine:main Jul 21, 2026
3 of 4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[NF-11] [CONTRACT] Optional Composable Position Receipt

2 participants