Conversation
…alyzers Add modular transfer policy enforcement to all BST token standards (BST-20, BST-721, BST-1155, BST-3525, BST-4626). Policies are deployed as independent contracts implementing ITransferPolicy/INftTransferPolicy and registered on tokens via PolicyEnforcer. Zero policies = zero overhead. Reference policies: HoldingLimitPolicy (0x0008), LockupPolicy (0x0009), JurisdictionPolicy (0x000A), SanctionsPolicy (0x000B). New Roslyn analyzers: BST009 (unchecked cross-contract return), BST010 (storage write before policy check), BST011 (non-deterministic collections), BST012 (missing policy enforcement in transfer override). Reentrancy guard relaxed to allow A→B→A callback chains (policy contracts querying token.BalanceOf). 79 new tests, all 2,868 passing.
…ision, edge cases Static call enforcement: re-entrant callbacks (A→B→A) are now automatically forced into read-only mode. ContractStorage.Set/Delete revert with "Static call: state modification not allowed" when IsStaticCall is true. ActiveCallers tracking detects re-entry without blocking view-method callbacks. Closes the reentrancy attack vector where a malicious policy could call back into state-mutating entrypoints. PolicyEnforcer: remove dead _policySlots field, replace O(n) duplicate scan with O(1) _policyExists StorageMap, remove dead null guard in GetPolicy. HoldingLimitPolicy: catch BalanceOf call failures (incompatible token) and UInt256 overflow gracefully — return false instead of crashing. JurisdictionPolicy: implement INftTransferPolicy so it works when registered on BST-721/3525 tokens. CrossContractReturnAnalyzer (BST009): use Roslyn semantic model to resolve Context.CallContract<bool> instead of string matching. PolicyOrderingAnalyzer (BST010): verify .Set() receiver is a Basalt storage type via semantic model to avoid false positives on unrelated Set methods. 8 new tests covering static call protection, re-add after remove, overflow handling, and jurisdiction+NFT integration.
…s, test coverage - Context.IsStaticCall: public set → internal set (prevent contract escape) - JurisdictionPolicy: deny unregistered addresses in whitelist mode - PolicyEnforcer: max 16 policies cap, revert on corrupted slots - Two-step TransferAdmin/AcceptAdmin on all 4 policy contracts - CollectionOrderingAnalyzer: StartsWith → exact match, fix field detection - PolicyEnforcementAnalyzer: extend to BST1155/BST3525 transfer methods - PolicyOrderingAnalyzer: also flag .Delete() before enforcement - Fix StaticCall E2E test (real A→B→A pattern with assertions) - Add 15 new tests (NFT enforcement, multi-policy deny, max cap, admin transfer)
There was a problem hiding this comment.
Pull request overview
Adds a composable “policy hook” layer to Basalt’s BST token standards so transfers can be approved/denied by registered policy contracts (sanctions, lockups, holding limits, jurisdiction rules), plus supporting analyzers, docs, examples, and tests.
Changes:
- Introduces
PolicyEnforcerplus policy interfaces/events and four reference policy contracts. - Integrates policy enforcement into BST-20 / 721 / 1155 / 3525 transfer paths and adds a static-callback reentrancy model (
Context.IsStaticCall+ storage write blocking). - Adds Roslyn analyzers BST009–BST012 and extensive unit/integration coverage, with docs/examples and registry updates.
Reviewed changes
Copilot reviewed 37 out of 37 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/Basalt.Sdk.Tests/PolicyTests/SanctionsPolicyTests.cs | Unit tests for sanctions list and admin two-step. |
| tests/Basalt.Sdk.Tests/PolicyTests/PolicyEnforcerTests.cs | Unit tests for policy list mgmt and enforcement behavior. |
| tests/Basalt.Sdk.Tests/PolicyTests/LockupPolicyTests.cs | Unit tests for lockup enforcement and admin gating. |
| tests/Basalt.Sdk.Tests/PolicyTests/JurisdictionPolicyTests.cs | Unit tests for whitelist/blacklist jurisdiction enforcement. |
| tests/Basalt.Sdk.Tests/PolicyTests/HoldingLimitPolicyTests.cs | Unit tests for holding limits including overflow deny behavior. |
| tests/Basalt.Sdk.Tests/PolicyTests/EndToEndPolicyTests.cs | End-to-end lifecycle tests + static-callback behavior + NFT integration. |
| tests/Basalt.Sdk.Tests/PolicyTests/BST721PolicyIntegrationTests.cs | BST-721 transfer integration with policy enforcement. |
| tests/Basalt.Sdk.Tests/PolicyTests/BST20PolicyIntegrationTests.cs | BST-20 transfer/transferFrom integration with policies. |
| tests/Basalt.Sdk.Tests/PolicyTests/BST1155PolicyIntegrationTests.cs | BST-1155 transfer and batch transfer integration with policies. |
| tests/Basalt.Sdk.Tests/CrossContractCallTests.cs | Tests for static-mode re-entrant callbacks and state-write blocking. |
| tests/Basalt.Sdk.Analyzers.Tests/AnalyzerTests.cs | Adds analyzer tests for BST009–BST012. |
| src/sdk/Basalt.Sdk.Contracts/Storage.cs | Blocks storage Set/Delete when Context.IsStaticCall is true. |
| src/sdk/Basalt.Sdk.Contracts/Standards/BST721Token.cs | Adds policy management + enforces policies before NFT state mutation. |
| src/sdk/Basalt.Sdk.Contracts/Standards/BST3525Token.cs | Adds policy mgmt + enforces on value transfers and token transfers. |
| src/sdk/Basalt.Sdk.Contracts/Standards/BST20Token.cs | Adds policy mgmt + enforces before fungible state mutation. |
| src/sdk/Basalt.Sdk.Contracts/Standards/BST1155Token.cs | Adds policy mgmt + enforces (including batch pre-check) before mutations. |
| src/sdk/Basalt.Sdk.Contracts/README.md | Documents the new policy hook architecture and usage. |
| src/sdk/Basalt.Sdk.Contracts/Policies/SanctionsPolicy.cs | Implements sanctions list policy + two-step admin transfer. |
| src/sdk/Basalt.Sdk.Contracts/Policies/PolicyEnforcer.cs | Storage-backed policy registry and enforcement via cross-contract calls. |
| src/sdk/Basalt.Sdk.Contracts/Policies/LockupPolicy.cs | Implements per-address per-token time lockups + two-step admin transfer. |
| src/sdk/Basalt.Sdk.Contracts/Policies/JurisdictionPolicy.cs | Implements whitelist/blacklist jurisdiction checks + admin controls. |
| src/sdk/Basalt.Sdk.Contracts/Policies/ITransferPolicy.cs | Adds policy interfaces plus policy-added/removed/denied events. |
| src/sdk/Basalt.Sdk.Contracts/Policies/HoldingLimitPolicy.cs | Implements recipient holding limit checks via token balance callback. |
| src/sdk/Basalt.Sdk.Contracts/Context.cs | Implements static-mode re-entry detection and restores static state across call chains. |
| src/sdk/Basalt.Sdk.Analyzers/README.md | Documents new analyzer rules BST009–BST012. |
| src/sdk/Basalt.Sdk.Analyzers/PolicyOrderingAnalyzer.cs | New BST010 analyzer: detects storage writes before enforcement. |
| src/sdk/Basalt.Sdk.Analyzers/PolicyEnforcementAnalyzer.cs | New BST012 analyzer: detects transfer overrides missing enforcement. |
| src/sdk/Basalt.Sdk.Analyzers/DiagnosticIds.cs | Registers descriptors for BST009–BST012. |
| src/sdk/Basalt.Sdk.Analyzers/CrossContractReturnAnalyzer.cs | New BST009 analyzer: discarded CallContract<bool> return value. |
| src/sdk/Basalt.Sdk.Analyzers/CollectionOrderingAnalyzer.cs | New BST011 analyzer: bans Dictionary/HashSet usage in contracts. |
| src/sdk/Basalt.Sdk.Analyzers/AnalyzerReleases.Unshipped.md | Records newly added analyzer IDs. |
| src/execution/Basalt.Execution/VM/ContractRegistry.cs | Registers policy contracts type IDs 0x0008–0x000B. |
| examples/Basalt.Example.Contracts/PolicyVaultExample.cs | Example showing dual-layer (asset + vault share) policy enforcement. |
| examples/Basalt.Example.Contracts/ComplianceTokenExample.cs | Example showing BST-20 with sanctions/lockup/jurisdiction policies. |
| examples/Basalt.Example.Contracts/Basalt.Example.Contracts.csproj | Adds a non-packable example project referencing SDK/testing. |
| README.md | Mentions policy hooks and updates overall test count blurb. |
| Basalt.sln | Adds the example contracts project to the solution. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Update test counts in 5 test READMEs to match current suite (2,891 total). Rewrite Sdk.Tests README with all 32 test categories including policy hooks. Add implementation status banners (DONE/PARTIAL/READY/NEEDS ORACLE) to 28 smart-contract specs and create smart-contracts/README.md catalog. Update root README with current project/test counts and structure.
…static guard Fix BST010 diagnostic to show actual method name (.Set/.Delete) instead of hardcoded ".Set()". Correct JurisdictionPolicy doc comment to match admin-only implementation. Add IsStaticCall guard to ContractStorage.Clear. Reword README overhead claim to "near-zero (single storage read)".
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 70 out of 70 changed files in this pull request and generated 7 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // === 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); | ||
|
|
There was a problem hiding this comment.
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.
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).
| [Fact] | ||
| public void CheckTransfer_AllowsWhenNoLimitConfigured() | ||
| { | ||
| var result = _host.Call(() => _policy.CheckTransfer(_tokenAddr, _admin, _alice, new UInt256(999))); | ||
| result.Should().BeTrue(); | ||
| } |
There was a problem hiding this comment.
In this test, the call to _policy.CheckTransfer is executed without setting Context.Self back to the policy address. Because the constructor leaves Context.Self as _tokenAddr, this test currently depends on BasaltTestHost’s global (non-address-scoped) storage behavior and won’t accurately model real execution where Self would be the policy contract. Set Context.Self = _policyAddr (and caller as needed) before invoking policy methods to keep tests representative and robust.
There was a problem hiding this comment.
No change needed. The tests DO set Context.Self = _policyAddr before every policy method call (see lines 39, 50, 63, 74, 84, 93). BasaltTestHost.PrepareContext() only sets Caller/BlockTimestamp/BlockHeight/CallDepth - it does not touch Context.Self, which is managed explicitly. The constructor sets Self = _tokenAddr at the end (for the BST20Token deploy), but each test method resets it to _policyAddr before invoking policy methods.
…rd, test fixes Use semantic model in BST012 analyzer to walk full inheritance chain (catches indirect BST token derivation). Add null guard to PolicyEnforcer.GetPolicy. Fix JurisdictionPolicy class doc. Set Context.Self in HoldingLimitPolicyTests. Tighten multi-policy assertion.
GET /v1/dex/quote returns expected output, price impact, fees, TWAP, and volatility for a token pair swap. Searches all fee tiers for the best output, supports both constant-product and concentrated liquidity pools (with proper sqrtPrice-derived spot price for CL pools). DexSwapIntentBuilder encodes the 114-byte intent payload for plaintext (type 10) and encrypted (type 18) swap intents, matching ParsedIntent wire format exactly. Wallet SDK client and provider expose GetDexQuoteAsync. 14 new tests covering builder roundtrip, payload encoding, quote math, best-pool selection, price impact formula, and dynamic fee behavior.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 80 out of 80 changed files in this pull request and generated 4 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| public bool CheckTransfer(byte[] token, byte[] from, byte[] to, UInt256 amount) | ||
| { | ||
| var limit = GetEffectiveLimit(token, to); | ||
| if (limit.IsZero) return true; // No limit configured | ||
|
|
||
| // Query recipient's current balance on the token. | ||
| // If the call fails (e.g. incompatible BalanceOf signature), deny conservatively. | ||
| UInt256 currentBalance; | ||
| try | ||
| { | ||
| currentBalance = Context.CallContract<UInt256>(token, "BalanceOf", to); | ||
| } | ||
| catch | ||
| { | ||
| return false; | ||
| } |
There was a problem hiding this comment.
Partially addressed - documented the constraint. Added <remarks> explicitly stating this policy targets BalanceOf(byte[]) -> UInt256 (BST-20/BST-721) and that BST-1155/BST-3525 will fail and be denied conservatively. We chose not to introduce a universal balance interface or standard-specific policy variants in this PR because: (1) the try/catch already handles incompatible signatures safely (deny, not crash), (2) a universal IBalanceQuery interface would require modifying all 4 token standards and their source-generated dispatch - a larger refactor best done in a dedicated PR, and (3) holding limits are most commonly needed on fungible tokens (BST-20) where the signature already works.
…ngLimit constraint, valid BLS key in test
…doc accuracy, test assertion
…uit breaker cooldown Move behind-detection before the circuit breaker check so sync can trigger even during cooldown. Lower the peer gap threshold from >10 to >0 so a single-block gap is enough to initiate catch-up sync. Previously, the circuit breaker's `continue` skipped behind-detection entirely, and the >10 threshold meant small gaps never triggered sync, causing a permanent stall when all validators tripped simultaneously.
Summary
GET /v1/dex/quoteREST endpoint returning expected swap output, price impact, fees, TWAP, and volatility for the Caldera Fusion DEXDexSwapIntentBuilderwallet SDK builder for plaintext and encrypted swap intent transactions, plusGetDexQuoteAsyncon the RPC client and providerWhat's included
Policy infrastructure
ITransferPolicy/INftTransferPolicyinterfaces — return true/false to allow/denyPolicyEnforcer— storage-backed policy list with add/remove/enforce, max 16 policies per tokenTransferAdmin/AcceptAdminon all policy contracts4 reference policies
HoldingLimitPolicyLockupPolicyJurisdictionPolicySanctionsPolicyToken standard hooks
PolicyEnforcerbefore state mutationsRe-entrancy model
Context.IsStaticCallenforced atContractStorage.Set/Deletelevel (internal setter, not escapable by contract code)ActiveCallersuses ref-counting (Dictionary<string, int>) so nested outgoing calls from the same contract are tracked correctly4 new analyzers
CallContract<bool>return value.Set()/.Delete()beforeEnforceTransfer()Dictionary<>/HashSet<>fields in contracts (non-deterministic iteration)DEX quote endpoint
GET /v1/dex/quote?tokenIn={addr}&tokenOut={addr}&amountIn={uint256}&feeBps={uint?}— returns best-pool output across fee tiers [1,5,30,100]Wallet SDK additions
DexSwapIntentBuilder— fluent builder for plaintext (type 10) and encrypted (type 18) swap intent transactionsDexQuoteInfomodel +GetDexQuoteAsynconIBasaltClient,BasaltClient, andBasaltProviderParsedIntent.Parse()exactlyExamples
ComplianceTokenExample— BST-20 with holding limits + jurisdiction + lockupPolicyVaultExample— BST-4626 vault with dual-layer policy enforcementTest plan