Skip to Content

Strategy: Accounting

Strategy accounting is based on externally posted Net Asset Value (NAV), represented by pricePerShare. The strategy does not compute NAV from token balances. Instead, pricePerShare is the single source of truth for share minting, redemption accounting, and totalAssets().


totalAssets() is a derived figure, not a token balance sum:

totalAssets() = totalSupply() × pricePerShare / 10^decimals()

pricePerShare represents asset wei per one full share unit (i.e. per 10^decimals() share tokens). It is initialised to 10^decimals() (1:1) at deployment and updated by an account with PRICE_SETTER_ROLE.

Because NAV is oracle-posted, the strategy’s actual ERC-20 balance of asset() may differ from totalAssets() at any point. Capital may be deployed to shards, invested externally, or in transit. Shards must pushToStrategy sufficient asset() before an operator calls fulfillRedeemRequests.


Share and asset conversions

All share and asset conversions derive from pricePerShare:

shares = assets × 10^decimals() / pricePerShare (floor) assets = shares × pricePerShare / 10^decimals() (floor or ceil depending on path)

For deposits, listed deposit tokens are first converted into strategy base units via their IAssetConverter, then those base units are converted into shares.

For redemptions, burned shares are converted into pending redemption assets using the current pricePerShare:

pendingAssets = netShares × pricePerShare / 10^decimals() (floor)

Posting a new price

pricePerShare is the strategy’s posted exchange rate: asset wei per 10 ** decimals() strategy shares. It drives totalAssets(), deposit share minting, and async redemption asset amounts.

Unlike a simple setter, postPricePerShare accepts an off-chain NAV snapshot and reconciles share supply changes that happened between the snapshot and the on-chain transaction.

function postPricePerShare( uint256 navAtSnapshot, uint256 supplyAtSnapshot, bool revertOnRateLimit ) external;

Requires:

  • No pause conditions in effect
  • navAtSnapshot and supplyAtSnapshot must not be zero.

If successful, the function stores the reconciled price and emits:

PricePerShareUpdated(pricePerShare, totalAssets(), postedAt)

Inputs

ParameterMeaning
navAtSnapshotTotal strategy NAV in asset() units observed off-chain at snapshot time
supplyAtSnapshottotalSupply() observed at the same time as the NAV snapshot
revertOnRateLimitWhether to revert or auto-pause when the price move exceeds rate-limit capacity

The snapshot pair must be internally consistent: navAtSnapshot and supplyAtSnapshot should describe the same off-chain valuation moment.


Reconcilitation from snapshot

Deposits and async redemption requests can happen after the off-chain NAV snapshot but before the postPricePerShare transaction executes.

Those same-epoch flows are priced using the old stored PPS, because the new PPS has not yet been posted. Without reconciliation, a deposit at stale PPS could inflate on-chain NAV after the price update.

postPricePerShare adjusts the snapshot NAV for supply changes at the old stored price, then computes the final PPS against current supply.

Async redemption fulfillments do not change share supply or active-share NAV; they only pay already queued requests, so they do not need reconciliation.

Hence:

  • If supply increased after the snapshot but before the new PPS is posted, the added shares are treated as capital that entered at the old PPS.
  • If supply decreased after the snapshot but before the new PPS is posted, the removed shares are treated as capital that exited at the old PPS.

Price rate limiter

pricePerShare moves are capped by a token-bucket rate limiter. The bucket has:

  • maxBucketBps: maximum burst capacity, limiting how large a single price move can be before the bucket is empty.
  • refillRate: capacity refilled per second, allowing larger cumulative moves over time.

The rate limiter is configured via setPricePerShareRateLimit(maxBucketBps, refillRate) by MANAGER_ROLE. The bucket starts at zero capacity on deployment, so it must be configured before the first postPricePerShare succeeds.

Current bucket state, with refill virtually applied, is readable via pricePerShareRateLimiterState().

Last updated on