account_id is that token’s token_id, and whoever holds the token owns the
account.
The controller stores no owner address. It calls owner_of(account_id) on
this contract for every authority check, so the NFT is the single source of truth
for who may borrow, withdraw, or run a strategy on an account.
Where it sits
contracts/position-nft implements OpenZeppelin’s NonFungibleToken with the
Enumerable contract type, plus the NonFungibleEnumerable extension. Holders
therefore get the standard non-fungible surface, and the protocol adds five
entry points of its own.
The controller owns it. Governance deploys it through
AdminOperation::DeployPositionNft and upgrades it through
UpgradePositionNft(hash). Skip the deploy and every account-touching call
fails with #53 PositionNftNotSet.
Protocol entry points
mint, burn, and renew all call controller.require_auth(). You cannot mint
yourself an account or burn someone else’s — those happen only as a side effect
of supply with account_id = 0, and of the account being emptied.Account deletion always routes through the controller’s
remove_account_and_burn_nft, so a deleted account can never leave a live token
behind.Standard non-fungible surface
These come from the OpenZeppelin base and behave as the standard specifies. Ownership and transfer
Approvals
Enumeration
Metadata
name(), symbol(), and token_uri(token_id). The URI is
{base_uri}{token_id}?isStatic=true&chain=STELLAR, and it panics with the
OpenZeppelin NonExistentToken error for a burned or never-minted id.
Two details that surprise people
token_id is a u32, but account_id is a u64. The controller’s API takes
and returns u64 account ids; the NFT stores them as u32. The live id space is
therefore bounded by u32, not u64.
Token id 0 is consumed at construction. That is what makes
account_id = 0 safe to use as the “create a new account” sentinel across
supply, multiply, flash_position, migrate_from_blend, and
SeizeMode::Credit(0) — no real account can ever have id 0.
What a transfer means for the new holder
The new holder inherits the account exactly as it stood:- the same collateral and the same debt;
- the same spoke binding, which never changes;
- the same position mode, which never changes;
- the same stamped risk parameters on each supply position.
DelegateGrant records
granted_by, so the moment the token moves, the old owner’s grants read as empty
for everyone else. The stale entry is purged on the new owner’s next delegate
write. No cleanup call exists or is needed.
If the token returns to the granting address before that write, the original
grant list re-arms. That is an intended property of the lazy-revoke design, not a
bug — but it is worth knowing if you move a position and move it back.
Next
Accounts and risk
Account storage, delegates, spoke binding, and the risk gates.
Controller ABI
Every entry point that resolves ownership through this contract.
Security model
Where position ownership sits in the trust boundary.
Addresses
Deployed position-NFT addresses per network.

