CompoundV3Strategy¶
ERC-4626-shaped MSV strategy. Supplies USDC to Compound V3 (Comet); permissionless harvest() claims COMP rewards and swaps back to base asset via Uniswap V3.
Source: khomdev-keep/src/strategies/CompoundV3Strategy.vy (Vyper 0.4.x)
Consumer: MultiStrategyVault fallback / smart-routed strategy
Implementation overview¶
- NAV —
totalAssets = comet.balanceOf(self)(interest accrues in Comet balance) - Harvest — claim COMP from
CometRewards→ Uni V3 swap → resupply to Comet; guarded by adminmin_out_rate+max_slippage_bps - Shares — internal accounting; only
vaultdeposits/withdraws - APR — cached
apr_bpsfor smart routing (admin-set) - sync — no-op (Comet balance live on read)
flowchart LR
MSV[MultiStrategyVault] --> C3[CompoundV3Strategy]
C3 --> Comet[Compound V3 Comet]
C3 -->|harvest| Uni[Uniswap V3]
Uni -->|USDC| Comet
Immutables¶
| Name | Role |
|---|---|
asset |
Base asset (USDC) |
vault |
Parent MSV |
comet |
Compound V3 market (e.g. cUSDCv3) |
comet_rewards |
CometRewards distributor |
comp_token |
COMP reward token |
Admin config¶
| Name | Role |
|---|---|
swap_router |
Uniswap V3 router for COMP→asset |
swap_pool_fee |
V3 fee tier (e.g. 3000 = 0.3%) |
min_out_rate |
Fair COMP price floor (asset per 1e18 COMP) |
max_slippage_bps |
Slippage cap below fair price (≤ 1000) |
Roles¶
| Role | Powers |
|---|---|
DEFAULT_ADMIN_ROLE |
APR, swap config, pause deposits, sweep stray tokens |
Events¶
| Event | When |
|---|---|
Deposit / Withdraw |
Vault capital flows |
Harvested |
COMP claimed + swapped + resupplied |
AprUpdated / SwapConfigUpdated / DepositsPaused |
Admin config |
ERC-4626 views¶
totalAssets¶
CompoundV3Strategy.totalAssets() -> uint256
Strategy NAV — Comet supplier balance in base asset (includes accrued interest).
Returns: uint256 — comet.balanceOf(self).
Access: view, any caller.
Source (khomdev-keep/src/strategies/CompoundV3Strategy.vy:147-150, _total_assets 436-443):
@external
@view
def totalAssets() -> uint256:
return self._total_assets()
# return staticcall ICompoundV3(comet).balanceOf(self)
Example:
Cross-links: convertToShares · MultiStrategyVault report · harvest
balanceOf¶
CompoundV3Strategy.balanceOf(account) -> uint256
Internal share balance for account (typically only MSV holds shares).
Inputs:
| Name | Type | Description |
|---|---|---|
account |
address |
Share holder |
Returns: uint256 — shares_held[account].
Access: view, any caller.
Source (khomdev-keep/src/strategies/CompoundV3Strategy.vy:152-155):
Example:
Cross-links: totalSupply · convertToAssets
totalSupply¶
CompoundV3Strategy.totalSupply() -> uint256
Total strategy shares outstanding.
Returns: uint256 — total_shares.
Access: view, any caller.
Source (khomdev-keep/src/strategies/CompoundV3Strategy.vy:157-160):
Cross-links: totalAssets · balanceOf
convertToAssets¶
CompoundV3Strategy.convertToAssets(shares) -> uint256
Shares → base asset quote (floor).
| Param | Type | Description |
|---|---|---|
shares |
uint256 |
Strategy shares |
Returns: uint256 — asset value at current Comet NAV.
Access: view, any caller.
Source (khomdev-keep/src/strategies/CompoundV3Strategy.vy:162-165, _convert_to_assets 457-465):
@external
@view
def convertToAssets(shares: uint256) -> uint256:
return self._convert_to_assets(shares, False)
Cross-links: convertToShares · previewRedeem
convertToShares¶
CompoundV3Strategy.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/CompoundV3Strategy.vy:167-170, _convert_to_shares 445-455):
@external
@view
def convertToShares(assets: uint256) -> uint256:
return self._convert_to_shares(assets, False)
Cross-links: convertToAssets · previewDeposit
maxDeposit¶
CompoundV3Strategy.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/CompoundV3Strategy.vy:172-177):
@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¶
CompoundV3Strategy.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/CompoundV3Strategy.vy:179-184):
@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¶
CompoundV3Strategy.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/CompoundV3Strategy.vy:186-191):
@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¶
CompoundV3Strategy.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/CompoundV3Strategy.vy:193-198):
@external
@view
def maxRedeem(owner: address) -> uint256:
if owner != vault:
return 0
return self.shares_held[owner]
Cross-links: redeem · maxWithdraw
previewDeposit¶
CompoundV3Strategy.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/CompoundV3Strategy.vy:200-203):
@external
@view
def previewDeposit(assets: uint256) -> uint256:
return self._convert_to_shares(assets, False)
Cross-links: deposit · convertToShares
previewWithdraw¶
CompoundV3Strategy.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/CompoundV3Strategy.vy:205-208):
@external
@view
def previewWithdraw(assets: uint256) -> uint256:
return self._convert_to_shares(assets, True)
Cross-links: withdraw · previewRedeem
previewRedeem¶
CompoundV3Strategy.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/CompoundV3Strategy.vy:210-213):
@external
@view
def previewRedeem(shares: uint256) -> uint256:
return self._convert_to_assets(shares, False)
Cross-links: redeem · convertToAssets
IStrategy hooks¶
getApr¶
CompoundV3Strategy.getApr() -> uint256
Cached APR for MSV smart routing (off-chain Compound supply rate → admin update).
Returns: uint256 — apr_bps.
Access: view, any caller.
Source (khomdev-keep/src/strategies/CompoundV3Strategy.vy:219-222):
Cross-links: set_apr_bps · Rebalancer compute_smart_targets
sync¶
CompoundV3Strategy.sync()
No-op. Comet balanceOf returns interest-included balance on every read.
Access: Any caller.
Source (khomdev-keep/src/strategies/CompoundV3Strategy.vy:224-230):
Cross-links: totalAssets · harvest
deployAvailable¶
CompoundV3Strategy.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/CompoundV3Strategy.vy:237-244):
Cross-links: MultiStrategyVault allocateCapital
harvest¶
CompoundV3Strategy.harvest() -> uint256
Claim COMP from CometRewards, swap to base asset via Uniswap V3, resupply to Comet.
Returns: uint256 — asset received from swap; 0 if no COMP or min_out_rate == 0.
Access: Permissionless. nonreentrant.
Events: Harvested.
Slippage: min_out = comp * min_out_rate / 1e18 * (10000 - max_slippage_bps) / 10000. If min_out_rate == 0, swap skipped (COMP stays until admin sets rate).
Source (khomdev-keep/src/strategies/CompoundV3Strategy.vy:247-295):
@external
@nonreentrant
def harvest() -> uint256:
extcall ICometRewards(comet_rewards).claim(comet, self, True)
comp_balance = IERC20(comp_token).balanceOf(self)
if comp_balance == 0:
return 0
if self.min_out_rate == 0:
return 0
expected = comp_balance * self.min_out_rate // 10**18
min_out = expected * (10_000 - self.max_slippage_bps) // 10_000
# Uni V3 exactInputSingle COMP → asset, then comet.supply(asset, received)
return received
Example:
Cross-links: set_swap_config · totalAssets · MultiStrategyVault report
ERC-4626 writes¶
deposit¶
CompoundV3Strategy.deposit(assets, receiver) -> uint256
Vault supplies base asset to Comet; mints internal shares to receiver (must be vault).
| Param | Type | Description |
|---|---|---|
assets |
uint256 |
Base asset amount |
receiver |
address |
Share recipient (vault only) |
Returns: uint256 — shares minted.
Returns / state: transferFrom vault → comet.supply → supplier balance accrues interest.
Access: vault only. nonreentrant. Reverts if deposits_paused.
Events: Deposit.
Source (khomdev-keep/src/strategies/CompoundV3Strategy.vy:301-322):
@external
@nonreentrant
def deposit(assets: uint256, receiver: address) -> uint256:
self._only_vault()
assert not self.deposits_paused
assert receiver == vault
shares = self._convert_to_shares(assets, False)
IERC20(asset).transferFrom(vault, self, assets)
ICompoundV3(comet).supply(asset, assets)
self.total_shares += shares
self.shares_held[receiver] += shares
return shares
Cross-links: previewDeposit · withdraw
mint¶
CompoundV3Strategy.mint(shares, receiver) -> uint256
Mint exact shares; vault pays ceil base asset → Comet 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/CompoundV3Strategy.vy:324-344):
@external
@nonreentrant
def mint(shares: uint256, receiver: address) -> uint256:
assets = self._convert_to_assets(shares, True)
IERC20(asset).transferFrom(vault, self, assets)
ICompoundV3(comet).supply(asset, assets)
self.total_shares += shares
self.shares_held[receiver] += shares
return assets
Cross-links: deposit · maxMint
withdraw¶
CompoundV3Strategy.withdraw(assets, receiver, owner) -> uint256
Burn shares; pull base asset from Comet to receiver.
| Param | Type | Description |
|---|---|---|
assets |
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/CompoundV3Strategy.vy:346-364):
@external
@nonreentrant
def withdraw(assets, receiver, owner) -> uint256:
shares = self._convert_to_shares(assets, True)
if shares > held:
shares = held
assets_to_pull = self._convert_to_assets(shares, False)
ICompoundV3(comet).withdrawTo(receiver, asset, assets_to_pull)
return shares
Cross-links: redeem · previewWithdraw
redeem¶
CompoundV3Strategy.redeem(shares, receiver, owner) -> uint256
Burn exact shares; pull base asset from Comet 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/CompoundV3Strategy.vy:366-379):
@external
@nonreentrant
def redeem(shares: uint256, receiver: address, owner: address) -> uint256:
assert shares <= self.shares_held[owner]
assets = self._convert_to_assets(shares, False)
self.shares_held[owner] -= shares
self.total_shares -= shares
ICompoundV3(comet).withdrawTo(receiver, asset, assets)
return assets
Cross-links: withdraw · previewRedeem
Admin¶
set_apr_bps¶
CompoundV3Strategy.set_apr_bps(new_bps)
Update cached APR for smart routing (from off-chain Compound supply rate).
| Param | Type | Description |
|---|---|---|
new_bps |
uint256 |
APR bps (≤ 5000) |
Access: DEFAULT_ADMIN_ROLE only.
Events: AprUpdated(new_bps).
Source (khomdev-keep/src/strategies/CompoundV3Strategy.vy:385-390):
@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_swap_config¶
CompoundV3Strategy.set_swap_config(router_, pool_fee_, min_out_rate_, slippage_bps_)
Atomic update of Uni V3 harvest swap params.
| Param | Type | Description |
|---|---|---|
router_ |
address |
Uniswap V3 router (non-zero) |
pool_fee_ |
uint24 |
V3 fee tier |
min_out_rate_ |
uint256 |
Fair COMP price floor (asset per 1e18 COMP) |
slippage_bps_ |
uint256 |
Max slippage below fair price (≤ 1000) |
Access: DEFAULT_ADMIN_ROLE only.
Events: SwapConfigUpdated.
Source (khomdev-keep/src/strategies/CompoundV3Strategy.vy:392-412):
@external
def set_swap_config(router_, pool_fee_, min_out_rate_, slippage_bps_):
access_control._check_role(DEFAULT_ADMIN_ROLE, msg.sender)
assert router_ != empty(address)
assert slippage_bps_ <= MAX_SLIPPAGE_CAP
self.swap_router = router_
self.min_out_rate = min_out_rate_
self.max_slippage_bps = slippage_bps_
Cross-links: harvest
set_deposits_paused¶
CompoundV3Strategy.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/CompoundV3Strategy.vy:414-418):
@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¶
CompoundV3Strategy.sweep(token, to)
Recover stray ERC-20. Cannot sweep base asset (held in Comet), COMP, or Comet itself.
| Param | Type | Description |
|---|---|---|
token |
address |
Token to sweep |
to |
address |
Recipient |
Access: DEFAULT_ADMIN_ROLE only.
Source (khomdev-keep/src/strategies/CompoundV3Strategy.vy:420-430):
@external
def sweep(token: address, to: address):
assert token != asset and token != comp_token and token != comet
bal = IERC20(token).balanceOf(self)
IERC20(token).transfer(to, bal)