> For the complete documentation index, see [llms.txt](https://docs.kpk.io/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.kpk.io/funds/infrastructure/onchain-accounting/assisted-balance-adapters/nexus-mutual-staking.md).

# Nexus Mutual Staking

An **assisted** adapter that reports an account's **staked NXM across Nexus Mutual V2 staking pools**, emitted **per `StakingNFT` position** as NXM-denominated `Staking` and `Rewards` legs. Each stake is an ERC-721 `StakingNFT`; the NFT is not owner-enumerable and the StakingViewer exposes no owner→tokenIds view, so a pure-view adapter cannot discover which positions belong to an account. The owner feeds the `StakingNFT` `tokenId` itself (`submit(uint256)`, keyed on `msg.sender`); the adapter then values it entirely from live on-chain reads, re-verifying ownership on every read.

* **Type:** [Assisted adapter](/funds/infrastructure/onchain-accounting/assisted-balance-adapters.md) — permissionless, `msg.sender`-keyed (`AssistedCoordCache`)
* **protocolSubId:** `keccak256("nexus-mutual-staking-pools")`
* **Source:** [`NexusMutualStakingPoolsAssistedBalanceAdapter.sol`](https://github.com/karpatkey/onchain-accounting/blob/main/src/balances/AssistedBalanceAdapters/NexusMutualStakingPoolsAssistedBalanceAdapter.sol)
* **Assistant:** [`NexusMutualStakingPoolsAssistant.sol`](https://github.com/karpatkey/onchain-accounting/blob/main/src/balances/AssistedBalanceAdapters/assistants/NexusMutualStakingPoolsAssistant.sol)

{% hint style="info" %}
The cache stores **only tokenIds, never amounts**, so the feeder cannot fabricate value — at worst it omits an id (NAV under-counts), never over-counts. See [Assisted balance adapters](/funds/infrastructure/onchain-accounting/assisted-balance-adapters.md) for the family trust model.
{% endhint %}

***

## Positions returned

Up to two legs **per `StakingNFT`** the account holds (a holder can have several, across several pools):

| Leg     | PositionKind | isDebt  | isLocked | Description                                                                                                   |
| ------- | ------------ | ------- | -------- | ------------------------------------------------------------------------------------------------------------- |
| Stake   | `Staking`    | `false` | `false`  | `activeStake + expiredStake` — staked NXM principal (expired tranches no longer earn but remain withdrawable) |
| Rewards | `Rewards`    | `false` | `false`  | Accrued, unclaimed NXM rewards for that position                                                              |

A leg with a zero amount is skipped. Positions are denominated in **NXM** and dropped if NXM is not registered in NAVCalculator. `isLocked` is `false` for both legs.

***

## Feed surface & assistant

The adapter exposes a small, permissionless, `msg.sender`-keyed surface (no roles, no `account` argument):

| Function                      | Purpose                                                                                                                                                  |
| ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `submit(uint256 tokenId)`     | Track `tokenId` for the caller (idempotent). Reverts `CoordinateNotOwned` if the caller doesn't own it, `CoordinateCacheFull` past `MAX_TOKEN_IDS` (256) |
| `remove(uint256 tokenId)`     | Stop tracking `tokenId` for the caller. Reverts `CoordinateNotCached` if untracked, `CoordinateStillLive` if the position is still live                  |
| `tokenIdsOf(address account)` | View the candidate tokenIds cached for `account` (display/debug; not ownership-filtered)                                                                 |
| `clearAccount()`              | Drop **all** of the caller's cached ids (unconditional self-service GC)                                                                                  |

{% hint style="warning" %}
**`_removable` is value-based.** Nexus does **not** burn the `StakingNFT` on withdraw, so "no longer owned" is not a reliable liveness proxy. An id is removable only once it is either no longer owned by the account **or** fully emptied (no active/expired stake and no rewards). A **partial** or rewards-only withdraw leaves a live position that stays tracked; on an unresolvable `getTokens` read the adapter errs toward *keeping* the id — so a still-staked NFT is never dropped.
{% endhint %}

### The assistant (`NexusMutualStakingPoolsAssistant`)

**Use the assistant in place of the Nexus staking-pool contract.** Instead of staking directly by calling the pool's `depositTo` (which would leave the resulting `StakingNFT` invisible to NAV), you delegatecall the assistant: it forwards the **same** `depositTo` to the pool and, in the same transaction, `submit`s the minted `tokenId` to the adapter so the stake is tracked. Withdrawing works the same way through `withdraw`. It is a thin wrapper over Nexus's own staking channel — same action, plus the bookkeeping that makes the position visible to NAV.

| Entrypoint                                                                     | Does                                                                                                                                                      | Adapter call                  |
| ------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------- |
| `depositTo(poolAddress, amount, trancheId, requestTokenId)`                    | Approves the Nexus `TokenController` for the Safe's own NXM and stakes it (`destination = the Safe`), minting/topping-up a `StakingNFT` owned by the Safe | `submit(tokenId)`             |
| `withdraw(poolAddress, tokenId, withdrawStake, withdrawRewards, trancheIds[])` | Withdraws stake and/or rewards to the Safe, then tries to `remove(tokenId)`                                                                               | `remove(tokenId)` — see below |

Both are `onlyDelegateCall`. Because the module runs in the Safe's context, the Safe holds the NXM, is the Nexus member, and owns the minted NFT — no custody or asset-forwarding. On `withdraw` the id is untracked **best-effort** — removal never blocks the withdraw itself: a partial / rewards-only withdraw leaves live stake (adapter reverts `CoordinateStillLive` → keep it tracked), and an id that isn't tracked reverts `CoordinateNotCached` (nothing to untrack); the assistant **swallows** both and re-throws anything else. So the id is effectively dropped only once the position is fully emptied.

<figure><img src="/files/1tZwrd9Mh247RoHXtscR" alt="Flow to stake into Nexus: the Manager Safe drives the Portfolio Safe to delegatecall the assistant, which stakes NXM into the pool and submits the resulting StakingNFT tokenId to the adapter."><figcaption><p><strong>Staking.</strong> The Safe delegatecalls the assistant, which stakes its own NXM into the pool (minting/topping-up a <code>StakingNFT</code>) and <code>submit</code>s the returned tokenId to the adapter — in one transaction.</p></figcaption></figure>

<figure><img src="/files/55qD4XGB7U6sggjMJFlx" alt="Flow to withdraw from Nexus: the assistant withdraws stake/rewards to the Safe then best-effort removes the tokenId — kept if still staked (CoordinateStillLive), dropped once fully emptied."><figcaption><p><strong>Withdrawing.</strong> The assistant withdraws stake and/or rewards to the Safe, then best-effort <code>remove</code>s the tokenId: a partial withdraw keeps it tracked (<code>CoordinateStillLive</code>); it is dropped only once the position is fully emptied.</p></figcaption></figure>

***

## Balance calculation

<figure><img src="/files/jQwxBzcDGxjYTt5R87C0" alt="Flowchart of the Nexus Mutual Staking balance adapter: an assistant-submitted StakingNFT tokenId is ownership-verified, then valued via the StakingViewer into Staking and Rewards PositionBalance entries."><figcaption><p>Nexus Mutual Staking adapter — how the underlying balances reported to the NAV Calculator are derived.</p></figcaption></figure>

For each cached `tokenId`, the adapter values the position via the StakingViewer and re-verifies ownership against the StakingNFT:

```
for each cached tokenId:
    if StakingNFT.tokenInfo(tokenId).owner != account: skip      // live ownership re-check
    token = StakingViewer.getTokens([tokenId])
    staking = token.activeStake + token.expiredStake;  rewards = token.rewards
    if staking == 0 and rewards == 0: skip
    pool = StakingViewer.stakingPool(token.poolId)               // positionId
    emit Staking leg (if staking > 0) and Rewards leg (if rewards > 0)
```

Ownership is re-checked every read via `StakingNFT.tokenInfo`, so a stale or transferred id contributes nothing. Stake and rewards are read live from `StakingViewer.getTokens`; the cache holds no amounts. Each id is valued behind the base's per-coordinate self-`staticcall` (fail-open), so a revert in one position's live reads — Nexus's `getTokens` is revert-prone — discards just that id and never drops the account's whole Nexus stake.

***

## Identity

* **positionId:** `abi.encode(address stakingPool)` where `stakingPool = StakingViewer.stakingPool(poolId)` — the pool address. Both legs of a position share the same key. The NFT `tokenId` is **not** part of the positionId — it rides in `positionInstanceId`.
* **positionKind:** `Staking` · `Rewards`
* **positionInstanceId:** `bytes32(tokenId)` (the per-position `StakingNFT` id; ephemeral — both legs of a position carry the same instance id)
* **Labels:** the adapter implements `positionLabels(positionId)`, returning `["Staking Pools", "Pool #<poolId>"]` — the numeric pool id read live via `INexusMutualStakingPool.getPoolId()` (falling back to the pool's `"0x…"` address if that read reverts), matching the Nexus UI's pool numbering (surfaced only on the verbose read path; full breadcrumb = `["Nexus Mutual", "Staking Pools", "Pool #<poolId>"]`). The NFT id is **not** in the label — it rides in `positionInstanceId`.

An account's NFTs in the same pool share that pool's positionId. The reconciliation key is `keccak256(abi.encode(chainId, protocolSubId, positionId, balanceAsset.asset, positionKind))` — the `Staking` and `Rewards` legs reconcile independently via `positionKind` (and `positionInstanceId` is excluded, so multiple NFTs in one pool reconcile into one coordinate per kind).

***

## Constructor

```solidity
constructor(
    address stakingNft_,
    address stakingViewer_,
    address underlyingToken_,
    address navCalculator_
)
```

| Parameter          | Description                                                                                                                               |
| ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------- |
| `stakingNft_`      | Nexus Mutual `StakingNFT` (ERC-721) — source of ownership re-verification via `tokenInfo`. Must be a contract                             |
| `stakingViewer_`   | Nexus Mutual `StakingViewer` — per-token active/expired stake + rewards, and `stakingPool(poolId)` for the positionId. Must be a contract |
| `underlyingToken_` | Asset positions are denominated in — NXM. Must be non-zero                                                                                |
| `navCalculator_`   | NAVCalculator address, used for asset-registry metadata. Must be a contract                                                               |

There is **no** `admin_` / `assistant_` argument — the family is permissionless. The paired `NexusMutualStakingPoolsAssistant` is deployed separately, pinned to this adapter (plus the NXM token and Nexus `TokenController`).

***

## Registration

Registered as a plain adapter with `addBalanceAdapters([adapter])` and removed with `removeBalanceAdapters([adapter])`. The coordinate cache is then kept current by the position owner's own transactions — typically via the assistant, delegatecalled inside the same `depositTo` / `withdraw`.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.kpk.io/funds/infrastructure/onchain-accounting/assisted-balance-adapters/nexus-mutual-staking.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
