Version 2 Released! Our biggest documentation upgrade to date.

thBonds Implementation

Tharwa Bonds solve a fundamental problem in DeFi: how to offer predictable, fixed yields without relying on token emissions or unsustainable farming mechanics. This page explains how our bond system works, from the economic model to the technical implementation.

What Problem Do Tharwa Bonds Solve?

Traditional DeFi yield comes from:

  • Liquidity mining: Unsustainable token emissions

  • Lending protocols: Variable rates that can crash during stress

  • Yield farming: Complex strategies with smart contract risks

Tharwa Bonds offer something different: Fixed yields backed by real-world asset performance, structured like traditional bonds but with DeFi composability.

The Bond Model Explained

How Traditional Bonds Work:

  • Buy bond for $950

  • Receive $1000 at maturity

  • $50 profit = yield

Problems for DeFi:

  • No composability

  • No secondary markets

  • Complex legal structures

Contract Overview

Property
Value

Contract

TharwaBondVaultV1.sol

Standard

ERC-1155 with financial metadata

Address

0xAc02FF90bC709A134cD4Ad0b50BaB8be9e0f504e (Mainnet)

Security

0 Critical, 0 High findings


How Tharwa Bonds Work: Technical Architecture

The Token ID System: Why It's Brilliant

Tharwa Bonds use a unique approach to NFT IDs that makes them both predictable and composable:

Why This Matters:

  1. Predictable IDs: Anyone can calculate a bond's ID without querying the contract

  2. No Collisions: Same maturity date = same token ID = fungible bonds

  3. Easy Integration: External protocols can work with bonds without complex lookups

  4. Metadata Efficiency: Token ID contains the maturity information

Bond Configuration System

Instead of hardcoding bond parameters, Tharwa uses a flexible configuration system:

enum BondDuration {
    NinetyDays,      // 0: 90-day bonds
    OneEightyDays,   // 1: 180-day bonds  
    ThreeSixtyDays   // 2: 360-day bonds
}

struct BondConfig {
    uint256 price;           // Discounted purchase price (e.g., 0.96e18 = 96 cents)
    uint256 cap;             // Maximum total issuance (prevents over-issuance)
    uint256 issued;          // Current amount issued (tracks against cap)
    bool paused;             // Emergency pause for individual bond types
    string uri;              // Metadata URI for NFT marketplaces
}

Real-World Example:

bondConfigs[BondDuration.NinetyDays] = BondConfig({
    price: 0.96e18,        // Buy for 96 cents
    cap: 1_000_000e18,     // Max 1M thUSD issuance
    issued: 0,             // Nothing issued yet
    paused: false,         // Active
    uri: "metadata/90day"  // NFT metadata
});

What this means:

  • Users pay 0.96 thUSD to buy a bond

  • They receive 1.00 thUSD at maturity (90 days later)

  • Maximum 1M thUSD worth of these bonds can be issued

  • 4.17% return over 90 days ≈ 3.94% APY

User Bond Tracking

For each bond purchase, the contract stores essential information:

struct UserBond {
    uint256 principal;       // How much thUSD the user originally paid
    uint256 maturityAmount;  // How much thUSD they'll receive at maturity
    uint256 purchaseTime;    // When they bought it (for penalty calculations)
}

// Mapping: user address => token ID => bond details
mapping(address => mapping(uint256 => UserBond)) public userBonds;

Why We Track This:

  • principal: Needed for early exit penalty calculations

  • maturityAmount: The guaranteed payout (always 1:1 with face value)

  • purchaseTime: Determines how much penalty decays over time

Example User Bond:

userBonds[0x123...][1712102400] = UserBond({
    principal: 960e18,      // Paid 960 thUSD
    maturityAmount: 1000e18, // Will receive 1000 thUSD
    purchaseTime: 1704067200 // Bought on January 1st, 2025
});

Token ID Calculation: The Technical Deep Dive

The token ID system is one of Tharwa Bonds' most innovative features. Let's break down exactly how it works and why it's designed this way:

The Algorithm

function _calculateTokenId(BondDuration duration) internal view returns (uint256) {
    uint256 durationSeconds;
    
    if (duration == BondDuration.NinetyDays) {
        durationSeconds = 90 days;
    } else if (duration == BondDuration.OneEightyDays) {
        durationSeconds = 180 days;
    } else if (duration == BondDuration.ThreeSixtyDays) {
        durationSeconds = 360 days;
    }
    
    uint256 maturityTime = block.timestamp + durationSeconds;
    
    // This is the key: truncate to UTC midnight for consistent IDs
    return (maturityTime / 1 days) * 1 days;
}

Why Truncate to Midnight?

Without Truncation:

  • Bond bought at 14:30 UTC → Token ID: 1712156200

  • Bond bought at 09:15 UTC → Token ID: 1712137300

  • Same maturity day, different token IDs

  • No fungibility between bonds

  • Complex secondary markets

Result: Fragmented liquidity and poor user experience

Real-World Example

Scenario: Two users buy 90-day bonds on the same day:

  • User A buys at 2:30 PM UTC

  • User B buys at 9:15 AM UTC

Traditional Approach: Different token IDs, no fungibility Tharwa Approach: Same token ID (maturity_date_at_midnight), perfect fungibility

Impact: User A can sell to User B seamlessly, or both can trade on secondary markets as the same asset.

function _calculateTokenId(BondDuration duration) internal view returns (uint256) {
    uint256 durationSeconds;
    
    if (duration == BondDuration.NinetyDays) {
        durationSeconds = 90 days;
    } else if (duration == BondDuration.OneEightyDays) {
        durationSeconds = 180 days;
    } else if (duration == BondDuration.ThreeSixtyDays) {
        durationSeconds = 360 days;
    }
    
    uint256 maturityTime = block.timestamp + durationSeconds;
    
    // Truncate to UTC midnight for consistent token IDs
    return (maturityTime / 1 days) * 1 days;
}

Bond Economics: How Fixed Yields Work

The Discount Bond Model

Tharwa Bonds use a discount bond structure - the foundation of institutional fixed-income markets:

How Discount Bonds Work:

  1. Purchase below par: Buy bond for less than face value

  2. Hold to maturity: No coupon payments during term

  3. Redeem at par: Receive full face value at maturity

  4. Profit = Discount: Yield comes from the price difference

Why This Model:

  • Simple structure: No complex payment schedules

  • Predictable returns: Known yield from day one

  • Capital efficient: No need for coupon reserves

  • Institutional standard: Familiar to traditional investors

Yield Source & Sustainability

Issuance Caps: Preventing Over-Leverage

Each bond type has strict issuance caps to prevent over-leverage:

Duration
Current Cap
Purpose
Risk Management

90 days

1M thUSD

Short-term liquidity

Minimal duration risk

180 days

2M thUSD

Medium-term allocation

Balanced risk/reward

360 days

4M thUSD

Long-term commitment

Higher cap for better yields

Why Caps Matter:

  • Prevents over-issuance beyond portfolio capacity

  • Maintains yield sustainability by limiting obligations

  • Protects protocol solvency during stress scenarios

  • Enables gradual scaling as the protocol matures


Core Functions

Bond Purchase

function purchaseBond(
    BondDuration duration,
    uint256 faceAmount,
    address receiver
) external whenNotPaused nonReentrant returns (uint256 tokenId) {
    BondConfig storage config = bondConfigs[duration];
    
    // Validation
    if (faceAmount == 0) revert ZeroAmount();
    if (faceAmount < MINIMUM_FACE_AMOUNT) revert BelowMinimumFaceAmount();
    if (config.issued + faceAmount > config.cap) revert CapExceeded();
    if (config.paused) revert BondPaused();
    
    // Calculate purchase price
    uint256 purchasePrice = faceAmount.mulDiv(config.price, 1e18);
    tokenId = _calculateTokenId(duration);
    
    // Check for maturity collision
    if (exists(tokenId)) revert MaturityCollision();
    
    // Update state
    config.issued += faceAmount;
    userBonds[receiver][tokenId] = UserBond({
        principal: purchasePrice,
        maturityAmount: faceAmount,
        purchaseTime: block.timestamp
    });
    
    // Transfer payment and mint bond
    IERC20(THUSD).safeTransferFrom(msg.sender, address(this), purchasePrice);
    _mint(receiver, tokenId, 1, "");
    
    emit BondIssued(receiver, tokenId, faceAmount, purchasePrice, duration);
}

Early Exit System: Balancing Liquidity and Commitment

The early exit system is designed to provide liquidity while protecting the protocol from excessive redemption pressure.

The Penalty Calculation Algorithm

function earlyExit(uint256 tokenId, address receiver) 
    external whenNotPaused nonReentrant returns (uint256 amount) 
{
    // Validation checks
    if (block.timestamp >= tokenId) revert BondMatured();
    if (balanceOf(msg.sender, tokenId) == 0) revert InsufficientBalance();
    
    UserBond memory bond = userBonds[msg.sender][tokenId];
    
    // Core penalty calculation
    uint256 timeElapsed = block.timestamp - bond.purchaseTime;
    uint256 totalTime = tokenId - bond.purchaseTime;
    uint256 penalty = INITIAL_PENALTY.mulDiv(totalTime - timeElapsed, totalTime);
    
    // Apply penalty to principal (not face value)
    amount = bond.principal.mulDiv(10000 - penalty, 10000);
    
    // State cleanup and transfer
    delete userBonds[msg.sender][tokenId];
    _burn(msg.sender, tokenId, 1);
    IERC20(THUSD).safeTransfer(receiver, amount);
    
    emit EarlyExit(msg.sender, tokenId, amount, penalty);
}

Understanding the Penalty Formula

Penalty Calculation:

timeElapsed = now - purchaseTime
totalTime = maturityTime - purchaseTime  
penalty = INITIAL_PENALTY × (totalTime - timeElapsed) / totalTime

Key Variables:

  • INITIAL_PENALTY: 20% (2000 basis points)

  • timeElapsed: How long you've held the bond

  • totalTime: Full bond duration

  • penalty: Decreases linearly to 0% at maturity

Exit Amount:

exitAmount = principal × (10000 - penalty) / 10000

Why Penalty on Principal, Not Face Value?

Critical Design Decision: Penalties are applied to the principal (amount paid), not the face value (amount due at maturity).

Example: 360-day bond

  • Principal: 0.88 thUSD (what user paid)

  • Face Value: 1.00 thUSD (what user gets at maturity)

  • 20% penalty: Applied to 0.88 thUSD, not 1.00 thUSD

Why This Matters: Users never lose more than they invested, even with maximum penalty. This protects against catastrophic losses while still discouraging early exits.

Secondary Market Alternative

Instead of early exit penalties, users can trade bonds on secondary markets:

Direct Protocol Exit:

  • Immediate: Get thUSD instantly

  • Penalty: Time-decaying penalty applied

  • Predictable: Exact amount calculable

  • Guaranteed: Protocol always honors exits

Best for: Emergency liquidity needs

Maturity Redemption

function redeemBond(uint256 tokenId, address receiver) 
    external whenNotPaused nonReentrant returns (uint256 amount) 
{
    if (block.timestamp < tokenId) revert BondNotMatured();
    if (balanceOf(msg.sender, tokenId) == 0) revert InsufficientBalance();
    
    UserBond memory bond = userBonds[msg.sender][tokenId];
    amount = bond.maturityAmount;
    
    // Clean up state
    delete userBonds[msg.sender][tokenId];
    _burn(msg.sender, tokenId, 1);
    
    // Transfer maturity amount (1:1 with face value)
    IERC20(THUSD).safeTransfer(receiver, amount);
    
    emit BondRedeemed(msg.sender, tokenId, amount);
}

Bond Metadata

On-Chain Metadata Structure

Each bond NFT contains rich metadata for composability:

{
  "name": "Tharwa Bond - 90 Day",
  "description": "Fixed-term bond redeemable for thUSD at maturity",
  "image": "https://metadata.tharwa.finance/bonds/90day.png",
  "attributes": [
    {
      "trait_type": "Duration",
      "value": "90 days"
    },
    {
      "trait_type": "Maturity Date", 
      "value": "2025-12-20T00:00:00Z"
    },
    {
      "trait_type": "Face Amount",
      "value": "1000.00 thUSD"
    },
    {
      "trait_type": "Purchase Price",
      "value": "960.00 thUSD"
    },
    {
      "trait_type": "APY",
      "value": "3.94%"
    },
    {
      "trait_type": "Status",
      "value": "Active"
    }
  ]
}

Dynamic Metadata Updates

function uri(uint256 tokenId) public view override returns (string memory) {
    // Generate dynamic metadata based on current bond state
    return string(abi.encodePacked(
        "https://metadata.tharwa.finance/bonds/",
        tokenId.toString(),
        ".json"
    ));
}

Integration Examples

Purchase Bond via Contract

contract BondIntegration {
    ITharwaBonds constant bonds = ITharwaBonds(0xAc02FF90bC709A134cD4Ad0b50BaB8be9e0f504e);
    IERC20 constant thUSD = IERC20(0x76972F054aB43829064d31fF5f3AcC5Fabe57FE8);
    
    function buyBond(uint256 faceAmount) external {
        // Approve thUSD spending
        thUSD.approve(address(bonds), type(uint256).max);
        
        // Purchase 90-day bond
        uint256 tokenId = bonds.purchaseBond(
            ITharwaBonds.BondDuration.NinetyDays,
            faceAmount,
            msg.sender
        );
        
        emit BondPurchased(msg.sender, tokenId, faceAmount);
    }
}

Secondary Market Trading

contract BondMarketplace {
    function listBond(uint256 tokenId, uint256 price) external {
        // Verify ownership
        require(bonds.balanceOf(msg.sender, tokenId) > 0, "Not owner");
        
        // Create listing
        listings[tokenId] = Listing({
            seller: msg.sender,
            price: price,
            active: true
        });
    }
    
    function buyListing(uint256 tokenId) external {
        Listing memory listing = listings[tokenId];
        require(listing.active, "Not active");
        
        // Transfer payment
        thUSD.transferFrom(msg.sender, listing.seller, listing.price);
        
        // Transfer bond NFT
        bonds.safeTransferFrom(listing.seller, msg.sender, tokenId, 1, "");
        
        delete listings[tokenId];
    }
}

Developer Resources

Last updated