Skip to main content
Issue tokenized corporate bonds on Ethereum where investors subscribe with stablecoins, receive ERC-20 bond tokens, collect periodic coupon payments, and redeem principal at maturity — all transactions signed through Dfns managed wallets.

Get the code

dfns/dfns-solutions — bond-issuance

Overview

The system models a bond issuance with two actors:
  • Issuer — deploys the bond contract, manages the lifecycle (close issuance, fund coupons, return principal)
  • Investor — subscribes with stablecoins, claims bond tokens, collects coupons, redeems at maturity
The bond lifecycle works as follows:
  1. The Issuer deploys a stablecoin (EURC) and the Bond contract
  2. Investors subscribe by depositing EURC into escrow
  3. The Issuer closes the issuance — the interest accrual clock starts
  4. Investors claim ERC-20 bond tokens proportional to their investment
  5. Each coupon period, the Issuer funds and Investors claim their pro-rata share
  6. At maturity, the Issuer deposits principal and Investors redeem bonds for EURC
Issuer (Dfns Wallet)                Bond Contract               Investor (Dfns Wallet)
       |                                  |                              |
       |-- deploy bond ----------------->|                              |
       |                                  |                              |
       |                                  |<--- approve + subscribe ----|
       |                                  |     EURC held in escrow      |
       |                                  |                              |
       |-- close issuance -------------->|                              |
       |-- withdraw proceeds ----------->|                              |
       |                                  |                              |
       |                                  |<--- claim bond -------------|
       |                                  |     ERC-20 tokens minted     |
       |                                  |                              |
       |-- deposit coupon (periodic) --->|                              |
       |                                  |<--- claim coupon -----------|
       |                                  |     pro-rata EURC payout     |
       |                                  |                              |
       |-- return principal ------------>|  (at maturity)               |
       |                                  |<--- redeem -----------------|
       |                                  |     bonds burned, EURC back  |
The contract also includes default protection: if the Issuer fails to fund a coupon within a 5-day grace period, anyone can trigger a default flag that halts further operations.
ComponentTechnology
ContractsSolidity 0.8.28 (OpenZeppelin, Hardhat v3)
SigningDfns KMS via @dfns/sdk
ScriptsTypeScript (tsx, viem)
Web UISingle-page Express app with split Issuer/Investor dashboards
StablecoinERC-20 with mint/burn/pause (6 decimals)
NetworkEthereum Sepolia

Prerequisites

Project Structure

contracts/
  Bond.sol              ERC-20 bond token with full lifecycle management
  BondMath.sol          Library for accrued interest calculation
  StableCoin.sol        ERC-20 stablecoin with mint, burn, pause, and roles

scripts/
  dfns.ts               Shared Dfns API client and viem public client
  server.ts             Express server with REST API for the web UI
  ui.html               Single-page web UI (Issuer + Investor dashboards)
  e2e.ts                Non-interactive end-to-end lifecycle script
  deploy-stablecoin.ts  Deploy a stablecoin contract
  deploy-bond.ts        Deploy a bond contract with configurable terms
  mint-stablecoin.ts    Mint stablecoin to any address
  issuer-ops.ts         Interactive CLI for issuer operations
  holder-ops.ts         Interactive CLI for investor operations
  stablecoin-ops.ts     Interactive CLI for stablecoin management

test/
  Bond.test.ts          Core lifecycle tests
  BondExtended.test.ts  Over/under-subscription, grace period, interest math
  BondFull.test.ts      Full lifecycle with 4 investors
  BondSpecific.test.ts  Short-duration bond scenario

Configuration

1

Clone and install

git clone https://github.com/dfns/dfns-solutions.git
cd dfns-solutions/bond-issuance
npm install
2

Set up environment variables

Copy the example environment file and fill in your values:
cp .env.example .env
.env
# Dfns API Configuration
DFNS_API_URL=https://api.dfns.io
DFNS_ORG_ID=or-xxx-xxx
DFNS_AUTH_TOKEN=eyJ...
DFNS_CRED_ID=cred-xxx
DFNS_PRIVATE_KEY="-----BEGIN EC PRIVATE KEY-----\n...\n-----END EC PRIVATE KEY-----"

# Dfns Wallet IDs
ISSUER_WALLET_ID=wa-xxx-xxx
INVESTOR_WALLET_ID=wa-xxx-xxx

# Blockchain Configuration (optional)
SEPOLIA_RPC_URL=https://ethereum-sepolia-rpc.publicnode.com
VariableDescription
DFNS_API_URLDfns API base URL (api.dfns.io)
DFNS_ORG_IDYour Dfns organization ID. See how to locate it.
DFNS_AUTH_TOKENService account auth token. Refer to creating a service account.
DFNS_CRED_IDCredential ID for the service account signing key. Find it on the dashboard, on the Service Account page.
DFNS_PRIVATE_KEYService account private key for request signing (PEM format, including the ----BEGIN/END PRIVATE KEY----)
ISSUER_WALLET_IDWallet ID for the bond issuer (wa-xxxx...)
INVESTOR_WALLET_IDWallet ID for the bond investor (wa-xxxx...)
SEPOLIA_RPC_URLRPC endpoint (defaults to public Sepolia endpoint if omitted)
3

Compile contracts

npm run compile
4

Run tests

The test suite runs against a local Hardhat network (no Dfns credentials needed):
npm test
16 tests cover the full lifecycle: subscription, over/under-subscription, coupon claims, redemption, grace periods, default triggering, and interest math.

Deploy

Step 1: Deploy the stablecoin

npm run deploy:stablecoin
The script prompts for a name and symbol (defaults to “Euro Coin” / “EURC”) and deploys from the Issuer wallet. Save the contract address.

Step 2: Mint stablecoin to the investor

npm run mint:stablecoin
Enter the stablecoin address, the investor’s wallet address, and the amount to mint.

Step 3: Deploy the bond

npm run deploy:bond
The script prompts for bond parameters:
ParameterDefaultDescription
NameCorporate BondBond token name
SymbolCBBond token symbol
CurrencyStablecoin contract address (required)
Notional100Face value per bond in stablecoin units
APR400 (4%)Annual rate in basis points
Coupon Frequency7,776,000 (3 months)Seconds between coupon periods
Duration31,536,000 (1 year)Seconds until maturity
Cap1,000,000Maximum stablecoin to raise
Save the bond contract address.

End-to-End Bond Lifecycle

Step 1: Investor subscribes

npm run ops:holder
# Select option 2: Subscribe
# Enter the subscription amount in stablecoin units
This approves the Bond contract to spend the investor’s EURC, then calls subscribe(). The stablecoin is held in escrow.

Step 2: Issuer closes issuance

npm run ops:issuer
# Select option 2: Close Issuance
This locks the subscription total, starts the interest accrual clock, and calculates the coupon amount per period.

Step 3: Issuer withdraws proceeds

npm run ops:issuer
# Select option 3: Withdraw Proceeds
The issuer receives the raised stablecoin.

Step 4: Investor claims bond tokens

npm run ops:holder
# Select option 3: Claim Bond
The investor receives ERC-20 bond tokens. The number of tokens is calculated as:
bondTokens = (investmentAmount * 10^decimals) / notional
Where decimals is 6 (matching the stablecoin).

Step 5: Coupon payments (each period)

Issuer deposits the coupon:
npm run ops:issuer
# Select option 5: Deposit Coupon
The required amount is calculated automatically: (totalPrincipal * APR * frequency) / (365 days * 10000). Investor claims:
npm run ops:holder
# Select option 4: Claim Coupon
The CLI auto-detects all due, funded, unclaimed coupons and claims them in sequence. Each investor’s share is proportional to their bond holdings:
share = (userBalance / totalSupply) * couponAmount

Step 6: Redemption at maturity

Issuer returns principal:
npm run ops:issuer
# Select option 4: Return Principal
Investor redeems:
npm run ops:holder
# Select option 5: Redeem
Bond tokens are burned and the original investment amount is returned in EURC.

Web UI

The browser UI covers the full bond lifecycle. It shows two dashboards side by side — Issuer (blue) and Investor (green) — with numbered steps to follow in order.
npm run ui
Open http://localhost:3000. The UI calls a lightweight Express server that signs and broadcasts transactions via the Dfns API. Bond status auto-refreshes every 5 seconds. Each step in the UI maps to a contract function call:
  1. Deploy StableCoin — deploy the EURC contract
  2. Mint EURC — fund both wallets
  3. Deploy Bond — configure face value, APR, coupon frequency, and duration
  4. Subscribe — investor deposits EURC into escrow
  5. Close Issuance — lock subscriptions, start interest clock
  6. Withdraw Proceeds — issuer collects raised EURC
  7. Claim Bond Tokens — investor mints ERC-20 bond tokens
  8. Deposit Coupon — issuer funds the next coupon period
  9. Claim Coupon — investor collects pro-rata interest (auto-detects coupon index)
  10. Return Principal — issuer deposits EURC for redemption
  11. Redeem — investor burns bonds, gets principal back

End-to-End Script

Run the full lifecycle non-interactively:
npm run e2e
This deploys both contracts, mints stablecoins, subscribes, closes issuance, claims bonds, deposits and claims a coupon — all in a single run. Useful for verifying the setup end to end.

Smart Contract Reference

Bond.sol

An ERC-20 token representing a corporate bond with full lifecycle management:
FunctionCallerDescription
subscribe(amount)InvestorDeposit stablecoin into escrow during issuance
closePrimaryIssuance()IssuerLock subscriptions, start interest clock
withdrawProceeds()IssuerWithdraw raised stablecoin
claimBond()InvestorMint bond tokens proportional to subscription
depositCoupon()IssuerFund the next coupon period
claimCoupon(index)InvestorClaim pro-rata coupon payment
returnPrincipal(amount)IssuerDeposit stablecoin for redemption
redeem()InvestorBurn bonds, receive principal
checkDefault(index)AnyoneTrigger default if coupon unfunded past grace period
accruedInterest(user)ViewReal-time accrued interest for a holder
timeToNextCoupon()ViewSeconds until the next coupon date
Bond lifecycle: OpenIssuance ClosedCouponsMaturity / Redemption Default path: Coupon DueGrace Period (5 days)Default

BondMath.sol

Library for interest calculations:
Accrued = (principal * APR * timeElapsed) / (365 days * 10000)
APR is in basis points (400 = 4%). The contract treats a year as exactly 365 days.

StableCoin.sol

ERC-20 stablecoin used as the settlement currency:
FeatureImplementation
StandardERC-20 with 6 decimals (matching USDC/EURC convention)
MintingRole-based via MINTER_ROLE — only authorized addresses can mint
BurningAny holder can burn their own tokens via ERC20Burnable
PausingOwner can pause/unpause all transfers
PermitsERC-2612 gasless approvals via ERC20Permit
Access controlOpenZeppelin AccessControl for role management

How Dfns Is Used

All on-chain transactions are signed and broadcast through the Dfns API. No private keys are stored locally.
OperationDfns SDK call
Get wallet addresswallets.getWallet({ walletId })
Deploy contractwallets.broadcastTransaction({ walletId, body: { kind: "Evm", data: encodedBytecode } })
Call contract functionwallets.broadcastTransaction({ walletId, body: { kind: "Evm", to: address, data: encodedCalldata } })
The scripts use viem to encode deployment data and function calldata, then broadcast via Dfns. A viem PublicClient reads on-chain state (balances, bond status, coupon dates) without requiring signing.

Adapting for Production

Different networks — Replace the RPC URL in .env with one for your target network (Ethereum mainnet, Polygon, Arbitrum). Update the chain import in scripts/dfns.ts. Real stablecoins — Replace the demo StableCoin contract with USDC or EURC addresses. The Bond contract accepts any ERC-20 with 6 decimals as the settlement currency. Multiple investors — The system supports any number of investors. Each uses a separate Dfns wallet. Add more wallet IDs to .env or build a backend that maps users to Dfns wallets (see the bank-custody blueprint for this pattern). Secondary trading — Bond tokens are standard ERC-20 and can be traded on any DEX. For institutional use, consider an RFQ model (UniswapX or CowSwap) where professional market makers provide quotes based on yield-to-maturity. Approval policies — Dfns policies can enforce approval quorums or velocity limits on sensitive operations (closing issuance, large coupon deposits). This adds governance without changing the smart contracts.
Last modified on March 6, 2026