MorphoStrategy¶
ERC-4626-shaped MSV strategy. Single-market supply-only Morpho Blue; market identity fixed at deploy.
Source: khomdev-keep/src/strategies/MorphoStrategy.vy (Vyper 0.4.x)
Consumer: MultiStrategyVault fallback / smart-routed strategy
Implementation overview¶
- NAV — Morpho supply position via
morpho.position(market_id, self)+ SharesMathLib virtual offsets - Market — immutable
MarketParams(loan/collateral/oracle/IRM/LLTV); multi-market routing belongs in a separate adapter - Shares — internal accounting; only
vaultdeposits/withdraws - APR — cached
apr_bpsfor smart routing (admin-set) - Harvest — no-op (no Morpho protocol rewards)
- sync —
morpho.accrueInterest(market)before MSVreport()(audit A7)
flowchart LR
MSV[MultiStrategyVault] --> M[MorphoStrategy]
M --> MB[Morpho Blue market]
M -->|sync| MB
Immutables¶
| Name | Role |
|---|---|
asset |
Base loan token (must match market loanToken) |
vault |
Parent MSV |
morpho |
Morpho Blue singleton |
loan_token / collateral_token / oracle / irm / lltv |
Fixed market identity |
market_id |
keccak256(abi.encode(MarketParams)) |
Roles¶
| Role | Powers |
|---|---|
DEFAULT_ADMIN_ROLE |
APR, pause deposits, sweep stray tokens |
Events¶
| Event | When |
|---|---|
Deposit / Withdraw |
Vault capital flows |
AprUpdated / DepositsPaused |
Admin config |
ERC-4626 views¶
totalAssets¶
MorphoStrategy.totalAssets() -> uint256
Strategy NAV — pro-rata Morpho supply shares valued with SharesMathLib virtual offsets.
Returns: uint256 — supplyShares * (totalSupplyAssets + VIRTUAL_ASSETS) / (totalSupplyShares + VIRTUAL_SHARES).
Access: view, any caller.
Note: Reads last-accrued market state; MSV report() calls sync first (audit A7).
Source (khomdev-keep/src/strategies/MorphoStrategy.vy:132-135, _total_assets 380-394):
@external
@view
def totalAssets() -> uint256:
return self._total_assets()
# pos = morpho.position(market_id, self)
# return pos.supplyShares * total_assets // total_shares # with virtual offsets
Example:
Cross-links: sync · convertToShares · MultiStrategyVault report
balanceOf¶
MorphoStrategy.balanceOf(account) -> uint256
Internal share balance for account (typically only MSV holds shares).
| Param | Type | Description |
|---|---|---|
account |
address |
Share holder |
Returns: uint256 — shares_held[account].
Access: view, any caller.
Source (khomdev-keep/src/strategies/MorphoStrategy.vy:137-140):
Cross-links: totalSupply · convertToAssets
totalSupply¶
MorphoStrategy.totalSupply() -> uint256
Total strategy shares outstanding.
Returns: uint256 — total_shares.
Access: view, any caller.
Source (khomdev-keep/src/strategies/MorphoStrategy.vy:142-145):
Cross-links: totalAssets · balanceOf
convertToAssets¶
MorphoStrategy.convertToAssets(shares) -> uint256
Shares → base asset quote (floor).
| Param | Type | Description |
|---|---|---|
shares |
uint256 |
Strategy shares |
Returns: uint256 — asset value at current Morpho NAV.
Access: view, any caller.
Source (khomdev-keep/src/strategies/MorphoStrategy.vy:147-150, _convert_to_assets 408-416):
@external
@view
def convertToAssets(shares: uint256) -> uint256:
return self._convert_to_assets(shares, False)
Cross-links: convertToShares · previewRedeem
convertToShares¶
MorphoStrategy.convertToShares(assets) -> uint256
Base asset → shares quote (floor).
| Param | Type | Description |
|---|---|---|
assets |
uint256 |
Base asset amount |
Returns: uint256 — shares at current NAV. No virtual-share offset (vault-only depositor).
Access: view, any caller.
Source (khomdev-keep/src/strategies/MorphoStrategy.vy:152-155, _convert_to_shares 396-406):
@external
@view
def convertToShares(assets: uint256) -> uint256:
return self._convert_to_shares(assets, False)
Cross-links: convertToAssets · previewDeposit
maxDeposit¶
MorphoStrategy.maxDeposit(receiver) -> uint256
Max base asset depositable (ERC-4626 view).
| Param | Type | Description |
|---|---|---|
receiver |
address |
Must be vault |
Returns: uint256 — 0 if paused or receiver != vault; else max_uint256.
Access: view, any caller.
Source (khomdev-keep/src/strategies/MorphoStrategy.vy:157-162):
@external
@view
def maxDeposit(receiver: address) -> uint256:
if self.deposits_paused or receiver != vault:
return 0
return max_value(uint256)
Cross-links: deposit · maxMint · set_deposits_paused
maxMint¶
MorphoStrategy.maxMint(receiver) -> uint256
Max shares mintable (vault-only).
| Param | Type | Description |
|---|---|---|
receiver |
address |
Must be vault |
Returns: uint256 — same gates as maxDeposit.
Access: view, any caller.
Source (khomdev-keep/src/strategies/MorphoStrategy.vy:164-169):
@external
@view
def maxMint(receiver: address) -> uint256:
if self.deposits_paused or receiver != vault:
return 0
return max_value(uint256)
Cross-links: mint · maxDeposit
maxWithdraw¶
MorphoStrategy.maxWithdraw(owner) -> uint256
Max base asset withdrawable for owner.
| Param | Type | Description |
|---|---|---|
owner |
address |
Must be vault |
Returns: uint256 — convertToAssets(shares_held[vault]); 0 if not vault.
Access: view, any caller.
Source (khomdev-keep/src/strategies/MorphoStrategy.vy:171-176):
@external
@view
def maxWithdraw(owner: address) -> uint256:
if owner != vault:
return 0
return self._convert_to_assets(self.shares_held[owner], False)
Cross-links: withdraw · maxRedeem
maxRedeem¶
MorphoStrategy.maxRedeem(owner) -> uint256
Max shares redeemable by owner.
| Param | Type | Description |
|---|---|---|
owner |
address |
Must be vault |
Returns: uint256 — full shares_held[owner]; 0 if not vault.
Access: view, any caller.
Source (khomdev-keep/src/strategies/MorphoStrategy.vy:178-183):
@external
@view
def maxRedeem(owner: address) -> uint256:
if owner != vault:
return 0
return self.shares_held[owner]
Cross-links: redeem · maxWithdraw
previewDeposit¶
MorphoStrategy.previewDeposit(assets) -> uint256
Simulate deposit — shares minted (floor).
| Param | Type | Description |
|---|---|---|
assets |
uint256 |
Base asset in |
Returns: uint256 — expected shares (= convertToShares).
Access: view, any caller.
Source (khomdev-keep/src/strategies/MorphoStrategy.vy:185-188):
@external
@view
def previewDeposit(assets: uint256) -> uint256:
return self._convert_to_shares(assets, False)
Cross-links: deposit · convertToShares
previewWithdraw¶
MorphoStrategy.previewWithdraw(assets) -> uint256
Simulate withdraw — shares burned (ceil).
| Param | Type | Description |
|---|---|---|
assets |
uint256 |
Base asset out |
Returns: uint256 — shares required.
Access: view, any caller.
Source (khomdev-keep/src/strategies/MorphoStrategy.vy:190-193):
@external
@view
def previewWithdraw(assets: uint256) -> uint256:
return self._convert_to_shares(assets, True)
Cross-links: withdraw · previewRedeem
previewRedeem¶
MorphoStrategy.previewRedeem(shares) -> uint256
Simulate redeem — base asset out (floor).
| Param | Type | Description |
|---|---|---|
shares |
uint256 |
Shares to burn |
Returns: uint256 — expected asset (= convertToAssets).
Access: view, any caller.
Source (khomdev-keep/src/strategies/MorphoStrategy.vy:195-198):
@external
@view
def previewRedeem(shares: uint256) -> uint256:
return self._convert_to_assets(shares, False)
Cross-links: redeem · convertToAssets
IStrategy hooks¶
getApr¶
MorphoStrategy.getApr() -> uint256
Cached APR for MSV smart routing (from off-chain Morpho supply APY).
Returns: uint256 — apr_bps.
Access: view, any caller.
Source (khomdev-keep/src/strategies/MorphoStrategy.vy:204-207):
Cross-links: set_apr_bps · Rebalancer compute_smart_targets
harvest¶
MorphoStrategy.harvest() -> uint256
No-op. Morpho Blue has no protocol rewards; interest accrues via sync.
Returns: uint256 — always 0.
Access: Any caller.
Source (khomdev-keep/src/strategies/MorphoStrategy.vy:209-215):
Cross-links: sync · totalAssets
sync¶
MorphoStrategy.sync()
Pokes Morpho Blue lazy interest accrual before MSV report() (audit A7).
Access: Any caller.
Source (khomdev-keep/src/strategies/MorphoStrategy.vy:217-224):
Cross-links: totalAssets · harvest · MultiStrategyVault report
deployAvailable¶
MorphoStrategy.deployAvailable(amount) -> uint256
Fallback deploy capacity — always accepts full amount (vault max_debt is real cap).
| Param | Type | Description |
|---|---|---|
amount |
uint256 |
Requested base asset deployment |
Returns: uint256 — max_uint256.
Access: view, any caller.
Source (khomdev-keep/src/strategies/MorphoStrategy.vy:231-238):
Cross-links: MultiStrategyVault allocateCapital
ERC-4626 writes¶
deposit¶
MorphoStrategy.deposit(assets_in, receiver) -> uint256
Vault supplies base asset to Morpho Blue; mints internal shares to receiver (must be vault).
| Param | Type | Description |
|---|---|---|
assets_in |
uint256 |
Base asset amount |
receiver |
address |
Share recipient (vault only) |
Returns: uint256 — shares minted.
Returns / state: transferFrom vault → morpho.supply(market, assets, 0, self).
Access: vault only. nonreentrant. Reverts if deposits_paused.
Events: Deposit.
Source (khomdev-keep/src/strategies/MorphoStrategy.vy:245-267):
@external
@nonreentrant
def deposit(assets_in: uint256, receiver: address) -> uint256:
self._only_vault()
shares = self._convert_to_shares(assets_in, False)
IERC20(asset).transferFrom(vault, self, assets_in)
IMorpho(morpho).supply(self._market_params(), assets_in, 0, self, b"")
self.total_shares += shares
self.shares_held[receiver] += shares
return shares
Cross-links: previewDeposit · withdraw
mint¶
MorphoStrategy.mint(shares, receiver) -> uint256
Mint exact shares; vault pays ceil base asset → Morpho supply.
| Param | Type | Description |
|---|---|---|
shares |
uint256 |
Target shares |
receiver |
address |
Must be vault |
Returns: uint256 — base asset pulled from vault.
Access: vault only. Same gates as deposit.
Events: Deposit.
Source (khomdev-keep/src/strategies/MorphoStrategy.vy:269-290):
@external
@nonreentrant
def mint(shares: uint256, receiver: address) -> uint256:
assets_in = self._convert_to_assets(shares, True)
IMorpho(morpho).supply(self._market_params(), assets_in, 0, self, b"")
return assets_in
Cross-links: deposit · maxMint
withdraw¶
MorphoStrategy.withdraw(assets_out, receiver, owner) -> uint256
Burn shares; pull base asset from Morpho to receiver.
| Param | Type | Description |
|---|---|---|
assets_out |
uint256 |
Target base asset |
receiver |
address |
Payout address |
owner |
address |
Must be vault |
Returns: uint256 — shares burned. Full exit caps pull at share value (audit A3).
Access: vault only. nonreentrant.
Events: Withdraw.
Source (khomdev-keep/src/strategies/MorphoStrategy.vy:292-310):
@external
@nonreentrant
def withdraw(assets_out, receiver, owner) -> uint256:
shares = self._convert_to_shares(assets_out, True)
if shares > held:
shares = held
assets_to_pull = self._convert_to_assets(shares, False)
IMorpho(morpho).withdraw(self._market_params(), assets_to_pull, 0, self, receiver)
return shares
Cross-links: redeem · previewWithdraw
redeem¶
MorphoStrategy.redeem(shares, receiver, owner) -> uint256
Burn exact shares; pull base asset from Morpho to receiver.
| Param | Type | Description |
|---|---|---|
shares |
uint256 |
Shares to burn |
receiver |
address |
Asset recipient |
owner |
address |
Must be vault |
Returns: uint256 — base asset withdrawn (convertToAssets floor).
Access: vault only. nonreentrant.
Events: Withdraw.
Source (khomdev-keep/src/strategies/MorphoStrategy.vy:312-325):
@external
@nonreentrant
def redeem(shares, receiver, owner) -> uint256:
assets_out = self._convert_to_assets(shares, False)
IMorpho(morpho).withdraw(self._market_params(), assets_out, 0, self, receiver)
return assets_out
Cross-links: withdraw · previewRedeem
Admin¶
set_apr_bps¶
MorphoStrategy.set_apr_bps(new_bps)
Update cached APR for smart routing (from off-chain Morpho supply APY).
| Param | Type | Description |
|---|---|---|
new_bps |
uint256 |
APR bps (≤ 5000) |
Access: DEFAULT_ADMIN_ROLE only.
Events: AprUpdated(new_bps).
Source (khomdev-keep/src/strategies/MorphoStrategy.vy:331-336):
@external
def set_apr_bps(new_bps: uint256):
access_control._check_role(DEFAULT_ADMIN_ROLE, msg.sender)
assert new_bps <= 5_000
self.apr_bps = new_bps
Cross-links: getApr
set_deposits_paused¶
MorphoStrategy.set_deposits_paused(state)
Pause/resume vault deposits. Withdrawals always allowed.
| Param | Type | Description |
|---|---|---|
state |
bool |
True = pause deposits |
Access: DEFAULT_ADMIN_ROLE only.
Events: DepositsPaused(state).
Source (khomdev-keep/src/strategies/MorphoStrategy.vy:338-342):
@external
def set_deposits_paused(state: bool):
access_control._check_role(DEFAULT_ADMIN_ROLE, msg.sender)
self.deposits_paused = state
Cross-links: deposit · maxDeposit
sweep¶
MorphoStrategy.sweep(token, to)
Recover stray ERC-20. Cannot sweep underlying (custody lives in Morpho Blue).
| Param | Type | Description |
|---|---|---|
token |
address |
Token to sweep |
to |
address |
Recipient |
Access: DEFAULT_ADMIN_ROLE only. Cannot sweep asset.
Source (khomdev-keep/src/strategies/MorphoStrategy.vy:344-354):
@external
def sweep(token: address, to: address):
assert token != asset
bal = IERC20(token).balanceOf(self)
IERC20(token).transfer(to, bal)