-
Notifications
You must be signed in to change notification settings - Fork 0
Policy hooks for BST token standards #73
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
65c18c3
feat(sdk): policy hooks for BST token standards + developer safety an…
0xZunia 4a88fab
fix(sdk): harden policy hooks — static call protection, analyzer prec…
0xZunia 7baf799
fix(sdk): address review findings — security hardening, analyzer fixe…
0xZunia eaf296d
docs: update READMEs and smart-contract spec status across repository
0xZunia 81f4bc3
fix(sdk): address Copilot review — diagnostic message, doc accuracy, …
0xZunia 2b674b5
fix(sdk): address Copilot review round 2 — inheritance walk, null gua…
0xZunia b56bf28
feat(dex): add quote endpoint and wallet SDK swap intent builder
0xZunia d97821e
fix(sdk): address Copilot review — ref-count ActiveCallers, doc Holdi…
0xZunia d427ed2
fix(sdk): address Copilot review round 2 - analyzer false positives, …
0xZunia f7e9c30
fix(node): prevent consensus deadlock when 1 block behind during circ…
0xZunia File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
15 changes: 15 additions & 0 deletions
15
examples/Basalt.Example.Contracts/Basalt.Example.Contracts.csproj
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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
127
examples/Basalt.Example.Contracts/ComplianceTokenExample.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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
130
examples/Basalt.Example.Contracts/PolicyVaultExample.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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); | ||
|
|
||
| // === 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()}"); | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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
BasaltTestHostwhich 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).