> ## Documentation Index
> Fetch the complete documentation index at: https://xoxno.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Position NFT

> The token that owns a lending account: account_id is its token_id, transferring it transfers collateral and debt, and the full callable surface.

Every lending account is one token in the position-NFT collection. The
`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.

<Warning>
  **Transferring this token transfers the whole position — collateral and debt
  together, in one step.** There is no protocol-side handover, no acceptance step,
  and no way to move the collateral without the debt. Treat the token as the
  position itself, not as a receipt for it.
</Warning>

## 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

| Function                        | Auth              | Notes                                                                              |
| ------------------------------- | ----------------- | ---------------------------------------------------------------------------------- |
| `mint(to) -> u32`               | Controller only   | Called when an account is created. Returns the new `token_id`.                     |
| `burn(token_id)`                | Controller only   | Called when an account is removed. Not the holder's to call.                       |
| `owner_of(token_id) -> Address` | Open read         | The authority check the controller runs on every call.                             |
| `renew(token_id)`               | Controller only   | Extends the token's storage TTL. Reached through the controller's `renew_account`. |
| `upgrade(new_wasm_hash)`        | Governance-routed | Replaces the contract Wasm.                                                        |

<Note>
  `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.
</Note>

## Standard non-fungible surface

These come from the OpenZeppelin base and behave as the standard specifies.

**Ownership and transfer**

| Function                                     | Returns                                    |
| -------------------------------------------- | ------------------------------------------ |
| `balance(account) -> u32`                    | How many accounts that address owns.       |
| `owner_of(token_id) -> Address`              | The account's owner.                       |
| `transfer(from, to, token_id)`               | Moves the position. `from` must authorize. |
| `transfer_from(spender, from, to, token_id)` | Moves the position using an approval.      |

**Approvals**

| Function                                                   | Returns                                             |
| ---------------------------------------------------------- | --------------------------------------------------- |
| `approve(approver, approved, token_id, live_until_ledger)` | Approves one address for one token, until a ledger. |
| `approve_for_all(owner, operator, live_until_ledger)`      | Approves an operator for every token you hold.      |
| `get_approved(token_id) -> Option<Address>`                | Current single-token approval.                      |
| `is_approved_for_all(owner, operator) -> bool`             | Whether an operator is approved.                    |

**Enumeration**

| Function                                  | Returns                                                                                  |
| ----------------------------------------- | ---------------------------------------------------------------------------------------- |
| `total_supply() -> u32`                   | Live accounts in the collection.                                                         |
| `get_owner_token_id(owner, index) -> u32` | The `index`-th account owned by `owner`. Use with `balance` to list a wallet's accounts. |
| `get_token_id(index) -> u32`              | The `index`-th account in the collection.                                                |

**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.

<Warning>
  **NFT approvals are not controller delegates.** Approving an address here lets it
  *transfer the token*. It does **not** let it borrow or withdraw on the account —
  that needs a controller delegate grant via `add_delegate`, and the delegate must
  already be a governance-approved position manager.

  The two systems are independent. Read
  [Accounts and risk](/docs/stellar-lending/dev/accounts-and-risk) before assuming one
  implies the other.
</Warning>

## 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.

They do **not** inherit the previous owner's delegates. A `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.

<Note>
  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.
</Note>

## Next

<CardGroup cols={2}>
  <Card title="Accounts and risk" icon="user-shield" href="/docs/stellar-lending/dev/accounts-and-risk">
    Account storage, delegates, spoke binding, and the risk gates.
  </Card>

  <Card title="Controller ABI" icon="code" href="/docs/stellar-lending/dev/controller-abi">
    Every entry point that resolves ownership through this contract.
  </Card>

  <Card title="Security model" icon="shield-halved" href="/docs/stellar-lending/dev/security-model">
    Where position ownership sits in the trust boundary.
  </Card>

  <Card title="Addresses" icon="address-book" href="/docs/stellar-lending/dev/addresses">
    Deployed position-NFT addresses per network.
  </Card>
</CardGroup>
