CoilFeeRouter¶
Coil DEX fee_recipient. Claims accrued protocol fees, routes USDC slices to Keep + gauge bribes, swaps remainder to TARE, donates to engine surplus.
Source: TARE-Stablecoin/src/CoilFeeRouter.vy (Vyper 0.4.3)
Concept guide: CoilFeeRouter protocol page
Wired by: Coil fee_recipient · TareEngine donate_to_surplus
Implementation overview¶
- Permissionless harvest —
harvest(token)pulls Coil fees + sweeps router balance. - TARE — donated as-is to engine surplus.
- USDC — three-way split:
keep_bps→ CoilMakerStrategy,gauge_bps→ USDC EmissionRouter, remainder swapped USDC→TARE via Curve NG EMA oracle. - Swap safety —
min_dyfromprice_oracle(0)+slippage_bps;max_swap_inmandatory cap (0 = swap locked). - Core tokens non-recoverable — owner
recoverrejects TARE/USDC.
flowchart LR
Coil[Coil DEX] -->|accrued fees| CFR[CoilFeeRouter]
CFR -->|keep_bps USDC| Keep[CoilMakerStrategy]
CFR -->|gauge_bps USDC| Gauge[EmissionRouter]
CFR -->|swap remainder| TARE[TARE]
TARE -->|donate_to_surplus| Engine[TareEngine]
Immutables¶
| Name | Role |
|---|---|
TARE |
Engine-bound stablecoin (donated) |
USDC |
Paired stable (swapped → TARE) |
ENGINE |
TareEngine surplus receiver |
COIL |
Fee source DEX |
POOL |
TARE/USDC Curve StableSwap-NG (USDC idx 0, TARE idx 1) |
ORACLE_NUMER |
Decimal scaler for EMA price math |
Constants¶
| Name | Value | Role |
|---|---|---|
MAX_SLIPPAGE_BPS |
1_000 | 10% swap tolerance cap |
MAX_GAUGE_BPS |
5_000 | 50% gauge slice cap |
MAX_KEEP_BPS |
5_000 | 50% Keep slice cap |
Events¶
| Event | When |
|---|---|
FeesHarvested |
TARE donated to engine |
KeepFeesRouted / GaugeFeesRouted |
USDC slices routed |
SlippageUpdated / MaxSwapInUpdated / GaugeBpsUpdated / KeepBpsUpdated |
Admin param changes |
EmissionSinkUpdated / KeeperUpdated |
Sink addresses set |
Recovered |
Non-core token escape hatch |
Admin¶
set_slippage_bps¶
CoilFeeRouter.set_slippage_bps(new_bps)
Set USDC→TARE swap slippage tolerance for harvest. Used with EMA price_oracle to compute min_dy.
| Param | Type | Description |
|---|---|---|
new_bps |
uint256 |
Slippage below oracle quote (0–MAX_SLIPPAGE_BPS, max 1000 = 10%) |
Returns / state: Updates slippage_bps. Default at deploy: 50 (0.5%).
Access: owner only. Reverts if new_bps > MAX_SLIPPAGE_BPS.
Events: SlippageUpdated(new_bps).
Source (TARE-Stablecoin/src/CoilFeeRouter.vy:235-245):
@external
def set_slippage_bps(new_bps: uint256):
ownable._check_owner()
assert new_bps <= MAX_SLIPPAGE_BPS, "router: slippage too high"
self.slippage_bps = new_bps
log SlippageUpdated(new_bps=new_bps)
Example:
Cross-links: set_max_swap_in · harvest
set_max_swap_in¶
CoilFeeRouter.set_max_swap_in(new_max)
Absolute USDC cap per harvest swap to TARE. Bounds multi-block EMA drift exposure (TARE-7).
| Param | Type | Description |
|---|---|---|
new_max |
uint256 |
Max USDC notional per swap (0 = swap path locked) |
Returns / state: Updates max_swap_in. Must be set non-zero before USDC harvest can swap.
Access: owner only.
Events: MaxSwapInUpdated(new_max).
Source (TARE-Stablecoin/src/CoilFeeRouter.vy:248-261):
@external
def set_max_swap_in(new_max: uint256):
ownable._check_owner()
self.max_swap_in = new_max
log MaxSwapInUpdated(new_max=new_max)
Example:
Cross-links: set_slippage_bps · harvest
set_gauge_bps¶
CoilFeeRouter.set_gauge_bps(new_bps)
Set USDC slice routed to emission_sink (veForge EmissionRouter) on each USDC harvest.
| Param | Type | Description |
|---|---|---|
new_bps |
uint256 |
Gauge share of USDC harvest (0–MAX_GAUGE_BPS, max 5000 = 50%) |
Returns / state: Updates gauge_bps. No effect until emission_sink set and harvest runs.
Access: owner only. Reverts if new_bps > MAX_GAUGE_BPS or new_bps + keep_bps > MAX_GAUGE_BPS (surplus always ≥50%).
Events: GaugeBpsUpdated(new_bps).
Source (TARE-Stablecoin/src/CoilFeeRouter.vy:264-277):
@external
def set_gauge_bps(new_bps: uint256):
ownable._check_owner()
assert new_bps <= MAX_GAUGE_BPS, "router: gauge bps too high"
assert new_bps + self.keep_bps <= MAX_GAUGE_BPS, "router: keep+gauge starves surplus"
self.gauge_bps = new_bps
log GaugeBpsUpdated(new_bps=new_bps)
Example:
Cross-links: set_emission_sink · set_keep_bps · harvest
set_emission_sink¶
CoilFeeRouter.set_emission_sink(sink)
Set USDC EmissionRouter receiving gauge_bps slice on USDC harvests. Pass empty to disable gauge routing.
| Param | Type | Description |
|---|---|---|
sink |
address |
veForge EmissionRouter (reward_token = USDC); empty = 100% to surplus path |
Returns / state: Updates emission_sink storage.
Access: owner only.
Events: EmissionSinkUpdated(sink).
Source (TARE-Stablecoin/src/CoilFeeRouter.vy:280-288):
@external
def set_emission_sink(sink: address):
ownable._check_owner()
self.emission_sink = sink
log EmissionSinkUpdated(sink=sink)
Example:
Cross-links: set_gauge_bps · harvest · set_keeper
set_keep_bps¶
CoilFeeRouter.set_keep_bps(new_bps)
Set USDC slice routed to keeper (CoilMakerStrategy) on each USDC harvest. FLYWHEEL 2.0 Keep path.
| Param | Type | Description |
|---|---|---|
new_bps |
uint256 |
Keep share of USDC harvest (0–MAX_KEEP_BPS, max 5000 = 50%) |
Returns / state: Updates keep_bps. No effect until keeper set and harvest runs.
Access: owner only. Reverts if new_bps > MAX_KEEP_BPS or new_bps + gauge_bps > MAX_GAUGE_BPS.
Events: KeepBpsUpdated(new_bps).
Source (TARE-Stablecoin/src/CoilFeeRouter.vy:295-306):
@external
def set_keep_bps(new_bps: uint256):
ownable._check_owner()
assert new_bps <= MAX_KEEP_BPS, "router: keep bps too high"
assert new_bps + self.gauge_bps <= MAX_GAUGE_BPS, "router: keep+gauge starves surplus"
self.keep_bps = new_bps
log KeepBpsUpdated(new_bps=new_bps)
Example:
Cross-links: set_keeper · set_gauge_bps · harvest
set_keeper¶
CoilFeeRouter.set_keeper(new_keeper)
Set CoilMakerStrategy receiving keep_bps USDC slice on harvest. Must NOT be bare MultiStrategyVault.
| Param | Type | Description |
|---|---|---|
new_keeper |
address |
CoilMakerStrategy; empty = Keep slice disabled |
Returns / state: Updates keeper storage.
Access: owner only.
Events: KeeperUpdated(keeper).
Source (TARE-Stablecoin/src/CoilFeeRouter.vy:309-319):
@external
def set_keeper(new_keeper: address):
ownable._check_owner()
self.keeper = new_keeper
log KeeperUpdated(keeper=new_keeper)
Example:
Cross-links: set_keep_bps · harvest · SurplusSplitter
Harvest¶
harvest¶
CoilFeeRouter.harvest(token) -> uint256
Permissionless. Claim Coil accrued fees for token, route USDC slices, convert to TARE, donate to engine surplus.
| Param | Type | Description |
|---|---|---|
token |
address |
Fee token to harvest (TARE or USDC only) |
Returns: uint256 — TARE donated this call (0 if nothing to route).
Flow:
- Pull from Coil if accrued_fees > 0, then sweep router's full token balance.
- TARE — donate as-is via donate_to_surplus.
- USDC — keep_bps → keeper, gauge_bps → emission_sink, remainder swapped USDC→TARE (EMA min_dy, max_swap_in cap).
Access: Any caller (@nonreentrant). Reverts on unsupported token.
Events: FeesHarvested, KeepFeesRouted, GaugeFeesRouted (USDC path).
Source (TARE-Stablecoin/src/CoilFeeRouter.vy:326-384):
@external
@nonreentrant
def harvest(token: address) -> uint256:
if staticcall COIL.accrued_fees(token) > 0:
extcall COIL.withdraw_accrued_fees(token)
bal: uint256 = staticcall IERC20(token).balanceOf(self)
if bal == 0:
return 0
# TARE: donate; USDC: keep/gauge slices then _swap_usdc_to_tare
if tare_amount > 0:
self._donate(tare_amount)
return tare_amount
Example:
Cross-links: set_keep_bps · set_gauge_bps · set_max_swap_in · TareEngine donate_to_surplus · pending
Escape hatch¶
recover¶
CoilFeeRouter.recover(token, to)
Owner escape hatch for non-core tokens stranded in router (unsupported Coil fee tokens).
| Param | Type | Description |
|---|---|---|
token |
address |
Token to recover — not TARE or USDC |
to |
address |
Recipient |
Returns / state: Transfers full router balance of token to to.
Access: owner only. Reverts if token is TARE/USDC (core flywheel tokens cannot be redirected).
Events: Recovered(token, to, amount).
Source (TARE-Stablecoin/src/CoilFeeRouter.vy:391-408):
@external
@nonreentrant
def recover(token: address, to: address):
ownable._check_owner()
assert token != TARE and token != USDC, "router: cannot recover core"
assert to != empty(address), "router: zero recipient"
bal: uint256 = staticcall IERC20(token).balanceOf(self)
extcall IERC20(token).transfer(to, bal, default_return_value=True)
log Recovered(token=token, to=to, amount=bal)
Example:
Cross-links: harvest
Views¶
pending¶
CoilFeeRouter.pending(token) -> uint256
Coil-accrued plus router-held balance available to harvest.
| Param | Type | Description |
|---|---|---|
token |
address |
Token to quote |
Returns: uint256 — coil.accrued_fees(token) + balanceOf(router).
Access: view, any caller.
Source (TARE-Stablecoin/src/CoilFeeRouter.vy:415-419):
@external
@view
def pending(token: address) -> uint256:
return staticcall COIL.accrued_fees(token) + staticcall IERC20(token).balanceOf(self)
Example:
Cross-links: harvest