Skip to main content

Policy Hooks

Policy hooks provide a composable transfer enforcement system for Basalt token contracts. Token issuers attach policies to their contracts that are automatically checked on every transfer. If any single policy denies a transfer, the entire operation reverts.

Interfaces

The policy system defines two interfaces, one for fungible tokens and one for NFTs:

ITransferPolicy

Verifies fungible token transfers. Implementations inspect the sender, receiver, and amount and either allow or deny the transfer.

public interface ITransferPolicy
{
bool CheckTransfer(Address from, Address to, UInt256 amount);
}

INftTransferPolicy

Verifies non-fungible token transfers. Implementations inspect the sender, receiver, and token ID.

public interface INftTransferPolicy
{
bool CheckNftTransfer(Address from, Address to, UInt256 tokenId);
}

Both interfaces return true to allow the transfer and false to deny it.

PolicyEnforcer

PolicyEnforcer is a storage-backed policy list manager that attaches to token contracts. It maintains an ordered list of policy contract addresses and invokes each one sequentially on every transfer.

Behavior

  • Maximum 16 policies per token. Attempting to register a 17th policy reverts.
  • Ordered evaluation. Policies are invoked in the order they were registered.
  • First-deny-reverts. If any single policy returns false, the entire transfer reverts immediately. Subsequent policies are not evaluated.
  • Owner-restricted management. Only the contract owner can add or remove policies.

Events

EventDescription
PolicyAddedEventEmitted when a new policy is registered. Contains the policy address and the current policy count.
PolicyRemovedEventEmitted when a policy is removed. Contains the policy address and the updated policy count.
TransferDeniedEventEmitted when a transfer is denied by a policy. Contains the policy address, sender, receiver, and the reason.

Reference Policies

Basalt ships four reference policy implementations that cover common regulatory and operational requirements.

PolicyPurposeExample Use Case
HoldingLimitPolicyEnforces a maximum token balance per address.Cap individual holdings at 5% of total supply to comply with concentration regulations.
LockupPolicyEnforces time-based transfer restrictions.Lock tokens until a vesting cliff date. Transfers from locked addresses revert before the lockup expires.
JurisdictionPolicyEnforces geographic transfer restrictions.Block transfers to or from addresses in sanctioned jurisdictions. Jurisdiction codes are mapped to addresses via the compliance engine.
SanctionsPolicyScreens addresses against a sanctions list.Deny transfers involving addresses on an OFAC-style sanctions list. The list is maintained on-chain and updateable by the compliance officer.

Usage in a Contract

To integrate policy hooks into a token contract, instantiate a PolicyEnforcer, expose methods to manage policies, and call EnforceAll in the transfer hook.

public class RegulatedToken : BST20Token
{
private readonly PolicyEnforcer _enforcer = new();

[ContractMethod]
public void AddPolicy(Address policyAddress)
{
RequireOwner();
_enforcer.AddPolicy(policyAddress);
}

[ContractMethod]
public void RemovePolicy(Address policyAddress)
{
RequireOwner();
_enforcer.RemovePolicy(policyAddress);
}

protected override void BeforeTransfer(Address from, Address to, UInt256 amount)
{
_enforcer.EnforceAll(from, to, amount);
}
}

The BeforeTransfer hook is called by the base class before any balance modification occurs. If EnforceAll reverts, no state changes take effect and the transfer is denied.

Deploying and Registering a Policy

Policies are standalone contracts deployed independently and then registered with the token:

// Deploy the policy contract
var holdingLimit = host.Deploy<HoldingLimitPolicy>(owner);

// Configure the policy (e.g., max 5% of supply per address)
host.Call(holdingLimit, owner, p => p.SetLimit(tokenAddress, maxAmount));

// Register the policy with the token
host.Call(token, owner, t => t.AddPolicy(holdingLimit.Address));

Once registered, the policy is enforced automatically on every subsequent transfer. No changes to the transfer call site are required.

Analyzers

Two Roslyn analyzers provide compile-time safety checks specific to policy hooks:

BST010: State Before Policy

Severity: Warning

Warns if contract state is written before a policy check executes. This is a correctness issue: if state is modified before the policy enforcer runs and the policy subsequently denies the transfer, the state modification may not be properly reverted in all code paths.

Pattern detected:

// BST010: State written before policy check
_balances.Set(from, newBalance); // State modification
_enforcer.EnforceAll(from, to, amount); // Policy check happens after

Fix: Always invoke EnforceAll before any state modifications.

BST012: Missing Policy Enforcement

Severity: Warning

Warns if a token contract (any class inheriting from BST20Token, BST721Token, or BST1155Token) does not include policy enforcement in its transfer path. This diagnostic catches cases where a developer creates a regulated token but forgets to wire up the PolicyEnforcer.

Fix: Add a PolicyEnforcer and call EnforceAll in the BeforeTransfer override, or suppress the diagnostic with justification if the token is intentionally unregulated.