A modular Soroban smart contract platform for token minting on the Stellar blockchain, with a TypeScript SDK for seamless integration.
Built for open-source collaboration via drips.network.
- SEP-41 Compliant Token — Full
TokenInterfaceimplementation (balance, transfer, approve, burn) - Admin-Controlled Minting — Only the contract admin can mint new tokens
- Pausable Lifecycle — Emergency pause/unpause to halt all operations
- Ownership Transfer — Securely hand over admin rights
- Total Supply Tracking — Accurate supply updated on every mint/burn
- TypeScript SDK — High-level client for all contract interactions
- Modular Architecture — Separate crates for admin, lifecycle, and token logic
- Reentrancy Protection — Comprehensive reentrancy guards for all state-modifying functions
- Rate Limiting — Configurable global and per-address rate limits for mint and transfer operations
- Batch Payout — Per-recipient failure isolation in batch payments; failed transfers recorded for retry
- Property-Based Fuzz Testing — Enhanced proptest framework for invariant verification
- End-to-End Integration Tests — Complete lifecycle testing on Stellar testnet
- Automatic Storage TTL Management — Shared helper module extends Soroban contract and persistent storage TTL across calls
bc-forge/
├── contracts/ # Soroban smart contracts (Rust)
│ ├── admin/ # Admin access control, proposals, multisig pool
│ ├── lifecycle/ # Pause/unpause lifecycle module
│ ├── rate-limit/ # Rate limiting module
│ ├── split/ # Batch payout with per-recipient failure isolation
│ ├── token/ # Core SEP-41 token contract
│ ├── ttl/ # Shared storage TTL helpers
│ ├── vesting/ # Vesting schedules
│ ├── wrapper/ # Wrapper vault
│ └── yield_vault/ # Yield-bearing vault with share accounting
├── cli/ # TypeScript CLI (deploy, upgrade, multisig flows)
├── sdk/ # TypeScript SDK consumed by dApps and the CLI
├── react/ # React hooks and components for the SDK
├── indexer/ # Event indexer and query API
├── e2e/ # End-to-end integration tests
├── docs/ # Long-form docs (architecture, vaults, upgrades)
├── deployments/ # Recorded deployment addresses per network
├── migrations/ # Database migrations for the indexer
├── scripts/ # Repo maintenance and CI helper scripts
├── .github/
│ ├── ISSUE_TEMPLATE/ # Bug, Feature, Contract Improvement
│ ├── PULL_REQUEST_TEMPLATE.md
│ └── workflows/ci.yml # CI pipeline
├── Cargo.toml # Rust workspace manifest
├── package.json # Root Node workspace manifest
├── config.example.json # Example CLI/indexer configuration
├── CONTRIBUTING.md # Contributor guide (drips.network)
├── SECURITY.md # Security policy and disclosure
├── VAULTS.md # Vault integration guide
├── LICENSE # MIT
└── README.md # This file
contracts/yield_vaultis listed in theexcludearray of the rootCargo.toml. It is not part of the built workspace and is not shipped; treat it as experimental. (#923 removed the excludedcompound_feesstub and promotedflash_loan_guardinto the workspace — see below.)
flowchart TD
subgraph Clients
DApp[dApp / Frontend]
CLI[cli - TypeScript CLI]
React[react - hooks and components]
end
subgraph TypeScript
SDK[sdk - bcForgeClient]
Indexer[indexer - event indexer and query API]
end
subgraph Contracts[Soroban contracts]
Token[token - SEP-41 token]
Admin[admin - access control and multisig pool]
Lifecycle[lifecycle - pause / unpause]
RateLimit[rate-limit]
Vesting[vesting]
Split[split - batch payout]
Wrapper[wrapper - wrapper vault]
YieldVault[yield_vault - share accounting]
TTL[ttl - shared storage TTL helpers]
end
Ledger[(Stellar ledger)]
DApp --> SDK
React --> SDK
CLI --> SDK
SDK --> Token
SDK --> Admin
CLI --> Token
CLI --> Admin
Token --> TTL
Admin --> TTL
Lifecycle --> TTL
Split --> TTL
Wrapper --> TTL
YieldVault --> TTL
Wrapper --> Token
YieldVault --> Token
Split --> Token
Admin -.governs.-> Token
Admin -.governs.-> Lifecycle
Admin -.governs.-> YieldVault
Token --> Ledger
Wrapper --> Ledger
YieldVault --> Ledger
Ledger -.events.-> Indexer
Indexer --> SDK
The contracts share the ttl helper crate so that instance and persistent
storage entries stay live. admin holds the multisig pool that governs
upgrades and privileged operations on the value-bearing contracts.
To keep Soroban contract state active, bc-forge now includes shared TTL logic that:
- extends the contract instance TTL on every public token, admin, and lifecycle call
- refreshes persistent storage TTL for balances, allowances, lockups, roles, and proposals
- treats expired balances and allowances as zero instead of panicking
This makes the system more resilient to Soroban storage expiry while preserving on-chain security semantics.
| Tool | Version | Install |
|---|---|---|
| Rust | 1.74+ | rustup.rs |
| Wasm target | — | rustup target add wasm32-unknown-unknown |
| Stellar CLI | 22.0+ | cargo install stellar-cli --locked |
| Node.js | 18+ | nodejs.org |
git clone https://github.com/BCPathway/bc-forge.git
cd bc-forge# Install Rust (if not already installed)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# Add the WebAssembly target
rustup target add wasm32-unknown-unknown
# Install Stellar CLI (includes Soroban)
cargo install stellar-cli --locked# Build all contracts (debug)
cargo build
# Build optimized WASM for deployment
cargo build --target wasm32-unknown-unknown --release
# Or use Stellar CLI
stellar contract buildcargo test --testsExpected output:
running 5 tests (admin) ... ok
running 5 tests (lifecycle) ... ok
running 16 tests (token) ... ok
cd sdk
npm install
npm run buildThe CLI reads .bc-forge.json from the current working directory. Start with the ready-to-use config.example.json:
cp config.example.json .bc-forge.jsonYou can also generate a minimal file with bc-forge config init. The example contains these fields:
| Field | Required | Description |
|---|---|---|
version |
No | Configuration schema version. Defaults to 1.0.0. |
name |
Yes | Token or project name. |
symbol |
Yes | Token symbol. |
decimals |
No | Token decimal precision, from 0 to 18. Defaults to 7. |
admin |
No | Stellar public G... address that administers the token. Required when initializing a contract. |
superAdmin |
No | Stellar public G... address assigned the initial SuperAdmin role during RBAC initialization. Defaults to admin when omitted. |
network |
No | Deployment environment: mainnet, testnet, futurenet, standalone, or custom. Defaults to testnet. |
rpcUrl |
No | Soroban RPC endpoint URL. |
networkPassphrase |
No | Stellar network passphrase. |
secretKey |
No | Stellar S... secret key used to sign transactions. Prefer SECRET_KEY or another secret manager. |
contracts |
No | Map of deployed contract metadata keyed by contract name. |
contracts.<name>.contractId |
No | Deployed Soroban contract ID. |
contracts.<name>.wasmHash |
No | Hash of the deployed contract WASM. |
contracts.<name>.deployer |
No | Stellar public address that deployed the contract. |
Environment variables take precedence over file and local-store values for RPC_URL, NETWORK_PASSPHRASE, CONTRACT_ID, and SECRET_KEY. Replace every placeholder before deploying, and do not commit real secret keys.
stellar keys generate --global deployer --network testnetstellar keys fund deployer --network testnetstellar contract deploy \
--wasm target/wasm32-unknown-unknown/release/bc_forge_token.wasm \
--source deployer \
--network testnetSave the returned Contract ID (e.g., CABC...XYZ).
stellar contract invoke \
--id <CONTRACT_ID> \
--source deployer \
--network testnet \
-- \
initialize \
--admin <YOUR_PUBLIC_KEY> \
--decimal 7 \
--name "bc-forge Token" \
--symbol "SFG"After initialization, run the init_rbac step to bootstrap role-based access
control and assign the initial SuperAdmin role:
# Bootstrap the SuperAdmin mapping from the configured admin (idempotent)
stellar contract invoke \
--id <CONTRACT_ID> \
--source deployer \
--network testnet \
-- \
migrate_admin
# Assign the initial SuperAdmin role (the contract admin can perform this grant)
stellar contract invoke \
--id <CONTRACT_ID> \
--source deployer \
--network testnet \
-- \
grant_role \
--caller <YOUR_PUBLIC_KEY> \
--role SuperAdmin \
--address <SUPER_ADMIN_PUBLIC_KEY>
# Verify the assignment
stellar contract invoke \
--id <CONTRACT_ID> \
--network testnet \
-- \
has_role \
--role SuperAdmin \
--address <SUPER_ADMIN_PUBLIC_KEY>stellar contract invoke \
--id <CONTRACT_ID> \
--source deployer \
--network testnet \
-- \
mint \
--to <RECIPIENT_ADDRESS> \
--amount 10000000000stellar contract invoke \
--id <CONTRACT_ID> \
--network testnet \
-- \
balance \
--id <ADDRESS>If you want to build and test against a local Soroban network, run the Stellar Quickstart container instead of using public testnet services.
docker run -d \
-p "8000:8000" \
--name stellar \
stellar/quickstart \
--localThis starts a local Stellar network with RPC, Horizon, and Friendbot on your machine.
Register the local network once, then switch the CLI to it:
stellar network add local \
--rpc-url http://localhost:8000/rpc \
--network-passphrase "Test SDF Network ; September 2015"
stellar network use localCreate a local identity and fund it from the local Friendbot instance:
stellar keys generate deployer
stellar keys fund deployerYou can use stellar keys public-key deployer to print the address, then use that keypair as the source account for contract deploy and invoke commands on the local network.
When using bcForgeClient, point rpcUrl at the local Quickstart instance:
import { bcForgeClient } from '@bc-forge/sdk';
const client = new bcForgeClient({
rpcUrl: 'http://localhost:8000',
networkPassphrase: 'Test SDF Network ; September 2015',
contractId: 'CABC...XYZ',
});If your local Quickstart setup exposes RPC on a different path, keep the same host and update the URL to match your container configuration.
A complete testnet dApp example that connects a wallet, reads a balance through @bc-forge/sdk, renders it with @bc-forge/react, and shows recent mints from the indexer API.
Location: examples/quickstart/
cd examples/quickstart
npm install
npm run devSee the quickstart README for full setup instructions, including deploying a testnet token and running the indexer.
import { bcForgeClient } from '@bc-forge/sdk';
import { Keypair } from '@stellar/stellar-sdk';
const client = new bcForgeClient({
rpcUrl: 'https://soroban-testnet.stellar.org',
networkPassphrase: 'Test SDF Network ; September 2015',
contractId: 'CABC...XYZ',
});
// Query balance
const balance = await client.getBalance('GABC...DEF');
console.log('Balance:', balance.toString());
// Mint tokens (admin only)
const admin = Keypair.fromSecret('SXXX...');
await client.mint('GABC...DEF', BigInt(1000_0000000), admin);
// Transfer tokens
const sender = Keypair.fromSecret('SYYY...');
await client.transfer(
sender.publicKey(),
'GXYZ...ABC',
BigInt(100_0000000),
sender
);See sdk/README.md for the full API reference.
See the access-control diagrams for the current role hierarchy, authorization sequence, protected operations, and governance flow. See the Vault Integration Guide for details on yield-bearing fee vaults, APY calculations, and frontend dApp integration.
┌─────────────────────────────────────────────────┐
│ BcForgeToken │
│ ┌───────────┐ ┌──────────────┐ ┌───────────┐│
│ │ Admin │ │ Lifecycle │ │ SEP-41 ││
│ │ Module │ │ Module │ │ Interface ││
│ │ │ │ │ │ ││
│ │ set_admin │ │ pause() │ │ balance() ││
│ │ get_admin │ │ unpause() │ │ transfer()││
│ │ require_ │ │ is_paused() │ │ approve() ││
│ │ admin() │ │ require_not_ │ │ burn() ││
│ │ │ │ paused() │ │ mint() ││
│ └───────────┘ └──────────────┘ └───────────┘│
│ ┌──────────────────────────────────────────┐ │
│ │ Split Module │ │
│ │ ┌────────────────────────────────────┐ │ │
│ │ │ release_payment(invoice_id) │ │ │
│ │ │ └─ try_transfer(recipient, amt) │ │ │
│ │ │ retry_failed_payout(invoice_id, │ │ │
│ │ │ recipient) │ │ │
│ │ │ get_failed_payout / get_invoice │ │ │
│ │ └────────────────────────────────────┘ │ │
│ └──────────────────────────────────────────┘ │
└─────────────────────────────────────────────────┘
The docs site is built with VitePress from the
markdown under docs/, plus API pages generated from TSDoc (TypeDoc)
and Rust doc comments (cargo doc).
From the repository root:
npm install # once; also installs VitePress and TypeDoc
npm run docs:gen # generate the SDK and contract API pages
npm run docs:dev # serve at http://localhost:5173
npm run docs:build # production build into docs/.vitepress/distnpm run docs:gen needs the Rust toolchain because the contract reference is
generated with cargo doc. If you only want the SDK page, run
npm run docs:gen:sdk.
The generated API pages under docs/api/sdk/ and docs/public/api/ are not
checked in. Run npm run docs:gen after changing sdk/src/* or any contract
doc comments. CI builds the site on every pull request and uploads the built
site as a docs-site artifact. Deployment is left to the maintainer because no
docs host is configured in this repository.
The site includes the spec-to-code traceability matrix, the admin key runbook, and the upgrade guide. Supported Node, Rust, Stellar CLI, and package ranges are in the compatibility matrix.
New here? Follow the contributor walkthrough for a start-to-finish example of setting up the repository, making a small change, running checks, and opening a pull request.
The project does not currently publish verified Discord or Telegram invite URLs in the repository. Maintainers can enable the badges below after adding the official invites; contributors should not invent or copy unverified invite links.
We welcome contributions! bc-forge is maintained on drips.network — contributors can earn rewards by resolving posted issues.
- Browse open issues — Look for issues labeled
good first issue,smart-contract, orsdk - Fork & branch — Create a branch:
feature/<issue-number>-<short-description> - Implement & test — Write code, add/update tests, ensure
cargo testandnpm run buildpass - Submit a PR — Use the PR template; reference the issue number
See CONTRIBUTING.md for the full guide.
Contributor work on bc-forge is funded through Drips. Bounties are attached to issues that maintainers have posted for funding, and the same three steps apply whether the issue is a first contribution or a larger change.
1. Claim the issue. Comment on the GitHub issue to claim it before you start
work. The maintainer posts funded issues on the bc-forge project page on Drips;
find them from the open issues
list, and start with issues labeled good first issue if you are new to the
codebase.
2. Open a pull request. Branch from main using the naming convention below,
make one focused change, and open a PR against BCPathway/bc-forge:main. Use
Closes #<issue-number> in the PR description so the issue is linked.
3. Get paid after merge. Once a maintainer reviews and merges your PR, the reward for the issue is distributed to you through Drips. Rewards are paid after merge, not on submission.
To receive a payout, create a profile at drips.network and link your GitHub account before you open the PR. The linked address is where merged work is paid, so set it up first.
| Step | Where | What happens |
|---|---|---|
| Claim | The GitHub issue | Comment to claim; avoid two people on one issue |
| Submit | A PR against main |
Include Closes #<issue-number> |
| Get paid | Drips | Reward distributed after the PR is merged |
Funding does not change the review bar: every PR is still reviewed against CONTRIBUTING.md, and security reports are handled separately and privately as described in SECURITY.md. See also docs/WALKTHROUGH.md for a full end-to-end example.
feature/<issue-number>-<description> # New features
fix/<issue-number>-<description> # Bug fixes
docs/<issue-number>-<description> # Documentation
test/<issue-number>-<description> # Test improvements
The following contracts are experimental, untested, or incomplete. Do not deploy them in a production environment.
contracts/yield_vault: A yield vault that holds or routes token balances. High risk if deployed with unchecked sources.
contracts/compound_fees— removed. It was a placeholder with no implementation (no contract entry points and no compiled tests), so it was deleted instead of shipping an empty crate. Restore it from git history if a fee-compounding vault is implemented later.contracts/flash_loan_guard— finished and added to the workspace. See Flash Loan Guard below.
- Purpose: a same-ledger reentrancy guard for deposit/withdraw flows. It records the ledger sequence of a user's most recent
depositand rejects anywithdrawattempted in the same ledger block, cutting off flash-loan-funded withdrawal loops. - Who may call it: any authenticated user — both entry points (
deposit(user),withdraw(user)) require the authorization of the user address they act on, and the contract holds no admin or privileged role. - CI: the crate is now part of the Cargo workspace, so
cargo test -p bc-forge-flash-loan-guardand clippy cover it in CI like every other contract.
Security is our top priority. If you discover a security vulnerability in bc-forge, please report it responsibly following our Security Policy.
Important: Do not report security vulnerabilities through GitHub issues, discussions, or other public channels. All security reports must be made privately to isaacsamson88@gmail.com.
For more details about our vulnerability disclosure process, supported versions, scope, and response timeline, please review the SECURITY.md file.
How security reports are rewarded, and whether a hosted bounty program is live, is tracked in docs/BUG_BOUNTY.md.
MIT — Free for personal and commercial use.
Names match the package manifests. Nothing in this table is published yet.
| Deliverable | Channel | Install or build | Release notes |
|---|---|---|---|
SDK (sdk/, @bc-forge/sdk) |
npm | Not yet published. After a release: npm install @bc-forge/sdk |
Maintainers, .github/workflows/release.yml |
CLI (cli/, @bc-forge/cli) |
npm | Not yet published. After a release: npm install -g @bc-forge/cli |
Maintainers, .github/workflows/release.yml |
React (react/, @bc-forge/react) |
npm | Not yet published. After a release: npm install @bc-forge/react |
Maintainers, .github/workflows/release.yml |
Indexer (indexer/) |
source | Not yet published as an image. Run it from this repo with docker compose up in indexer/. |
indexer/README.md |
Contracts (contracts/) |
source WASM | Not yet published as release assets. Build with cargo build --target wasm32-unknown-unknown --release. |
Upgrade guide |