CoilMakerStrategy¶
ERC-4626-shaped MSV strategy (asset = USDC). Rests oracle-floored sell-USDC / buy-TARE Coil intents; accumulates TARE; harvest swaps back to USDC via Curve EMA pool.
Source: khomdev-keep/src/strategies/CoilMakerStrategy.vy (Vyper 0.4.x)
Concept: FLYWHEEL 2.0 priority allocation — MultiStrategyVault coil_maker_strategy
Pairs with: Coil · TARE/USDC Curve NG pool · CoilFeeRouter
Implementation overview¶
- Inventory — one-sided maker: USDC → TARE via Coil fills; valuation = USDC + TARE×EMA (not spot)
- Order auth — keeper
approve_intentstores EIP-712 digest;isValidSignature(1271) for Coil fills - Exit — vault-only
withdraw/redeem;harvest()permissionless TARE→USDC (capped bymax_swap_in) - Maker floor — default
maker_margin_bps= carry-positive spread above EMA; deposits blocked if0 - Depeg exit — optional
depeg_exit_bps+ Chainlink TARE/USD feed confirms before spot-realised exit - Shares — internal
total_shares/shares_held(not ERC-20); onlyvaultmay deposit/withdraw
flowchart LR
MSV[MultiStrategyVault] --> CMS[CoilMakerStrategy]
CMS -->|resting intents| Coil[Coil DEX]
Coil -->|TARE| CMS
CMS -->|harvest swap| Pool[Curve TARE/USDC]
Immutables¶
| Name | Role |
|---|---|
asset |
USDC |
vault |
Parent MSV (sole depositor/withdrawer) |
coil |
Coil batch-auction DEX |
pool |
TARE/USDC Curve NG (EMA price_oracle) |
TARE |
Buy-side inventory token |
ORACLE_NUMER |
Scale bridge for EMA valuation |
Roles¶
| Role | Powers |
|---|---|
DEFAULT_ADMIN_ROLE |
APR, slippage, margins, feeds, pause deposits, sweep stray tokens |
KEEPER_ROLE |
approve_intent, cancel_intent, set_coil_allowance |
Events¶
| Event | When |
|---|---|
Deposit / Withdraw |
Vault capital in/out |
IntentApproved / IntentCancelled |
Resting Coil orders |
Harvested |
TARE → USDC realisation |
AprUpdated, SlippageUpdated, … |
Admin config |
ERC-4626 views¶
totalAssets¶
CoilMakerStrategy.totalAssets() -> uint256
Strategy NAV in USDC terms for MSV debt accounting.
Returns: uint256 — USDC.balanceOf + TARE.balanceOf * price_oracle / ORACLE_NUMER. Pending-order USDC still in balance (counted once).
Access: view, any caller.
Source (khomdev-keep/src/strategies/CoilMakerStrategy.vy:322-323, _total_assets 778-785):
Example:
Cross-links: convertToShares · harvest · MultiStrategyVault report
balanceOf¶
CoilMakerStrategy.balanceOf(account) -> uint256
Internal strategy shares held by account (MSV only in practice).
| Param | Type | Description |
|---|---|---|
account |
address |
Share holder |
Returns: uint256 — shares_held[account].
Access: view, any caller.
Source (khomdev-keep/src/strategies/CoilMakerStrategy.vy:327-328):
Cross-links: totalSupply · convertToAssets
totalSupply¶
CoilMakerStrategy.totalSupply() -> uint256
Total strategy shares outstanding (vault's position).
Returns: uint256 — total_shares.
Access: view, any caller.
Source (khomdev-keep/src/strategies/CoilMakerStrategy.vy:332-333):
Cross-links: totalAssets · balanceOf
convertToAssets¶
CoilMakerStrategy.convertToAssets(shares) -> uint256
Shares → USDC quote (floor).
| Param | Type | Description |
|---|---|---|
shares |
uint256 |
Strategy shares |
Returns: uint256 — USDC value at current NAV.
Access: view, any caller.
Source (khomdev-keep/src/strategies/CoilMakerStrategy.vy:337-338):
@external
@view
def convertToAssets(shares: uint256) -> uint256:
return self._convert_to_assets(shares, False)
Cross-links: convertToShares · previewRedeem
convertToShares¶
CoilMakerStrategy.convertToShares(assets) -> uint256
USDC → shares quote (floor).
| Param | Type | Description |
|---|---|---|
assets |
uint256 |
USDC amount |
Returns: uint256 — shares at current NAV.
Access: view, any caller.
Source (khomdev-keep/src/strategies/CoilMakerStrategy.vy:342-343):
@external
@view
def convertToShares(assets: uint256) -> uint256:
return self._convert_to_shares(assets, False)
Cross-links: convertToAssets · previewDeposit
maxDeposit¶
CoilMakerStrategy.maxDeposit(receiver) -> uint256
Max USDC depositable via ERC-4626 view.
| Param | Type | Description |
|---|---|---|
receiver |
address |
Must be vault |
Returns: uint256 — 0 if deposits_paused or receiver != vault; else max_uint256. Actual deposit also requires maker_margin_bps > 0.
Access: view, any caller.
Source (khomdev-keep/src/strategies/CoilMakerStrategy.vy:347-350):
@external
@view
def maxDeposit(receiver: address) -> uint256:
if self.deposits_paused or receiver != vault:
return 0
return max_value(uint256)
Cross-links: deposit · maxMint
maxWithdraw¶
CoilMakerStrategy.maxWithdraw(owner) -> uint256
Max USDC withdrawable for owner.
| Param | Type | Description |
|---|---|---|
owner |
address |
Must be vault |
Returns: uint256 — convertToAssets(shares_held[vault]); 0 if owner != vault.
Access: view, any caller.
Source (khomdev-keep/src/strategies/CoilMakerStrategy.vy:354-357):
@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
previewDeposit¶
CoilMakerStrategy.previewDeposit(assets) -> uint256
Simulate deposit — shares minted (floor).
| Param | Type | Description |
|---|---|---|
assets |
uint256 |
USDC in |
Returns: uint256 — expected shares.
Access: view, any caller.
Source (khomdev-keep/src/strategies/CoilMakerStrategy.vy:361-362):
@external
@view
def previewDeposit(assets: uint256) -> uint256:
return self._convert_to_shares(assets, False)
Cross-links: deposit · convertToShares
previewWithdraw¶
CoilMakerStrategy.previewWithdraw(assets) -> uint256
Simulate withdraw — shares burned (ceil).
| Param | Type | Description |
|---|---|---|
assets |
uint256 |
USDC out |
Returns: uint256 — shares required.
Access: view, any caller.
Source (khomdev-keep/src/strategies/CoilMakerStrategy.vy:366-367):
@external
@view
def previewWithdraw(assets: uint256) -> uint256:
return self._convert_to_shares(assets, True)
Cross-links: withdraw · previewRedeem
previewRedeem¶
CoilMakerStrategy.previewRedeem(shares) -> uint256
Simulate redeem — USDC out (floor).
| Param | Type | Description |
|---|---|---|
shares |
uint256 |
Shares burned |
Returns: uint256 — expected USDC (may differ on exit if TARE swap needed).
Access: view, any caller.
Source (khomdev-keep/src/strategies/CoilMakerStrategy.vy:371-372):
@external
@view
def previewRedeem(shares: uint256) -> uint256:
return self._convert_to_assets(shares, False)
Cross-links: redeem · convertToAssets
maxMint¶
CoilMakerStrategy.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/CoilMakerStrategy.vy:376-379):
@external
@view
def maxMint(receiver: address) -> uint256:
if self.deposits_paused or receiver != vault:
return 0
return max_value(uint256)
Cross-links: mint · maxDeposit
maxRedeem¶
CoilMakerStrategy.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/CoilMakerStrategy.vy:383-386):
@external
@view
def maxRedeem(owner: address) -> uint256:
if owner != vault:
return 0
return self.shares_held[owner]
Cross-links: redeem · maxWithdraw
Order-flow queries¶
getAvailableOrderVolume¶
CoilMakerStrategy.getAvailableOrderVolume() -> uint256
Idle USDC not committed to resting Coil intents — deploy capacity for new orders.
Returns: uint256 — USDC.balanceOf - pending_order_volume (floored at 0).
Access: view, any caller.
Source (khomdev-keep/src/strategies/CoilMakerStrategy.vy:395-401, _get_available_order_volume 953-960):
Example:
Cross-links: deployAvailable · Rebalancer compute_coilmaker_allocation
deployAvailable¶
CoilMakerStrategy.deployAvailable(amount) -> uint256
How much of amount the strategy can deploy to Coil right now.
| Param | Type | Description |
|---|---|---|
amount |
uint256 |
Requested USDC deployment |
Returns: uint256 — min(amount, getAvailableOrderVolume()).
Access: view, any caller.
Source (khomdev-keep/src/strategies/CoilMakerStrategy.vy:406-413):
@external
@view
def deployAvailable(amount: uint256) -> uint256:
return min(amount, self._get_available_order_volume())
Cross-links: getAvailableOrderVolume · MultiStrategyVault allocateCapital
IStrategy hooks¶
getApr¶
CoilMakerStrategy.getApr() -> uint256
Cached APR signal for MSV smart routing (keeper/admin set).
Returns: uint256 — apr_bps (not on-chain realised yield).
Access: view, any caller.
Source (khomdev-keep/src/strategies/CoilMakerStrategy.vy:421-422):
Cross-links: set_apr_bps · Rebalancer compute_smart_targets
harvest¶
CoilMakerStrategy.harvest() -> uint256
Permissionless: swap held TARE → USDC via Curve EMA pool; realise maker yield in vault asset.
Returns: uint256 — USDC received (0 if no TARE). Updates pending_order_volume / filled_order_volume tracking.
Returns / state: Enforces max_swap_in cap on harvest path (not on vault exits). min_dy from EMA oracle.
Access: Any caller. nonreentrant.
Events: FilledVolumeUpdated, Harvested(tare_in, usdc_out).
Source (khomdev-keep/src/strategies/CoilMakerStrategy.vy:426-454):
@external
@nonreentrant
def harvest() -> uint256:
tare_bal = IERC20(TARE).balanceOf(self)
if tare_bal == 0: return 0
# reduce pending_order_volume by oracle-estimated fill
out = self._swap_tare_for_usdc(tare_bal, is_exit=False)
return out
Example:
Cross-links: set_max_swap_in · MultiStrategyVault report · Accountant assess
sync¶
CoilMakerStrategy.sync()
No-op. TARE valued live from pool EMA on every totalAssets read.
Access: Any caller.
Source (khomdev-keep/src/strategies/CoilMakerStrategy.vy:457-459):
Cross-links: totalAssets · MultiStrategyVault report
ERC-4626 writes¶
deposit¶
CoilMakerStrategy.deposit(assets, receiver) -> uint256
Vault pulls USDC into strategy; mints internal shares to receiver (must be vault).
| Param | Type | Description |
|---|---|---|
assets |
uint256 |
USDC amount |
receiver |
address |
Share recipient (vault only) |
Returns: uint256 — shares minted.
Access: vault only. Requires maker_margin_bps > 0, not deposits_paused.
Events: Deposit.
Source (khomdev-keep/src/strategies/CoilMakerStrategy.vy:467-485):
@external
@nonreentrant
def deposit(assets, receiver) -> uint256:
self._only_vault()
assert maker_margin_bps > 0 and not deposits_paused
IERC20(asset).transferFrom(vault, self, assets)
total_shares += shares; shares_held[receiver] += shares
Cross-links: previewDeposit · withdraw
mint¶
CoilMakerStrategy.mint(shares, receiver) -> uint256
Mint exact shares; vault pays ceil-rounded USDC.
| Param | Type | Description |
|---|---|---|
shares |
uint256 |
Target shares |
receiver |
address |
Must be vault |
Returns: uint256 — USDC pulled from vault.
Access: vault only. Same gates as deposit.
Events: Deposit.
Source (khomdev-keep/src/strategies/CoilMakerStrategy.vy:489-505):
@external
@nonreentrant
def mint(shares, receiver) -> uint256:
assets = self._convert_to_assets(shares, True)
# same deposit path as deposit()
Cross-links: deposit · previewMint
withdraw¶
CoilMakerStrategy.withdraw(assets, receiver, owner) -> uint256
Burn shares; pay USDC to receiver. Swaps TARE→USDC via _pay_usdc if idle short.
| Param | Type | Description |
|---|---|---|
assets |
uint256 |
Target USDC |
receiver |
address |
Payout address |
owner |
address |
Must be vault |
Returns: uint256 — shares burned.
Access: vault only. Exit path bypasses max_swap_in; may use M-2 depeg exit.
Events: Withdraw (logs actual USDC paid).
Source (khomdev-keep/src/strategies/CoilMakerStrategy.vy:509-527):
@external
@nonreentrant
def withdraw(assets, receiver, owner) -> uint256:
# burn shares; _pay_usdc(assets_eff, receiver, full)
return shares
redeem¶
CoilMakerStrategy.redeem(shares, receiver, owner) -> uint256
Burn exact shares; pay USDC to receiver.
| Param | Type | Description |
|---|---|---|
shares |
uint256 |
Shares to burn |
receiver |
address |
USDC recipient |
owner |
address |
Must be vault |
Returns: uint256 — USDC paid (got from _pay_usdc).
Access: vault only.
Events: Withdraw.
Source (khomdev-keep/src/strategies/CoilMakerStrategy.vy:531-543):
@external
@nonreentrant
def redeem(shares, receiver, owner) -> uint256:
assets_eff = self._convert_to_assets(shares, False)
return self._pay_usdc(assets_eff, receiver, full)
Cross-links: withdraw · previewRedeem
Keeper / Coil¶
approve_intent¶
CoilMakerStrategy.approve_intent(intent)
Register a resting sell-USDC / buy-TARE Coil intent for EIP-1271 fills.
| Param | Type | Description |
|---|---|---|
intent |
ICoil.Intent |
Resting order (owner must be this strategy) |
Returns / state: Stores intent_digest → deadline in approved_intents; increments pending_order_volume (capped at 90% NAV).
Access: KEEPER_ROLE only. Rejects min_buy_amount below EMA oracle floor (maker_margin_bps spread or legacy slippage floor).
Events: IntentApproved, PendingVolumeUpdated.
Source (khomdev-keep/src/strategies/CoilMakerStrategy.vy:550-602):
@external
def approve_intent(intent: ICoil.Intent):
access_control._check_role(KEEPER_ROLE, msg.sender)
assert intent.min_buy_amount >= min_buy_floor # EMA*(1+margin) or EMA*(1-slippage)
digest = ICoil(coil).intent_digest(intent)
approved_intents[digest] = intent.deadline
Cross-links: isValidSignature · cancel_intent · Coil
cancel_intent¶
CoilMakerStrategy.cancel_intent(intent)
Revoke approved intent; release pending_order_volume; cancel on Coil if still fillable.
| Param | Type | Description |
|---|---|---|
intent |
ICoil.Intent |
Order to revoke |
Access: KEEPER_ROLE only.
Events: IntentCancelled, PendingVolumeUpdated.
Source (khomdev-keep/src/strategies/CoilMakerStrategy.vy:605-624):
@external
def cancel_intent(intent: ICoil.Intent):
approved_intents[digest] = 0
pending_order_volume -= intent.sell_amount # bounded
if ICoil(coil).is_fillable(intent):
ICoil(coil).cancel_intent(intent)
Cross-links: approve_intent · set_coil_allowance
set_coil_allowance¶
CoilMakerStrategy.set_coil_allowance(amount)
Set standing USDC allowance Coil may pull on intent fills.
| Param | Type | Description |
|---|---|---|
amount |
uint256 |
New USDC allowance for coil |
Returns / state: Reset-approve pattern: approve(coil, 0) then approve(coil, amount).
Access: KEEPER_ROLE only. Fills still gated per-order by isValidSignature + oracle floor.
Events: CoilAllowanceSet(amount).
Source (khomdev-keep/src/strategies/CoilMakerStrategy.vy:627-637):
@external
def set_coil_allowance(amount: uint256):
access_control._check_role(KEEPER_ROLE, msg.sender)
IERC20(asset).approve(coil, 0)
IERC20(asset).approve(coil, amount)
Example:
Cross-links: approve_intent · Coil
isValidSignature¶
CoilMakerStrategy.isValidSignature(_hash, _signature) -> bytes4
EIP-1271 hook Coil calls before filling a resting intent.
| Param | Type | Description |
|---|---|---|
_hash |
bytes32 |
Intent digest (from Coil) |
_signature |
Bytes[1024] |
Ignored — auth is on-chain registry |
Returns: bytes4 — 0x1626ba7e (magic) if digest approved and unexpired; else 0xffffffff.
Access: view, any caller.
Source (khomdev-keep/src/strategies/CoilMakerStrategy.vy:645-654):
@external
@view
def isValidSignature(_hash: bytes32, _signature: Bytes[1024]) -> bytes4:
deadline = approved_intents[_hash]
if deadline != 0 and block.timestamp <= deadline:
return ERC1271_MAGIC_VALUE
return ERC1271_INVALID
Cross-links: approve_intent · cancel_intent · Coil
Admin¶
set_apr_bps¶
CoilMakerStrategy.set_apr_bps(new_bps)
Update cached APR for MSV smart-routing (getApr).
| Param | Type | Description |
|---|---|---|
new_bps |
uint256 |
APR signal bps (≤ MAX_APR_BPS = 5000) |
Returns / state: Sets apr_bps.
Access: DEFAULT_ADMIN_ROLE only.
Events: AprUpdated(new_bps).
Source (khomdev-keep/src/strategies/CoilMakerStrategy.vy:661-665):
@external
def set_apr_bps(new_bps: uint256):
access_control._check_role(DEFAULT_ADMIN_ROLE, msg.sender)
assert new_bps <= MAX_APR_BPS
self.apr_bps = new_bps
Cross-links: getApr · MultiStrategyVault smart routing
set_slippage_bps¶
CoilMakerStrategy.set_slippage_bps(new_bps)
Swap tolerance for EMA-floored min_dy on TARE→USDC exits and harvest.
| Param | Type | Description |
|---|---|---|
new_bps |
uint256 |
Slippage bps (≤ MAX_SLIPPAGE_BPS = 1000) |
Returns / state: Sets slippage_bps. Used in legacy intent floor when maker_margin_bps == 0.
Access: DEFAULT_ADMIN_ROLE only.
Events: SlippageUpdated(new_bps).
Source (khomdev-keep/src/strategies/CoilMakerStrategy.vy:668-672):
@external
def set_slippage_bps(new_bps: uint256):
access_control._check_role(DEFAULT_ADMIN_ROLE, msg.sender)
assert new_bps <= MAX_SLIPPAGE_BPS
self.slippage_bps = new_bps
Cross-links: set_maker_margin_bps · harvest · approve_intent
set_max_swap_in¶
CoilMakerStrategy.set_max_swap_in(new_max)
Absolute TARE cap per permissionless harvest swap.
| Param | Type | Description |
|---|---|---|
new_max |
uint256 |
Max TARE notional per harvest; 0 locks harvest |
Returns / state: Sets max_swap_in. Vault exit path ignores this cap.
Access: DEFAULT_ADMIN_ROLE only. Production must set non-zero for harvest to work.
Events: MaxSwapInUpdated(new_max).
Source (khomdev-keep/src/strategies/CoilMakerStrategy.vy:675-688):
@external
def set_max_swap_in(new_max: uint256):
access_control._check_role(DEFAULT_ADMIN_ROLE, msg.sender)
self.max_swap_in = new_max
Cross-links: harvest · withdraw · redeem
set_tare_usd_feed¶
CoilMakerStrategy.set_tare_usd_feed(feed)
Set independent Chainlink TARE/USD feed for M-2 depeg exit confirmation.
| Param | Type | Description |
|---|---|---|
feed |
address |
Chainlink AggregatorV3; 0x0 unsets |
Returns / state: Sets tare_usd_feed + caches tare_usd_feed_decimals. Unset clears decimals. Required before enabling set_depeg_exit_bps.
Access: DEFAULT_ADMIN_ROLE only.
Events: TareUsdFeedSet(feed, decimals).
Source (khomdev-keep/src/strategies/CoilMakerStrategy.vy:692-712):
@external
def set_tare_usd_feed(feed: address):
access_control._check_role(DEFAULT_ADMIN_ROLE, msg.sender)
if feed == empty(address):
tare_usd_feed = empty(address); tare_usd_feed_decimals = 0
else:
dec = IAggregatorV3(feed).decimals()
tare_usd_feed = feed; tare_usd_feed_decimals = dec
Cross-links: set_depeg_exit_bps · withdraw · oracle_lib
set_maker_margin_bps¶
CoilMakerStrategy.set_maker_margin_bps(new_bps)
Maker spread above EMA mid for resting intent floor (carry-positive mode).
| Param | Type | Description |
|---|---|---|
new_bps |
uint256 |
Spread bps (≤ MAX_MAKER_MARGIN_BPS = 1000); 0 = legacy slippage floor |
Returns / state: Sets maker_margin_bps. If new_bps > 0, must exceed slippage_bps. deposit blocked while 0.
Access: DEFAULT_ADMIN_ROLE only.
Events: MakerMarginUpdated(new_bps).
Source (khomdev-keep/src/strategies/CoilMakerStrategy.vy:715-732):
@external
def set_maker_margin_bps(new_bps: uint256):
access_control._check_role(DEFAULT_ADMIN_ROLE, msg.sender)
assert new_bps <= MAX_MAKER_MARGIN_BPS
assert new_bps == 0 or new_bps > slippage_bps
self.maker_margin_bps = new_bps
Cross-links: approve_intent · set_slippage_bps · deposit
set_depeg_exit_bps¶
CoilMakerStrategy.set_depeg_exit_bps(new_bps)
Depeg threshold for forced vault exits: realise TARE at spot when EMA lags.
| Param | Type | Description |
|---|---|---|
new_bps |
uint256 |
Bps below EMA to trigger M-2 exit (≤ MAX_DEPEG_EXIT_BPS = 5000); 0 = disabled |
Returns / state: Sets depeg_exit_bps. Enabling (> 0) requires set_tare_usd_feed first. Spot + Chainlink must both confirm depeg.
Access: DEFAULT_ADMIN_ROLE only. Applies to vault exit path only — not harvest or intent floor.
Events: DepegExitBpsUpdated(new_bps).
Source (khomdev-keep/src/strategies/CoilMakerStrategy.vy:735-755):
@external
def set_depeg_exit_bps(new_bps: uint256):
access_control._check_role(DEFAULT_ADMIN_ROLE, msg.sender)
assert new_bps <= MAX_DEPEG_EXIT_BPS
assert new_bps == 0 or tare_usd_feed != empty(address)
self.depeg_exit_bps = new_bps
Cross-links: set_tare_usd_feed · withdraw · redeem
set_deposits_paused¶
CoilMakerStrategy.set_deposits_paused(state)
Pause/resume vault deposits into strategy. Withdrawals always allowed.
| Param | Type | Description |
|---|---|---|
state |
bool |
True = pause new deposits |
Returns / state: Sets deposits_paused.
Access: DEFAULT_ADMIN_ROLE only.
Events: DepositsPaused(state).
Source (khomdev-keep/src/strategies/CoilMakerStrategy.vy:758-761):
@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¶
CoilMakerStrategy.sweep(token, to)
Recover stray ERC-20 (not USDC or TARE).
| Param | Type | Description |
|---|---|---|
token |
address |
Token to sweep |
to |
address |
Recipient |
Access: DEFAULT_ADMIN_ROLE only. Cannot sweep asset or TARE.
Source (khomdev-keep/src/strategies/CoilMakerStrategy.vy:764-770):
@external
def sweep(token: address, to: address):
assert token != asset and token != TARE
bal = IERC20(token).balanceOf(self)
IERC20(token).transfer(to, bal)