Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions Basalt.sln
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "examples", "examples", "{B3
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Basalt.Example.Wallet", "examples\Basalt.Example.Wallet\Basalt.Example.Wallet.csproj", "{D02C2C1A-95BD-4F1C-B018-C7E98C7BD834}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Basalt.Example.Contracts", "examples\Basalt.Example.Contracts\Basalt.Example.Contracts.csproj", "{3BE52F08-293B-47A2-A309-8ABC1DD098E1}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -623,6 +625,18 @@ Global
{D02C2C1A-95BD-4F1C-B018-C7E98C7BD834}.Release|x64.Build.0 = Release|Any CPU
{D02C2C1A-95BD-4F1C-B018-C7E98C7BD834}.Release|x86.ActiveCfg = Release|Any CPU
{D02C2C1A-95BD-4F1C-B018-C7E98C7BD834}.Release|x86.Build.0 = Release|Any CPU
{3BE52F08-293B-47A2-A309-8ABC1DD098E1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3BE52F08-293B-47A2-A309-8ABC1DD098E1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3BE52F08-293B-47A2-A309-8ABC1DD098E1}.Debug|x64.ActiveCfg = Debug|Any CPU
{3BE52F08-293B-47A2-A309-8ABC1DD098E1}.Debug|x64.Build.0 = Debug|Any CPU
{3BE52F08-293B-47A2-A309-8ABC1DD098E1}.Debug|x86.ActiveCfg = Debug|Any CPU
{3BE52F08-293B-47A2-A309-8ABC1DD098E1}.Debug|x86.Build.0 = Debug|Any CPU
{3BE52F08-293B-47A2-A309-8ABC1DD098E1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3BE52F08-293B-47A2-A309-8ABC1DD098E1}.Release|Any CPU.Build.0 = Release|Any CPU
{3BE52F08-293B-47A2-A309-8ABC1DD098E1}.Release|x64.ActiveCfg = Release|Any CPU
{3BE52F08-293B-47A2-A309-8ABC1DD098E1}.Release|x64.Build.0 = Release|Any CPU
{3BE52F08-293B-47A2-A309-8ABC1DD098E1}.Release|x86.ActiveCfg = Release|Any CPU
{3BE52F08-293B-47A2-A309-8ABC1DD098E1}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -682,5 +696,6 @@ Global
{495AC7A3-0727-484A-9E79-C41689D66C8F} = {51B637F8-9C04-5803-F48E-8CB9352021B0}
{6B4DC31B-3BE6-4C85-A32E-6F1DBC0E4843} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
{D02C2C1A-95BD-4F1C-B018-C7E98C7BD834} = {B36A84DF-456D-A817-6EDD-3EC3E7F6E11F}
{3BE52F08-293B-47A2-A309-8ABC1DD098E1} = {B36A84DF-456D-A817-6EDD-3EC3E7F6E11F}
EndGlobalSection
EndGlobal
10 changes: 7 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ A high-performance Layer 1 blockchain built on .NET 9 with Native AOT compilatio
- **Merkle Patricia Trie** -- Cryptographically verifiable state with RocksDB persistence
- **Smart Contracts** -- C# contracts with gas metering, sandboxed execution, and Roslyn analyzers
- **Token Standards** -- BST-20 (ERC-20), BST-721 (ERC-721), BST-1155 (ERC-1155), BST-3525 (ERC-3525 SFT), BST-4626 (ERC-4626 Vault), BST-VC (W3C Verifiable Credentials), BST-DID
- **Policy Hooks** -- Modular transfer policy enforcement on all token standards: sanctions, holding limits, lockup periods, jurisdiction whitelist/blacklist. Deploy policies as independent contracts, register on any token.
- **EIP-1559 Gas Pricing** -- Dynamic base fee with elastic block gas, tip/burn fee split, MaxFeePerGas/MaxPriorityFeePerGas
- **On-Chain Governance** -- Stake-weighted quadratic voting, single-hop delegation, timelock, executable proposals
- **Block Explorer** -- Blazor WASM explorer with responsive design, dark/light theme, live WebSocket updates, Faucet, and Network Stats
Expand Down Expand Up @@ -60,7 +61,7 @@ dotnet build
dotnet test
```

2,789 tests across 16 test projects covering core types, cryptography, codec serialization, storage, networking, consensus, execution (including DEX), API, compliance, bridge, confidentiality, node configuration, SDK contracts, analyzers, wallet, and end-to-end integration.
2,891 tests across 16 test projects covering core types, cryptography, codec serialization, storage, networking, consensus, execution (including DEX), API, compliance, bridge, confidentiality, node configuration, SDK contracts, analyzers, wallet, and end-to-end integration.

### Run a Local Node

Expand Down Expand Up @@ -115,7 +116,7 @@ dotnet run --project tools/Basalt.Cli -- init MyToken
## Project Structure

```
Basalt.sln (42 C# projects)
Basalt.sln (43 C# projects)
+-- src/
| +-- core/
| | +-- Basalt.Core/ # Hash256, Address, UInt256, chain parameters
Expand Down Expand Up @@ -152,7 +153,7 @@ Basalt.sln (42 C# projects)
| | +-- Basalt.Explorer/ # Blazor WASM block explorer (responsive, dark/light theme, WebSocket live updates)
| +-- node/
| +-- Basalt.Node/ # Composition root, single binary
+-- tests/ # 16 test projects, 2,781 tests
+-- tests/ # 16 test projects, 2,891 tests
| +-- Basalt.Core.Tests/
| +-- Basalt.Crypto.Tests/
| +-- Basalt.Codec.Tests/
Expand All @@ -175,7 +176,10 @@ Basalt.sln (42 C# projects)
| +-- Basalt.Cli/ # CLI tool (account, tx, block, faucet, contract init/compile/test)
| +-- Basalt.DevNet/ # Docker devnet genesis config + validator setup script
| +-- TestVectorGen/ # Codec test vector generator
+-- examples/
| +-- Basalt.Example.Contracts/ # Example contracts (ComplianceToken, PolicyVault)
+-- contracts/ # Solidity bridge contracts (BasaltBridge, WBST)
+-- smart-contracts/ # 78 contract specifications with implementation status
+-- docs/ # Design plan, technical specification
```

Expand Down
15 changes: 15 additions & 0 deletions examples/Basalt.Example.Contracts/Basalt.Example.Contracts.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Library</OutputType>
<RootNamespace>Basalt.Example.Contracts</RootNamespace>
<IsPackable>false</IsPackable>
<!-- Disable AOT/trim analyzers for example project -->
<EnableTrimAnalyzer>false</EnableTrimAnalyzer>
<EnableSingleFileAnalyzer>false</EnableSingleFileAnalyzer>
<EnableAotAnalyzer>false</EnableAotAnalyzer>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="../../src/sdk/Basalt.Sdk.Contracts/Basalt.Sdk.Contracts.csproj" />
<ProjectReference Include="../../src/sdk/Basalt.Sdk.Testing/Basalt.Sdk.Testing.csproj" />
</ItemGroup>
</Project>
127 changes: 127 additions & 0 deletions examples/Basalt.Example.Contracts/ComplianceTokenExample.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
using Basalt.Core;
using Basalt.Sdk.Contracts;
using Basalt.Sdk.Contracts.Policies;
using Basalt.Sdk.Contracts.Standards;
using Basalt.Sdk.Testing;

namespace Basalt.Example.Contracts;

/// <summary>
/// Example: A regulated security token using BST-20 with policy hooks.
///
/// This shows how to:
/// 1. Deploy a BST-20 token
/// 2. Deploy compliance policies (sanctions, lockup, jurisdiction)
/// 3. Register policies on the token
/// 4. All transfers are automatically enforced
///
/// Run this as a standalone program to see the compliance workflow.
/// </summary>
public static class ComplianceTokenExample
{
public static void Run()
{
using var host = new BasaltTestHost();

// --- Addresses ---
var issuer = BasaltTestHost.CreateAddress(1);
var alice = BasaltTestHost.CreateAddress(2);
var bob = BasaltTestHost.CreateAddress(3);
var charlie = BasaltTestHost.CreateAddress(4); // Will be sanctioned
var tokenAddr = BasaltTestHost.CreateAddress(0xA0);
var sanctionsAddr = BasaltTestHost.CreateAddress(0xA1);
var lockupAddr = BasaltTestHost.CreateAddress(0xA2);
var jurisdictionAddr = BasaltTestHost.CreateAddress(0xA3);

// === Step 1: Deploy the security token ===
host.SetCaller(issuer);
Context.Self = tokenAddr;
var token = new BST20Token("RegulatedToken", "RSEC", 18, new UInt256(1_000_000));
host.Deploy(tokenAddr, token);

// === Step 2: Deploy compliance policies ===

// Sanctions: blocks transfers involving sanctioned addresses
Context.Self = sanctionsAddr;
var sanctions = new SanctionsPolicy();
host.Deploy(sanctionsAddr, sanctions);

// Lockup: time-based transfer restrictions for insiders
Context.Self = lockupAddr;
var lockup = new LockupPolicy();
host.Deploy(lockupAddr, lockup);

// Jurisdiction: country-based whitelist/blacklist
Context.Self = jurisdictionAddr;
var jurisdiction = new JurisdictionPolicy();
host.Deploy(jurisdictionAddr, jurisdiction);

Context.IsDeploying = false;

// === Step 3: Configure policies ===

// Sanction Charlie
host.SetCaller(issuer);
Context.Self = sanctionsAddr;
sanctions.AddSanction(charlie);

// Set 6-month lockup on Alice (insider)
host.SetBlockTimestamp(1_000_000);
Context.Self = lockupAddr;
lockup.SetLockup(tokenAddr, alice, 16_000_000); // ~6 months

// Whitelist US + EU jurisdictions
Context.Self = jurisdictionAddr;
jurisdiction.SetMode(tokenAddr, true); // whitelist mode
jurisdiction.SetJurisdiction(tokenAddr, 840, true); // US
jurisdiction.SetJurisdiction(tokenAddr, 276, true); // Germany
jurisdiction.SetAddressJurisdiction(alice, 840);
jurisdiction.SetAddressJurisdiction(bob, 276);

// === Step 4: Register all policies on the token ===
host.SetCaller(issuer);
Context.Self = tokenAddr;
token.AddPolicy(sanctionsAddr);
token.AddPolicy(lockupAddr);
token.AddPolicy(jurisdictionAddr);

// Token now has 3 active policies
Console.WriteLine($"Policies registered: {token.PolicyCount()}");

// === Step 5: Distribute tokens ===
token.Transfer(alice, new UInt256(50_000));
token.Transfer(bob, new UInt256(50_000));
Console.WriteLine($"Alice balance: {token.BalanceOf(alice)}");
Console.WriteLine($"Bob balance: {token.BalanceOf(bob)}");

// === Step 6: Demonstrate enforcement ===

// Bob -> Alice: works (both in whitelisted jurisdictions, no lockup on Bob)
host.SetCaller(bob);
Context.Self = tokenAddr;
token.Transfer(alice, new UInt256(1_000));
Console.WriteLine("Bob -> Alice: OK");

// Alice -> Bob: BLOCKED (Alice is locked up)
host.SetCaller(alice);
Context.Self = tokenAddr;
try { token.Transfer(bob, new UInt256(100)); }
catch (ContractRevertException e) { Console.WriteLine($"Alice -> Bob: DENIED ({e.Message})"); }

// Bob -> Charlie: BLOCKED (Charlie is sanctioned)
host.SetCaller(bob);
Context.Self = tokenAddr;
try { token.Transfer(charlie, new UInt256(100)); }
catch (ContractRevertException e) { Console.WriteLine($"Bob -> Charlie: DENIED ({e.Message})"); }

// === Step 7: Time passes, lockup expires ===
host.SetBlockTimestamp(16_000_001);
host.SetCaller(alice);
Context.Self = tokenAddr;
token.Transfer(bob, new UInt256(5_000));
Console.WriteLine("Alice -> Bob after lockup expiry: OK");

Console.WriteLine($"Final Alice balance: {token.BalanceOf(alice)}");
Console.WriteLine($"Final Bob balance: {token.BalanceOf(bob)}");
}
}
130 changes: 130 additions & 0 deletions examples/Basalt.Example.Contracts/PolicyVaultExample.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
using Basalt.Core;
using Basalt.Sdk.Contracts;
using Basalt.Sdk.Contracts.Policies;
using Basalt.Sdk.Contracts.Standards;
using Basalt.Sdk.Testing;

namespace Basalt.Example.Contracts;

/// <summary>
/// Example: A BST-4626 tokenized vault where both the underlying asset
/// and the vault shares have independent policy enforcement.
///
/// Scenario:
/// - An underlying BST-20 "stablecoin" with a sanctions policy
/// - A BST-4626 vault that wraps the stablecoin
/// - The vault shares have a holding limit policy (max 100K shares per address)
/// - Sanctioned users can't deposit (asset transfer blocked)
/// - Large holders can't accumulate unlimited shares
/// </summary>
public static class PolicyVaultExample
{
public static void Run()
{
using var host = new BasaltTestHost();

var admin = BasaltTestHost.CreateAddress(1);
var alice = BasaltTestHost.CreateAddress(2);
var bob = BasaltTestHost.CreateAddress(3);
var assetAddr = BasaltTestHost.CreateAddress(0xA0);
var vaultAddr = BasaltTestHost.CreateAddress(0xA1);
var sanctionsAddr = BasaltTestHost.CreateAddress(0xA2);
var holdingLimitAddr = BasaltTestHost.CreateAddress(0xA3);

host.SetCaller(admin);

// === Deploy underlying asset (stablecoin) ===
Context.Self = assetAddr;
var asset = new BST20Token("USD Coin", "USDC", 6, new UInt256(10_000_000));
host.Deploy(assetAddr, asset);

// === Deploy vault ===
Context.Self = vaultAddr;
var vault = new BST4626Vault("Vault USDC", "vUSDC", 6, assetAddr);
host.Deploy(vaultAddr, vault);

Comment on lines +36 to +45
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This example deploys both a BST20Token asset and a BST4626Vault (which inherits BST20Token) into the same BasaltTestHost. BasaltTestHost uses a single global ContractStorage without per-contract address scoping, and BST20Token uses fixed storage prefixes (e.g., "balances"/"allowances"), so the asset and vault share-token storage can collide and the example can behave incorrectly. Consider isolating storage per deployed contract in the test host (or avoid co-deploying multiple BST20-based contracts in one host in this example).

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Won't fix in this PR. This is a known limitation of BasaltTestHost which uses global (non-address-scoped) storage. The example is intentionally simple - it demonstrates the policy pattern, not production storage isolation. Fixing this requires per-contract storage scoping in BasaltTestHost, which is a separate infrastructure improvement. The example already works correctly for its purpose (showing policy enforcement flow).

// === Deploy policies ===
Context.Self = sanctionsAddr;
var sanctions = new SanctionsPolicy();
host.Deploy(sanctionsAddr, sanctions);

Context.Self = holdingLimitAddr;
var holdingLimit = new HoldingLimitPolicy();
host.Deploy(holdingLimitAddr, holdingLimit);

Context.IsDeploying = false;

// === Configure ===

// Sanctions on the underlying asset
host.SetCaller(admin);
Context.Self = assetAddr;
asset.AddPolicy(sanctionsAddr);

// Holding limit on vault shares (max 100K shares per address)
Context.Self = holdingLimitAddr;
holdingLimit.SetDefaultLimit(vaultAddr, new UInt256(100_000));

Context.Self = vaultAddr;
vault.AddPolicy(holdingLimitAddr);

// === Distribute stablecoins ===
host.SetCaller(admin);
Context.Self = assetAddr;
asset.Transfer(alice, new UInt256(500_000));
asset.Transfer(bob, new UInt256(500_000));
Console.WriteLine($"Alice USDC: {asset.BalanceOf(alice)}");

// === Alice deposits into vault ===
// First approve vault to pull tokens
host.SetCaller(alice);
Context.Self = assetAddr;
asset.Approve(vaultAddr, new UInt256(200_000));

// Deposit
Context.Self = vaultAddr;
var shares = vault.Deposit(new UInt256(50_000));
Console.WriteLine($"Alice deposited 50K USDC, got {shares} shares");
Console.WriteLine($"Alice vault shares: {vault.BalanceOf(alice)}");
Console.WriteLine($"Vault total assets: {vault.TotalAssets()}");

// === Bob deposits too ===
host.SetCaller(bob);
Context.Self = assetAddr;
asset.Approve(vaultAddr, new UInt256(200_000));

Context.Self = vaultAddr;
shares = vault.Deposit(new UInt256(80_000));
Console.WriteLine($"Bob deposited 80K USDC, got {shares} shares");

// === Alice tries to transfer shares to Bob (holding limit check) ===
// Bob has ~80K shares, limit is 100K — small transfer works
host.SetCaller(alice);
Context.Self = vaultAddr;
vault.Transfer(bob, new UInt256(10_000));
Console.WriteLine("Alice -> Bob 10K shares: OK");

// Large transfer would exceed Bob's 100K limit
try { vault.Transfer(bob, new UInt256(50_000)); }
catch (ContractRevertException e)
{
Console.WriteLine($"Alice -> Bob 50K shares: DENIED ({e.Message})");
}

// === Sanction Bob — he can't withdraw (asset transfer blocked) ===
host.SetCaller(admin);
Context.Self = sanctionsAddr;
sanctions.AddSanction(bob);

// Bob tries to redeem shares for USDC — asset transfer to sanctioned Bob blocked
host.SetCaller(bob);
Context.Self = vaultAddr;
try { vault.Redeem(new UInt256(1_000)); }
catch (ContractRevertException e)
{
Console.WriteLine($"Bob redeem: DENIED ({e.Message})");
}

Console.WriteLine($"Final vault total assets: {vault.TotalAssets()}");
}
}
2 changes: 2 additions & 0 deletions smart-contracts/01-amm-dex.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
> **STATUS: DONE** — Superseded by **Caldera Fusion DEX** (Phases A-E): batch auction, order book, TWAP oracle, dynamic fees, concentrated liquidity, encrypted intents, solver network.

# Automated Market Maker (AMM DEX)

## Category
Expand Down
2 changes: 2 additions & 0 deletions smart-contracts/02-order-book-dex.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
> **STATUS: DONE** — Included in **Caldera Fusion DEX** order book engine.

# Order Book DEX

## Category
Expand Down
2 changes: 2 additions & 0 deletions smart-contracts/03-lending-protocol.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
> **STATUS: PARTIAL** — Foundation exists (BST-4626 Vault). Needs: collateral baskets, liquidation engine, variable rates, oracle price feeds.

# Lending/Borrowing Protocol

## Category
Expand Down
2 changes: 2 additions & 0 deletions smart-contracts/04-stablecoin-cdp.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
> **STATUS: NEEDS ORACLE** — Requires decentralized oracle (41) for collateral price feeds and peg stability.

# Collateralized Stablecoin (CDP)

## Category
Expand Down
2 changes: 2 additions & 0 deletions smart-contracts/05-yield-aggregator.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
> **STATUS: PARTIAL** — Foundation exists (BST-4626 Vault). Needs: multi-strategy orchestration, auto-harvest, reward swapping.

# Yield Aggregator

## Category
Expand Down
2 changes: 2 additions & 0 deletions smart-contracts/06-liquid-staking.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
> **STATUS: DONE** — Implemented as **StakingPool** system contract (0x0105) at `0x...1005`.

# Liquid Staking Token (stBSLT)

## Category
Expand Down
2 changes: 2 additions & 0 deletions smart-contracts/07-flash-loans.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
> **STATUS: PARTIAL** — Foundation exists (Escrow 0x0103). Needs: atomic single-tx borrow/repay mechanics, fee model.

# Flash Loan Pool

## Category
Expand Down
Loading
Loading