Skip to content

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 harvestharvest(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 safetymin_dy from price_oracle(0) + slippage_bps; max_swap_in mandatory cap (0 = swap locked).
  • Core tokens non-recoverable — owner recover rejects 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 (0MAX_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:

router.set_slippage_bps(100)  # 1% below EMA oracle

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:

router.set_max_swap_in(500_000 * 10**6)  # 500k USDC per harvest swap

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 (0MAX_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:

router.set_gauge_bps(2000)  # 20% of USDC fees to gauge bribes

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:

router.set_emission_sink(emission_router)

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 (0MAX_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:

router.set_keep_bps(2500)  # 25% of USDC fees to CoilMakerStrategy

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:

router.set_keeper(coil_maker_strategy)

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. - USDCkeep_bpskeeper, gauge_bpsemission_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:

donated = router.harvest(usdc)
router.harvest(tare)

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:

router.recover(stranded_token, treasury)

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: uint256coil.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:

claimable = router.pending(usdc)

Cross-links: harvest