This document explains how stNXM (Staked NXM) works under the hood: components, flows, accounting, delays, risks, and developer interface.
- High-Level Design
- Core Components
- Asset Accounting
- Lifecycle Flows
- Withdraw Delay & Pause Logic
- Integrations
- Admin & Parameters
- Events
- Developer Interface (Quick Ref)
- Security Considerations
- Testing Checklist
stNXM is an ERC4626Upgradeable vault accepting wNXM and minting stNXM shares.
Capital is allocated to Nexus Mutual staking pools in which it provides underwriting capacity for the mutual's insurance alternatives.
To begin with, small amounts of capital will also be allocated to Uniswap V3 and Morpho Blue in order to hotstart the LST ecosystem with immediately-available trading, lending, and borrowing.
Rewards from Nexus + Uniswap + Morpho are auto-compounded into the vault. Withdrawals are delayed and may be paused during claim events.
| Module | Purpose (selected functions) |
|---|---|
| ERC4626Upgradeable | Vault share logic: deposit, mint, withdraw, redeem. |
| Nexus staking | Track pool NFTs & tranches: stakeNxm, _withdrawFromPool, getRewards, resetTranches, stakedNxm. |
| Uniswap V3 | LP via position NFTs: _mintNewPosition, collectDexFees, decreaseLiquidity, dexBalances. |
| Morpho Blue | Lend wNXM using Uniswap TWAP oracle: morphoDeposit, morphoRedeem, morphoBalance. |
| Withdrawal control | Delay/queue/pause: _withdraw (request), withdrawFinalize, maxWithdraw, maxRedeem, togglePause. |
| Fees & admin | Admin fee accrual & payout: update (modifier), getRewards, withdrawAdminFees, changeAdminPercent. |
Total assets are the sum of all productive capital minus admin fees:
totalAssets() = stakedNxm() + unstakedNxm() - adminFeesWhere:
stakedNxm()= stake across all Nexus Mutual pool NFTs & tranches (handles unexpired & expired tranches).unstakedNxm()= wallet wNXM + wallet NXM + Uniswap LP wNXM leg + Morpho supplied assets (converted shares→assets).- Admin Fees:
adminFeesaccrue when balance grows (rewards) peradminPercent.
Total supply excludes “virtual stNXM” minted to the Uniswap pool:
totalSupply() = super.totalSupply() - virtualSharesFromDexsequenceDiagram
autonumber
participant U as User
participant V as Vault (stNXM)
participant N as Nexus Pools
participant L as Uniswap V3
participant M as Morpho
U->>V: deposit(assets) / mint(shares)
Note over V: ERC4626 share mint
V->>N: Optional stake via stakeNxm(...)
V->>L: Optional LP via _mintNewPosition(...)
V->>M: Optional supply via morphoDeposit(...)
N-->>V: rewards accrue (later)
L-->>V: LP fees accrue (later)
M-->>V: supply yield accrues (later)
-
getRewards():- Iterates all staking NFTs and withdraws rewards (and expired stake if chosen elsewhere).
- Calls
collectDexFees()to pull Uniswap fees (burns stNXM fee leg; keeps wNXM). - Updates
adminFeeson newly realized rewards.
-
Rewards sit as wNXM and are implicitly reflected in
totalAssets()→ stNXM exchange rate increases.
-
withdraw/redeem:- Initiates a 2 day delay until finalize. Shares are transferred to the contract but not burnt.
- stNXM continues to adjust to changes in the underlying assets (if rewards accrue during the delay, they're received).
- Ater 2 days, the user has 1 day to finalize otherwise the withdraw fails (to avoid abuse by keeping a withdrawal pending until a hack occurs).
- Shares are burnt, assets are sent to the user.
-
The most important things for safety in redemptions is that a pending withdrawal is affected by slashing (i.e. we cannot decide on value immediately) and that a withdrawal cannot be maintained in a pending state and immediately withdrawn. Both of these are to avoid users looking to abuse the slashing mechanic.
Delay & queue
-
_withdraw()does not pay immediately. It:- Moves user shares to the vault,
- Records a single active
WithdrawalRequestper user, - Increments
pending(protects liquidity), - Emits
WithdrawRequested.
-
withdrawFinalize(user):- Enforces
requestTime + withdrawDelay <= block.timestamp, - One-day finalize window; else shares are returned.
- Enforces
Pause
notPausedmodifier blocks_withdrawandwithdrawFinalizewhenpaused = true.togglePause()is owner-only; intended for admin multisig activation during coverage events.
-
Stores pool NFTs in
tokenIds. -
Tracks per-NFT tranche arrays in
tokenIdToTranches[tokenId]. -
Pro-rata stake calc uses pool
activeStake,stakeSharesSupply, and expired tranche snapshots when available. -
Functions:
stakeNxm(_amount, pool, tranche, requestTokenId)(owner),unstakeNxm(tokenId, trancheIds)(anyone; collects expired stake),getRewards()(anyone; pulls rewards),resetTranches()(refreshes tracked tranche windows; 91-day cadence).
- Pair: stNXM/wNXM, fee tier
500. - Vault mints stNXM and pairs with wNXM to create LP positions (NFTs in
dexTokenIds). - Fees pulled via
collectDexFees(); stNXM leg is burned; wNXM added to assets. totalSupply()excludes virtual stNXM in LP to keep share accounting accurate.
-
morphoIdderived from:- asset
wNXM, collateralstNXM,morphoOracle(Uniswap TWAP),irm(interest rate model), and LTV params.
- asset
-
morphoDeposit(_assetAmount)supplies wNXM;morphoRedeem(_shareAmount)withdraws. -
morphoBalance()converts supplyShares → assets using market totals.
| Variable | Meaning | Default |
|---|---|---|
withdrawDelay |
Delay before a requested withdrawal can be finalized | 2 days |
paused |
Global pause (affects withdraws/finalize) | false |
adminPercent |
10 = 1% fee on rewards (capped at 50% i.e. <=500) |
100 (10%) |
beneficiary |
Recipient of admin fees | set on initialize |
Admin functions (owner-only unless noted):
togglePause()changeWithdrawDelay(uint256)changeAdminPercent(uint256)(require<=500)changeBeneficiary(address)stakeNxm,extendDeposit,decreaseLiquidity,morphoDeposit,morphoRedeemremoveTokenIdAtIndex,rescueToken(cannot rescuewNXMorstNXM)- Anyone can call:
getRewards(),withdrawAdminFees(),unstakeNxm()
Deposit(user, asset, share, timestamp)WithdrawRequested(user, share, asset, requestTime, withdrawTime)Withdrawal(user, asset, share, timestamp)NxmReward(reward, timestamp)
User-facing (ERC4626)
deposit(uint256 assets, address receiver) returns (uint256 shares)
mint(uint256 shares, address receiver) returns (uint256 assets)
withdraw(uint256 assets, address receiver, address owner) returns (uint256 shares) // request-based
redeem(uint256 shares, address receiver, address owner) returns (uint256 assets) // request-based
withdrawFinalize(address user) // finalize after delay
maxWithdraw(address owner) view returns (uint256) // capped by in-vault wNXM balance
maxRedeem(address owner) view returns (uint256) // capped by in-vault wNXM balanceView/accounting
totalAssets() view returns (uint256)
totalSupply() view returns (uint256) // excludes virtual stNXM in LP
stakedNxm() view returns (uint256)
unstakedNxm() view returns (uint256)
dexBalances() view returns (uint256 assets, uint256 shares)
morphoBalance() view returns (uint256)
trancheAndPoolAllocations() view returns (...) // for frontendsRewards & fees
getRewards() returns (uint256 rewards) // pulls Nexus rewards + collects LP fees; accrues adminFees
collectDexFees() returns (uint256) // internal use in getRewards()
withdrawAdminFees() // pays out accumulated adminFees to beneficiaryAdmin/allocation
stakeNxm(uint256 amount, address pool, uint256 tranche, uint256 requestTokenId)
extendDeposit(uint256 tokenId, uint256 initialTranche, uint256 newTranche, uint256 topUpAmount)
decreaseLiquidity(uint256 tokenId, uint128 liquidity)
morphoDeposit(uint256 assetAmount)
morphoRedeem(uint256 shareAmount)
togglePause()
changeWithdrawDelay(uint256)
changeAdminPercent(uint256) // <= 500 (50%)
changeBeneficiary(address)- Withdrawal Throttling:
pendingshares +maxWithdraw/maxRedeemensure users can only exit against actual wNXM on hand. - Claim Events:
pausedprotects pool funds during claim/slash windows. stNXM can still be sold on dexes which will determine market price. - Admin Fee Bound:
changeAdminPercentenforces<= 50%cap at contract level. - Virtual Supply: Excluding LP-minted stNXM from
totalSupply()prevents share price distortion. - Oracle: Morpho uses Uniswap TWAP for robust pricing; LP uses fee tier 500 with explicit ticks.
- Rescue Guard:
rescueTokencannot withdrawwNXM,NXM, orstNXM.
- ERC4626 invariants: share/asset conversions, rounding,
totalAssets()monotonicity with rewards. - Withdraw delay: request → finalize timelines; missed-window path returns shares.
- Pause behavior: blocks
_withdrawandwithdrawFinalize; allowsgetRewards. - Rewards flow: Nexus rewards collection; Uniswap fee collection (burn stNXM leg); admin fee accrual.
- LP supply math:
totalSupply()excludes virtual shares;dexBalances()mirrors NFT positions. - Morpho position: supply/redeem round-trips;
morphoBalance()shares→assets conversion. - Tranche roll:
resetTranches()around 91-day boundaries; expired tranche reads. - Access control: owner-only functions, rescue guardrails, fee cap.
TL;DR stNXM tokenizes Nexus underwriting into a liquid ERC4626: capital flows to Nexus pools; rewards compound; withdrawals are delayed & pausable to handle claims — with robust accounting to keep share price honest.
- Use Foundry:
forge install
forge test- Use Hardhat:
npm install
npx hardhat test- Create a .env file with your Infura API
INFURA_API=https://mainnet.infura.io/v3/{YOUR_API_KEY}- Write / run tests with either Hardhat or Foundry or Both:
forge test
# or
npx hardhat test
# or
npm test (to run both)- Install libraries with Foundry which work with Hardhat.
forge install transmissions11/solmate # Already in this repo, just an example
# and
forge remappings > remappings.txt # allows resolve libraries installed with forge or npm