On-chain core of Heliobond — a green bond platform built on Stellar. Two Soroban smart contracts manage the full lifecycle from project registration through investor deposits and capital disbursement.
| Contract | Crate | Purpose |
|---|---|---|
ProjectRegistry |
project_registry |
Stores project metadata and oracle-updated impact scores |
InvestmentVault |
investment_vault |
SEP-41 token vault; accepts USDC and mints HBS shares |
graph TD
subgraph Actors
Admin["Admin (owner)"]
Whitelister["Whitelister"]
Investor["Investor"]
Oracle["Oracle (backend)"]
end
subgraph ProjectRegistry
PR_Store["Storage\n───────────────\nDataKey::Whitelister\nDataKey::ProjectCounter\nDataKey::Project(id)\nDataKey::Whitelist(addr)"]
PR[ProjectRegistry\ncontract]
end
subgraph InvestmentVault
IV_Store["Storage\n───────────────\nVaultKey::UsdcSac\nVaultKey::Registry\nVaultKey::TotalInvestments\nVaultKey::ProjectInvestment(id)"]
IV[InvestmentVault\ncontract\nHBS token]
end
USDC["USDC SAC\n(Stellar Asset Contract)"]
Whitelister -- "set_whitelist()" --> PR
Investor -- "create_project()" --> PR
Oracle -- "update_impact_score()" --> PR
Admin -- "fund_project()" --> IV
Investor -- "deposit() / withdraw()" --> IV
IV -- "get_project() / total_projects()" --> PR
IV -- "transfer() / balance()" --> USDC
Admin -- "owns both contracts via stellar-access Ownable" --> PR
Admin -- "owns both contracts via stellar-access Ownable" --> IV
Data flow summary
- The Whitelister approves project creator addresses via
set_whitelist. - A whitelisted creator calls
create_project, which records metadata inProjectRegistryand returns a sequentialproject_id. - The Oracle (off-chain backend) calls
update_impact_scoreto setcredit_qualityandgreen_impact(both 0–100) for each project. - Investors call
depositonInvestmentVault, which pulls USDC and mints HBS shares proportional to vault NAV. - The Admin calls
fund_project, which cross-callsProjectRegistryto fetch the project owner address and then transfers USDC from the vault to that owner. - Investors call
withdrawto burn HBS shares and redeem liquid USDC.
Constructor
__constructor(admin: Address, whitelister: Address)
Sets the Ownable owner to admin and records the whitelister address.
Public functions
| Function | Auth required | Description |
|---|---|---|
set_whitelist(account, status) |
Whitelister |
Grant or revoke whitelist status for a creator address |
create_project(creator, uri) |
creator |
Register a new project; panics if caller not whitelisted; returns project_id (u32, auto-incremented) |
get_project(id) |
none | Return ProjectData for a given project_id; panics if not found |
total_projects() |
none | Return the current project counter |
update_impact_score(project_id, credit_quality, green_impact) |
Admin (#[only_owner]) |
Set impact scores (0–100 each) for a project |
get_all_projects() |
none | Return Vec<(u32, ProjectData)> of all registered projects |
transfer_ownership(new_owner) |
Admin |
Transfer contract ownership (via stellar-access Ownable) |
ProjectData struct
pub struct ProjectData {
pub owner: Address, // project creator
pub uri: String, // off-chain metadata URI
pub credit_quality: u32, // 0–100 set by oracle
pub green_impact: u32, // 0–100 set by oracle
}Constructor
__constructor(admin: Address, usdc_sac: Address, registry: Address)
Sets the Ownable owner to admin, stores USDC SAC and Registry addresses, initialises TotalInvestments to 0, and sets the SEP-41 token metadata (symbol: HBS, name: Heliobond Shares, decimals: 7).
Public functions
| Function | Auth required | Description |
|---|---|---|
deposit(from, usdc_amount) |
from |
Transfer USDC from investor into vault; mint HBS shares; return shares minted |
withdraw(from, shares_amount) |
from (via Base::burn) |
Burn HBS shares; transfer proportional liquid USDC back to investor; return USDC returned |
fund_project(project_id, amount) |
Admin (#[only_owner]) |
Cross-call Registry to resolve project owner; transfer USDC from vault to owner; record investment |
total_assets() |
none | Return liquid_USDC + total_investments + expected_returns |
convert_to_shares(usdc_amount) |
none | Preview how many HBS a given USDC deposit would mint |
convert_to_assets(shares_amount) |
none | Preview how much USDC a given HBS redemption would return |
get_expected_returns() |
none | Iterate funded projects; sum investment * (credit_quality + green_impact) / 200 |
transfer_ownership(new_owner) |
Admin |
Transfer contract ownership |
The vault also exposes the full SEP-41 FungibleToken interface (balance, transfer, allowance, approve, etc.) and FungibleBurnable (burn, burn_from) from stellar-tokens.
The vault uses a proportional NAV model identical to ERC-4626.
First deposit (no shares in existence)
shares_minted = usdc_deposited (1 : 1)
Subsequent deposits
shares_minted = usdc_deposited × total_supply / total_assets
Redemption
usdc_returned = shares_burned × total_assets / total_supply
total_assets = liquid USDC held by the vault + TotalInvestments + expected yield.
Expected yield per project = investment × (credit_quality + green_impact) / 200, where both scores are in [0, 100].
Redemption is limited to the vault's liquid USDC balance; funds disbursed to project owners via fund_project are not available for immediate withdrawal.
Prerequisites: Stellar CLI and a Rust toolchain with the wasm32v1-none target.
# Add the wasm target if not already present
rustup target add wasm32v1-none
# Build both contracts
make build
# Equivalent: stellar contract build
# Output: target/wasm32v1-none/release/project_registry.wasm
# target/wasm32v1-none/release/investment_vault.wasm
# Run all 15 tests
make test
# Equivalent: cargo testThe two contracts must be deployed in order because InvestmentVault takes the registry contract ID as a constructor argument.
export STELLAR_SECRET_KEY=S... # deployer secret key
export ADMIN_ADDRESS=G... # admin/owner address
export WHITELISTER_ADDRESS=G... # whitelister address
export USDC_SAC_ADDRESS=G... # USDC Stellar Asset Contract on testnet
# 1. Deploy ProjectRegistry
REGISTRY_ID=$(stellar contract deploy \
--wasm target/wasm32v1-none/release/project_registry.wasm \
--source "$STELLAR_SECRET_KEY" \
--network testnet \
-- \
--admin "$ADMIN_ADDRESS" \
--whitelister "$WHITELISTER_ADDRESS")
echo "Registry: $REGISTRY_ID"
# 2. Deploy InvestmentVault (references the registry deployed above)
VAULT_ID=$(stellar contract deploy \
--wasm target/wasm32v1-none/release/investment_vault.wasm \
--source "$STELLAR_SECRET_KEY" \
--network testnet \
-- \
--admin "$ADMIN_ADDRESS" \
--usdc_sac "$USDC_SAC_ADDRESS" \
--registry "$REGISTRY_ID")
echo "Vault: $VAULT_ID"The Makefile target make deploy-testnet runs the same two steps using the environment variables STELLAR_SECRET_KEY, ADMIN_ADDRESS, WHITELISTER_ADDRESS, USDC_SAC_ADDRESS, and REGISTRY_CONTRACT_ID.
| Component | Version |
|---|---|
| Language | Rust (edition 2021, #![no_std]) |
| Soroban SDK | soroban-sdk = 26.1.0 |
| OZ stellar-tokens | stellar-tokens = 0.7.2 |
| OZ stellar-access | stellar-access = 0.7.2 |
| OZ stellar-macros | stellar-macros = 0.7.2 |
| Compile target | wasm32v1-none |
| Release profile | LTO, opt-level = "z", panic = "abort" |