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
Contract
TharwaBondVaultV1.sol
Standard
ERC-1155 with financial metadata
Address
0xAc02FF90bC709A134cD4Ad0b50BaB8be9e0f504e
(Mainnet)
Audit
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:
Key Insight: Each bond's NFT ID equals its maturity timestamp truncated to UTC midnight
Example: A 90-day bond purchased on January 1st, 2025 would have token ID 1712102400
(April 1st, 2025 at 00:00:00 UTC)
Why This Matters:
Predictable IDs: Anyone can calculate a bond's ID without querying the contract
No Collisions: Same maturity date = same token ID = fungible bonds
Easy Integration: External protocols can work with bonds without complex lookups
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 calculationsmaturityAmount
: 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:
Purchase below par: Buy bond for less than face value
Hold to maturity: No coupon payments during term
Redeem at par: Receive full face value at maturity
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:
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 bondtotalTime
: Full bond durationpenalty
: Decreases linearly to 0% at maturity
Exit Amount:
exitAmount = principal × (10000 - penalty) / 10000
Why Penalty on Principal, Not Face Value?
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];
}
}
Production Status thBonds are live on Ethereum mainnet with over $XXX TVL and 0 security incidents since launch.
Last updated