Coupon Token

CouponToken is the stateful contract driving the BT/CT lifecycle — minting, redemption, interest accounting, and post-expiry behavior.

circle-info

Source: src/fira_bonding/core/YieldContracts/CouponToken.sol, src/fira_bonding/core/YieldContracts/InterestManagerCT.sol

Contract summary

CT extends InterestManagerCT and FiraERC20. Like BT, the FW, BT, factory, expiry, and doCacheIndexSameBlock fields are all immutable. All external mutating functions use nonReentrant.

Minting BT+CT

mintBC(receiverBT, receiverCT) takes FW that has been pre-transferred to the CT contract, converts the FW amount to underlying asset units via the BC index, and mints equal BT and CT. The BC index is max(FW.exchangeRate(), storedIndex) — monotonically non-decreasing.

mintBCMulti does the same for multiple receivers in one call.

Redeeming BT+CT

redeemBC(receiver) burns BT from address(this) (must be pre-transferred) and burns CT if not expired. FW is returned based on the current BC index.

Post-expiry: The redeemable FW amount is calculated at the current BC index, but the excess between the post-expiry first index and the current index accrues as treasury interest. Post-expiry yield goes to the protocol, not to redeemers.

Interest accounting

InterestManagerCT tracks per-user interest via UserInterest { index, accrued }. On every CT transfer (_beforeTokenTransfer), interest is distributed to both sender and receiver:

interest = principal × (currentIndex - prevIndex) / (prevIndex × currentIndex)

Claims go through _doTransferOutInterest, which deducts interestFeeRate and sends the fee to treasury.

Key design decisions

  • Interest is based on CT balance, not FW balance

  • Protocol fees are taken at claim time, not at accrual time

  • _distributeInterestForTwo skips address(0) and address(this) to avoid phantom accounting

BC index caching

If doCacheIndexSameBlock is true, the BC index is only updated once per block. This prevents sandwich attacks where the FW exchange rate is manipulated within a block to extract value from CT holders.

Post-expiry state

_setPostExpiryData() is called on the first interaction after expiry. It snapshots the BC index and reward indices. After this, the interest index is frozen to postExpiry.firstBCIndex, and all subsequent yield flows to treasury.

redeemInterestAndRewardsPostExpiryForTreasury() sweeps all post-expiry interest to the treasury.

Inherited contracts

  • InterestManagerCT — Per-user interest tracking

  • FiraERC20 — ERC-20 with reentrancy guard

Last updated