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/script/asset-compliance/DeployFreezableStablecoin.s.sol b/script/asset-compliance/DeployFreezableStablecoin.s.sol new file mode 100644 index 00000000..e319c7da --- /dev/null +++ b/script/asset-compliance/DeployFreezableStablecoin.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 {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 separate role holders. + * @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 \ + * --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.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); + 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..3f4553d7 --- /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 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. + * + * 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/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" diff --git a/src/examples/asset-compliance/FreezableStablecoin.sol b/src/examples/asset-compliance/FreezableStablecoin.sol new file mode 100644 index 00000000..b2be379e --- /dev/null +++ b/src/examples/asset-compliance/FreezableStablecoin.sol @@ -0,0 +1,245 @@ +// 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 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, + * 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, + 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 { + // 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_); + __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 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). + * 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, + 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 (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. 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, + address to, + uint256 value + ) internal override { + _requireNotPaused(); + if (from != address(0)) _revertIfFrozen(from); + if (to != address(0)) _revertIfFrozen(to); + super._update(from, to, value); + } + + /** + * @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 (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); + } + + /// @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..e52cf8fd --- /dev/null +++ b/src/examples/asset-compliance/README.md @@ -0,0 +1,60 @@ +# 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. + +## 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) diff --git a/test/asset-compliance/FreezableStablecoin.t.sol b/test/asset-compliance/FreezableStablecoin.t.sol new file mode 100644 index 00000000..6f06106f --- /dev/null +++ b/test/asset-compliance/FreezableStablecoin.t.sol @@ -0,0 +1,334 @@ +// 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 roleBurn; + 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(); + roleBurn = token.BURNER_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_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_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); + vm.prank(pauser); + token.pause(); + + vm.prank(seizer); + token.forceTransfer(alice, treasury, INITIAL); + 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 { + 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_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( + 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); + } + + /* ============ 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 { + 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); + } +}