Strategy: Deposits and Redemptions
Multi-asset deposits
The strategy accepts multiple ERC-20 tokens for deposits. Each listed token has its own pricing converter (IAssetConverter) and optional deposit fee. There is no ERC-4626 deposit(assets, receiver); the token address is always an explicit parameter.
Deposit asset management
| Function | Role | Description |
|---|---|---|
addDepositAsset(token, converter, feeBps) | DEFAULT_ADMIN_ROLE | Lists a new deposit token with a converter and initial fee (feeBps < 10_000) |
removeDepositAsset(token) | DEFAULT_ADMIN_ROLE | Removes token from the allowlist; reverts StrategyDeposit_AssetNotListed if not listed |
setAssetConverter(token, converter) | DEFAULT_ADMIN_ROLE | Replaces the pricing converter for a listed token |
setDepositFeeBps(token, bps) | MANAGER_ROLE | Adjusts the deposit fee for a listed token (bps < 10_000) |
isDepositAsset(token) | - | Returns whether a token is currently listed |
depositFee(token) | - | Returns configured deposit fee in bps |
converterOf(token) | - | Returns the IAssetConverter for a token |
Deposit and mint entrypoints
All deposit/mint paths require deposit not paused and are nonReentrant. Blocklist checks run on both sender and receiver.
// Deposit exact token amount, receive shares
uint256 shares = strategy.deposit(token, assets, receiver);
// Deposit exact token amount with slippage guard (reverts if shares < minSharesOut)
uint256 shares = strategy.deposit(token, assets, receiver, minSharesOut);
// Mint exact share amount, pay required tokens
uint256 assets = strategy.mint(token, shares, receiver);
// Mint exact share amount with cost cap (reverts if assets > maxAssetsIn)
uint256 assets = strategy.mint(token, shares, receiver, maxAssetsIn);Deposit flow (step by step)
- Blocklist check on
msg.senderandreceiver. - Management fee auto-accrual: mints any pending management fee shares to treasury before supply changes. See Fee System.
safeTransferFrom(sender, strategy, assets): pulls the full gross amount from the sender.- Fee split:
feeAssets = assets × depositFeeBps / 10_000(ceiling).netAssets = assets - feeAssets.- If
feeAssets > 0, transfers fee tokens tofeeTreasury. RevertsStrategyDeposit_FeeTreasuryNotSetif treasury is not configured.
- If
- Convert:
IAssetConverter(converter).toBaseUnits(netAssets, Floor)translates net deposit tokens into strategy base units. - Share mint:
_convertToShares(baseUnits, Floor)→ mints shares toreceiver. EmitsDeposit(sender, receiver, token, netAssets, shares, feeAssets). - Default shard routing: if
defaultShard != address(0), transfersnetAssetsof the deposit token to the default shard. EmitsDepositRoutedToDefaultShard. This is the primary mechanism for deploying freshly deposited capital.
Preview and conversion functions
| Function | Returns | Notes |
|---|---|---|
previewDeposit(token, assets) | Shares you receive | After fee deduction and converter; returns 0 if token unlisted |
previewMint(token, shares) | Gross assets required | Includes fee gross-up; ceiling rounding |
convertToShares(token, assets) | Shares for given assets | No fee applied; for informational use |
convertToAssets(token, shares) | Assets for given shares | No fee applied; ceiling rounding |
maxDeposit(token, receiver) | type(uint256).max or 0 | 0 when paused or token not listed |
maxMint(token, receiver) | type(uint256).max or 0 | 0 when paused or token not listed |
Asset converters
Every listed deposit token requires an IAssetConverter to translate between token units and strategy base units (the underlying asset’s units).
interface IAssetConverter {
function toBaseUnits(uint256 amount, Math.Rounding rounding) external view returns (uint256);
function fromBaseUnits(uint256 amount, Math.Rounding rounding) external view returns (uint256);
}Four concrete implementations are provided:
| Converter | Description |
|---|---|
DirectConverter | 1:1 identity. toBaseUnits and fromBaseUnits return the input unchanged. Use when the deposit token is the same denomination as the strategy’s base asset. |
ChainlinkConverter | Prices via a single Chainlink AggregatorV3 feed with configurable staleness tolerance. Supports an inverted mode for quote-currency feeds. |
ChainlinkCompositeConverter | Derives a ratio from two Chainlink feeds (e.g. BTC/ETH and ETH/USD to get BTC/USD). Handles decimal scaling between feeds. |
StakedLBTCConverter | Uses IOracle.ratio() (1e18-scaled) from the LBTC staking oracle to price staked LBTC against LBTC. |
Accounting
Deposit shares and redemption assets are calculated from the strategy’s externally posted pricePerShare. See Accounting for the full NAV, conversion, and rate-limiter model.
Async redemptions
Redemptions are not immediate. Users burn shares upfront via requestRedeem; an operator with PAY_REDEMPTIONS_ROLE fulfils pending requests in a later transaction once the strategy has sufficient balance for the redemption asset available.
Lifecycle
requestRedeem
uint256 requestId = strategy.requestRedeem(shares, owner);- Requires redeem not paused;
nonReentrant. - If
msg.sender != owner, spends ERC-20 allowance fromownertomsg.sender. - Blocklist check on
owner. - Management fee auto-accrual before share burn.
- Redeem fee (if configured and
feeTreasuryis set):feeShares = shares × redeemFeeBps / 10_000(ceiling). Fee shares are transferred fromownertofeeTreasury. IffeeShares >= shares, revertsAsyncRedemption_FeeSharesGreaterThanShares. netShares = shares - feeSharesare burned fromowner.pendingAssets = netShares × pricePerShare / 10^decimals()(floor) is recorded.- A new
requestId(monotonic from 1) is assigned and the request is stored. - Emits
RedeemRequested(requestId, owner, caller, pendingAssets, netShares, feeShares).
Pending request queries
| Function | Returns |
|---|---|
pendingRedeemRequest(requestId) | RedeemRequest{pendingShares, pendingAssets, owner} for an open request |
pendingAssetsOf(owner) | Total asset() owed across all open requests for an owner |
totalPending() | Aggregate (totalPendingShares, totalPendingAssets) across all open requests |
redeemFee() | Current redeemFeeBps |
fulfillRedeemRequests
strategy.fulfillRedeemRequests([requestId1, requestId2, ...]);- Requires
PAY_REDEMPTIONS_ROLE;nonReentrant. - Not blocked by deposit pause, redeem pause, or ERC-20 transfer pause: payout always proceeds once queued.
- For each
requestId:- Reverts
AsyncRedemption_NothingToFulfill(requestId)ifpendingAssets == 0. - Subtracts from global
totalPendingSharesandtotalPendingAssets. - Deletes the request record.
safeTransfer(asset(), owner, pendingAssets)transfers directly to the request owner. No separate user claim step required.- Emits
Redeem(operator, owner, assets, shares).
- Reverts
- Emits
RedeemRequestsFulfilled(requestIds)after the batch.
Important: The strategy’s
asset()balance must be sufficient at the timesafeTransferis called. If shards hold the assets off-chain, they must callpushToStrategy(asset, amount)before the operator callsfulfillRedeemRequests. The ERC-20 transfer will revert if the balance is insufficient.
Setting the redeem fee
strategy.setRedeemFeeBps(bps); // DEFAULT_ADMIN_ROLE; bps < 10_000A fee of 0 bps (or an unset treasury) means no shares are deducted.