From 7fd1cfabce497242fd3afc2f06275b46c3dfcab3 Mon Sep 17 00:00:00 2001 From: jessesawa Date: Sun, 28 Jun 2026 09:52:28 -0400 Subject: [PATCH 1/8] Add EVM asset-compliance examples Reference tokens that enforce compliance on-chain and that Predicate can drive automatically by freezing flagged addresses: - FreezableStablecoin: upgradeable ERC-20 on the Freezable base (implements IFreezable), with EIP-2612 permit, pause, role-gated mint/burn, and a forced-transfer seize. Freeze is enforced in _update. Includes deploy and grant-role scripts and a test suite. - FreezableToken: trimmed to a minimal IFreezable example. - docs/asset-compliance.md: overview, role model, and a compatibility matrix. The freeze role granted to Predicate is scoped to freeze/unfreeze only; seize, mint, burn, pause, and upgrade stay with the issuer (enforced by tests). --- docs/asset-compliance.md | 61 +++++ .../DeployFreezableStablecoin.s.sol | 53 ++++ .../asset-compliance/GrantFreezeManager.s.sol | 55 ++++ .../asset-compliance/FreezableStablecoin.sol | 235 ++++++++++++++++++ .../asset-compliance/FreezableToken.sol | 13 +- src/examples/asset-compliance/README.md | 6 + .../FreezableStablecoin.t.sol | 234 +++++++++++++++++ 7 files changed, 649 insertions(+), 8 deletions(-) create mode 100644 docs/asset-compliance.md create mode 100644 script/asset-compliance/DeployFreezableStablecoin.s.sol create mode 100644 script/asset-compliance/GrantFreezeManager.s.sol create mode 100644 src/examples/asset-compliance/FreezableStablecoin.sol create mode 100644 src/examples/asset-compliance/README.md create mode 100644 test/asset-compliance/FreezableStablecoin.t.sol diff --git a/docs/asset-compliance.md b/docs/asset-compliance.md new file mode 100644 index 00000000..87149261 --- /dev/null +++ b/docs/asset-compliance.md @@ -0,0 +1,61 @@ +# Asset Compliance Examples + +Token contracts that enforce compliance on-chain — freeze, seize, and role-gated admin — and that Predicate can drive automatically. + +Code: [`src/examples/asset-compliance/`](../src/examples/asset-compliance/). + +## How it works + +1. Your token exposes a freeze interface with role-based access control — the [`IFreezable`](../src/interfaces/IFreezable.sol) interface. +2. You grant only the freeze role to Predicate's enforcement address. +3. Predicate monitors the data sources in your policy (e.g. OFAC sanctions) and calls `freeze` on your contract automatically — you can also act manually from the dashboard. You run no infrastructure, and Predicate never holds your admin keys. + +Predicate's enforcement address (grant it `FREEZE_MANAGER_ROLE`): `0x363c256D368277BBFaf6EaF65beE123a7AdbA464` + +## Role separation + +The role you grant Predicate authorizes freeze/unfreeze and nothing else. Mint, burn, seize/clawback, pause, upgrade, and role administration are separate roles you keep. The example token enforces this and tests it. + +If your token bundles seize or burn into the same role as freeze — for example a single "asset protection" role that gates both `freeze` and a balance-wiping `wipeFrozenAddress` — split it before integrating, so the role Predicate holds can never move or destroy funds. + +| Capability | Role | Granted to Predicate | +| --- | --- | :---: | +| Freeze / unfreeze (+ batch) | `FREEZE_MANAGER_ROLE` | yes — only this | +| Seize / clawback | `FORCED_TRANSFER_MANAGER_ROLE` | no | +| Pause | `PAUSER_ROLE` | no | +| Mint / burn | `MINTER_ROLE` / `BURNER_ROLE` | no | +| Admin / upgrade / roles | `DEFAULT_ADMIN_ROLE` | no | + +## Examples + +| File | What it shows | +| --- | --- | +| [`FreezableStablecoin.sol`](../src/examples/asset-compliance/FreezableStablecoin.sol) | Upgradeable ERC-20 on the M^0 [`Freezable`](../src/Freezable.sol) base (implements `IFreezable`), with EIP-2612 permit, pause, role-gated mint/burn, and seize (`forceTransfer`). Freeze is enforced in `_update` — blocks send and receive. | +| [`FreezableToken.sol`](../src/examples/asset-compliance/FreezableToken.sol) | The smallest token that implements `IFreezable`: the bare interface, no extras. | +| [`Freezable.sol`](../src/Freezable.sol) · [`IFreezable.sol`](../src/interfaces/IFreezable.sol) | The reusable freeze base and its interface. | +| [`DeployFreezableStablecoin.s.sol`](../script/asset-compliance/DeployFreezableStablecoin.s.sol) | Deploys the token behind an ERC-1967 proxy with initial roles. | +| [`GrantFreezeManager.s.sol`](../script/asset-compliance/GrantFreezeManager.s.sol) | Grant (`run()`), revoke (`revoke()`), and verify (`check()`) Predicate's `FREEZE_MANAGER_ROLE`. Includes `cast` equivalents. | +| [`FreezableStablecoin.t.sol`](../test/asset-compliance/FreezableStablecoin.t.sol) | Tests, including proofs that the freeze role cannot seize/mint/pause/upgrade and that revocation disables enforcement. | + +```bash +forge test --match-path "*asset-compliance*" +``` + +## Does your existing token already work? + +Predicate drives your token's existing freeze/blocklist interface; most regulated tokens already have one. + +| Token shape | Example | Fit | What it takes | +| --- | --- | --- | --- | +| M^0 `IFreezable` (`freeze`/`unfreeze`/`freezeAccounts` + `FREEZE_MANAGER_ROLE`) | mUSD, USDR | Native | Grant `FREEZE_MANAGER_ROLE`. `FreezableStablecoin` is this shape. | +| Paxos `PaxosTokenV2` (`freeze`/`freezeBatch`/`wipeFrozenAddress` + `ASSET_PROTECTION_ROLE`) | USDG | Compatible | Grant the role, but split off `wipeFrozenAddress` first — that one role also burns (see role separation). Method/role names differ from `IFreezable`, so it uses a dedicated enforcement adapter. | +| Circle FiatToken-style blocklist (`blacklist`/`isBlacklisted` + `blacklister`) | USDC family | Compatible | Grant the blacklister role; dedicated adapter. | +| Standalone blocklist contract (`addToBlocklist`/`isBlocked`) | Ondo `Blocklist` | Drivable | Grant write access; prefer a role-scoped owner over a single `Ownable` owner. | + +No freeze interface yet? Start from [`FreezableStablecoin.sol`](../src/examples/asset-compliance/FreezableStablecoin.sol). + +## Further reading + +- [Compliance for Assets — overview](https://docs.predicate.io/v2/assets/overview) +- [Enroll your asset](https://docs.predicate.io/v2/assets/get-started) +- [Security model](https://docs.predicate.io/v2/assets/security) diff --git a/script/asset-compliance/DeployFreezableStablecoin.s.sol b/script/asset-compliance/DeployFreezableStablecoin.s.sol new file mode 100644 index 00000000..26595b79 --- /dev/null +++ b/script/asset-compliance/DeployFreezableStablecoin.s.sol @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.28; + +import {Script} from "forge-std/Script.sol"; +import {console2} from "forge-std/console2.sol"; +import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; + +import {FreezableStablecoin} from "../../src/examples/asset-compliance/FreezableStablecoin.sol"; + +/** + * @title DeployFreezableStablecoin + * @notice Deploys {FreezableStablecoin} behind an ERC-1967 proxy with least-privilege role holders. + * @dev By default `FREEZE_MANAGER_ROLE` is assigned to Predicate's freezer so the asset is + * enrollment-ready; override any holder via env vars. Seize/mint/burn/pause default to the + * admin and should be moved to dedicated issuer keys in production. + * + * Usage: + * ADMIN=0x... forge script script/asset-compliance/DeployFreezableStablecoin.s.sol \ + * --rpc-url $RPC_URL --broadcast + */ +contract DeployFreezableStablecoin is Script { + /// @notice Predicate's authorized freezer across all EVM chains (docs.predicate.io/v2/assets). + address public constant PREDICATE_FREEZER = 0x363c256D368277BBFaf6EaF65beE123a7AdbA464; + + function run() external returns (FreezableStablecoin token, address proxy) { + address admin = vm.envOr("ADMIN", msg.sender); + address freezeManager = vm.envOr("FREEZE_MANAGER", PREDICATE_FREEZER); + address pauser = vm.envOr("PAUSER", admin); + address seizeManager = vm.envOr("SEIZE_MANAGER", admin); + address minter = vm.envOr("MINTER", admin); + address burner = vm.envOr("BURNER", admin); + + string memory name_ = vm.envOr("TOKEN_NAME", string("Compliant USD")); + string memory symbol_ = vm.envOr("TOKEN_SYMBOL", string("cUSD")); + + vm.startBroadcast(); + + FreezableStablecoin impl = new FreezableStablecoin(); + bytes memory initData = abi.encodeCall( + FreezableStablecoin.initialize, (name_, symbol_, admin, freezeManager, pauser, seizeManager, minter, burner) + ); + ERC1967Proxy proxy_ = new ERC1967Proxy(address(impl), initData); + + vm.stopBroadcast(); + + proxy = address(proxy_); + token = FreezableStablecoin(proxy); + + console2.log("FreezableStablecoin implementation:", address(impl)); + console2.log("FreezableStablecoin proxy:", proxy); + console2.log("FREEZE_MANAGER_ROLE -> ", freezeManager); + } +} diff --git a/script/asset-compliance/GrantFreezeManager.s.sol b/script/asset-compliance/GrantFreezeManager.s.sol new file mode 100644 index 00000000..d69e26ea --- /dev/null +++ b/script/asset-compliance/GrantFreezeManager.s.sol @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.28; + +import {Script} from "forge-std/Script.sol"; +import {console2} from "forge-std/console2.sol"; + +import {FreezableStablecoin} from "../../src/examples/asset-compliance/FreezableStablecoin.sol"; + +/** + * @title GrantFreezeManager + * @notice Grants / revokes / verifies Predicate's freezer on a deployed asset — the on-chain step + * that makes a token enforceable by Predicate asset compliance. + * @dev `FREEZE_MANAGER_ROLE` is the ONLY authority Predicate needs. It is revocable in one + * transaction (`revoke`), verifiable (`check`), and never confers seize/mint/burn/pause/upgrade. + * + * Grant: TOKEN=0x.. forge script script/asset-compliance/GrantFreezeManager.s.sol --sig "run()" --rpc-url $RPC_URL --broadcast + * Revoke: TOKEN=0x.. forge script script/asset-compliance/GrantFreezeManager.s.sol --sig "revoke()" --rpc-url $RPC_URL --broadcast + * Check: TOKEN=0x.. forge script script/asset-compliance/GrantFreezeManager.s.sol --sig "check()" --rpc-url $RPC_URL + * + * Equivalent `cast` (mirrors https://docs.predicate.io/v2/assets/get-started): + * cast send "grantRole(bytes32,address)" $(cast keccak "FREEZE_MANAGER_ROLE") \ + * 0x363c256D368277BBFaf6EaF65beE123a7AdbA464 --rpc-url $RPC_URL --private-key $PK + * cast call "hasRole(bytes32,address)(bool)" $(cast keccak "FREEZE_MANAGER_ROLE") \ + * 0x363c256D368277BBFaf6EaF65beE123a7AdbA464 --rpc-url $RPC_URL + */ +contract GrantFreezeManager is Script { + /// @notice Predicate's authorized freezer across all EVM chains. + address public constant PREDICATE_FREEZER = 0x363c256D368277BBFaf6EaF65beE123a7AdbA464; + + function run() external { + FreezableStablecoin token = _token(); + vm.startBroadcast(); + token.grantRole(token.FREEZE_MANAGER_ROLE(), PREDICATE_FREEZER); + vm.stopBroadcast(); + console2.log("Granted FREEZE_MANAGER_ROLE to Predicate freezer on", address(token)); + } + + function revoke() external { + FreezableStablecoin token = _token(); + vm.startBroadcast(); + token.revokeRole(token.FREEZE_MANAGER_ROLE(), PREDICATE_FREEZER); + vm.stopBroadcast(); + console2.log("Revoked FREEZE_MANAGER_ROLE from Predicate freezer on", address(token)); + } + + function check() external view returns (bool granted) { + FreezableStablecoin token = _token(); + granted = token.hasRole(token.FREEZE_MANAGER_ROLE(), PREDICATE_FREEZER); + console2.log("Predicate freezer holds FREEZE_MANAGER_ROLE:", granted); + } + + function _token() internal view returns (FreezableStablecoin) { + return FreezableStablecoin(vm.envAddress("TOKEN")); + } +} diff --git a/src/examples/asset-compliance/FreezableStablecoin.sol b/src/examples/asset-compliance/FreezableStablecoin.sol new file mode 100644 index 00000000..75809072 --- /dev/null +++ b/src/examples/asset-compliance/FreezableStablecoin.sol @@ -0,0 +1,235 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.28; + +import {ERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; +import { + ERC20PermitUpgradeable +} from "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC20PermitUpgradeable.sol"; +import {PausableUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; +import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; + +import {Freezable} from "../../Freezable.sol"; + +/** + * @title FreezableStablecoin + * @author Predicate Labs, Inc (https://predicate.io) + * @notice Upgradeable ERC-20 with on-chain freeze enforcement, pause, role-gated mint/burn, and + * a forced-transfer seize. Frozen accounts cannot send or receive. + * @dev Implements {IFreezable} via the {Freezable} base; the freeze check lives in {_update}. + * FREEZE_MANAGER_ROLE (granted to Predicate) only freezes and unfreezes; seize, pause, + * mint, burn, and upgrades are separate, issuer-held roles. See docs/asset-compliance.md + * for the role model and integration steps. + */ +contract FreezableStablecoin is + ERC20Upgradeable, + ERC20PermitUpgradeable, + PausableUpgradeable, + UUPSUpgradeable, + Freezable +{ + /* ============ Roles ============ */ + + /// @notice Role allowed to perform compliance seizures (forced transfers). Issuer-held; never granted to Predicate. + bytes32 public constant FORCED_TRANSFER_MANAGER_ROLE = keccak256("FORCED_TRANSFER_MANAGER_ROLE"); + + /// @notice Role allowed to pause/unpause all transfers. + bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE"); + + /// @notice Role allowed to mint new supply. + bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE"); + + /// @notice Role allowed to burn supply. + bytes32 public constant BURNER_ROLE = keccak256("BURNER_ROLE"); + + /* ============ Events ============ */ + + /** + * @notice Emitted when a compliance seizure moves a frozen account's balance. + * @param from The frozen account funds were seized from. + * @param to The recipient of the seized funds (e.g. an issuer treasury). + * @param amount The amount seized. + */ + event ForcedTransfer(address indexed from, address indexed to, uint256 amount); + + /* ============ Errors ============ */ + + /// @notice Thrown when a required address argument is the zero address. + error ZeroAddress(); + + /// @notice Thrown when batch input arrays have mismatched lengths. + error LengthMismatch(); + + /* ============ Constructor / Initializer ============ */ + + /// @dev Locks the implementation; state is initialized on the proxy via {initialize}. + constructor() { + _disableInitializers(); + } + + /** + * @notice Initializes the token and assigns roles to distinct holders (least privilege). + * @param name_ ERC-20 name. + * @param symbol_ ERC-20 symbol. + * @param admin DEFAULT_ADMIN_ROLE holder (role admin + upgrade authority). + * @param freezeManager FREEZE_MANAGER_ROLE holder — grant this to Predicate's freezer. + * @param pauser PAUSER_ROLE holder. + * @param forcedTransferManager FORCED_TRANSFER_MANAGER_ROLE holder (seize) — keep issuer-side. + * @param minter MINTER_ROLE holder. + * @param burner BURNER_ROLE holder. + */ + function initialize( + string memory name_, + string memory symbol_, + address admin, + address freezeManager, + address pauser, + address forcedTransferManager, + address minter, + address burner + ) external initializer { + if (admin == address(0)) revert ZeroAddress(); + + __ERC20_init(name_, symbol_); + __ERC20Permit_init(name_); + __Pausable_init(); + __AccessControl_init(); + __UUPSUpgradeable_init(); + __Freezable_init(freezeManager); // grants FREEZE_MANAGER_ROLE to `freezeManager` + + _grantRole(DEFAULT_ADMIN_ROLE, admin); + _grantRole(PAUSER_ROLE, pauser); + _grantRole(FORCED_TRANSFER_MANAGER_ROLE, forcedTransferManager); + _grantRole(MINTER_ROLE, minter); + _grantRole(BURNER_ROLE, burner); + } + + /* ============ Supply Management (issuer-only) ============ */ + + /** + * @notice Mints `amount` to `to`. Reverts if `to` is frozen or the token is paused. + * @dev Routes through {_update}, so compliance checks apply to minting. + */ + function mint( + address to, + uint256 amount + ) external onlyRole(MINTER_ROLE) { + _mint(to, amount); + } + + /** + * @notice Burns `amount` from `from`. Reverts if `from` is frozen or the token is paused. + * @dev To retire a sanctioned balance, first {forceTransfer} it to a treasury, then burn there. + */ + function burn( + address from, + uint256 amount + ) external onlyRole(BURNER_ROLE) { + _burn(from, amount); + } + + /* ============ Pause (issuer-only) ============ */ + + /// @notice Pauses all ordinary transfers, mints, and burns. + function pause() external onlyRole(PAUSER_ROLE) { + _pause(); + } + + /// @notice Resumes ordinary transfers. + function unpause() external onlyRole(PAUSER_ROLE) { + _unpause(); + } + + /* ============ Seize / Forced Transfer (issuer-only) ============ */ + + /** + * @notice Seizes `amount` from a frozen `from` and moves it to `to`. + * @dev The source MUST already be frozen — this enforces the freeze-then-seize workflow and + * keeps the two powers in different hands (Predicate freezes; the issuer seizes). + * Intentionally bypasses the pause and frozen checks (see {_forceTransfer}). + */ + function forceTransfer( + address from, + address to, + uint256 amount + ) external onlyRole(FORCED_TRANSFER_MANAGER_ROLE) { + _forceTransfer(from, to, amount); + } + + /** + * @notice Batch variant of {forceTransfer}. + * @dev All input arrays must have equal length. + */ + function forceTransfers( + address[] calldata froms, + address[] calldata tos, + uint256[] calldata amounts + ) external onlyRole(FORCED_TRANSFER_MANAGER_ROLE) { + if (froms.length != tos.length || froms.length != amounts.length) revert LengthMismatch(); + for (uint256 i; i < froms.length; ++i) { + _forceTransfer(froms[i], tos[i], amounts[i]); + } + } + + /* ============ Transfers ============ */ + + /** + * @notice ERC-20 `transferFrom` with an added check that the spender is not frozen. + * @dev {_update} already blocks a frozen `from` or `to`; this additionally blocks a frozen + * spender from moving a third party's funds, mirroring mUSD / Paxos USDG semantics + * (a sanctioned operator must not be able to initiate transfers it has an allowance for). + */ + function transferFrom( + address from, + address to, + uint256 value + ) public override returns (bool) { + _revertIfFrozen(_msgSender()); + return super.transferFrom(from, to, value); + } + + /* ============ View ============ */ + + /// @inheritdoc ERC20Upgradeable + function decimals() public pure override returns (uint8) { + return 6; + } + + /* ============ Internal ============ */ + + /** + * @dev Compliance hook for all balance movements (transfers, mints, burns). + * Blocks the movement when the token is paused or when either party is frozen. + * Seizures call `super._update` directly and therefore skip this override on purpose. + */ + function _update( + address from, + address to, + uint256 value + ) internal override { + _requireNotPaused(); + _revertIfFrozen(from); + _revertIfFrozen(to); + super._update(from, to, value); + } + + /** + * @dev Executes a seizure. Requires the source to be frozen, then calls `super._update` + * directly to bypass the pause/frozen guards in {_update} — a compliance seizure must + * succeed precisely when the account is frozen (and even while the token is paused). + */ + function _forceTransfer( + address from, + address to, + uint256 amount + ) internal { + if (to == address(0)) revert ZeroAddress(); + _revertIfNotFrozen(from); // seize only operates on already-frozen accounts + super._update(from, to, amount); + emit ForcedTransfer(from, to, amount); + } + + /// @dev Restricts upgrades to the admin (UUPS). + function _authorizeUpgrade( + address newImplementation + ) internal override onlyRole(DEFAULT_ADMIN_ROLE) {} +} diff --git a/src/examples/asset-compliance/FreezableToken.sol b/src/examples/asset-compliance/FreezableToken.sol index bade6a7e..c31917d6 100644 --- a/src/examples/asset-compliance/FreezableToken.sol +++ b/src/examples/asset-compliance/FreezableToken.sol @@ -6,14 +6,11 @@ import {Freezable} from "../../Freezable.sol"; /** * @title FreezableToken * @author Predicate Labs, Inc (https://predicate.io) - * @notice Minimal example demonstrating asset compliance via account freezing. - * @dev Shows how to integrate Freezable into a token to block frozen accounts from transfers. - * - * Asset Compliance vs Application Compliance: - * - Asset Compliance: Enforcement at the asset level (this example) - * The token itself blocks frozen accounts from transferring. - * - Application Compliance: Enforcement via Predicate attestations - * See BasicVault.sol and AdvancedVault.sol for those patterns. + * @notice Minimal token built on the {Freezable} base — the smallest example of enforcing a + * freeze list in `transfer`. See {FreezableStablecoin} for a full ERC-20 with pause, + * mint/burn, and seize. + * @dev Frozen accounts can neither send nor receive. `freeze`/`unfreeze` are gated by + * FREEZE_MANAGER_ROLE; grant that role to Predicate's freezer to enable enforcement. */ contract FreezableToken is Freezable { mapping(address => uint256) public balances; diff --git a/src/examples/asset-compliance/README.md b/src/examples/asset-compliance/README.md new file mode 100644 index 00000000..ee224fb5 --- /dev/null +++ b/src/examples/asset-compliance/README.md @@ -0,0 +1,6 @@ +# EVM Asset Compliance Examples + +- `FreezableStablecoin.sol` — upgradeable ERC-20 with freeze, pause, mint/burn, and seize. +- `FreezableToken.sol` — the minimal token implementing `IFreezable`. + +See [`docs/asset-compliance.md`](../../../docs/asset-compliance.md) for the overview, role model, and the "does your token fit?" matrix. Scripts live in `script/asset-compliance/`, tests in `test/asset-compliance/`. diff --git a/test/asset-compliance/FreezableStablecoin.t.sol b/test/asset-compliance/FreezableStablecoin.t.sol new file mode 100644 index 00000000..c2432a41 --- /dev/null +++ b/test/asset-compliance/FreezableStablecoin.t.sol @@ -0,0 +1,234 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.28; + +import {Test} from "forge-std/Test.sol"; +import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; +import {IAccessControl} from "@openzeppelin/contracts/access/IAccessControl.sol"; +import {PausableUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; + +import {IFreezable} from "../../src/interfaces/IFreezable.sol"; +import {FreezableStablecoin} from "../../src/examples/asset-compliance/FreezableStablecoin.sol"; + +/// @notice Tests the asset-compliance invariants of {FreezableStablecoin}, with emphasis on +/// role separation: Predicate's FREEZE_MANAGER_ROLE must NOT be able to seize/mint/pause/upgrade. +contract FreezableStablecoinTest is Test { + // Local mirror of the event for expectEmit matching. + event ForcedTransfer(address indexed from, address indexed to, uint256 amount); + + FreezableStablecoin internal token; + + address internal admin = makeAddr("admin"); + address internal predicateFreezer = makeAddr("predicateFreezer"); // FREEZE_MANAGER_ROLE (Predicate) + address internal pauser = makeAddr("pauser"); + address internal seizer = makeAddr("seizer"); // FORCED_TRANSFER_MANAGER_ROLE (issuer) + address internal minter = makeAddr("minter"); + address internal burner = makeAddr("burner"); + address internal treasury = makeAddr("treasury"); + address internal alice = makeAddr("alice"); + address internal bob = makeAddr("bob"); + + // Role IDs cached in setUp. Reading them via an external call between vm.prank and the target + // call would consume the prank, so they are resolved once here. + bytes32 internal roleFreeze; + bytes32 internal roleSeize; + bytes32 internal rolePause; + bytes32 internal roleMint; + bytes32 internal roleAdmin; + + uint256 internal constant INITIAL = 1_000_000e6; + + function setUp() public { + FreezableStablecoin impl = new FreezableStablecoin(); + bytes memory initData = abi.encodeCall( + FreezableStablecoin.initialize, + ("Compliant USD", "cUSD", admin, predicateFreezer, pauser, seizer, minter, burner) + ); + token = FreezableStablecoin(address(new ERC1967Proxy(address(impl), initData))); + + roleFreeze = token.FREEZE_MANAGER_ROLE(); + roleSeize = token.FORCED_TRANSFER_MANAGER_ROLE(); + rolePause = token.PAUSER_ROLE(); + roleMint = token.MINTER_ROLE(); + roleAdmin = token.DEFAULT_ADMIN_ROLE(); + + vm.prank(minter); + token.mint(alice, INITIAL); + } + + /* ============ Freeze blocks send AND receive ============ */ + + function test_freeze_blocksSend() public { + vm.prank(predicateFreezer); + token.freeze(alice); + vm.prank(alice); + vm.expectRevert(abi.encodeWithSelector(IFreezable.AccountFrozen.selector, alice)); + token.transfer(bob, 1e6); + } + + function test_freeze_blocksReceive() public { + vm.prank(predicateFreezer); + token.freeze(bob); + vm.prank(alice); + vm.expectRevert(abi.encodeWithSelector(IFreezable.AccountFrozen.selector, bob)); + token.transfer(bob, 1e6); + } + + function test_freeze_blocksTransferFromWhenOwnerFrozen() public { + vm.prank(alice); + token.approve(bob, 1e6); + vm.prank(predicateFreezer); + token.freeze(alice); + vm.prank(bob); + vm.expectRevert(abi.encodeWithSelector(IFreezable.AccountFrozen.selector, alice)); + token.transferFrom(alice, bob, 1e6); + } + + function test_freeze_blocksTransferFromWhenSpenderFrozen() public { + // A frozen spender must not be able to move a third party's funds, even with an allowance. + vm.prank(alice); + token.approve(bob, 1e6); + vm.prank(predicateFreezer); + token.freeze(bob); + vm.prank(bob); + vm.expectRevert(abi.encodeWithSelector(IFreezable.AccountFrozen.selector, bob)); + token.transferFrom(alice, treasury, 1e6); + } + + function test_batchFreezeUnfreeze() public { + address[] memory accts = new address[](2); + accts[0] = alice; + accts[1] = bob; + + vm.prank(predicateFreezer); + token.freezeAccounts(accts); + assertTrue(token.isFrozen(alice)); + assertTrue(token.isFrozen(bob)); + + vm.prank(predicateFreezer); + token.unfreezeAccounts(accts); + assertFalse(token.isFrozen(alice)); + assertFalse(token.isFrozen(bob)); + } + + /* ============ Seize (forced transfer) ============ */ + + function test_seize_movesFrozenBalance() public { + vm.prank(predicateFreezer); + token.freeze(alice); + + vm.expectEmit(true, true, false, true, address(token)); + emit ForcedTransfer(alice, treasury, INITIAL); + + vm.prank(seizer); + token.forceTransfer(alice, treasury, INITIAL); + + assertEq(token.balanceOf(alice), 0); + assertEq(token.balanceOf(treasury), INITIAL); + } + + function test_seize_requiresFrozenSource() public { + vm.prank(seizer); + vm.expectRevert(abi.encodeWithSelector(IFreezable.AccountNotFrozen.selector, alice)); + token.forceTransfer(alice, treasury, 1e6); + } + + function test_seize_worksWhilePaused() public { + vm.prank(predicateFreezer); + token.freeze(alice); + vm.prank(pauser); + token.pause(); + + vm.prank(seizer); + token.forceTransfer(alice, treasury, INITIAL); + assertEq(token.balanceOf(treasury), INITIAL); + } + + /* ============ Pause ============ */ + + function test_pause_blocksTransfers() public { + vm.prank(pauser); + token.pause(); + vm.prank(alice); + vm.expectRevert(PausableUpgradeable.EnforcedPause.selector); + token.transfer(bob, 1e6); + } + + /* ============ Role separation (the core invariant) ============ */ + + function test_roleSeparation_freezeManagerCannotSeize() public { + vm.prank(predicateFreezer); + token.freeze(alice); + vm.prank(predicateFreezer); + vm.expectRevert( + abi.encodeWithSelector( + IAccessControl.AccessControlUnauthorizedAccount.selector, predicateFreezer, roleSeize + ) + ); + token.forceTransfer(alice, treasury, 1e6); + } + + function test_roleSeparation_freezeManagerCannotMint() public { + vm.prank(predicateFreezer); + vm.expectRevert( + abi.encodeWithSelector(IAccessControl.AccessControlUnauthorizedAccount.selector, predicateFreezer, roleMint) + ); + token.mint(predicateFreezer, 1e6); + } + + function test_roleSeparation_freezeManagerCannotPause() public { + vm.prank(predicateFreezer); + vm.expectRevert( + abi.encodeWithSelector( + IAccessControl.AccessControlUnauthorizedAccount.selector, predicateFreezer, rolePause + ) + ); + token.pause(); + } + + function test_roleSeparation_freezeManagerCannotUpgrade() public { + address newImpl = address(new FreezableStablecoin()); + vm.prank(predicateFreezer); + vm.expectRevert( + abi.encodeWithSelector( + IAccessControl.AccessControlUnauthorizedAccount.selector, predicateFreezer, roleAdmin + ) + ); + token.upgradeToAndCall(newImpl, ""); + } + + function test_roleSeparation_seizerCannotFreeze() public { + vm.prank(seizer); + vm.expectRevert( + abi.encodeWithSelector(IAccessControl.AccessControlUnauthorizedAccount.selector, seizer, roleFreeze) + ); + token.freeze(alice); + } + + /* ============ Revocation kill-switch ============ */ + + function test_revokeFreezeManager_disablesFreeze() public { + vm.prank(admin); + token.revokeRole(roleFreeze, predicateFreezer); + + vm.prank(predicateFreezer); + vm.expectRevert( + abi.encodeWithSelector( + IAccessControl.AccessControlUnauthorizedAccount.selector, predicateFreezer, roleFreeze + ) + ); + token.freeze(alice); + } + + /* ============ Sanity ============ */ + + function test_normalTransferSucceeds() public { + vm.prank(alice); + token.transfer(bob, 10e6); + assertEq(token.balanceOf(bob), 10e6); + assertEq(token.balanceOf(alice), INITIAL - 10e6); + } + + function test_decimalsIsSix() public { + assertEq(token.decimals(), 6); + } +} From f6d6ecfaf19fc19fad68991c964306ef18b33ca4 Mon Sep 17 00:00:00 2001 From: jessesawa Date: Mon, 29 Jun 2026 11:01:55 -0400 Subject: [PATCH 2/8] Reframe asset-compliance roles as a recommended model The defined roles and their holders are ultimately the issuer's choice. Present least-privilege RBAC with separation of duties as the recommended industry-standard pattern, add a dedicated freeze-manager-role section scoping FREEZE_MANAGER_ROLE, and make the role table a recommendation. --- docs/asset-compliance.md | 28 ++++++++++++++++--------- src/examples/asset-compliance/README.md | 2 +- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/docs/asset-compliance.md b/docs/asset-compliance.md index 87149261..cb47e3f0 100644 --- a/docs/asset-compliance.md +++ b/docs/asset-compliance.md @@ -12,19 +12,27 @@ Code: [`src/examples/asset-compliance/`](../src/examples/asset-compliance/). Predicate's enforcement address (grant it `FREEZE_MANAGER_ROLE`): `0x363c256D368277BBFaf6EaF65beE123a7AdbA464` -## Role separation +## Recommended role model -The role you grant Predicate authorizes freeze/unfreeze and nothing else. Mint, burn, seize/clawback, pause, upgrade, and role administration are separate roles you keep. The example token enforces this and tests it. +The roles a token defines, and who holds them, are ultimately the issuer's choice — it's your contract and your access-control policy. What follows is the pattern we **recommend** and that the example token implements: standard role-based access control with separation of duties, the same least-privilege model regulated stablecoins such as M^0's mUSD, Circle's USDC, and Paxos's USDG follow. -If your token bundles seize or burn into the same role as freeze — for example a single "asset protection" role that gates both `freeze` and a balance-wiping `wipeFrozenAddress` — split it before integrating, so the role Predicate holds can never move or destroy funds. +The principle: no single key should be able to both freeze an account and move or destroy its funds. Each capability lives in its own role, each role is held by a different key, and a `DEFAULT_ADMIN_ROLE` — ideally a multisig or timelock — administers the others and authorizes upgrades. -| Capability | Role | Granted to Predicate | -| --- | --- | :---: | -| Freeze / unfreeze (+ batch) | `FREEZE_MANAGER_ROLE` | yes — only this | -| Seize / clawback | `FORCED_TRANSFER_MANAGER_ROLE` | no | -| Pause | `PAUSER_ROLE` | no | -| Mint / burn | `MINTER_ROLE` / `BURNER_ROLE` | no | -| Admin / upgrade / roles | `DEFAULT_ADMIN_ROLE` | no | +| Capability | Role | Recommended holder | +| --- | --- | --- | +| Freeze / unfreeze (+ batch) | `FREEZE_MANAGER_ROLE` | Predicate enforcer (and/or issuer ops) | +| Seize / clawback | `FORCED_TRANSFER_MANAGER_ROLE` | Issuer — separate key from freeze | +| Pause | `PAUSER_ROLE` | Issuer ops | +| Mint / burn | `MINTER_ROLE` / `BURNER_ROLE` | Issuer treasury | +| Admin / upgrade / roles | `DEFAULT_ADMIN_ROLE` | Issuer multisig / timelock | + +### The freeze manager role + +`FREEZE_MANAGER_ROLE` is the only role you grant Predicate, and the one worth scoping most carefully: + +- **Grant it to Predicate's enforcement address and nothing else.** The role authorizes `freeze`, `unfreeze`, and their batch variants only — it cannot mint, burn, seize, pause, upgrade, or administer roles. Freezing is reversible and moves no funds, so an automated holder is low-risk by construction. The example token enforces this boundary and [tests it](../test/asset-compliance/FreezableStablecoin.t.sol). +- **Keep it separate from seize.** The freeze-then-seize split is deliberate: an automated or shared key freezes a flagged account, and a distinct issuer-held role decides whether to claw the balance back. If your token bundles both into a single "asset protection" role — for example one role that gates `freeze` and a balance-wiping `wipeFrozenAddress` — split it before integrating, so the role Predicate (or any shared key) holds can never move or destroy funds. +- **You stay in control.** You can hold this role yourself and freeze manually from the dashboard, delegate it to Predicate for automated enforcement, or both. Granting and revoking it is a single `AccessControl` call — see [`GrantFreezeManager.s.sol`](../script/asset-compliance/GrantFreezeManager.s.sol). ## Examples diff --git a/src/examples/asset-compliance/README.md b/src/examples/asset-compliance/README.md index ee224fb5..6446a589 100644 --- a/src/examples/asset-compliance/README.md +++ b/src/examples/asset-compliance/README.md @@ -3,4 +3,4 @@ - `FreezableStablecoin.sol` — upgradeable ERC-20 with freeze, pause, mint/burn, and seize. - `FreezableToken.sol` — the minimal token implementing `IFreezable`. -See [`docs/asset-compliance.md`](../../../docs/asset-compliance.md) for the overview, role model, and the "does your token fit?" matrix. Scripts live in `script/asset-compliance/`, tests in `test/asset-compliance/`. +See [`docs/asset-compliance.md`](../../../docs/asset-compliance.md) for the overview, the recommended role model, and the "does your token fit?" matrix. Scripts live in `script/asset-compliance/`, tests in `test/asset-compliance/`. From cbb7aa36e79250f0455c76d31ec0c5340f3da971 Mon Sep 17 00:00:00 2001 From: jessesawa Date: Mon, 29 Jun 2026 12:57:15 -0400 Subject: [PATCH 3/8] Trim asset-compliance docs and use 'onchain' spelling Remove third-party token name-drops, tighten to a technical tone, and apply the onchain spelling across the docs and contract/script NatSpec. --- docs/asset-compliance.md | 50 +++++++++---------- .../DeployFreezableStablecoin.s.sol | 2 +- .../asset-compliance/GrantFreezeManager.s.sol | 2 +- .../asset-compliance/FreezableStablecoin.sol | 6 +-- 4 files changed, 29 insertions(+), 31 deletions(-) diff --git a/docs/asset-compliance.md b/docs/asset-compliance.md index cb47e3f0..496a64f7 100644 --- a/docs/asset-compliance.md +++ b/docs/asset-compliance.md @@ -1,64 +1,62 @@ # Asset Compliance Examples -Token contracts that enforce compliance on-chain — freeze, seize, and role-gated admin — and that Predicate can drive automatically. +Example token contracts that enforce compliance onchain — freeze, seize, and role-gated admin — and that Predicate can drive automatically. Code: [`src/examples/asset-compliance/`](../src/examples/asset-compliance/). ## How it works -1. Your token exposes a freeze interface with role-based access control — the [`IFreezable`](../src/interfaces/IFreezable.sol) interface. +1. Your token exposes a freeze interface gated by role-based access control — the [`IFreezable`](../src/interfaces/IFreezable.sol) interface. 2. You grant only the freeze role to Predicate's enforcement address. -3. Predicate monitors the data sources in your policy (e.g. OFAC sanctions) and calls `freeze` on your contract automatically — you can also act manually from the dashboard. You run no infrastructure, and Predicate never holds your admin keys. +3. Predicate watches the data sources in your policy (e.g. OFAC sanctions) and calls `freeze` on your contract automatically. You can also freeze manually from the dashboard. You run no infrastructure, and Predicate never holds your admin keys. Predicate's enforcement address (grant it `FREEZE_MANAGER_ROLE`): `0x363c256D368277BBFaf6EaF65beE123a7AdbA464` -## Recommended role model +## Role model -The roles a token defines, and who holds them, are ultimately the issuer's choice — it's your contract and your access-control policy. What follows is the pattern we **recommend** and that the example token implements: standard role-based access control with separation of duties, the same least-privilege model regulated stablecoins such as M^0's mUSD, Circle's USDC, and Paxos's USDG follow. +The roles a token defines, and who holds them, are the issuer's choice. The pattern below is the one the example token implements: role-based access control where no single key can both freeze an account and move or destroy its funds. Each capability is its own role held by a separate key, and `DEFAULT_ADMIN_ROLE` (ideally a multisig or timelock) administers the others and authorizes upgrades. -The principle: no single key should be able to both freeze an account and move or destroy its funds. Each capability lives in its own role, each role is held by a different key, and a `DEFAULT_ADMIN_ROLE` — ideally a multisig or timelock — administers the others and authorizes upgrades. - -| Capability | Role | Recommended holder | +| Capability | Role | Holder | | --- | --- | --- | -| Freeze / unfreeze (+ batch) | `FREEZE_MANAGER_ROLE` | Predicate enforcer (and/or issuer ops) | -| Seize / clawback | `FORCED_TRANSFER_MANAGER_ROLE` | Issuer — separate key from freeze | +| Freeze / unfreeze (+ batch) | `FREEZE_MANAGER_ROLE` | Predicate enforcer, and/or issuer ops | +| Seize / clawback | `FORCED_TRANSFER_MANAGER_ROLE` | Issuer, separate key from freeze | | Pause | `PAUSER_ROLE` | Issuer ops | | Mint / burn | `MINTER_ROLE` / `BURNER_ROLE` | Issuer treasury | -| Admin / upgrade / roles | `DEFAULT_ADMIN_ROLE` | Issuer multisig / timelock | +| Admin / upgrade / roles | `DEFAULT_ADMIN_ROLE` | Issuer multisig or timelock | ### The freeze manager role -`FREEZE_MANAGER_ROLE` is the only role you grant Predicate, and the one worth scoping most carefully: +`FREEZE_MANAGER_ROLE` is the only role you grant Predicate. -- **Grant it to Predicate's enforcement address and nothing else.** The role authorizes `freeze`, `unfreeze`, and their batch variants only — it cannot mint, burn, seize, pause, upgrade, or administer roles. Freezing is reversible and moves no funds, so an automated holder is low-risk by construction. The example token enforces this boundary and [tests it](../test/asset-compliance/FreezableStablecoin.t.sol). -- **Keep it separate from seize.** The freeze-then-seize split is deliberate: an automated or shared key freezes a flagged account, and a distinct issuer-held role decides whether to claw the balance back. If your token bundles both into a single "asset protection" role — for example one role that gates `freeze` and a balance-wiping `wipeFrozenAddress` — split it before integrating, so the role Predicate (or any shared key) holds can never move or destroy funds. -- **You stay in control.** You can hold this role yourself and freeze manually from the dashboard, delegate it to Predicate for automated enforcement, or both. Granting and revoking it is a single `AccessControl` call — see [`GrantFreezeManager.s.sol`](../script/asset-compliance/GrantFreezeManager.s.sol). +- It authorizes `freeze`, `unfreeze`, and their batch variants, and nothing else: it cannot mint, burn, seize, pause, upgrade, or administer roles. Freezing is reversible and moves no funds, though it does stop the account from sending and receiving. The example token enforces this boundary, and the [tests](../test/asset-compliance/FreezableStablecoin.t.sol) check it. +- Keep it separate from seize. Freezing and seizing are different powers: a freeze flags an account, and a distinct issuer-held role decides whether to claw the balance back. If your token combines both in one role, split them before granting, so the freeze key can never move or destroy funds. +- Hold the role yourself, delegate it to Predicate, or both. Granting and revoking it is a single `AccessControl` call — see [`GrantFreezeManager.s.sol`](../script/asset-compliance/GrantFreezeManager.s.sol). ## Examples | File | What it shows | | --- | --- | -| [`FreezableStablecoin.sol`](../src/examples/asset-compliance/FreezableStablecoin.sol) | Upgradeable ERC-20 on the M^0 [`Freezable`](../src/Freezable.sol) base (implements `IFreezable`), with EIP-2612 permit, pause, role-gated mint/burn, and seize (`forceTransfer`). Freeze is enforced in `_update` — blocks send and receive. | +| [`FreezableStablecoin.sol`](../src/examples/asset-compliance/FreezableStablecoin.sol) | Upgradeable ERC-20 on the [`Freezable`](../src/Freezable.sol) base (implements `IFreezable`), with EIP-2612 permit, pause, role-gated mint/burn, and seize (`forceTransfer`). Freeze is enforced in `_update`, blocking both send and receive. | | [`FreezableToken.sol`](../src/examples/asset-compliance/FreezableToken.sol) | The smallest token that implements `IFreezable`: the bare interface, no extras. | | [`Freezable.sol`](../src/Freezable.sol) · [`IFreezable.sol`](../src/interfaces/IFreezable.sol) | The reusable freeze base and its interface. | | [`DeployFreezableStablecoin.s.sol`](../script/asset-compliance/DeployFreezableStablecoin.s.sol) | Deploys the token behind an ERC-1967 proxy with initial roles. | -| [`GrantFreezeManager.s.sol`](../script/asset-compliance/GrantFreezeManager.s.sol) | Grant (`run()`), revoke (`revoke()`), and verify (`check()`) Predicate's `FREEZE_MANAGER_ROLE`. Includes `cast` equivalents. | -| [`FreezableStablecoin.t.sol`](../test/asset-compliance/FreezableStablecoin.t.sol) | Tests, including proofs that the freeze role cannot seize/mint/pause/upgrade and that revocation disables enforcement. | +| [`GrantFreezeManager.s.sol`](../script/asset-compliance/GrantFreezeManager.s.sol) | Grant (`run()`), revoke (`revoke()`), and verify (`check()`) Predicate's `FREEZE_MANAGER_ROLE`, with `cast` equivalents. | +| [`FreezableStablecoin.t.sol`](../test/asset-compliance/FreezableStablecoin.t.sol) | Tests covering freeze enforcement, the role boundaries (the freeze role cannot seize/mint/pause/upgrade), and that revoking the role disables enforcement. | ```bash forge test --match-path "*asset-compliance*" ``` -## Does your existing token already work? +## Driving an existing token -Predicate drives your token's existing freeze/blocklist interface; most regulated tokens already have one. +You don't have to deploy a new token — Predicate can drive a token's existing freeze or blocklist interface. Common shapes: -| Token shape | Example | Fit | What it takes | -| --- | --- | --- | --- | -| M^0 `IFreezable` (`freeze`/`unfreeze`/`freezeAccounts` + `FREEZE_MANAGER_ROLE`) | mUSD, USDR | Native | Grant `FREEZE_MANAGER_ROLE`. `FreezableStablecoin` is this shape. | -| Paxos `PaxosTokenV2` (`freeze`/`freezeBatch`/`wipeFrozenAddress` + `ASSET_PROTECTION_ROLE`) | USDG | Compatible | Grant the role, but split off `wipeFrozenAddress` first — that one role also burns (see role separation). Method/role names differ from `IFreezable`, so it uses a dedicated enforcement adapter. | -| Circle FiatToken-style blocklist (`blacklist`/`isBlacklisted` + `blacklister`) | USDC family | Compatible | Grant the blacklister role; dedicated adapter. | -| Standalone blocklist contract (`addToBlocklist`/`isBlocked`) | Ondo `Blocklist` | Drivable | Grant write access; prefer a role-scoped owner over a single `Ownable` owner. | +| Interface shape | What it takes | +| --- | --- | +| `freeze` / `unfreeze` (+ batch) gated by a freeze role (`IFreezable`-style) | Grant the freeze role. `FreezableStablecoin` is this shape. | +| Freeze plus a forced-burn governed by the same role | Split the burn out of that role first, so the freeze key can't destroy funds, then grant it. An adapter maps the method and role names if they differ. | +| Blocklist (`blacklist` / `isBlacklisted`) gated by a blocklister role | Grant the blocklister role; an adapter maps the names. | +| Standalone blocklist contract (`addToBlocklist` / `isBlocked`) | Grant write access; prefer a role over a single `Ownable` owner. | No freeze interface yet? Start from [`FreezableStablecoin.sol`](../src/examples/asset-compliance/FreezableStablecoin.sol). diff --git a/script/asset-compliance/DeployFreezableStablecoin.s.sol b/script/asset-compliance/DeployFreezableStablecoin.s.sol index 26595b79..246ba425 100644 --- a/script/asset-compliance/DeployFreezableStablecoin.s.sol +++ b/script/asset-compliance/DeployFreezableStablecoin.s.sol @@ -9,7 +9,7 @@ import {FreezableStablecoin} from "../../src/examples/asset-compliance/Freezable /** * @title DeployFreezableStablecoin - * @notice Deploys {FreezableStablecoin} behind an ERC-1967 proxy with least-privilege role holders. + * @notice Deploys {FreezableStablecoin} behind an ERC-1967 proxy with separate role holders. * @dev By default `FREEZE_MANAGER_ROLE` is assigned to Predicate's freezer so the asset is * enrollment-ready; override any holder via env vars. Seize/mint/burn/pause default to the * admin and should be moved to dedicated issuer keys in production. diff --git a/script/asset-compliance/GrantFreezeManager.s.sol b/script/asset-compliance/GrantFreezeManager.s.sol index d69e26ea..3f4553d7 100644 --- a/script/asset-compliance/GrantFreezeManager.s.sol +++ b/script/asset-compliance/GrantFreezeManager.s.sol @@ -8,7 +8,7 @@ import {FreezableStablecoin} from "../../src/examples/asset-compliance/Freezable /** * @title GrantFreezeManager - * @notice Grants / revokes / verifies Predicate's freezer on a deployed asset — the on-chain step + * @notice Grants / revokes / verifies Predicate's freezer on a deployed asset — the onchain step * that makes a token enforceable by Predicate asset compliance. * @dev `FREEZE_MANAGER_ROLE` is the ONLY authority Predicate needs. It is revocable in one * transaction (`revoke`), verifiable (`check`), and never confers seize/mint/burn/pause/upgrade. diff --git a/src/examples/asset-compliance/FreezableStablecoin.sol b/src/examples/asset-compliance/FreezableStablecoin.sol index 75809072..0bba685a 100644 --- a/src/examples/asset-compliance/FreezableStablecoin.sol +++ b/src/examples/asset-compliance/FreezableStablecoin.sol @@ -13,7 +13,7 @@ import {Freezable} from "../../Freezable.sol"; /** * @title FreezableStablecoin * @author Predicate Labs, Inc (https://predicate.io) - * @notice Upgradeable ERC-20 with on-chain freeze enforcement, pause, role-gated mint/burn, and + * @notice Upgradeable ERC-20 with onchain freeze enforcement, pause, role-gated mint/burn, and * a forced-transfer seize. Frozen accounts cannot send or receive. * @dev Implements {IFreezable} via the {Freezable} base; the freeze check lives in {_update}. * FREEZE_MANAGER_ROLE (granted to Predicate) only freezes and unfreezes; seize, pause, @@ -175,8 +175,8 @@ contract FreezableStablecoin is /** * @notice ERC-20 `transferFrom` with an added check that the spender is not frozen. * @dev {_update} already blocks a frozen `from` or `to`; this additionally blocks a frozen - * spender from moving a third party's funds, mirroring mUSD / Paxos USDG semantics - * (a sanctioned operator must not be able to initiate transfers it has an allowance for). + * spender from moving a third party's funds (a sanctioned operator must not be able to + * initiate transfers it has an allowance for). */ function transferFrom( address from, From 8f3f14bb5a7e430024d6f0f99c95eef768ab0641 Mon Sep 17 00:00:00 2001 From: jessesawa Date: Wed, 1 Jul 2026 10:12:14 -0400 Subject: [PATCH 4/8] Clarify asset-compliance docs: supported interfaces and role model --- docs/asset-compliance.md | 29 ++++++++++++----------------- 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/docs/asset-compliance.md b/docs/asset-compliance.md index 496a64f7..9f75fa52 100644 --- a/docs/asset-compliance.md +++ b/docs/asset-compliance.md @@ -1,20 +1,22 @@ # Asset Compliance Examples -Example token contracts that enforce compliance onchain — freeze, seize, and role-gated admin — and that Predicate can drive automatically. +Example token contracts with compliance controls built in: freeze, seize, and role-gated admin. Once you grant Predicate the freeze role, it can freeze accounts automatically based on your policy. Code: [`src/examples/asset-compliance/`](../src/examples/asset-compliance/). ## How it works -1. Your token exposes a freeze interface gated by role-based access control — the [`IFreezable`](../src/interfaces/IFreezable.sol) interface. +1. Your token implements a supported freeze interface gated by role-based access control. The examples here implement [`IFreezable`](../src/interfaces/IFreezable.sol), which is supported. 2. You grant only the freeze role to Predicate's enforcement address. -3. Predicate watches the data sources in your policy (e.g. OFAC sanctions) and calls `freeze` on your contract automatically. You can also freeze manually from the dashboard. You run no infrastructure, and Predicate never holds your admin keys. +3. Predicate watches the data sources in your policy (e.g. OFAC sanctions) and calls `freeze` on your contract automatically. You can also freeze manually from the dashboard. -Predicate's enforcement address (grant it `FREEZE_MANAGER_ROLE`): `0x363c256D368277BBFaf6EaF65beE123a7AdbA464` +Predicate's enforcement address: `0x363c256D368277BBFaf6EaF65beE123a7AdbA464` ## Role model -The roles a token defines, and who holds them, are the issuer's choice. The pattern below is the one the example token implements: role-based access control where no single key can both freeze an account and move or destroy its funds. Each capability is its own role held by a separate key, and `DEFAULT_ADMIN_ROLE` (ideally a multisig or timelock) administers the others and authorizes upgrades. +The roles a token defines, and who holds them, are the issuer's choice. The pattern below is the one the example token implements and the one we recommend: role-based access control where no single key can both freeze an account and move or destroy its funds. Each capability is its own role held by a separate key, and `DEFAULT_ADMIN_ROLE` (ideally a multisig or timelock) administers the others and authorizes upgrades. + +Nothing forces this separation. A token could define a single role and grant it to Predicate, but that is not adequate — it would let the freeze key also mint, seize, or upgrade. Implemented as below, Predicate holds only `FREEZE_MANAGER_ROLE`; you keep admin, seize, and mint. | Capability | Role | Holder | | --- | --- | --- | @@ -28,9 +30,7 @@ The roles a token defines, and who holds them, are the issuer's choice. The patt `FREEZE_MANAGER_ROLE` is the only role you grant Predicate. -- It authorizes `freeze`, `unfreeze`, and their batch variants, and nothing else: it cannot mint, burn, seize, pause, upgrade, or administer roles. Freezing is reversible and moves no funds, though it does stop the account from sending and receiving. The example token enforces this boundary, and the [tests](../test/asset-compliance/FreezableStablecoin.t.sol) check it. -- Keep it separate from seize. Freezing and seizing are different powers: a freeze flags an account, and a distinct issuer-held role decides whether to claw the balance back. If your token combines both in one role, split them before granting, so the freeze key can never move or destroy funds. -- Hold the role yourself, delegate it to Predicate, or both. Granting and revoking it is a single `AccessControl` call — see [`GrantFreezeManager.s.sol`](../script/asset-compliance/GrantFreezeManager.s.sol). +- It authorizes `freeze`, `unfreeze`, and their batch variants, and nothing else. In the example provided, freezing is reversible and moves no funds. ## Examples @@ -47,18 +47,13 @@ The roles a token defines, and who holds them, are the issuer's choice. The patt forge test --match-path "*asset-compliance*" ``` -## Driving an existing token +## Supported interfaces -You don't have to deploy a new token — Predicate can drive a token's existing freeze or blocklist interface. Common shapes: +Predicate calls a fixed freeze interface; it does not adapt to a token's own method or role names. To be enforceable, your token must implement one of the supported interfaces. -| Interface shape | What it takes | -| --- | --- | -| `freeze` / `unfreeze` (+ batch) gated by a freeze role (`IFreezable`-style) | Grant the freeze role. `FreezableStablecoin` is this shape. | -| Freeze plus a forced-burn governed by the same role | Split the burn out of that role first, so the freeze key can't destroy funds, then grant it. An adapter maps the method and role names if they differ. | -| Blocklist (`blacklist` / `isBlacklisted`) gated by a blocklister role | Grant the blocklister role; an adapter maps the names. | -| Standalone blocklist contract (`addToBlocklist` / `isBlocked`) | Grant write access; prefer a role over a single `Ownable` owner. | +[`IFreezable`](../src/interfaces/IFreezable.sol), which the examples here implement, is supported. If you don't have a freeze interface yet, start from [`FreezableStablecoin.sol`](../src/examples/asset-compliance/FreezableStablecoin.sol). -No freeze interface yet? Start from [`FreezableStablecoin.sol`](../src/examples/asset-compliance/FreezableStablecoin.sol). +If your token already uses a different freeze or blocklist interface, reach out to us to confirm whether it's supported before enrolling. ## Further reading From 8ad13aa07b3487175ec1e6715a05a6bec1d7adbe Mon Sep 17 00:00:00 2001 From: jessesawa Date: Wed, 1 Jul 2026 10:14:23 -0400 Subject: [PATCH 5/8] Move asset-compliance doc into examples README, drop docs folder --- docs/asset-compliance.md | 62 ------------------- .../asset-compliance/FreezableStablecoin.sol | 4 +- src/examples/asset-compliance/README.md | 62 +++++++++++++++++-- 3 files changed, 60 insertions(+), 68 deletions(-) delete mode 100644 docs/asset-compliance.md diff --git a/docs/asset-compliance.md b/docs/asset-compliance.md deleted file mode 100644 index 9f75fa52..00000000 --- a/docs/asset-compliance.md +++ /dev/null @@ -1,62 +0,0 @@ -# Asset Compliance Examples - -Example token contracts with compliance controls built in: freeze, seize, and role-gated admin. Once you grant Predicate the freeze role, it can freeze accounts automatically based on your policy. - -Code: [`src/examples/asset-compliance/`](../src/examples/asset-compliance/). - -## How it works - -1. Your token implements a supported freeze interface gated by role-based access control. The examples here implement [`IFreezable`](../src/interfaces/IFreezable.sol), which is supported. -2. You grant only the freeze role to Predicate's enforcement address. -3. Predicate watches the data sources in your policy (e.g. OFAC sanctions) and calls `freeze` on your contract automatically. You can also freeze manually from the dashboard. - -Predicate's enforcement address: `0x363c256D368277BBFaf6EaF65beE123a7AdbA464` - -## Role model - -The roles a token defines, and who holds them, are the issuer's choice. The pattern below is the one the example token implements and the one we recommend: role-based access control where no single key can both freeze an account and move or destroy its funds. Each capability is its own role held by a separate key, and `DEFAULT_ADMIN_ROLE` (ideally a multisig or timelock) administers the others and authorizes upgrades. - -Nothing forces this separation. A token could define a single role and grant it to Predicate, but that is not adequate — it would let the freeze key also mint, seize, or upgrade. Implemented as below, Predicate holds only `FREEZE_MANAGER_ROLE`; you keep admin, seize, and mint. - -| Capability | Role | Holder | -| --- | --- | --- | -| Freeze / unfreeze (+ batch) | `FREEZE_MANAGER_ROLE` | Predicate enforcer, and/or issuer ops | -| Seize / clawback | `FORCED_TRANSFER_MANAGER_ROLE` | Issuer, separate key from freeze | -| Pause | `PAUSER_ROLE` | Issuer ops | -| Mint / burn | `MINTER_ROLE` / `BURNER_ROLE` | Issuer treasury | -| Admin / upgrade / roles | `DEFAULT_ADMIN_ROLE` | Issuer multisig or timelock | - -### The freeze manager role - -`FREEZE_MANAGER_ROLE` is the only role you grant Predicate. - -- It authorizes `freeze`, `unfreeze`, and their batch variants, and nothing else. In the example provided, freezing is reversible and moves no funds. - -## Examples - -| File | What it shows | -| --- | --- | -| [`FreezableStablecoin.sol`](../src/examples/asset-compliance/FreezableStablecoin.sol) | Upgradeable ERC-20 on the [`Freezable`](../src/Freezable.sol) base (implements `IFreezable`), with EIP-2612 permit, pause, role-gated mint/burn, and seize (`forceTransfer`). Freeze is enforced in `_update`, blocking both send and receive. | -| [`FreezableToken.sol`](../src/examples/asset-compliance/FreezableToken.sol) | The smallest token that implements `IFreezable`: the bare interface, no extras. | -| [`Freezable.sol`](../src/Freezable.sol) · [`IFreezable.sol`](../src/interfaces/IFreezable.sol) | The reusable freeze base and its interface. | -| [`DeployFreezableStablecoin.s.sol`](../script/asset-compliance/DeployFreezableStablecoin.s.sol) | Deploys the token behind an ERC-1967 proxy with initial roles. | -| [`GrantFreezeManager.s.sol`](../script/asset-compliance/GrantFreezeManager.s.sol) | Grant (`run()`), revoke (`revoke()`), and verify (`check()`) Predicate's `FREEZE_MANAGER_ROLE`, with `cast` equivalents. | -| [`FreezableStablecoin.t.sol`](../test/asset-compliance/FreezableStablecoin.t.sol) | Tests covering freeze enforcement, the role boundaries (the freeze role cannot seize/mint/pause/upgrade), and that revoking the role disables enforcement. | - -```bash -forge test --match-path "*asset-compliance*" -``` - -## Supported interfaces - -Predicate calls a fixed freeze interface; it does not adapt to a token's own method or role names. To be enforceable, your token must implement one of the supported interfaces. - -[`IFreezable`](../src/interfaces/IFreezable.sol), which the examples here implement, is supported. If you don't have a freeze interface yet, start from [`FreezableStablecoin.sol`](../src/examples/asset-compliance/FreezableStablecoin.sol). - -If your token already uses a different freeze or blocklist interface, reach out to us to confirm whether it's supported before enrolling. - -## Further reading - -- [Compliance for Assets — overview](https://docs.predicate.io/v2/assets/overview) -- [Enroll your asset](https://docs.predicate.io/v2/assets/get-started) -- [Security model](https://docs.predicate.io/v2/assets/security) diff --git a/src/examples/asset-compliance/FreezableStablecoin.sol b/src/examples/asset-compliance/FreezableStablecoin.sol index 0bba685a..3ccb879e 100644 --- a/src/examples/asset-compliance/FreezableStablecoin.sol +++ b/src/examples/asset-compliance/FreezableStablecoin.sol @@ -17,8 +17,8 @@ import {Freezable} from "../../Freezable.sol"; * a forced-transfer seize. Frozen accounts cannot send or receive. * @dev Implements {IFreezable} via the {Freezable} base; the freeze check lives in {_update}. * FREEZE_MANAGER_ROLE (granted to Predicate) only freezes and unfreezes; seize, pause, - * mint, burn, and upgrades are separate, issuer-held roles. See docs/asset-compliance.md - * for the role model and integration steps. + * mint, burn, and upgrades are separate, issuer-held roles. See the README in this + * directory for the role model and integration steps. */ contract FreezableStablecoin is ERC20Upgradeable, diff --git a/src/examples/asset-compliance/README.md b/src/examples/asset-compliance/README.md index 6446a589..e52cf8fd 100644 --- a/src/examples/asset-compliance/README.md +++ b/src/examples/asset-compliance/README.md @@ -1,6 +1,60 @@ -# EVM Asset Compliance Examples +# Asset Compliance Examples -- `FreezableStablecoin.sol` — upgradeable ERC-20 with freeze, pause, mint/burn, and seize. -- `FreezableToken.sol` — the minimal token implementing `IFreezable`. +Example token contracts with compliance controls built in: freeze, seize, and role-gated admin. Once you grant Predicate the freeze role, it can freeze accounts automatically based on your policy. -See [`docs/asset-compliance.md`](../../../docs/asset-compliance.md) for the overview, the recommended role model, and the "does your token fit?" matrix. Scripts live in `script/asset-compliance/`, tests in `test/asset-compliance/`. +## How it works + +1. Your token implements a supported freeze interface gated by role-based access control. The examples here implement [`IFreezable`](../../interfaces/IFreezable.sol), which is supported. +2. You grant only the freeze role to Predicate's enforcement address. +3. Predicate watches the data sources in your policy (e.g. OFAC sanctions) and calls `freeze` on your contract automatically. You can also freeze manually from the dashboard. + +Predicate's enforcement address: `0x363c256D368277BBFaf6EaF65beE123a7AdbA464` + +## Role model + +The roles a token defines, and who holds them, are the issuer's choice. The pattern below is the one the example token implements and the one we recommend: role-based access control where no single key can both freeze an account and move or destroy its funds. Each capability is its own role held by a separate key, and `DEFAULT_ADMIN_ROLE` (ideally a multisig or timelock) administers the others and authorizes upgrades. + +Nothing forces this separation. A token could define a single role and grant it to Predicate, but that is not adequate — it would let the freeze key also mint, seize, or upgrade. Implemented as below, Predicate holds only `FREEZE_MANAGER_ROLE`; you keep admin, seize, and mint. + +| Capability | Role | Holder | +| --- | --- | --- | +| Freeze / unfreeze (+ batch) | `FREEZE_MANAGER_ROLE` | Predicate enforcer, and/or issuer ops | +| Seize / clawback | `FORCED_TRANSFER_MANAGER_ROLE` | Issuer, separate key from freeze | +| Pause | `PAUSER_ROLE` | Issuer ops | +| Mint / burn | `MINTER_ROLE` / `BURNER_ROLE` | Issuer treasury | +| Admin / upgrade / roles | `DEFAULT_ADMIN_ROLE` | Issuer multisig or timelock | + +### The freeze manager role + +`FREEZE_MANAGER_ROLE` is the only role you grant Predicate. + +- It authorizes `freeze`, `unfreeze`, and their batch variants, and nothing else. In the example provided, freezing is reversible and moves no funds. + +## Examples + +| File | What it shows | +| --- | --- | +| [`FreezableStablecoin.sol`](FreezableStablecoin.sol) | Upgradeable ERC-20 on the [`Freezable`](../../Freezable.sol) base (implements `IFreezable`), with EIP-2612 permit, pause, role-gated mint/burn, and seize (`forceTransfer`). Freeze is enforced in `_update`, blocking both send and receive. | +| [`FreezableToken.sol`](FreezableToken.sol) | The smallest token that implements `IFreezable`: the bare interface, no extras. | +| [`Freezable.sol`](../../Freezable.sol) · [`IFreezable.sol`](../../interfaces/IFreezable.sol) | The reusable freeze base and its interface. | +| [`DeployFreezableStablecoin.s.sol`](../../../script/asset-compliance/DeployFreezableStablecoin.s.sol) | Deploys the token behind an ERC-1967 proxy with initial roles. | +| [`GrantFreezeManager.s.sol`](../../../script/asset-compliance/GrantFreezeManager.s.sol) | Grant (`run()`), revoke (`revoke()`), and verify (`check()`) Predicate's `FREEZE_MANAGER_ROLE`, with `cast` equivalents. | +| [`FreezableStablecoin.t.sol`](../../../test/asset-compliance/FreezableStablecoin.t.sol) | Tests covering freeze enforcement, the role boundaries (the freeze role cannot seize/mint/pause/upgrade), and that revoking the role disables enforcement. | + +```bash +forge test --match-path "*asset-compliance*" +``` + +## Supported interfaces + +Predicate calls a fixed freeze interface; it does not adapt to a token's own method or role names. To be enforceable, your token must implement one of the supported interfaces. + +[`IFreezable`](../../interfaces/IFreezable.sol), which the examples here implement, is supported. If you don't have a freeze interface yet, start from [`FreezableStablecoin.sol`](FreezableStablecoin.sol). + +If your token already uses a different freeze or blocklist interface, reach out to us to confirm whether it's supported before enrolling. + +## Further reading + +- [Compliance for Assets — overview](https://docs.predicate.io/v2/assets/overview) +- [Enroll your asset](https://docs.predicate.io/v2/assets/get-started) +- [Security model](https://docs.predicate.io/v2/assets/security) From 2360a50f6d8aed3ca873e2a1665ead39aa71d82c Mon Sep 17 00:00:00 2001 From: jessesawa Date: Mon, 6 Jul 2026 15:55:15 -0400 Subject: [PATCH 6/8] Fix address(0) freeze bypass in FreezableStablecoin - _update: exempt address(0) from the freeze check so freezing it can no longer brick mint (from==0) or burn (to==0). - _forceTransfer: reject a zero-address source (a seizure can never mint) and a frozen recipient (honors "frozen accounts cannot receive"). - Add regression tests for both fixes. --- .../asset-compliance/FreezableStablecoin.sol | 25 ++++++++----- .../FreezableStablecoin.t.sol | 37 +++++++++++++++++++ 2 files changed, 52 insertions(+), 10 deletions(-) diff --git a/src/examples/asset-compliance/FreezableStablecoin.sol b/src/examples/asset-compliance/FreezableStablecoin.sol index 3ccb879e..b60b1d2c 100644 --- a/src/examples/asset-compliance/FreezableStablecoin.sol +++ b/src/examples/asset-compliance/FreezableStablecoin.sol @@ -142,10 +142,11 @@ contract FreezableStablecoin is /* ============ Seize / Forced Transfer (issuer-only) ============ */ /** - * @notice Seizes `amount` from a frozen `from` and moves it to `to`. + * @notice Seizes `amount` from a frozen `from` and moves it to a non-frozen `to`. * @dev The source MUST already be frozen — this enforces the freeze-then-seize workflow and * keeps the two powers in different hands (Predicate freezes; the issuer seizes). - * Intentionally bypasses the pause and frozen checks (see {_forceTransfer}). + * Runs while the token is paused, but still refuses a frozen recipient and rejects the + * zero address on either side (see {_forceTransfer}). */ function forceTransfer( address from, @@ -198,8 +199,10 @@ contract FreezableStablecoin is /** * @dev Compliance hook for all balance movements (transfers, mints, burns). - * Blocks the movement when the token is paused or when either party is frozen. - * Seizures call `super._update` directly and therefore skip this override on purpose. + * Blocks the movement when the token is paused or when either party is frozen. The + * zero address is exempt from the freeze check so that freezing it cannot brick mint + * (`from == 0`) or burn (`to == 0`). Seizures call `super._update` directly and + * therefore skip this override on purpose. */ function _update( address from, @@ -207,23 +210,25 @@ contract FreezableStablecoin is uint256 value ) internal override { _requireNotPaused(); - _revertIfFrozen(from); - _revertIfFrozen(to); + if (from != address(0)) _revertIfFrozen(from); + if (to != address(0)) _revertIfFrozen(to); super._update(from, to, value); } /** - * @dev Executes a seizure. Requires the source to be frozen, then calls `super._update` - * directly to bypass the pause/frozen guards in {_update} — a compliance seizure must - * succeed precisely when the account is frozen (and even while the token is paused). + * @dev Executes a seizure. Calls `super._update` directly to bypass the pause guard in + * {_update} — a compliance seizure must succeed even while the token is paused. It + * still upholds the compliance invariants: the source must be frozen, the recipient + * must not be, and neither side may be the zero address (so a seizure can never mint). */ function _forceTransfer( address from, address to, uint256 amount ) internal { - if (to == address(0)) revert ZeroAddress(); + if (from == address(0) || to == address(0)) revert ZeroAddress(); _revertIfNotFrozen(from); // seize only operates on already-frozen accounts + _revertIfFrozen(to); // never deliver seized funds to a frozen recipient super._update(from, to, amount); emit ForcedTransfer(from, to, amount); } diff --git a/test/asset-compliance/FreezableStablecoin.t.sol b/test/asset-compliance/FreezableStablecoin.t.sol index c2432a41..b569a4de 100644 --- a/test/asset-compliance/FreezableStablecoin.t.sol +++ b/test/asset-compliance/FreezableStablecoin.t.sol @@ -132,6 +132,26 @@ contract FreezableStablecoinTest is Test { token.forceTransfer(alice, treasury, 1e6); } + function test_seize_rejectsZeroFromSource() public { + // Even if address(0) were frozen, a seizure from it must never mint via super._update. + vm.prank(predicateFreezer); + token.freeze(address(0)); + vm.prank(seizer); + vm.expectRevert(FreezableStablecoin.ZeroAddress.selector); + token.forceTransfer(address(0), treasury, 1e6); + } + + function test_seize_blocksFrozenRecipient() public { + vm.startPrank(predicateFreezer); + token.freeze(alice); + token.freeze(treasury); + vm.stopPrank(); + + vm.prank(seizer); + vm.expectRevert(abi.encodeWithSelector(IFreezable.AccountFrozen.selector, treasury)); + token.forceTransfer(alice, treasury, 1e6); + } + function test_seize_worksWhilePaused() public { vm.prank(predicateFreezer); token.freeze(alice); @@ -143,6 +163,23 @@ contract FreezableStablecoinTest is Test { assertEq(token.balanceOf(treasury), INITIAL); } + /* ============ Freezing address(0) must not brick supply management ============ */ + + function test_mintBurn_unaffectedByFrozenZeroAddress() public { + vm.prank(predicateFreezer); + token.freeze(address(0)); + + // mint routes through _update with from == address(0); it must still succeed. + vm.prank(minter); + token.mint(bob, 5e6); + assertEq(token.balanceOf(bob), 5e6); + + // burn routes through _update with to == address(0); it must still succeed. + vm.prank(burner); + token.burn(alice, 5e6); + assertEq(token.balanceOf(alice), INITIAL - 5e6); + } + /* ============ Pause ============ */ function test_pause_blocksTransfers() public { From b96c7e3bf4a9ad7a0d5ff3d04054dcea2a63ae01 Mon Sep 17 00:00:00 2001 From: jessesawa Date: Mon, 6 Jul 2026 16:46:37 -0400 Subject: [PATCH 7/8] Track soroban Cargo.lock to fix CI build The soroban-build job re-resolved dependencies on every run because no lockfile was tracked (root .gitignore excludes *.lock). The newly published ed25519-dalek 3.0.0 is incompatible with soroban-env-host 23.0.1's testutils, breaking `cargo test`. Add a .gitignore exception for soroban/Cargo.lock and commit the lockfile (ed25519-dalek pinned to 2.2.0) so the Soroban build is reproducible. --- .gitignore | 3 + soroban/Cargo.lock | 1924 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 1927 insertions(+) create mode 100644 soroban/Cargo.lock diff --git a/.gitignore b/.gitignore index f5822ebf..2c06eb1e 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,9 @@ cache_hardhat artifacts *.lock +# Track the Soroban lockfile for reproducible contract builds +!soroban/Cargo.lock + #Foundry files out cache diff --git a/soroban/Cargo.lock b/soroban/Cargo.lock new file mode 100644 index 00000000..496c9fb8 --- /dev/null +++ b/soroban/Cargo.lock @@ -0,0 +1,1924 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "arbitrary" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110" +dependencies = [ + "derive_arbitrary", +] + +[[package]] +name = "ark-bls12-381" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c775f0d12169cba7aae4caeb547bb6a50781c7449a8aa53793827c9ec4abf488" +dependencies = [ + "ark-ec", + "ark-ff", + "ark-serialize", + "ark-std", +] + +[[package]] +name = "ark-ec" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "defd9a439d56ac24968cca0571f598a61bc8c55f71d50a89cda591cb750670ba" +dependencies = [ + "ark-ff", + "ark-poly", + "ark-serialize", + "ark-std", + "derivative", + "hashbrown 0.13.2", + "itertools", + "num-traits", + "zeroize", +] + +[[package]] +name = "ark-ff" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec847af850f44ad29048935519032c33da8aa03340876d351dfab5660d2966ba" +dependencies = [ + "ark-ff-asm", + "ark-ff-macros", + "ark-serialize", + "ark-std", + "derivative", + "digest", + "itertools", + "num-bigint", + "num-traits", + "paste", + "rustc_version", + "zeroize", +] + +[[package]] +name = "ark-ff-asm" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ed4aa4fe255d0bc6d79373f7e31d2ea147bcf486cba1be5ba7ea85abdb92348" +dependencies = [ + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ark-ff-macros" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7abe79b0e4288889c4574159ab790824d0033b9fdcb2a112a3182fac2e514565" +dependencies = [ + "num-bigint", + "num-traits", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ark-poly" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d320bfc44ee185d899ccbadfa8bc31aab923ce1558716e1997a1e74057fe86bf" +dependencies = [ + "ark-ff", + "ark-serialize", + "ark-std", + "derivative", + "hashbrown 0.13.2", +] + +[[package]] +name = "ark-serialize" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb7b85a02b83d2f22f89bd5cac66c9c89474240cb6207cb1efc16d098e822a5" +dependencies = [ + "ark-serialize-derive", + "ark-std", + "digest", + "num-bigint", +] + +[[package]] +name = "ark-serialize-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae3281bc6d0fd7e549af32b52511e1302185bd688fd3359fa36423346ff682ea" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ark-std" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94893f1e0c6eeab764ade8dc4c0db24caf4fe7cbbaafc0eba0a9030f447b5185" +dependencies = [ + "num-traits", + "rand", +] + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes-lit" +version = "0.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0adabf37211a5276e46335feabcbb1530c95eb3fdf85f324c7db942770aa025d" +dependencies = [ + "num-bigint", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "cc" +version = "1.2.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5d6cac793997bd970000024b2934968efe83b382de4fdcf4fcb46b6ee4ad996" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_eval" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45565fc9416b9896014f5732ac776f810ee53a66730c17e4020c3ec064a8f88f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "num-traits", + "serde", + "windows-link", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crate-git-revision" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c521bf1f43d31ed2f73441775ed31935d77901cb3451e44b38a1c1612fcbaf98" +dependencies = [ + "serde", + "serde_derive", + "serde_json", +] + +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "ctor" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67773048316103656a637612c4a62477603b777d91d9c62ff2290f9cde178fdb" +dependencies = [ + "ctor-proc-macro", + "dtor", +] + +[[package]] +name = "ctor-proc-macro" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2931af7e13dc045d8e9d26afccc6fa115d64e115c9c84b1166288b46f6782c2" + +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core 0.20.11", + "darling_macro 0.20.11", +] + +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core 0.23.0", + "darling_macro 0.23.0", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.118", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.118", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core 0.20.11", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core 0.23.0", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "data-encoding" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "zeroize", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "serde_core", +] + +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "derive_arbitrary" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", + "subtle", +] + +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + +[[package]] +name = "dtor" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "404d02eeb088a82cfd873006cb713fe411306c7d182c344905e101fb1167d301" +dependencies = [ + "dtor-proc-macro", +] + +[[package]] +name = "dtor-proc-macro" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f678cf4a922c215c63e0de95eb1ff08a958a81d47e485cf9da1e27bf6305cfa5" + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der", + "digest", + "elliptic-curve", + "rfc6979", + "signature", +] + +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" +dependencies = [ + "curve25519-dalek", + "ed25519", + "rand_core", + "serde", + "sha2", + "subtle", + "zeroize", +] + +[[package]] +name = "either" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" + +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest", + "ff", + "generic-array", + "group", + "rand_core", + "sec1", + "subtle", + "zeroize", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "escape-bytes" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bfcf67fea2815c2fc3b90873fae90957be12ff417335dfadc7f52927feb03b2" + +[[package]] +name = "ethnum" +version = "1.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40404c3f5f511ec4da6fe866ddf6a717c309fdbb69fbbad7b0f3edab8f2e835f" + +[[package]] +name = "example-compliant-token" +version = "0.1.0" +dependencies = [ + "ed25519-dalek", + "predicate-client", + "predicate-registry", + "rand", + "soroban-sdk", +] + +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "rand_core", + "subtle", +] + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" +dependencies = [ + "typenum", + "version_check", + "zeroize", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core", + "subtle", +] + +[[package]] +name = "hash32" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" +dependencies = [ + "byteorder", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" +dependencies = [ + "ahash", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heapless" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad" +dependencies = [ + "hash32", + "stable_deref_trait", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +dependencies = [ + "serde", +] + +[[package]] +name = "hex-literal" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fe2267d4ed49bc07b63801559be28c718ea06c4738b7a03c94df7386d2cde46" + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "indexmap-nostd" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e04e2fd2b8188ea827b32ef11de88377086d690286ab35747ef7f9bf3ccb590" + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "js-sys" +version = "0.3.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "k256" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b" +dependencies = [ + "cfg-if", + "ecdsa", + "elliptic-curve", + "sha2", +] + +[[package]] +name = "keccak" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653" +dependencies = [ + "cpufeatures", +] + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "log" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "macro-string" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b27834086c65ec3f9387b096d66e99f221cf081c2b738042aa252bcd41204e3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "memchr" +version = "2.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" + +[[package]] +name = "num-bigint" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "predicate-client" +version = "0.1.0" +dependencies = [ + "ed25519-dalek", + "predicate-registry", + "rand", + "soroban-sdk", +] + +[[package]] +name = "predicate-registry" +version = "0.1.0" +dependencies = [ + "ed25519-dalek", + "rand", + "soroban-sdk", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.118", +] + +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "schemars" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" +dependencies = [ + "dyn-clone", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "subtle", + "zeroize", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_with" +version = "3.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a5c54c7310e7b8b9577c286d7e399ddd876c3e12b3ed917a8aabc4b96e9e8c" +dependencies = [ + "base64", + "bs58", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.14.0", + "schemars 0.8.22", + "schemars 0.9.0", + "schemars 1.2.1", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84d57bc0c8b9a17920c178daa6bb924850d54a9c97ab45194bb8c17ad66bb660" +dependencies = [ + "darling 0.23.0", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha3" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77fd7028345d415a4034cf8777cd4f8ab1851274233b45f84e3d955502d93874" +dependencies = [ + "digest", + "keccak", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "soroban-builtin-sdk-macros" +version = "23.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9336adeabcd6f636a4e0889c8baf494658ef5a3c4e7e227569acd2ce9091e85" +dependencies = [ + "itertools", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "soroban-env-common" +version = "23.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00067f52e8bbf1abf0de03fe3e2fbb06910893cfbe9a7d9093d6425658833ff3" +dependencies = [ + "arbitrary", + "crate-git-revision", + "ethnum", + "num-derive", + "num-traits", + "serde", + "soroban-env-macros", + "soroban-wasmi", + "static_assertions", + "stellar-xdr", + "wasmparser", +] + +[[package]] +name = "soroban-env-guest" +version = "23.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccd1e40963517b10963a8e404348d3fe6caf9c278ac47a6effd48771297374d6" +dependencies = [ + "soroban-env-common", + "static_assertions", +] + +[[package]] +name = "soroban-env-host" +version = "23.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9766c5ad78e9d8ae10afbc076301f7d610c16407a1ebb230766dbe007a48725" +dependencies = [ + "ark-bls12-381", + "ark-ec", + "ark-ff", + "ark-serialize", + "curve25519-dalek", + "ecdsa", + "ed25519-dalek", + "elliptic-curve", + "generic-array", + "getrandom", + "hex-literal", + "hmac", + "k256", + "num-derive", + "num-integer", + "num-traits", + "p256", + "rand", + "rand_chacha", + "sec1", + "sha2", + "sha3", + "soroban-builtin-sdk-macros", + "soroban-env-common", + "soroban-wasmi", + "static_assertions", + "stellar-strkey 0.0.13", + "wasmparser", +] + +[[package]] +name = "soroban-env-macros" +version = "23.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0e6a1c5844257ce96f5f54ef976035d5bd0ee6edefaf9f5e0bcb8ea4b34228c" +dependencies = [ + "itertools", + "proc-macro2", + "quote", + "serde", + "serde_json", + "stellar-xdr", + "syn 2.0.118", +] + +[[package]] +name = "soroban-ledger-snapshot" +version = "23.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea7299402f5f445089fde192cc68587baf0cc6432be300bce99d997fd85b4cb" +dependencies = [ + "serde", + "serde_json", + "serde_with", + "soroban-env-common", + "soroban-env-host", + "thiserror", +] + +[[package]] +name = "soroban-sdk" +version = "23.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd1e11f9fd537f9df29ec1a66c1b78d666094df56e196565d292ebf9a72732b4" +dependencies = [ + "arbitrary", + "bytes-lit", + "crate-git-revision", + "ctor", + "derive_arbitrary", + "ed25519-dalek", + "rand", + "rustc_version", + "serde", + "serde_json", + "soroban-env-guest", + "soroban-env-host", + "soroban-ledger-snapshot", + "soroban-sdk-macros", + "stellar-strkey 0.0.16", + "visibility", +] + +[[package]] +name = "soroban-sdk-macros" +version = "23.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caa7114e2f031b6fbd30376e844f3c55f6daf56f6f9d33ce309f846ffced316d" +dependencies = [ + "darling 0.20.11", + "heck", + "itertools", + "macro-string", + "proc-macro2", + "quote", + "sha2", + "soroban-env-common", + "soroban-spec", + "soroban-spec-rust", + "stellar-xdr", + "syn 2.0.118", +] + +[[package]] +name = "soroban-spec" +version = "23.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1303589e67e7c76d0571b8d797b75f9fa33a1ceb1b4361a981570a3ebf00ac19" +dependencies = [ + "base64", + "stellar-xdr", + "thiserror", + "wasmparser", +] + +[[package]] +name = "soroban-spec-rust" +version = "23.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "956188f28b750b80a2bb4cd5698038746edae1be51ec19a29c7efc6dcd922e5f" +dependencies = [ + "prettyplease", + "proc-macro2", + "quote", + "sha2", + "soroban-spec", + "stellar-xdr", + "syn 2.0.118", + "thiserror", +] + +[[package]] +name = "soroban-wasmi" +version = "0.31.1-soroban.20.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "710403de32d0e0c35375518cb995d4fc056d0d48966f2e56ea471b8cb8fc9719" +dependencies = [ + "smallvec", + "spin", + "wasmi_arena", + "wasmi_core", + "wasmparser-nostd", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "stellar-access" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "974e461afe1538ee54dc9314ed46144f393265c7625606c240760cd6a2baaadf" +dependencies = [ + "soroban-sdk", +] + +[[package]] +name = "stellar-contract-utils" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a918daa29a4cfc0fe97118a2425893a0336a33b436e8f70ca4b4f5c739f23e4" +dependencies = [ + "soroban-sdk", +] + +[[package]] +name = "stellar-strkey" +version = "0.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee1832fb50c651ad10f734aaf5d31ca5acdfb197a6ecda64d93fcdb8885af913" +dependencies = [ + "crate-git-revision", + "data-encoding", +] + +[[package]] +name = "stellar-strkey" +version = "0.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "084afcb0d458c3d5d5baa2d294b18f881e62cc258ef539d8fdf68be7dbe45520" +dependencies = [ + "crate-git-revision", + "data-encoding", + "heapless", +] + +[[package]] +name = "stellar-tokens" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbc8be39a374a3520726017623d7c79d7d0530e626778619ba8f9462037e37aa" +dependencies = [ + "soroban-sdk", + "stellar-contract-utils", +] + +[[package]] +name = "stellar-xdr" +version = "23.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89d2848e1694b0c8db81fd812bfab5ea71ee28073e09ccc45620ef3cf7a75a9b" +dependencies = [ + "arbitrary", + "base64", + "cfg_eval", + "crate-git-revision", + "escape-bytes", + "ethnum", + "hex", + "serde", + "serde_with", + "sha2", + "stellar-strkey 0.0.13", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "test-stablecoin" +version = "0.1.0" +dependencies = [ + "soroban-sdk", + "stellar-access", + "stellar-tokens", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "time" +version = "0.3.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18dfaaeddcb932337b5e7866ee7d0ce9b76d2fd092997146f187ec09b4558a50" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "time-macros" +version = "0.2.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c431b87111666e491a90baa837f914fb45cd5dc3c268591b0220ff5057f2085f" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "visibility" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d674d135b4a8c1d7e813e2f8d1c9a58308aee4a680323066025e53132218bd91" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.118", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasmi_arena" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "104a7f73be44570cac297b3035d76b169d6599637631cf37a1703326a0727073" + +[[package]] +name = "wasmi_core" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcf1a7db34bff95b85c261002720c00c3a6168256dcb93041d3fa2054d19856a" +dependencies = [ + "downcast-rs", + "libm", + "num-traits", + "paste", +] + +[[package]] +name = "wasmparser" +version = "0.116.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a58e28b80dd8340cb07b8242ae654756161f6fc8d0038123d679b7b99964fa50" +dependencies = [ + "indexmap 2.14.0", + "semver", +] + +[[package]] +name = "wasmparser-nostd" +version = "0.100.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5a015fe95f3504a94bb1462c717aae75253e39b9dd6c3fb1062c934535c64aa" +dependencies = [ + "indexmap-nostd", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "zerocopy" +version = "0.8.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75726053136156d419e285b9b7eddaaea9e3fea6ce32eed44a89901f0bd98de1" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4714fd92cf900833d49538023a9b3915155210801d1c1169eba513b2addefd71" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" From 5b212b75fcc1f3ce146e278e9c5e45035640ade6 Mon Sep 17 00:00:00 2001 From: jessesawa Date: Mon, 6 Jul 2026 17:37:54 -0400 Subject: [PATCH 8/8] Harden FreezableStablecoin example per PR review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - initialize: validate every role holder (pauser/seize/mint/burn), not just admin — a role granted to address(0) is unrecoverable. - Deploy script: require ADMIN explicitly (fail-closed) so the upgrade authority is never defaulted to an implicit sender. - Tests: freeze manager cannot burn; batch seize happy-path and LengthMismatch; initialize rejects a zero role holder. --- .../DeployFreezableStablecoin.s.sol | 10 +-- .../asset-compliance/FreezableStablecoin.sol | 7 ++- .../FreezableStablecoin.t.sol | 63 +++++++++++++++++++ 3 files changed, 75 insertions(+), 5 deletions(-) diff --git a/script/asset-compliance/DeployFreezableStablecoin.s.sol b/script/asset-compliance/DeployFreezableStablecoin.s.sol index 246ba425..e319c7da 100644 --- a/script/asset-compliance/DeployFreezableStablecoin.s.sol +++ b/script/asset-compliance/DeployFreezableStablecoin.s.sol @@ -10,9 +10,11 @@ import {FreezableStablecoin} from "../../src/examples/asset-compliance/Freezable /** * @title DeployFreezableStablecoin * @notice Deploys {FreezableStablecoin} behind an ERC-1967 proxy with separate role holders. - * @dev By default `FREEZE_MANAGER_ROLE` is assigned to Predicate's freezer so the asset is - * enrollment-ready; override any holder via env vars. Seize/mint/burn/pause default to the - * admin and should be moved to dedicated issuer keys in production. + * @dev `ADMIN` is required (fail-closed): DEFAULT_ADMIN_ROLE also authorizes upgrades, so it must + * never fall back to an implicit address. By default `FREEZE_MANAGER_ROLE` is assigned to + * Predicate's freezer so the asset is enrollment-ready; override any holder via env vars. + * Seize/mint/burn/pause default to the admin and should be moved to dedicated issuer keys in + * production. * * Usage: * ADMIN=0x... forge script script/asset-compliance/DeployFreezableStablecoin.s.sol \ @@ -23,7 +25,7 @@ contract DeployFreezableStablecoin is Script { address public constant PREDICATE_FREEZER = 0x363c256D368277BBFaf6EaF65beE123a7AdbA464; function run() external returns (FreezableStablecoin token, address proxy) { - address admin = vm.envOr("ADMIN", msg.sender); + address admin = vm.envAddress("ADMIN"); // required: never default the upgrade authority address freezeManager = vm.envOr("FREEZE_MANAGER", PREDICATE_FREEZER); address pauser = vm.envOr("PAUSER", admin); address seizeManager = vm.envOr("SEIZE_MANAGER", admin); diff --git a/src/examples/asset-compliance/FreezableStablecoin.sol b/src/examples/asset-compliance/FreezableStablecoin.sol index b60b1d2c..b2be379e 100644 --- a/src/examples/asset-compliance/FreezableStablecoin.sol +++ b/src/examples/asset-compliance/FreezableStablecoin.sol @@ -87,7 +87,12 @@ contract FreezableStablecoin is address minter, address burner ) external initializer { - if (admin == address(0)) revert ZeroAddress(); + // Validate every role holder up front: a role granted to address(0) is unrecoverable and + // silently disables that capability. freezeManager is checked inside __Freezable_init. + if ( + admin == address(0) || pauser == address(0) || forcedTransferManager == address(0) || minter == address(0) + || burner == address(0) + ) revert ZeroAddress(); __ERC20_init(name_, symbol_); __ERC20Permit_init(name_); diff --git a/test/asset-compliance/FreezableStablecoin.t.sol b/test/asset-compliance/FreezableStablecoin.t.sol index b569a4de..6f06106f 100644 --- a/test/asset-compliance/FreezableStablecoin.t.sol +++ b/test/asset-compliance/FreezableStablecoin.t.sol @@ -33,6 +33,7 @@ contract FreezableStablecoinTest is Test { bytes32 internal roleSeize; bytes32 internal rolePause; bytes32 internal roleMint; + bytes32 internal roleBurn; bytes32 internal roleAdmin; uint256 internal constant INITIAL = 1_000_000e6; @@ -49,6 +50,7 @@ contract FreezableStablecoinTest is Test { roleSeize = token.FORCED_TRANSFER_MANAGER_ROLE(); rolePause = token.PAUSER_ROLE(); roleMint = token.MINTER_ROLE(); + roleBurn = token.BURNER_ROLE(); roleAdmin = token.DEFAULT_ADMIN_ROLE(); vm.prank(minter); @@ -152,6 +154,46 @@ contract FreezableStablecoinTest is Test { token.forceTransfer(alice, treasury, 1e6); } + function test_seizeBatch_movesFrozenBalances() public { + vm.prank(minter); + token.mint(bob, 500_000e6); + + address[] memory froms = new address[](2); + froms[0] = alice; + froms[1] = bob; + address[] memory tos = new address[](2); + tos[0] = treasury; + tos[1] = treasury; + uint256[] memory amounts = new uint256[](2); + amounts[0] = INITIAL; + amounts[1] = 500_000e6; + + vm.prank(predicateFreezer); + token.freezeAccounts(froms); + + vm.prank(seizer); + token.forceTransfers(froms, tos, amounts); + + assertEq(token.balanceOf(alice), 0); + assertEq(token.balanceOf(bob), 0); + assertEq(token.balanceOf(treasury), INITIAL + 500_000e6); + } + + function test_seizeBatch_revertsOnLengthMismatch() public { + address[] memory froms = new address[](2); + froms[0] = alice; + froms[1] = bob; + address[] memory tos = new address[](1); + tos[0] = treasury; + uint256[] memory amounts = new uint256[](2); + amounts[0] = 1e6; + amounts[1] = 1e6; + + vm.prank(seizer); + vm.expectRevert(FreezableStablecoin.LengthMismatch.selector); + token.forceTransfers(froms, tos, amounts); + } + function test_seize_worksWhilePaused() public { vm.prank(predicateFreezer); token.freeze(alice); @@ -212,6 +254,14 @@ contract FreezableStablecoinTest is Test { token.mint(predicateFreezer, 1e6); } + function test_roleSeparation_freezeManagerCannotBurn() public { + vm.prank(predicateFreezer); + vm.expectRevert( + abi.encodeWithSelector(IAccessControl.AccessControlUnauthorizedAccount.selector, predicateFreezer, roleBurn) + ); + token.burn(alice, 1e6); + } + function test_roleSeparation_freezeManagerCannotPause() public { vm.prank(predicateFreezer); vm.expectRevert( @@ -256,6 +306,19 @@ contract FreezableStablecoinTest is Test { token.freeze(alice); } + /* ============ Initialization ============ */ + + function test_initialize_rejectsZeroRoleHolder() public { + FreezableStablecoin impl = new FreezableStablecoin(); + // minter == address(0): a role granted to the zero address is unrecoverable. + bytes memory initData = abi.encodeCall( + FreezableStablecoin.initialize, + ("Compliant USD", "cUSD", admin, predicateFreezer, pauser, seizer, address(0), burner) + ); + vm.expectRevert(FreezableStablecoin.ZeroAddress.selector); + new ERC1967Proxy(address(impl), initData); + } + /* ============ Sanity ============ */ function test_normalTransferSucceeds() public {