Skip to content

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

13 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Fixed Income Protocol (EVM)

An on-chain fixed-income bond protocol built with Solidity and Foundry. Issuers create bond products; investors purchase notes that earn a simple-interest coupon and are redeemable at maturity.

Deployed Contracts

Hoodi Testnet

Contract Address
FixedIncomeFactory 0xA7c59f010700930003b33aB25a7a0679C860f29c

Explorer: explorer.hoodi.ethpandaops.io

Hedera Testnet

Contract Address
FixedIncomeFactory 0xfd3FFdf299b8E73e702A2e592Cb9B4D4ff9E44A1

Explorer: hashscan.io/testnet

The factory automatically deploys HoldingNFT and FixedIncomeEventEmitter on construction.


Protocol Architecture

Contract Overview

FixedIncomeFactory
    |-- owns --> HoldingNFT
    |-- owns --> FixedIncomeEventEmitter
    |-- deploys --> FixedIncomeProduct (one per bond issuance)

FixedIncomeFactory

The single entry point for the protocol. Deploying the factory also deploys the shared HoldingNFT registry and FixedIncomeEventEmitter. Any address can call createProduct to become the issuer of a new bond.

Key responsibilities:

  • Validate product parameters (faceValue > 0, maxSupply > 0, maturity > now)
  • Deploy a FixedIncomeProduct and set the caller as its owner
  • Authorize the new product on HoldingNFT and FixedIncomeEventEmitter
  • Maintain a paginated registry of all deployed products (allProducts, getProducts)

FixedIncomeProduct

One contract per bond issuance. Holds the payment token reserve and enforces all bond mechanics.

Immutable parameters (set at construction, never change):

Parameter Description
paymentToken ERC-20 token used for purchase and redemption (e.g. USDC)
faceValue Price per note in paymentToken units
couponBps Annual coupon rate in basis points (500 = 5%)
maturity Unix timestamp after which notes can be redeemed
maxSupply Maximum notes that can ever be sold
issuedAt Block timestamp at deployment

Mutable state:

  • totalNotesSold / totalNotesRedeemed -- cumulative counters
  • investorNoteCount -- per-address note balance
  • netAssetValue -- issuer-reported NAV (informational only)

Coupon math is delegated to the CouponMath library. All interest is simple (linear), not compound, on a 365-day year basis:

coupon = faceValue x couponBps x elapsedSeconds
         -----------------------------------------
               (365 days in seconds) x 10_000

HoldingNFT

A shared ERC-721 registry (name: "FixedIncomeHolding", symbol: "FIH"). Each token represents ownership of exactly one note in a FixedIncomeProduct. Only factory-authorized products can mint or burn tokens.

Each token stores a HoldingInfo struct:

  • issuingProduct -- which product the note belongs to
  • faceValue -- face value at time of purchase
  • purchasedAt -- block timestamp of the purchase

FixedIncomeEventEmitter

A protocol-level event sink shared across all products. Emits canonical events that indexers can subscribe to from a single contract address, regardless of how many products exist.

Events emitted: ProductCreated, NotePurchased, NoteRedeemed, NAVUpdated.

Only the factory and authorized products can call the emit functions.

CouponMath Library

A pure math library (src/libraries/CouponMath.sol) with three functions:

Function Description
accruedCoupon(principal, rateBps, start, end) Coupon for an arbitrary time window
termCoupon(principal, rateBps, issuedAt, maturity) Full-term coupon at maturity
accruedCouponAt(principal, rateBps, issuedAt, maturity, queryTime) Accrued coupon up to queryTime, capped at maturity

Bond Lifecycle

1. Product Creation (Issuer)

The issuer calls FixedIncomeFactory.createProduct(...):

address product = factory.createProduct(
    address(usdc),              // paymentToken
    1_000e6,                    // faceValue: 1,000 USDC per note
    500,                        // couponBps: 5% annual
    block.timestamp + 365 days, // maturity
    100                         // maxSupply: up to 100 notes
);

The factory deploys a FixedIncomeProduct, authorizes it on HoldingNFT and the event emitter, and emits ProductCreated.

2. Note Purchase (Investor)

The investor approves the product contract for faceValue tokens, then calls purchase():

usdc.approve(product, 1_000e6);
uint256 tokenId = FixedIncomeProduct(product).purchase();
  • faceValue tokens are transferred from the investor to the product contract (held as reserve)
  • A HoldingNFT is minted to the investor
  • totalNotesSold and investorNoteCount[investor] are incremented
  • NotePurchased is emitted on both the product and the event emitter

3. Reserve Funding (Issuer, before or at maturity)

Principal is already held in the contract from purchases. The issuer must deposit the coupon portion before investors can redeem:

uint256 coupon = product.couponPerNote(); // e.g. 50 USDC for a 1,000 USDC / 5% / 1yr bond
usdc.approve(product, coupon * notesOutstanding);
product.depositReserve(coupon * notesOutstanding);

The reserve required to cover all outstanding notes is readable at any time:

product.requiredReserve(); // totalNotesSold * (faceValue + couponPerNote)

4. Redemption (Investor, at or after maturity)

Once block.timestamp >= maturity and the reserve is funded, the investor redeems their token:

product.redeem(tokenId);
  • The HoldingNFT is burned
  • faceValue + couponPerNote() tokens are transferred to the investor
  • totalNotesRedeemed and investorNoteCount[investor] are updated
  • NoteRedeemed is emitted on both the product and the event emitter

Redemption reverts if:

  • Called before maturity -- NotMatured()
  • Caller is not the token owner -- NotTokenOwner()
  • Reserve balance is below the payout -- InsufficientReserve()

Position Queries

At any point, an investor's current exposure can be read:

IFixedIncomeProduct.InvestorPosition memory pos = product.getInvestorPosition(investor);
// pos.noteCount            -- number of notes held
// pos.principalValue       -- noteCount * faceValue
// pos.currentAccruedCoupon -- coupon accrued across all notes up to now
// pos.redemptionValue      -- full payout if redeemed today (post-maturity)

Full bond state snapshot:

IFixedIncomeProduct.BondInfo memory info = product.getBondInfo();

Deployment Steps

Prerequisites

git clone <repo>
cd fixed-income-evm
forge install

Build and Test

forge build
forge test -vv

All 61 tests should pass (44 integration in FixedIncome.t.sol + 17 unit/fuzz in CouponMath.t.sol).

Run the Demo (local simulation, no node required)

The demo script exercises the full bond lifecycle -- create, purchase, warp to maturity, redeem -- using vm.startPrank and vm.warp. No running node or private key is needed.

forge script script/Demo.s.sol --tc Demo -vv

Deploy to a Live Network

Deploy the factory (which automatically deploys HoldingNFT and FixedIncomeEventEmitter):

forge script script/Deploy.s.sol \
    --rpc-url <RPC_URL> \
    --private-key <PRIVATE_KEY> \
    --broadcast \
    --verify

Hedera testnet requires the --legacy flag (no EIP-1559 support):

forge script script/Deploy.s.sol \
    --rpc-url https://testnet.hashio.io/api \
    --private-key <PRIVATE_KEY> \
    --broadcast \
    --legacy

The script logs the three deployed contract addresses:

FixedIncomeFactory     : 0x...
HoldingNFT             : 0x...
FixedIncomeEventEmitter: 0x...

After deployment, any issuer can create a product on-chain via cast:

cast send <FACTORY_ADDRESS> \
    "createProduct(address,uint256,uint256,uint256,uint256)" \
    <PAYMENT_TOKEN> <FACE_VALUE> <COUPON_BPS> <MATURITY_UNIX> <MAX_SUPPLY> \
    --rpc-url <RPC_URL> \
    --private-key <ISSUER_PRIVATE_KEY>

Project Layout

src/
  FixedIncomeFactory.sol       # Protocol entry point
  FixedIncomeProduct.sol       # Bond issuance logic
  HoldingNFT.sol               # ERC-721 note registry
  FixedIncomeEventEmitter.sol  # Protocol-level event sink
  interfaces/
    IFixedIncomeProduct.sol
    IHoldingNFT.sol
    IFixedIncomeEventEmitter.sol
  libraries/
    CouponMath.sol             # Simple-interest coupon math
test/
  FixedIncome.t.sol            # Integration tests
  CouponMath.t.sol             # Unit + fuzz tests for the math library
script/
  Deploy.s.sol                 # Production deployment
  Demo.s.sol                   # End-to-end lifecycle simulation

Frontend

A web interface for the protocol lives in frontend/. Built with React, Vite, wagmi v2, and RainbowKit.

Features

  • Connect wallet (MetaMask, WalletConnect, etc.)
  • Browse all bond issuances — face value, APY, maturity, supply, status
  • Purchase notes (ERC-20 approve + purchase in two steps)
  • Redeem matured notes — token IDs resolved from on-chain events
  • Issue new bonds via the factory
  • Portfolio view — positions across all bonds, accrued coupon, redemption value
  • Issuer panel — deposit reserve, update NAV

Running locally

cd frontend
cp .env.example .env
# Fill in VITE_FACTORY_ADDRESS and VITE_WALLETCONNECT_PROJECT_ID

npm install
npm run dev

Open http://localhost:5173 and connect your wallet to the Hoodi network.

Environment variables

Variable Description
VITE_FACTORY_ADDRESS Deployed FixedIncomeFactory address
VITE_WALLETCONNECT_PROJECT_ID Project ID from cloud.walletconnect.com

Deploying to Vercel

  1. Push the repo to GitHub
  2. Import the project in Vercel — set Root Directory to frontend
  3. Add the two environment variables above in Settings → Environment Variables
  4. Deploy — every git push triggers a redeploy automatically

License

MIT

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages