Nexus Mutual Staking
An assisted adapter that reports an account's staked NXM across Nexus Mutual V2 staking pools, per StakingNFT position the owner has submitted.
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 — permissionless,
msg.sender-keyed (AssistedCoordCache)protocolSubId:
keccak256("nexus-mutual-staking-pools")Assistant:
NexusMutualStakingPoolsAssistant.sol
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 for the family trust model.
Positions returned
Up to two legs per StakingNFT the account holds (a holder can have several, across several pools):
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):
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)
_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.
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, submits 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.
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.
StakingNFT) and submits the returned tokenId to the adapter — in one transaction.removes the tokenId: a partial withdraw keeps it tracked (CoordinateStillLive); it is dropped only once the position is fully emptied.Balance calculation
For each cached tokenId, the adapter values the position via the StakingViewer and re-verifies ownership against the StakingNFT:
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)wherestakingPool = StakingViewer.stakingPool(poolId)— the pool address. Both legs of a position share the same key. The NFTtokenIdis not part of the positionId — it rides inpositionInstanceId.positionKind:
Staking·RewardspositionInstanceId:
bytes32(tokenId)(the per-positionStakingNFTid; 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 viaIStakingPool.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 inpositionInstanceId.
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
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.
Last updated