diff --git a/.github/workflows/certora-access.yml b/.github/workflows/certora-access.yml new file mode 100644 index 00000000..fd6e28a5 --- /dev/null +++ b/.github/workflows/certora-access.yml @@ -0,0 +1,50 @@ +name: Certora verification + +on: pull_request + +env: + CONFIGS: | + access_control_integrity.conf + access_control_invariants.conf + access_control_non_panics.conf + access_control_revoke_role_non_panic.conf + access_control_panics.conf + access_control_sanity.conf + ownable_integrity.conf + ownable_invariants.conf + ownable_non_panics.conf + ownable_panics.conf + ownable_sanity.conf + +jobs: + check: + runs-on: ubuntu-latest + permissions: + contents: read + statuses: write + pull-requests: write + id-token: write + steps: + - name: checkout repository + uses: actions/checkout@v4 + with: + submodules: recursive + - name: Install rust + uses: actions-rust-lang/setup-rust-toolchain@v1 + - name: Install just + uses: extractions/setup-just@v3 + - name: Install soroban + run: | + cargo update -p cvlr-soroban + rustup target add wasm32-unknown-unknown + - name: run configs + uses: Certora/certora-run-action@v2 + with: + ecosystem: soroban + configurations: ${{ env.CONFIGS }} + job-name: "Verified Soroban Rules" + certora-key: ${{ secrets.CERTORAKEY }} + working-directory: packages/access/confs + cli-release: stable + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/certora-contract-utils.yml b/.github/workflows/certora-contract-utils.yml new file mode 100644 index 00000000..0a6f9fde --- /dev/null +++ b/.github/workflows/certora-contract-utils.yml @@ -0,0 +1,49 @@ +name: Certora verification + +on: pull_request + +env: + CONFIGS: | + math_integrity.conf + math_non_panics.conf + math_panics.conf + math_sanity.conf + merkle_distributor_contract_sanity.conf + merkle_distributor_integrity.conf + pausable_integrity.conf + pausable_non_panics.conf + pausable_panics.conf + pausable_sanity.conf + +jobs: + check: + runs-on: ubuntu-latest + permissions: + contents: read + statuses: write + pull-requests: write + id-token: write + steps: + - name: checkout repository + uses: actions/checkout@v4 + with: + submodules: recursive + - name: Install rust + uses: actions-rust-lang/setup-rust-toolchain@v1 + - name: Install just + uses: extractions/setup-just@v3 + - name: Install soroban + run: | + cargo update -p cvlr-soroban + rustup target add wasm32-unknown-unknown + - name: run configs + uses: Certora/certora-run-action@v2 + with: + ecosystem: soroban + configurations: ${{ env.CONFIGS }} + job-name: "Verified Soroban Rules" + certora-key: ${{ secrets.CERTORAKEY }} + working-directory: packages/contract-utils/confs + cli-release: stable + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/certora-smart-accounts.yml b/.github/workflows/certora-smart-accounts.yml new file mode 100644 index 00000000..2a50a5f7 --- /dev/null +++ b/.github/workflows/certora-smart-accounts.yml @@ -0,0 +1,47 @@ +name: Certora verification + +on: pull_request + +env: + CONFIGS: | + simple_threshold_integrity.conf + simple_threshold_non_panics.conf + simple_threshold_panics.conf + simple_threshold_contract_sanity.conf + weighted_threshold_integrity.conf + weighted_threshold_contract_sanity.conf + spending_limit_integrity.conf + spending_limit_contract_sanity.conf + +jobs: + check: + runs-on: ubuntu-latest + permissions: + contents: read + statuses: write + pull-requests: write + id-token: write + steps: + - name: checkout repository + uses: actions/checkout@v4 + with: + submodules: recursive + - name: Install rust + uses: actions-rust-lang/setup-rust-toolchain@v1 + - name: Install just + uses: extractions/setup-just@v3 + - name: Install soroban + run: | + cargo update -p cvlr-soroban + rustup target add wasm32-unknown-unknown + - name: run configs + uses: Certora/certora-run-action@v2 + with: + ecosystem: soroban + configurations: ${{ env.CONFIGS }} + job-name: "Verified Soroban Rules" + certora-key: ${{ secrets.CERTORAKEY }} + working-directory: packages/accounts/confs + cli-release: stable + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/certora-token.yml b/.github/workflows/certora-token.yml new file mode 100644 index 00000000..3ca7e606 --- /dev/null +++ b/.github/workflows/certora-token.yml @@ -0,0 +1,53 @@ +name: Certora verification + +on: pull_request + +env: + CONFIGS: | + fungible_integrity.conf + fungible_invariants.conf + fungible_non_panics.conf + fungible_panics.conf + fungible_sanity.conf + allowlist.conf + blocklist.conf + burnable.conf + burnable_nft_sanity.conf + capped.conf + consecutive_nft_sanity.conf + enumerable_nft_sanity.conf + royalties_nft_sanity.conf + vault_sanity.conf + +jobs: + check: + runs-on: ubuntu-latest + permissions: + contents: read + statuses: write + pull-requests: write + id-token: write + steps: + - name: checkout repository + uses: actions/checkout@v4 + with: + submodules: recursive + - name: Install rust + uses: actions-rust-lang/setup-rust-toolchain@v1 + - name: Install just + uses: extractions/setup-just@v3 + - name: Install soroban + run: | + cargo update -p cvlr-soroban + rustup target add wasm32-unknown-unknown + - name: run configs + uses: Certora/certora-run-action@9596198c77a3f33d22c59226ff7757185a504f8f + with: + ecosystem: soroban + configurations: ${{ env.CONFIGS }} + job-name: "Verified Soroban Rules" + certora-key: ${{ secrets.CERTORAKEY }} + working-directory: packages/tokens/confs + cli-release: stable + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index b20111cd..4b8de65f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,15 @@ # certora .certora_internal +emv-* +local.conf +*.wat **/target/ /.idea .DS_Store .thumbs.db .vscode +.cursorrules # These are backup files generated by rustfmt **/*.rs.bk @@ -19,10 +23,6 @@ ENV/ env.bak/ venv.bak/ -# Documentation -docs/node_modules -docs/build - # Code Coverage htmlcov/ lcov.info diff --git a/Architecture.md b/Architecture.md index cd2e5504..743383b7 100644 --- a/Architecture.md +++ b/Architecture.md @@ -26,7 +26,6 @@ stellar-contracts/ │ │ └── non_fungible/ # Non-fungible token implementation │ └── lib.rs ├── examples/ # Example contract implementations -├── docs/ # Documentation └── audits/ # Security audit reports ``` diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 604e2df2..92510805 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -47,11 +47,14 @@ As a contributor, you are expected to fork this repository, work on your own for # run tests cargo test + # build + cargo build --target wasm32v1-none --release + # run linter cargo clippy --all-targets --all-features -- -D warnings # run formatter - cargo fmt --all -- --check + cargo +nightly fmt --all -- --check # run documentation checks cargo doc --all --no-deps diff --git a/Cargo.lock b/Cargo.lock index f8371c45..033c1de0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -162,6 +162,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + [[package]] name = "base64" version = "0.22.1" @@ -170,9 +176,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "base64ct" -version = "1.8.0" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" [[package]] name = "bit-set" @@ -191,9 +197,9 @@ checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" [[package]] name = "bitflags" -version = "2.9.4" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" [[package]] name = "block-buffer" @@ -219,14 +225,14 @@ dependencies = [ "num-bigint", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] name = "cc" -version = "1.2.36" +version = "1.2.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5252b3d2648e5eedbc1a6f501e3c795e07025c1e93bbf8bbdd6eef7f447a6d54" +checksum = "35900b6c8d709fb1d854671ae27aeaa9eec2f8b01b364e1619a40da3e6fe2afe" dependencies = [ "find-msvc-tools", "shlex", @@ -234,20 +240,9 @@ dependencies = [ [[package]] name = "cfg-if" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" - -[[package]] -name = "cfg_eval" -version = "0.1.2" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45565fc9416b9896014f5732ac776f810ee53a66730c17e4020c3ec064a8f88f" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "chrono" @@ -258,7 +253,7 @@ dependencies = [ "iana-time-zone", "num-traits", "serde", - "windows-link 0.2.0", + "windows-link", ] [[package]] @@ -317,20 +312,14 @@ dependencies = [ [[package]] name = "ctor" -version = "0.5.0" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67773048316103656a637612c4a62477603b777d91d9c62ff2290f9cde178fdb" +checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501" dependencies = [ - "ctor-proc-macro", - "dtor", + "quote", + "syn 2.0.110", ] -[[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" @@ -355,7 +344,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -387,7 +376,7 @@ checksum = "7c12f059e98caf58c289f08da48eeef11f2549afdf0fd7d3f3c9a08039040500" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -397,7 +386,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc2d32c7536052a4b72c27d2dce9a83b6ada5dd98756b850e6a1540fe80eabd7" dependencies = [ "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -415,10 +404,10 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2ddb1aab9e865d13f5b97d5eb3b8417457543b725e9d95febb8bcd9e124a4414" dependencies = [ - "darling", + "darling 0.20.11", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -443,7 +432,7 @@ dependencies = [ [[package]] name = "cvlr-soroban" version = "0.4.0" -source = "git+https://github.com/Certora/cvlr-soroban.git#7b221cd8441b215c2a5ece04346691f15d29b7b6" +source = "git+https://github.com/chandrakananandi/cvlr-soroban.git?branch=soroban-22.0.8#0edff4c27b94729ab8ba5bf57cf3faff5e007062" dependencies = [ "cvlr-asserts", "cvlr-log", @@ -454,19 +443,19 @@ dependencies = [ [[package]] name = "cvlr-soroban-derive" version = "0.4.0" -source = "git+https://github.com/Certora/cvlr-soroban.git#7b221cd8441b215c2a5ece04346691f15d29b7b6" +source = "git+https://github.com/chandrakananandi/cvlr-soroban.git?branch=soroban-22.0.8#0edff4c27b94729ab8ba5bf57cf3faff5e007062" dependencies = [ - "darling", + "darling 0.20.11", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", "uuid", ] [[package]] name = "cvlr-soroban-macros" version = "0.4.0" -source = "git+https://github.com/Certora/cvlr-soroban.git#7b221cd8441b215c2a5ece04346691f15d29b7b6" +source = "git+https://github.com/chandrakananandi/cvlr-soroban.git?branch=soroban-22.0.8#0edff4c27b94729ab8ba5bf57cf3faff5e007062" [[package]] name = "darling" @@ -474,8 +463,18 @@ version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" dependencies = [ - "darling_core", - "darling_macro", + "darling_core 0.20.11", + "darling_macro 0.20.11", +] + +[[package]] +name = "darling" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" +dependencies = [ + "darling_core 0.21.3", + "darling_macro 0.21.3", ] [[package]] @@ -489,7 +488,21 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.106", + "syn 2.0.110", +] + +[[package]] +name = "darling_core" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.110", ] [[package]] @@ -498,9 +511,20 @@ version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ - "darling_core", + "darling_core 0.20.11", "quote", - "syn 2.0.106", + "syn 2.0.110", +] + +[[package]] +name = "darling_macro" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" +dependencies = [ + "darling_core 0.21.3", + "quote", + "syn 2.0.110", ] [[package]] @@ -522,12 +546,12 @@ dependencies = [ [[package]] name = "deranged" -version = "0.5.3" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d630bccd429a5bb5a64b5e94f693bfc48c9f8566418fda4c494cc94f911f87cc" +checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" dependencies = [ "powerfmt", - "serde", + "serde_core", ] [[package]] @@ -549,7 +573,7 @@ checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -570,21 +594,6 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" -[[package]] -name = "dtor" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e58a0764cddb55ab28955347b45be00ade43d4d6f3ba4bf3dc354e4ec9432934" -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" @@ -664,9 +673,9 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" -version = "0.3.13" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", "windows-sys", @@ -708,9 +717,9 @@ checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" [[package]] name = "find-msvc-tools" -version = "0.1.1" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fd99930f64d146689264c637b5af2f0233a933bef0d8570e2526bf9e083192d" +checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127" [[package]] name = "fnv" @@ -720,7 +729,7 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "fungible-allowlist-example" -version = "0.4.1" +version = "0.5.1" dependencies = [ "soroban-sdk", "stellar-access", @@ -730,7 +739,7 @@ dependencies = [ [[package]] name = "fungible-blocklist-example" -version = "0.4.1" +version = "0.5.1" dependencies = [ "soroban-sdk", "stellar-access", @@ -740,7 +749,7 @@ dependencies = [ [[package]] name = "fungible-capped-example" -version = "0.4.1" +version = "0.5.1" dependencies = [ "soroban-sdk", "stellar-tokens", @@ -748,9 +757,9 @@ dependencies = [ [[package]] name = "fungible-merkle-airdrop-example" -version = "0.4.1" +version = "0.5.1" dependencies = [ - "hex-literal 1.0.0", + "hex-literal", "soroban-sdk", "stellar-access", "stellar-contract-utils", @@ -760,7 +769,7 @@ dependencies = [ [[package]] name = "fungible-pausable-example" -version = "0.4.1" +version = "0.5.1" dependencies = [ "soroban-sdk", "stellar-contract-utils", @@ -770,7 +779,7 @@ dependencies = [ [[package]] name = "fungible-vault-example" -version = "0.4.1" +version = "0.5.1" dependencies = [ "soroban-sdk", "stellar-access", @@ -780,9 +789,9 @@ dependencies = [ [[package]] name = "generic-array" -version = "0.14.7" +version = "0.14.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" dependencies = [ "typenum", "version_check", @@ -798,20 +807,20 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "wasi 0.11.1+wasi-snapshot-preview1", + "wasi", "wasm-bindgen", ] [[package]] name = "getrandom" -version = "0.3.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", "libc", "r-efi", - "wasi 0.14.4+wasi-0.2.4", + "wasip2", ] [[package]] @@ -842,15 +851,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.15.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" - -[[package]] -name = "heck" -version = "0.5.0" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" [[package]] name = "hex" @@ -867,12 +870,6 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6fe2267d4ed49bc07b63801559be28c718ea06c4738b7a03c94df7386d2cde46" -[[package]] -name = "hex-literal" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcaaec4551594c969335c98c903c1397853d4198408ea609190f420500f6be71" - [[package]] name = "hmac" version = "0.12.1" @@ -884,9 +881,9 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.63" +version = "0.1.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -925,13 +922,14 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.11.0" +version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2481980430f9f78649238835720ddccc57e52df14ffce1c6f37391d61b563e9" +checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" dependencies = [ "equivalent", - "hashbrown 0.15.5", + "hashbrown 0.16.0", "serde", + "serde_core", ] [[package]] @@ -957,9 +955,9 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "js-sys" -version = "0.3.78" +version = "0.3.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c0b063578492ceec17683ef2f8c5e89121fbd0b172cbc280635ab7567db2738" +checksum = "b011eec8cc36da2aab2d5cff675ec18454fad408585853910a202391cf9f8e65" dependencies = [ "once_cell", "wasm-bindgen", @@ -988,17 +986,11 @@ dependencies = [ "cpufeatures", ] -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" - [[package]] name = "libc" -version = "0.2.175" +version = "0.2.177" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" +checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" [[package]] name = "libm" @@ -1008,9 +1000,9 @@ checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" [[package]] name = "linux-raw-sys" -version = "0.9.4" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" [[package]] name = "log" @@ -1018,26 +1010,15 @@ version = "0.4.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" -[[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.106", -] - [[package]] name = "memchr" -version = "2.7.5" +version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" [[package]] name = "merkle-voting-example" -version = "0.4.1" +version = "0.5.1" dependencies = [ "soroban-sdk", "stellar-contract-utils", @@ -1045,7 +1026,7 @@ dependencies = [ [[package]] name = "multisig-account-example" -version = "0.4.1" +version = "0.5.1" dependencies = [ "soroban-sdk", "stellar-accounts", @@ -1053,7 +1034,7 @@ dependencies = [ [[package]] name = "multisig-ed25519-verifier-example" -version = "0.4.1" +version = "0.5.1" dependencies = [ "soroban-sdk", "stellar-accounts", @@ -1061,7 +1042,7 @@ dependencies = [ [[package]] name = "multisig-spending-limit-policy-example" -version = "0.4.1" +version = "0.5.1" dependencies = [ "soroban-sdk", "stellar-accounts", @@ -1069,7 +1050,7 @@ dependencies = [ [[package]] name = "multisig-threshold-policy-example" -version = "0.4.1" +version = "0.5.1" dependencies = [ "soroban-sdk", "stellar-accounts", @@ -1077,7 +1058,7 @@ dependencies = [ [[package]] name = "multisig-webauthn-verifier-example" -version = "0.4.1" +version = "0.5.1" dependencies = [ "soroban-sdk", "stellar-accounts", @@ -1085,7 +1066,7 @@ dependencies = [ [[package]] name = "nft-access-control-example" -version = "0.4.1" +version = "0.5.1" dependencies = [ "soroban-sdk", "stellar-access", @@ -1095,7 +1076,7 @@ dependencies = [ [[package]] name = "nft-consecutive-example" -version = "0.4.1" +version = "0.5.1" dependencies = [ "soroban-sdk", "stellar-tokens", @@ -1103,7 +1084,7 @@ dependencies = [ [[package]] name = "nft-enumerable-example" -version = "0.4.1" +version = "0.5.1" dependencies = [ "soroban-sdk", "stellar-macros", @@ -1112,7 +1093,7 @@ dependencies = [ [[package]] name = "nft-royalties-example" -version = "0.4.1" +version = "0.5.1" dependencies = [ "soroban-sdk", "stellar-access", @@ -1122,7 +1103,7 @@ dependencies = [ [[package]] name = "nft-sequential-minting-example" -version = "0.4.1" +version = "0.5.1" dependencies = [ "soroban-sdk", "stellar-macros", @@ -1153,7 +1134,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -1182,7 +1163,7 @@ checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] name = "ownable-example" -version = "0.4.1" +version = "0.5.1" dependencies = [ "soroban-sdk", "stellar-access", @@ -1209,7 +1190,7 @@ checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] name = "pausable-example" -version = "0.4.1" +version = "0.5.1" dependencies = [ "soroban-sdk", "stellar-contract-utils", @@ -1257,7 +1238,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -1271,23 +1252,22 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.101" +version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" dependencies = [ "unicode-ident", ] [[package]] name = "proptest" -version = "1.7.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fcdab19deb5195a31cf7726a210015ff1496ba1464fd42cb4f537b8b01b471f" +checksum = "bee689443a2bd0a16ab0348b52ee43e3b2d1b1f931c8aa5c9f8de4c86fbe8c40" dependencies = [ "bit-set", "bit-vec", "bitflags", - "lazy_static", "num-traits", "rand 0.9.2", "rand_chacha 0.9.0", @@ -1306,9 +1286,9 @@ checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" [[package]] name = "quote" -version = "1.0.40" +version = "1.0.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" dependencies = [ "proc-macro2", ] @@ -1375,7 +1355,7 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" dependencies = [ - "getrandom 0.3.3", + "getrandom 0.3.4", ] [[package]] @@ -1389,29 +1369,29 @@ dependencies = [ [[package]] name = "ref-cast" -version = "1.0.24" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a0ae411dbe946a674d89546582cea4ba2bb8defac896622d6496f14c23ba5cf" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" dependencies = [ "ref-cast-impl", ] [[package]] name = "ref-cast-impl" -version = "1.0.24" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1165225c21bff1f3bbce98f5a1f889949bc902d3575308cc7b0de30b4f6d27c7" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] name = "regex-syntax" -version = "0.8.6" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" [[package]] name = "rfc6979" @@ -1434,9 +1414,9 @@ dependencies = [ [[package]] name = "rustix" -version = "1.0.8" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" +checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" dependencies = [ "bitflags", "errno", @@ -1453,9 +1433,9 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "rusty-fork" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb3dcc6e454c328bb824492db107ab7c0ae8fcffe4ad210136ef014458c1bc4f" +checksum = "cc6bf79ff24e648f6da1f8d1f011e9cac26491b619e6b9280f2b47f1774e6ee2" dependencies = [ "fnv", "quick-error", @@ -1471,7 +1451,7 @@ checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" [[package]] name = "sac-admin-generic-example" -version = "0.4.1" +version = "0.5.1" dependencies = [ "ed25519-dalek", "soroban-sdk", @@ -1480,7 +1460,7 @@ dependencies = [ [[package]] name = "sac-admin-wrapper-example" -version = "0.4.1" +version = "0.5.1" dependencies = [ "soroban-sdk", "stellar-access", @@ -1488,17 +1468,6 @@ dependencies = [ "stellar-tokens", ] -[[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" @@ -1513,9 +1482,9 @@ dependencies = [ [[package]] name = "schemars" -version = "1.0.4" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82d20c4491bc164fa2f6c5d44565947a52ad80b9505d8e36f8d54c27c739fcd0" +checksum = "9558e172d4e8533736ba97870c4b2cd63f84b382a3d6eb063da41b91cce17289" dependencies = [ "dyn-clone", "ref-cast", @@ -1539,16 +1508,17 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.26" +version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" [[package]] name = "serde" -version = "1.0.219" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" dependencies = [ + "serde_core", "serde_derive", ] @@ -1562,45 +1532,53 @@ dependencies = [ "serde", ] +[[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.219" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] name = "serde_json" -version = "1.0.143" +version = "1.0.145" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" dependencies = [ "itoa", "memchr", "ryu", "serde", + "serde_core", ] [[package]] name = "serde_with" -version = "3.14.0" +version = "3.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2c45cd61fefa9db6f254525d46e392b852e0e61d9a1fd36e5bd183450a556d5" +checksum = "aa66c845eee442168b2c8134fec70ac50dc20e760769c8ba0ad1319ca1959b04" dependencies = [ - "base64", + "base64 0.22.1", "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.11.0", - "schemars 0.8.22", + "indexmap 2.12.0", "schemars 0.9.0", - "schemars 1.0.4", - "serde", - "serde_derive", + "schemars 1.1.0", + "serde_core", "serde_json", "serde_with_macros", "time", @@ -1608,14 +1586,14 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.14.0" +version = "3.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de90945e6565ce0d9a25098082ed4ee4002e047cb59892c318d66821e14bb30f" +checksum = "b91a903660542fced4e99881aa481bdbaec1634568ee02e0b8bd57c64cb38955" dependencies = [ - "darling", + "darling 0.21.3", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -1663,21 +1641,21 @@ checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "soroban-builtin-sdk-macros" -version = "23.0.1" +version = "22.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9336adeabcd6f636a4e0889c8baf494658ef5a3c4e7e227569acd2ce9091e85" +checksum = "cf2e42bf80fcdefb3aae6ff3c7101a62cf942e95320ed5b518a1705bc11c6b2f" dependencies = [ "itertools", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] name = "soroban-env-common" -version = "23.0.1" +version = "22.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00067f52e8bbf1abf0de03fe3e2fbb06910893cfbe9a7d9093d6425658833ff3" +checksum = "027cd856171bfd6ad2c0ffb3b7dfe55ad7080fb3050c36ad20970f80da634472" dependencies = [ "arbitrary", "crate-git-revision", @@ -1694,9 +1672,9 @@ dependencies = [ [[package]] name = "soroban-env-guest" -version = "23.0.1" +version = "22.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccd1e40963517b10963a8e404348d3fe6caf9c278ac47a6effd48771297374d6" +checksum = "9a07dda1ae5220d975979b19ad4fd56bc86ec7ec1b4b25bc1c5d403f934e592e" dependencies = [ "soroban-env-common", "static_assertions", @@ -1704,9 +1682,9 @@ dependencies = [ [[package]] name = "soroban-env-host" -version = "23.0.1" +version = "22.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9766c5ad78e9d8ae10afbc076301f7d610c16407a1ebb230766dbe007a48725" +checksum = "66e8b03a4191d485eab03f066336112b2a50541a7553179553dc838b986b94dd" dependencies = [ "ark-bls12-381", "ark-ec", @@ -1718,7 +1696,7 @@ dependencies = [ "elliptic-curve", "generic-array", "getrandom 0.2.16", - "hex-literal 0.4.1", + "hex-literal", "hmac", "k256", "num-derive", @@ -1740,9 +1718,9 @@ dependencies = [ [[package]] name = "soroban-env-macros" -version = "23.0.1" +version = "22.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0e6a1c5844257ce96f5f54ef976035d5bd0ee6edefaf9f5e0bcb8ea4b34228c" +checksum = "00eff744764ade3bc480e4909e3a581a240091f3d262acdce80b41f7069b2bd9" dependencies = [ "itertools", "proc-macro2", @@ -1750,14 +1728,14 @@ dependencies = [ "serde", "serde_json", "stellar-xdr", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] name = "soroban-ledger-snapshot" -version = "23.0.3" +version = "22.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdefc9240bddd3ff4d47fd4d8f8dd44784840e25a18e426c6c987db8572d6df9" +checksum = "2826e2c9d364edbb2ea112dc861077c74557bdad0a7a00487969088c7c648169" dependencies = [ "serde", "serde_json", @@ -1769,13 +1747,12 @@ dependencies = [ [[package]] name = "soroban-sdk" -version = "23.0.3" +version = "22.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cb0dc3eb3661962cb8833513953b5839df14d589d96f8370b5b0c3870a8b3b5" +checksum = "c7ac27d7573e62b745513fa1be8dab7a09b9676a7f39db97164f1d458a344749" dependencies = [ "arbitrary", "bytes-lit", - "crate-git-revision", "ctor", "derive_arbitrary", "ed25519-dalek", @@ -1792,31 +1769,31 @@ dependencies = [ [[package]] name = "soroban-sdk-macros" -version = "23.0.3" +version = "22.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7eab5f4e5f3836a4b4aeecb2837160e944621b2f8dbad775638a2ab8e10fd5bb" +checksum = "9ef0d7d62b2584696d306b8766728971c7d0731a03a5e047f1fc68722ac8cf0c" dependencies = [ - "darling", - "heck", + "crate-git-revision", + "darling 0.20.11", "itertools", - "macro-string", "proc-macro2", "quote", + "rustc_version", "sha2", "soroban-env-common", "soroban-spec", "soroban-spec-rust", "stellar-xdr", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] name = "soroban-spec" -version = "23.0.3" +version = "22.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd257b0365307e0b8d38040ee0364abcc610fc6e61960ff5e26803922d098921" +checksum = "a4ad0867aec99770ed614fedbec7ac4591791df162ff9e548ab7ebd07cd23a9c" dependencies = [ - "base64", + "base64 0.13.1", "stellar-xdr", "thiserror", "wasmparser", @@ -1824,9 +1801,9 @@ dependencies = [ [[package]] name = "soroban-spec-rust" -version = "23.0.3" +version = "22.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ec3c72de91fdcf637045f3351df029a98b9de9ad22ced4063f74d0b5873f526" +checksum = "aebe31c042adfa2885ec47b67b08fcead8707da80a3fe737eaf2a9ae1a8cfdc3" dependencies = [ "prettyplease", "proc-macro2", @@ -1834,19 +1811,19 @@ dependencies = [ "sha2", "soroban-spec", "stellar-xdr", - "syn 2.0.106", + "syn 2.0.110", "thiserror", ] [[package]] name = "soroban-test-helpers" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b21ea049bdfcfce7de5aa17f1a52ecab5f2bd599d40bd805747ef75b110ee5d" +checksum = "f7d51ae9c644ccd8dd2c41148ee31a037641b1aeb85b76d519c7f6e9a4cd55b0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] @@ -1886,8 +1863,9 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] name = "stellar-access" -version = "0.4.1" +version = "0.5.1" dependencies = [ + "base64ct", "cvlr", "cvlr-soroban", "cvlr-soroban-derive", @@ -1899,10 +1877,15 @@ dependencies = [ [[package]] name = "stellar-accounts" -version = "0.4.1" +version = "0.5.1" dependencies = [ + "base64ct", + "cvlr", + "cvlr-soroban", + "cvlr-soroban-derive", + "cvlr-soroban-macros", "ed25519-dalek", - "hex-literal 1.0.0", + "hex-literal", "p256", "serde", "serde-json-core", @@ -1913,21 +1896,22 @@ dependencies = [ [[package]] name = "stellar-contract-utils" -version = "0.4.1" +version = "0.5.1" dependencies = [ "cvlr", "cvlr-soroban", "cvlr-soroban-derive", "cvlr-soroban-macros", - "hex-literal 1.0.0", + "hex-literal", "proptest", "soroban-sdk", "stellar-event-assertion", + "stellar-macros", ] [[package]] name = "stellar-default-impl-macro-test" -version = "0.4.1" +version = "0.5.1" dependencies = [ "soroban-sdk", "stellar-access", @@ -1938,7 +1922,7 @@ dependencies = [ [[package]] name = "stellar-event-assertion" -version = "0.4.1" +version = "0.5.1" dependencies = [ "soroban-sdk", "stellar-tokens", @@ -1946,27 +1930,32 @@ dependencies = [ [[package]] name = "stellar-macros" -version = "0.4.1" +version = "0.5.1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] name = "stellar-strkey" -version = "0.0.13" +version = "0.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee1832fb50c651ad10f734aaf5d31ca5acdfb197a6ecda64d93fcdb8885af913" +checksum = "5e3aa3ed00e70082cb43febc1c2afa5056b9bb3e348bbb43d0cd0aa88a611144" dependencies = [ "crate-git-revision", "data-encoding", + "thiserror", ] [[package]] name = "stellar-tokens" -version = "0.4.1" +version = "0.5.1" dependencies = [ + "cvlr", + "cvlr-soroban", + "cvlr-soroban-derive", + "cvlr-soroban-macros", "ed25519-dalek", "k256", "p256", @@ -1978,20 +1967,17 @@ dependencies = [ [[package]] name = "stellar-xdr" -version = "23.0.0" +version = "22.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89d2848e1694b0c8db81fd812bfab5ea71ee28073e09ccc45620ef3cf7a75a9b" +checksum = "2ce69db907e64d1e70a3dce8d4824655d154749426a6132b25395c49136013e4" dependencies = [ "arbitrary", - "base64", - "cfg_eval", + "base64 0.13.1", "crate-git-revision", "escape-bytes", - "ethnum", "hex", "serde", "serde_with", - "sha2", "stellar-strkey", ] @@ -2020,9 +2006,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.106" +version = "2.0.110" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" +checksum = "a99801b5bd34ede4cf3fc688c5919368fea4e4814a4664359503e6015b280aea" dependencies = [ "proc-macro2", "quote", @@ -2031,12 +2017,12 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.21.0" +version = "3.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15b61f8f20e3a6f7e0649d825294eaf317edce30f82cf6026e7e4cb9222a7d1e" +checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" dependencies = [ "fastrand", - "getrandom 0.3.3", + "getrandom 0.3.4", "once_cell", "rustix", "windows-sys", @@ -2059,16 +2045,17 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] name = "time" -version = "0.3.43" +version = "0.3.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83bde6f1ec10e72d583d91623c939f623002284ef622b87de38cfd546cbf2031" +checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" dependencies = [ "deranged", + "itoa", "num-conv", "powerfmt", "serde", @@ -2094,9 +2081,9 @@ dependencies = [ [[package]] name = "typenum" -version = "1.18.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" [[package]] name = "unarray" @@ -2106,9 +2093,9 @@ checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" [[package]] name = "unicode-ident" -version = "1.0.18" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" [[package]] name = "upgradeable-v1-example" @@ -2130,7 +2117,7 @@ dependencies = [ [[package]] name = "upgrader-example" -version = "0.4.1" +version = "0.5.1" dependencies = [ "soroban-sdk", "stellar-access", @@ -2144,7 +2131,7 @@ version = "1.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" dependencies = [ - "getrandom 0.3.3", + "getrandom 0.3.4", "js-sys", "wasm-bindgen", ] @@ -2171,19 +2158,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] -name = "wasi" -version = "0.14.4+wasi-0.2.4" +name = "wasip2" +version = "1.0.1+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88a5f4a424faf49c3c2c344f166f0662341d470ea185e939657aaff130f0ec4a" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" dependencies = [ "wit-bindgen", ] [[package]] name = "wasm-bindgen" -version = "0.2.101" +version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e14915cadd45b529bb8d1f343c4ed0ac1de926144b746e2710f9cd05df6603b" +checksum = "da95793dfc411fbbd93f5be7715b0578ec61fe87cb1a42b12eb625caa5c5ea60" dependencies = [ "cfg-if", "once_cell", @@ -2192,25 +2179,11 @@ dependencies = [ "wasm-bindgen-shared", ] -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.101" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e28d1ba982ca7923fd01448d5c30c6864d0a14109560296a162f80f305fb93bb" -dependencies = [ - "bumpalo", - "log", - "proc-macro2", - "quote", - "syn 2.0.106", - "wasm-bindgen-shared", -] - [[package]] name = "wasm-bindgen-macro" -version = "0.2.101" +version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c3d463ae3eff775b0c45df9da45d68837702ac35af998361e2c84e7c5ec1b0d" +checksum = "04264334509e04a7bf8690f2384ef5265f05143a4bff3889ab7a3269adab59c2" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2218,22 +2191,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.101" +version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bb4ce89b08211f923caf51d527662b75bdc9c9c7aab40f86dcb9fb85ac552aa" +checksum = "420bc339d9f322e562942d52e115d57e950d12d88983a14c79b86859ee6c7ebc" dependencies = [ + "bumpalo", "proc-macro2", "quote", - "syn 2.0.106", - "wasm-bindgen-backend", + "syn 2.0.110", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.101" +version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f143854a3b13752c6950862c906306adb27c7e839f7414cec8fea35beab624c1" +checksum = "76f218a38c84bcb33c25ec7059b07847d465ce0e0a76b995e134a45adcb6af76" dependencies = [ "unicode-ident", ] @@ -2262,7 +2235,7 @@ version = "0.116.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a58e28b80dd8340cb07b8242ae654756161f6fc8d0038123d679b7b99964fa50" dependencies = [ - "indexmap 2.11.0", + "indexmap 2.12.0", "semver", ] @@ -2277,148 +2250,77 @@ dependencies = [ [[package]] name = "windows-core" -version = "0.61.2" +version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ "windows-implement", "windows-interface", - "windows-link 0.1.3", + "windows-link", "windows-result", "windows-strings", ] [[package]] name = "windows-implement" -version = "0.60.0" +version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] name = "windows-interface" -version = "0.59.1" +version = "0.59.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] name = "windows-link" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" - -[[package]] -name = "windows-link" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] name = "windows-result" -version = "0.3.4" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" dependencies = [ - "windows-link 0.1.3", + "windows-link", ] [[package]] name = "windows-strings" -version = "0.4.2" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" dependencies = [ - "windows-link 0.1.3", + "windows-link", ] [[package]] name = "windows-sys" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" -dependencies = [ - "windows-targets", -] - -[[package]] -name = "windows-targets" -version = "0.53.3" +version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ - "windows-link 0.1.3", - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows-link", ] -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" - -[[package]] -name = "windows_i686_gnu" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" - -[[package]] -name = "windows_i686_msvc" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" - [[package]] name = "wit-bindgen" -version = "0.45.1" +version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c573471f125075647d03df72e026074b7203790d41351cd6edc96f46bcccd36" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" [[package]] name = "zerocopy" @@ -2437,14 +2339,14 @@ checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] [[package]] name = "zeroize" -version = "1.8.1" +version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" dependencies = [ "zeroize_derive", ] @@ -2457,5 +2359,5 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.110", ] diff --git a/Cargo.toml b/Cargo.toml index 99d7d497..aadbc794 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,7 +25,7 @@ members = [ "packages/contract-utils", "packages/macros", "packages/test-utils/*", - "packages/tokens", + "packages/tokens" ] exclude = ["examples/upgradeable/testdata"] @@ -37,21 +37,24 @@ repository = "https://github.com/OpenZeppelin/stellar-contracts" documentation = "https://docs.openzeppelin.com/stellar-contracts/" keywords = ["stellar", "soroban", "smart-contracts", "standards"] categories = ["no-std", "wasm"] -version = "0.4.1" +version = "0.5.1" [workspace.dependencies] -soroban-sdk = "23.0.2" +# certora change +soroban-sdk = {package = "soroban-sdk", version = "=22.0.8", default-features = false} proc-macro2 = "1.0" proptest = "1" quote = "1.0" syn = { version = "2.0", features = ["full"] } soroban-test-helpers = "0.2.3" -hex-literal = "1.0.0" +# certora change +hex-literal = "0.4.0" # only used in tests, helps fix rust-analyzer issues ed25519-dalek = "2.1.1" k256 = "0.13.4" p256 = "0.13.2" serde = { version = "1", default-features = false } serde-json-core = { version = "0.6.0", default-features = false } +base64ct = { version = "=1.6.0" } # members stellar-access = { path = "packages/access" } @@ -81,13 +84,16 @@ version = "0.4.0" default-features = false [workspace.dependencies.cvlr-soroban] -git = "https://github.com/Certora/cvlr-soroban.git" +git = "https://github.com/chandrakananandi/cvlr-soroban.git" +branch = "soroban-22.0.8" # path = "/Users/chandrakananandi/research/cvlr-soroban/cvlr-soroban" [workspace.dependencies.cvlr-soroban-macros] -git = "https://github.com/Certora/cvlr-soroban.git" +git = "https://github.com/chandrakananandi/cvlr-soroban.git" +branch = "soroban-22.0.8" # path = "/Users/chandrakananandi/research/cvlr-soroban/cvlr-soroban-macros" [workspace.dependencies.cvlr-soroban-derive] -git = "https://github.com/Certora/cvlr-soroban.git" +git = "https://github.com/chandrakananandi/cvlr-soroban.git" +branch = "soroban-22.0.8" # path = "/Users/chandrakananandi/research/cvlr-soroban/cvlr-soroban-derive" \ No newline at end of file diff --git a/README.md b/README.md index 604460a9..54916e8a 100644 --- a/README.md +++ b/README.md @@ -14,13 +14,13 @@ OpenZeppelin Stellar Soroban Contracts is a collection of contracts for the Stel ## Project Structure - `packages/`: Source code - - `access/`: Role-based access controls and ownable - - `contract-utils/`: Utilities for contracts (pausable, upgradeable, crypography, etc.) - - `macros/`: Proc and derive macros for some of the modules (`#[only_owner]`, `#[when_not_paused]`, `#[derive(Upgradeable)]`, etc.) - - `test-utils/`: Utilities for testing - - `tokens/`: Various token types (fungible, non-fungible, etc.) + - [`access/`](packages/access): Role-based access controls and ownable + - [`accounts/`](packages/accounts): Smart accounts with custom authentication and authorization + - [`contract-utils/`](packages/contract-utils): Utilities for contracts (pausable, upgradeable, cryptography, etc.) + - [`macros/`](packages/macros): Proc and derive macros for some of the modules (`#[only_owner]`, `#[when_not_paused]`, `#[derive(Upgradeable)]`, etc.) + - [`test-utils/`](packages/test-utils): Utilities for testing + - [`tokens/`](packages/tokens): Various token types (fungible, non-fungible, real-world assets, vaults) - `examples/`: Example contracts -- `docs/`: Documentation - `audits/`: Audit reports @@ -54,9 +54,9 @@ We recommend pinning to a specific version, because rapid iterations are expecte ```toml [dependencies] -stellar-tokens = "=0.4.0" -stellar-access = "=0.4.0" -stellar-contract-utils = "=0.4.0" +stellar-tokens = "=0.5.1" +stellar-access = "=0.5.1" +stellar-contract-utils = "=0.5.1" ``` ## Notes diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..4d71e52e --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,13 @@ +# Security Policy + +Security vulnerabilities should be disclosed to the project maintainers through [Immunefi], or alternatively by email to security@openzeppelin.com. + +[Immunefi]: https://immunefi.com/bug-bounty/openzeppelin-stellar + +## Bug Bounty + +Responsible disclosure of security vulnerabilities is rewarded through a bug bounty program on [Immunefi]. + +## Legal + +Blockchain is a nascent technology and carries a high level of risk and uncertainty. OpenZeppelin makes certain software available under open source licenses, which disclaim all warranties in relation to the project and which limits the liability of OpenZeppelin. Subject to any particular licensing terms, your use of the project is governed by the terms found at [www.openzeppelin.com/tos](https://www.openzeppelin.com/tos) (the "Terms"). As set out in the Terms, you are solely responsible for any use of the project and you assume all risks associated with any such use. This Security Policy in no way evidences or represents an ongoing duty by any contributor, including OpenZeppelin, to correct any issues or vulnerabilities or alert you to all or any of the risks of utilizing the project. diff --git a/audits/Stellar Contracts Library v0.5.0 Audit.pdf b/audits/Stellar Contracts Library v0.5.0 Audit.pdf new file mode 100644 index 00000000..91ce6e1d Binary files /dev/null and b/audits/Stellar Contracts Library v0.5.0 Audit.pdf differ diff --git a/audits/Stellar Contracts Library v0.5.0 Re-Audit.pdf b/audits/Stellar Contracts Library v0.5.0 Re-Audit.pdf new file mode 100644 index 00000000..22cfb9f8 Binary files /dev/null and b/audits/Stellar Contracts Library v0.5.0 Re-Audit.pdf differ diff --git a/check_verification_status.py b/check_verification_status.py new file mode 100755 index 00000000..5eb4ff33 --- /dev/null +++ b/check_verification_status.py @@ -0,0 +1,372 @@ +#!/usr/bin/env python3 +""" +Script to analyze formal verification status across the project. + +Scans all files in /specs directories, counts #[rule] functions, +and checks their //status comments to identify unverified rules. +""" + +import os +import re +from pathlib import Path +from collections import defaultdict +from typing import Dict, List, Tuple, Optional +import json + + +class RuleInfo: + """Information about a single rule.""" + def __init__(self, name: str, status: Optional[str], line_num: int): + self.name = name + self.status = status + self.line_num = line_num + self.is_verified = self._check_verified() + self.is_bug = self._check_if_bug() + + def _check_verified(self) -> bool: + """Check if the rule is verified.""" + if self.status is None: + return False + status_lower = self.status.lower().strip() + # Check if status starts with "verified" + return status_lower.startswith("verified") + + def _check_if_bug(self) -> bool: + """Check if the rule is a bug.""" + if self.status is None: + return False + status_lower = self.status.lower().strip() + return status_lower.startswith("bug") + +class FileAnalysis: + """Analysis results for a single file.""" + def __init__(self, file_path: str): + self.file_path = file_path + self.rules: List[RuleInfo] = [] + self.total_rules = 0 + self.verified_rules = 0 + self.unverified_rules = 0 + self.bug_rules = 0 + + def add_rule(self, rule: RuleInfo): + """Add a rule to this file's analysis.""" + self.rules.append(rule) + self.total_rules += 1 + if rule.is_verified: + self.verified_rules += 1 + elif not rule.is_bug: + self.unverified_rules += 1 + if rule.is_bug: + self.bug_rules += 1 + + +def find_spec_files(root_dir: Path) -> List[Path]: + """Find all Rust files in specs directories.""" + spec_files = [] + excluded_dirs = {".certora_internal", "target", ".git"} + + for path in root_dir.rglob("*.rs"): + # Skip if any part of the path is in excluded directories + if any(excluded in path.parts for excluded in excluded_dirs): + continue + # Only include files in specs directories + if "specs" in path.parts: + spec_files.append(path) + + return sorted(spec_files) + + +def extract_status_comment(lines: List[str], rule_line_idx: int, func_line_idx: int) -> Optional[str]: + """ + Extract status comment from lines around a rule. + Looks for comments with 'status:' pattern before or after the #[rule] attribute. + Prioritizes forward search (between #[rule] and function) to avoid picking up + status comments from previous rules. + """ + # First, look forwards between #[rule] and function definition + # This is the most common case and avoids picking up previous rule's status + end_idx = min(rule_line_idx + 15, func_line_idx, len(lines)) + for i in range(rule_line_idx + 1, end_idx): + line = lines[i].strip() + # Match patterns like: + # // status: verified + # //status: verified + # // status: violated - bug + # // status: first assert verified + match = re.search(r'//\s*status\s*:\s*(.+)', line, re.IGNORECASE) + if match: + return match.group(1).strip() + + # Only if not found forward, look backwards from the rule line + # Stop if we encounter another #[rule] or function definition + for i in range(rule_line_idx - 1, max(-1, rule_line_idx - 11), -1): + line = lines[i].strip() + # Stop if we hit another #[rule] or function definition + if '#[rule]' in line or re.search(r'(?:pub\s+)?fn\s+\w+', line): + break + match = re.search(r'//\s*status\s*:\s*(.+)', line, re.IGNORECASE) + if match: + return match.group(1).strip() + + return None + + +def extract_function_name(line: str) -> Optional[str]: + """Extract function name from a function definition line.""" + # Match patterns like: + # pub fn function_name( + # fn function_name( + # pub fn function_name<...>( + match = re.search(r'(?:pub\s+)?fn\s+(\w+)', line) + if match: + return match.group(1) + return None + + +def analyze_file(file_path: Path) -> FileAnalysis: + """Analyze a single Rust file for rules and their status.""" + analysis = FileAnalysis(str(file_path)) + + try: + with open(file_path, 'r', encoding='utf-8') as f: + lines = f.readlines() + except Exception as e: + print(f"Error reading {file_path}: {e}", file=os.sys.stderr) + return analysis + + for i, line in enumerate(lines): + # Look for #[rule] attribute + if '#[rule]' in line or '#[ rule ]' in line.replace(' ', ''): + # Find the function definition (should be within next few lines) + func_name = None + func_line_idx = i + + # Look ahead for function definition (up to 15 lines to handle comments) + for j in range(i + 1, min(i + 15, len(lines))): + func_name = extract_function_name(lines[j]) + if func_name: + func_line_idx = j + break + + if func_name: + # Skip rules that end with _sanity + if func_name.endswith('_sanity'): + continue + + # Extract status comment + status = extract_status_comment(lines, i, func_line_idx) + rule = RuleInfo(func_name, status, i + 1) # 1-indexed line numbers + analysis.add_rule(rule) + + return analysis + + +def get_directory(path: str) -> str: + """Get the directory path from a file path.""" + return str(Path(path).parent) + + +def get_relative_path(file_path: str, root_dir: Path) -> str: + """Convert absolute file path to relative path, removing root and packages/ prefix.""" + try: + # Convert to Path and make relative to root + path = Path(file_path) + if path.is_absolute(): + try: + relative = path.relative_to(root_dir) + except ValueError: + # If not relative to root, return as is + return file_path + else: + relative = path + + # Convert to string and remove packages/ prefix if present + path_str = str(relative) + if path_str.startswith("packages/"): + path_str = path_str[len("packages/"):] + + return path_str + except Exception: + return file_path + + +def format_summary(analyses: List[FileAnalysis], output_format: str = "table", root_dir: Path = None) -> str: + """Format the analysis results.""" + if root_dir is None: + root_dir = Path.cwd() + if output_format == "json": + return format_json(analyses, root_dir) + else: + return format_table(analyses, root_dir) + + +def format_table(analyses: List[FileAnalysis], root_dir: Path) -> str: + """Format results as a table.""" + output = [] + + # Project-wide totals + total_rules = sum(a.total_rules for a in analyses) + total_verified = sum(a.verified_rules for a in analyses) + total_unverified = sum(a.unverified_rules for a in analyses) + total_bug = sum(a.bug_rules for a in analyses) + output.append("=" * 40) + output.append("Formal Verification Status Summary") + output.append("=" * 40) + output.append("") + output.append(f"Project Total:") + output.append(f" Total Rules: {total_rules}") + output.append(f" Verified: {total_verified}") + output.append(f" Unverified: {total_unverified}") + output.append(f" Bug: {total_bug}") + output.append("") + + # Group by directory + dir_stats: Dict[str, Dict[str, int]] = defaultdict(lambda: {"total": 0, "verified": 0, "unverified": 0, "bug": 0}) + for analysis in analyses: + if analysis.total_rules > 0: + dir_path = get_relative_path(get_directory(analysis.file_path), root_dir) + dir_stats[dir_path]["total"] += analysis.total_rules + dir_stats[dir_path]["verified"] += analysis.verified_rules + dir_stats[dir_path]["unverified"] += analysis.unverified_rules + dir_stats[dir_path]["bug"] += analysis.bug_rules + + output.append("=" * 40) + output.append("By Directory:") + output.append("=" * 40) + output.append("") + + for dir_path in sorted(dir_stats.keys()): + stats = dir_stats[dir_path] + output.append(f"{dir_path}:") + output.append(f" Total: {stats['total']}, Verified: {stats['verified']}, Unverified: {stats['unverified']}, Bug: {stats['bug']}") + output.append("") + + # By file + output.append("=" * 40) + output.append("By File:") + output.append("=" * 40) + output.append("") + + for analysis in sorted(analyses, key=lambda x: x.file_path): + if analysis.total_rules > 0: + rel_path = get_relative_path(analysis.file_path, root_dir) + output.append(f"{rel_path}:") + output.append(f" Total: {analysis.total_rules}, Verified: {analysis.verified_rules}, Unverified: {analysis.unverified_rules}, Bug: {analysis.bug_rules}") + + # List unverified rules + unverified = [r for r in analysis.rules if not r.is_verified and not r.is_bug] + if unverified: + output.append(" Unverified Rules:") + for rule in unverified: + status_str = rule.status if rule.status else "no status" + output.append(f" - {rule.name} (line {rule.line_num}): {status_str}") + output.append("") + + return "\n".join(output) + + +def format_json(analyses: List[FileAnalysis], root_dir: Path) -> str: + """Format results as JSON.""" + result = { + "project_total": { + "total_rules": sum(a.total_rules for a in analyses), + "verified_rules": sum(a.verified_rules for a in analyses), + "unverified_rules": sum(a.unverified_rules for a in analyses), + "bug_rules": sum(a.bug_rules for a in analyses) + }, + "by_directory": {}, + "by_file": [] + } + + # Group by directory + dir_stats: Dict[str, Dict[str, int]] = defaultdict(lambda: {"total": 0, "verified": 0, "unverified": 0, "bug": 0}) + + for analysis in analyses: + if analysis.total_rules > 0: + dir_path = get_relative_path(get_directory(analysis.file_path), root_dir) + dir_stats[dir_path]["total"] += analysis.total_rules + dir_stats[dir_path]["verified"] += analysis.verified_rules + dir_stats[dir_path]["unverified"] += analysis.unverified_rules + dir_stats[dir_path]["bug"] += analysis.bug_rules + + for dir_path, stats in sorted(dir_stats.items()): + result["by_directory"][dir_path] = stats.copy() + + # By file + for analysis in sorted(analyses, key=lambda x: x.file_path): + if analysis.total_rules > 0: + rel_path = get_relative_path(analysis.file_path, root_dir) + file_data = { + "file": rel_path, + "total_rules": analysis.total_rules, + "verified_rules": analysis.verified_rules, + "unverified_rules": analysis.unverified_rules, + "bug_rules": analysis.bug_rules, + "rules": [] + } + + for rule in analysis.rules: + file_data["rules"].append({ + "name": rule.name, + "line": rule.line_num, + "status": rule.status, + "verified": rule.is_verified, + "bug": rule.is_bug + }) + + result["by_file"].append(file_data) + + return json.dumps(result, indent=2) + + +def main(): + """Main entry point.""" + import argparse + + parser = argparse.ArgumentParser( + description="Analyze formal verification status across the project" + ) + parser.add_argument( + "--format", + choices=["table", "json"], + default="table", + help="Output format (default: table)" + ) + parser.add_argument( + "--root", + type=str, + default=".", + help="Root directory to search (default: current directory)" + ) + + args = parser.parse_args() + + root_dir = Path(args.root).resolve() + + if not root_dir.exists(): + print(f"Error: Root directory does not exist: {root_dir}", file=os.sys.stderr) + return 1 + + # Find all spec files + spec_files = find_spec_files(root_dir) + + if not spec_files: + print("No spec files found in specs directories.", file=os.sys.stderr) + return 1 + + # Analyze each file + analyses = [] + for spec_file in spec_files: + analysis = analyze_file(spec_file) + if analysis.total_rules > 0: + analyses.append(analysis) + + # Format and print results + output = format_summary(analyses, args.format, root_dir) + print(output) + + return 0 + + +if __name__ == "__main__": + exit(main()) diff --git a/docs/README.md b/docs/README.md deleted file mode 100644 index 034d41e0..00000000 --- a/docs/README.md +++ /dev/null @@ -1,14 +0,0 @@ -# Docs Editing Readme - -#### Build and run the documentation - -* Run `npm install` to install the dependencies -* Run `npm run docs` to build the directory -* Run `npm run docs:watch` to run the inner docs website -* Open `https://127.0.0.1:8080/` to see the compiled docs - -#### Add a new page - -* Create a new file at desired destination -* Add your info to the page -* Add a link to the `docs/modules/ROOT/nav.adoc` diff --git a/docs/antora.yml b/docs/antora.yml deleted file mode 100644 index e7b66fda..00000000 --- a/docs/antora.yml +++ /dev/null @@ -1,8 +0,0 @@ -name: stellar-contracts -title: Stellar Contracts -version: 0.4.0 -nav: - - modules/ROOT/nav.adoc -asciidoc: - attributes: - page-sidebar-collapse-default: false diff --git a/docs/modules/ROOT/nav.adoc b/docs/modules/ROOT/nav.adoc deleted file mode 100644 index bf2ae9c4..00000000 --- a/docs/modules/ROOT/nav.adoc +++ /dev/null @@ -1,17 +0,0 @@ -* Tokens -** xref:tokens/fungible/fungible.adoc[Fungible Tokens] -** xref:tokens/non-fungible/non-fungible.adoc[Non-Fungible Tokens] - -* Access -** xref:access/access-control.adoc[Access Control] -** xref:access/ownable.adoc[Ownable] - -* Utilities -** xref:utils/pausable.adoc[Pausable] -** xref:utils/upgradeable.adoc[Upgradeable] -** xref:utils/crypto.adoc[Cryptography] - -* Helpers -** xref:helpers/default-impl-macro.adoc[Default Implementation Macro] - -* xref:get-started.adoc[Get Started] diff --git a/docs/modules/ROOT/pages/access/access-control.adoc b/docs/modules/ROOT/pages/access/access-control.adoc deleted file mode 100644 index 94db21a8..00000000 --- a/docs/modules/ROOT/pages/access/access-control.adoc +++ /dev/null @@ -1,113 +0,0 @@ -:source-highlighter: highlight.js -:highlightjs-languages: rust -:github-icon: pass:[] -= Access Control - -https://github.com/OpenZeppelin/stellar-contracts/tree/main/packages/access/src/access-control[Source Code] - -== Overview - -The Access Control module provides a comprehensive role-based access control system for Soroban contracts. It enables developers to manage permissions through a hierarchical role system, with a renounceable single overarching admin and customizable role assignments. - -== Key Concepts - -=== Admin Management - -The system features a single top-level admin with privileges to call any function in the `AccessControl` trait. This admin must be set during contract initialization for the module to function properly. This overarching admin can renounce themselves for decentralization purposes. - -Admin transfers are implemented as a two-step process to prevent accidental or malicious takeovers: - -1. The current admin *initiates* the transfer by specifying the new admin and an expiration time (`live_until_ledger`). -2. The designated new admin must *explicitly accept* the transfer to complete it. - -Until the transfer is accepted, the original admin retains full control and can override or cancel the transfer by initiating a new one or using a `live_until_ledger` of `0`. - -=== Role Hierarchy - -The module supports a hierarchical role system where each role can have an "admin role" assigned to it. For example: - -* Create roles `minter` and `minter_admin` -* Assign `minter_admin` as the admin role for the `minter` role -* Accounts with the `minter_admin` role can grant/revoke the `minter` role to other accounts - -This allows for creating complex organizational structures with chains of command and delegated authority. - -=== Role Enumeration - -The system tracks account-role pairs in storage with additional enumeration logic: - -* When a role is granted to an account, the pair is stored and added to enumeration storage -* When a role is revoked, the pair is removed from storage and enumeration -* If all accounts are removed from a role, the helper storage items become empty or 0 - -Roles exist only through their relationships with accounts, so a role with zero accounts is indistinguishable from a role that never existed. - -== Usage Example - -Here's a simple example of using the Access Control module: - -[source,rust] ----- -use soroban_sdk::{contract, contractimpl, symbol_short, Address, Env}; -use stellar_access::access_control::{self as access_control, AccessControl}; -use stellar_macros::{has_role, only_admin}; - -#[contract] -pub struct MyContract; - -#[contractimpl] -impl MyContract { - pub fn __constructor(e: &Env, admin: Address) { - // Set the contract admin - access_control::set_admin(e, &admin); - - // Create a "minter" role with admin as its admin - access_control::set_role_admin_no_auth(e, &symbol_short!("minter"), &symbol_short!("admin")); - } - - #[only_admin] - pub fn admin_restricted_function(e: &Env) -> Vec { - vec![&e, String::from_str(e, "seems sus")] - } - - // we want `require_auth()` provided by the macro, since there is no - // `require_auth()` in `Base::mint`. - #[only_role(caller, "minter")] - pub fn mint(e: &Env, caller: Address, to: Address, token_id: u32) { - Base::mint(e, &to, token_id) - } - - // allows either minter or burner role, does not enforce `require_auth` in the macro - #[has_any_role(caller, ["minter", "burner"])] - pub fn multi_role_action(e: &Env, caller: Address) -> String { - caller.require_auth(); - String::from_str(e, "multi_role_action_success") - } - - // allows either minter or burner role AND enforces `require_auth` in the macro - #[only_any_role(caller, ["minter", "burner"])] - pub fn multi_role_auth_action(e: &Env, caller: Address) -> String { - String::from_str(e, "multi_role_auth_action_success") - } -} ----- - -== Benefits and Trade-offs - -=== Benefits - -* Flexible role-based permission system -* Hierarchical role management -* Secure admin transfer process -* Admin is renounceable -* Easy integration with procedural macros - -=== Trade-offs - -* More complex than single-owner models like Ownable - -== See Also - -* xref:access/ownable.adoc[Ownable] -* xref:tokens/fungible/fungible.adoc[Fungible Token] -* xref:tokens/non-fungible/non-fungible.adoc[Non-Fungible Token] diff --git a/docs/modules/ROOT/pages/access/ownable.adoc b/docs/modules/ROOT/pages/access/ownable.adoc deleted file mode 100644 index 69b8b2fc..00000000 --- a/docs/modules/ROOT/pages/access/ownable.adoc +++ /dev/null @@ -1,102 +0,0 @@ -:source-highlighter: highlight.js -:highlightjs-languages: rust -:github-icon: pass:[] -= Ownable - -https://github.com/OpenZeppelin/stellar-contracts/tree/main/packages/access/src/ownable[Source Code] - -== Overview - -The Ownable module provides a simple access control mechanism where a contract has a single account (owner) that can be granted exclusive access to specific functions. This pattern is useful for contracts that need a straightforward authorization system with a single privileged account. - -== Key Concepts - -=== Ownership Management - -The system designates a single owner with exclusive access to functions marked with the `#[only_owner]` macro. The initial owner must be ideally set during contract initialization for the module to function properly. - -Like the Access Control module, ownership transfers are implemented as a two-step process to prevent accidental or malicious takeovers: - -1. The current owner *initiates* the transfer by specifying the new owner and an expiration time (`live_until_ledger`). -2. The designated new owner must *explicitly accept* the transfer to complete it. - -Until the transfer is accepted, the original owner retains full control and can override or cancel the transfer by initiating a new one or using a `live_until_ledger` of `0`. - -=== Ownership Renunciation - -The Ownable module allows the owner to permanently renounce ownership of the contract. This is a one-way operation that cannot be undone. After ownership is renounced, all functions marked with `#[only_owner]` become permanently inaccessible. - -This feature is useful for contracts that need to become fully decentralized after an initial setup phase. - -=== Procedural Macro - -The module includes a procedural macro to simplify owner authorization checks: - -==== @only_owner - -Ensures the caller is the owner before executing the function: - -[source,rust] ----- -#[only_owner] -pub fn restricted_function(e: &Env, other_param: u32) { - // Function body - only accessible to owner -} ----- - -This expands to code that retrieves the owner from storage and requires authorization before executing the function body. - -== Usage Example - -Here's a simple example of using the Ownable module: - -[source,rust] ----- -use soroban_sdk::{contract, contractimpl, Address, Env}; -use stellar_access::ownable::{self as ownable, Ownable}; -use stellar_macros::only_owner; - -#[contract] -pub struct MyContract; - -#[contractimpl] -impl MyContract { - pub fn __constructor(e: &Env, initial_owner: Address) { - // Set the contract owner - ownable::set_owner(e, &initial_owner); - } - - #[only_owner] - pub fn update_config(e: &Env, new_value: u32) { - // Only the owner can call this function - // Implementation... - } - - // This function is accessible to anyone - pub fn get_config(e: &Env) -> u32 { - // Implementation... - 42 - } -} ----- - -== Benefits and Trade-offs - -=== Benefits - -* Simple and straightforward ownership model -* Secure two-step ownership transfer process -* Option to permanently renounce ownership -* Easy integration with procedural macro -* Event emission for important actions - -=== Trade-offs - -* Limited to a single privileged account (compared to role-based systems) -* Once ownership is renounced, privileged functions become permanently inaccessible - -== See Also - -* xref:access/access-control.adoc[Access Control] -* xref:tokens/fungible/fungible.adoc[Fungible Token] -* xref:tokens/non-fungible/non-fungible.adoc[Non-Fungible Token] diff --git a/docs/modules/ROOT/pages/get-started.adoc b/docs/modules/ROOT/pages/get-started.adoc deleted file mode 100644 index e85618f3..00000000 --- a/docs/modules/ROOT/pages/get-started.adoc +++ /dev/null @@ -1,10 +0,0 @@ -= Get Started - -Not sure where to start? Use the interactive generator below to bootstrap your contract and find about the components offered in OpenZeppelin Smart Contracts Suite for Stellar. You can also access the code generator from https://wizard.openzeppelin.com/stellar[here]. - -++++ - - - -++++ - diff --git a/docs/modules/ROOT/pages/helpers/default-impl-macro.adoc b/docs/modules/ROOT/pages/helpers/default-impl-macro.adoc deleted file mode 100644 index 466546c3..00000000 --- a/docs/modules/ROOT/pages/helpers/default-impl-macro.adoc +++ /dev/null @@ -1,98 +0,0 @@ -:source-highlighter: highlight.js -:highlightjs-languages: rust -:github-icon: pass:[] -= Default Implementation Macro - -https://github.com/OpenZeppelin/stellar-contracts/tree/main/packages/contract-utils/src/default-impl-macro[Source Code] - -== Overview - -The `#[default_impl]` macro is a utility that simplifies the implementation of OpenZeppelin Stellar -contract traits by automatically generating default implementations for trait methods. This allows developers -to focus only on overriding the methods they need to customize, while the macro handles the rest. - -== Background - -When using Soroban's `#[contractimpl]` macro, all methods (including default implementations) must be explicitly -included in the implementation block for them to be accessible to the generated client. This is due to how -Rust macros work - they cannot access default implementations of trait methods that are not in the scope of the macro. - -The `#[default_impl]` macro solves this problem by automatically generating the missing default implementations -for OpenZeppelin Stellar traits. - -== Supported Traits - -The `#[default_impl]` macro supports the following OpenZeppelin Stellar traits: - -* `FungibleToken` -* `FungibleBurnable` -* `NonFungibleToken` -* `NonFungibleBurnable` -* `NonFungibleEnumerable` -* `AccessControl` -* `Ownable` - -The `#[default_impl]` macro intentionally does not support the following traits: - -* `FungibleAllowlist` -* `FungibleBlocklist` -* `NonFungibleRoyalties` - -This limitation is by design: authorization configurations require specific implementation tailored to -each project's security requirements. By requiring manual implementation of these traits, we ensure -developers carefully consider and explicitly define their authorization logic rather than relying on generic defaults. - -== Usage - -To use the `#[default_impl]` macro, place it above the `#[contractimpl]` macro when implementing one of the supported traits: - -[source,rust] ----- -#[default_impl] // IMPORTANT: place this above `#[contractimpl]` -#[contractimpl] -impl NonFungibleToken for MyContract { - type ContractType = Base; - - // Only override the methods you need to customize - // All other methods will be automatically implemented with their default behavior -} ----- - -== How It Works - -The `#[default_impl]` macro: - -. Identifies which trait is being implemented -. Determines which methods are explicitly defined by the user -. Uses the user defined methods to overwrite the default implementations -. Fills the rest of the methods (not defined by the user) with default implementations -. Adds any necessary imports for the trait - -This process ensures that all trait methods are available to the client generated by `#[contractimpl]`, while allowing developers to only write the code they need to customize. - -== Examples - -=== Fungible Token Example - -[source,rust] ----- -use soroban_sdk::{contract, contractimpl, Address, Env}; -use stellar_tokens::fungible::FungibleToken; -use stellar_macros::default_impl; - -#[contract] -pub struct MyToken; - -#[default_impl] -#[contractimpl] -impl FungibleToken for MyToken { - type ContractType = Base; - - // Only override methods that need custom behavior - fn transfer(e: &Env, from: Address, to: Address, amount: i128) { - // custom transfer logic here - } - - // All other FungibleToken methods will be automatically implemented -} ----- diff --git a/docs/modules/ROOT/pages/index.adoc b/docs/modules/ROOT/pages/index.adoc deleted file mode 100644 index 17495da6..00000000 --- a/docs/modules/ROOT/pages/index.adoc +++ /dev/null @@ -1,52 +0,0 @@ -:source-highlighter: highlight.js -:highlightjs-languages: bash - -= Stellar Smart Contracts Suite - -A comprehensive collection of secure, scalable smart contracts and utilities for the Stellar network, -supporting Fungible, Non-Fungible, and Multi-Token standards. - -== Tokens -Explore our implementations for token standards on Stellar Soroban: - -- **xref:tokens/fungible/fungible.adoc[Fungible Tokens]**: Digital assets representing a fixed or dynamic supply of identical units. -- **xref:tokens/non-fungible/non-fungible.adoc[Non-Fungible Tokens]**: Unique digital assets with verifiable ownership. -- **Multi-Token**: Hybrid tokens enabling both fungible and non-fungible token functionalities (work in progress). - -== Utilities -Discover our utility contracts for Stellar Soroban, applicable to all token standards mentioned above: - -- **xref:utils/pausable.adoc[Pausable]** -- **xref:utils/upgradeable.adoc[Upgrades and Migrations]** - -== Error Codes -In Stellar Soroban, each error variant is assigned an integer. To prevent duplication of error codes, -we use the following convention: - -* Fungible: `1XX` -* Non-Fungible: `2XX` -* Multi-Token: `3XX` - -Any future tokens will continue from `4XX`, `5XX`, and so on. - -* Utilities: `1XXX` -** Pausable: `10XX` -** Upgradeable: `11XX` -** Access: `12XX` -*** Role Transfer (internal common module for 2-step role transfer): `120X` -*** Access Control: `121X` -*** Ownable: `122X` -** Merkle Distributor: `13XX` - -Any future utilities will continue from `14XX`, `15XX`, and so on. - -== Important Notes -As a deliberate design choice, this library manages the TTL for temporary and persistent storage items. -To provide flexibility to the owner of the contract, this library deliberately does not manage the TTL for instance storage items. -It is the responsibility of the developer to manage the TTL for instance storage items. - -== Audits -You can find our audit reports https://github.com/OpenZeppelin/stellar-contracts/tree/main/audits[here]. - -== Get Started -Get started xref:get-started.adoc[here]. diff --git a/docs/modules/ROOT/pages/tokens/fungible/fungible.adoc b/docs/modules/ROOT/pages/tokens/fungible/fungible.adoc deleted file mode 100644 index 95e76ac1..00000000 --- a/docs/modules/ROOT/pages/tokens/fungible/fungible.adoc +++ /dev/null @@ -1,146 +0,0 @@ -:source-highlighter: highlight.js -:highlightjs-languages: rust -:github-icon: pass:[] -= Fungible Token - -https://github.com/OpenZeppelin/stellar-contracts/tree/main/packages/tokens/src/fungible[Source Code] - -Fungible tokens represent assets where each unit is identical and interchangeable, such as currencies, -commodities, or utility tokens. On Stellar, you can create fungible tokens where each token has the -same value and properties, with balances and ownership tracked through Soroban smart contracts. - -== Overview - -The https://github.com/OpenZeppelin/stellar-contracts/tree/main/packages/tokens/src/fungible[fungible] -module provides three different Fungible Token variants that differ in how certain features like -token transfers and approvals are handled: - - -The module provides several implementation options to suit different use cases: - -1. *Base implementation* (`FungibleToken` with `Base` contract type): Suitable for most standard token use cases. -2. *AllowList extension* (`FungibleToken` with `AllowList` contract type): For tokens that require an allowlist mechanism to control who can transfer tokens. -3. *BlockList extension* (`FungibleToken` with `BlockList` contract type): For tokens that need to block specific addresses from transferring tokens. - -These implementations share core functionality and a common interface, exposing identical contract functions as entry-points. However, the extensions provide specialized behavior by overriding certain functions to implement their specific requirements. - -== Usage - -We'll create a simple token for a game's in-game currency. Players can earn tokens by completing tasks, -and they can spend tokens on in-game items. The contract owner can mint new tokens as needed, -and players can transfer tokens between accounts. - -Here's what a basic fungible token contract might look like: - -[source,rust] ----- -use soroban_sdk::{contract, contractimpl, Address, Env, String}; -use stellar_tokens::fungible::{burnable::FungibleBurnable, Base, ContractOverrides, FungibleToken}; -use stellar_access::ownable::{self as ownable, Ownable}; -use stellar_macros::{default_impl, only_owner}; - -#[contract] -pub struct GameCurrency; - -#[contractimpl] -impl GameCurrency { - pub fn __constructor(e: &Env, initial_owner: Address) { - // Set token metadata - Base::set_metadata( - e, - 8, // 8 decimals - String::from_str(e, "Game Currency"), - String::from_str(e, "GCUR"), - ); - - // Set the contract owner - ownable::set_owner(e, &initial_owner); - } - - #[only_owner] - pub fn mint_tokens(e: &Env, to: Address, amount: i128) { - // Mint tokens to the recipient - Base::mint(e, &to, amount); - } -} - -#[default_impl] -#[contractimpl] -impl FungibleToken for GameCurrency { - type ContractType = Base; -} - -#[default_impl] -#[contractimpl] -impl FungibleBurnable for GameCurrency {} ----- - -== Extensions - -The following optional extensions are provided: - -=== - Burnable -https://github.com/OpenZeppelin/stellar-contracts/tree/main/packages/tokens/src/fungible/extensions/burnable[Source Code] - -The `FungibleBurnable` trait extends the `FungibleToken` trait to provide the capability to burn tokens. -To fully comply with the SEP-41 specification, a contract must implement both the `FungibleToken` -and `FungibleBurnable` traits. - -=== - Capped -https://github.com/OpenZeppelin/stellar-contracts/tree/main/packages/tokens/src/fungible/extensions/capped[Source Code] - -Unlike other extensions, the capped extension does not expose a separate trait. Instead, -it offers helper functions designed to assist in implementing the mint function, enforcing a supply cap. - -=== - AllowList -https://github.com/OpenZeppelin/stellar-contracts/tree/main/packages/tokens/src/fungible/extensions/allowlist[Source Code] - -The `FungibleAllowList` trait extends the `FungibleToken` trait to provide an allowlist mechanism that -can be managed by an authorized account. This extension ensures that only allowed accounts can -transfer/receive tokens or approve token transfers. - -=== - BlockList -https://github.com/OpenZeppelin/stellar-contracts/tree/main/packages/tokens/src/fungible/extensions/blocklist[Source Code] - -The `FungibleBlockList` trait extends the `FungibleToken` trait to provide a blocklist mechanism that -can be managed by an authorized account. This extension ensures that blocked accounts cannot transfer/receive -tokens, or approve token transfers. - -=== TokenInterface Macro - -For contracts that implement both `FungibleToken` and `FungibleBurnable` and also need to implement -`soroban_sdk::token::TokenInterface`, we provide the `impl_token_interface!` macro. This macro automatically -generates the required boilerplate, simplifying the implementation process. - -== Utility Modules - -The package includes utility modules to help with common token implementation patterns: - -=== - SAC Admin Generic -https://github.com/OpenZeppelin/stellar-contracts/tree/main/packages/tokens/src/fungible/utils/sac_admin_generic[Source Code] - -Provides generic admin functionality similar to the Stellar Asset Contract (SAC). This approach leverages the `__check_auth` function to handle authentication and authorization logic while maintaining a unified interface. - -For detailed documentation, see xref:tokens/fungible/sac-admin-generic.adoc[SAC Admin Generic]. - -=== - SAC Admin Wrapper -https://github.com/OpenZeppelin/stellar-contracts/tree/main/packages/tokens/src/fungible/utils/sac_admin_wrapper[Source Code] - -Provides a wrapper around the SAC admin functionality for easier integration. This approach defines specific entry points for each admin function and forwards calls to the corresponding SAC functions. - -For detailed documentation, see xref:tokens/fungible/sac-admin-wrapper.adoc[SAC Admin Wrapper]. - -== Compatibility and Compliance - -The module is designed to ensure full compatibility with SEP-0041. It also closely mirrors the Ethereum ERC-20 -standard, facilitating cross-ecosystem familiarity and ease of use. - -To comply with the SEP-41 specification, a contract must implement both the `FungibleToken` and -`FungibleBurnable` traits. These traits together provide all the necessary methods to conform to -`soroban_sdk::token::TokenInterface`. - -== TTL Management - -The library handles the TTL (Time-To-Live) of only `temporary` and `persistent` storage entries declared -by the library. The `instance` TTL management is left to the implementor due to flexibility. The library -exposes default values for extending the TTL: `INSTANCE_TTL_THRESHOLD` and `INSTANCE_EXTEND_AMOUNT`. diff --git a/docs/modules/ROOT/pages/tokens/fungible/sac-admin-generic.adoc b/docs/modules/ROOT/pages/tokens/fungible/sac-admin-generic.adoc deleted file mode 100644 index 51c21109..00000000 --- a/docs/modules/ROOT/pages/tokens/fungible/sac-admin-generic.adoc +++ /dev/null @@ -1,200 +0,0 @@ -:source-highlighter: highlight.js -:highlightjs-languages: rust -:github-icon: pass:[] -= SAC Admin Generic - -https://github.com/OpenZeppelin/stellar-contracts/tree/main/packages/tokens/src/fungible/utils/sac_admin_generic[Source Code] - -== Overview - -The Stellar Asset Contract (SAC) Admin Generic module provides a way to implement custom administrative -functionality for Stellar Asset Contracts (SACs) using the generic approach. This approach leverages the -`__check_auth` function to handle authentication and authorization logic while maintaining a unified -interface for both user-facing and admin functions. - -== Key Concepts - -When a classic Stellar asset is ported to Soroban, it is represented by a SAC - a smart contract that provides -both user-facing and administrative functions for asset management. SACs expose standard functions for handling -fungible tokens, such as `transfer`, `approve`, `burn`, etc. Additionally, they include administrative functions -(`mint`, `clawback`, `set_admin`, `set_authorized`) that are initially restricted to the issuer (a G-account). - -The `set_admin` function enables transferring administrative control to a custom contract, allowing for more -complex authorization logic. This flexibility opens up possibilities for implementing custom rules, such as -role-based access control, two-step admin transfers, mint rate limits, and upgradeability. - -== Generic Approach - -The Generic approach to SAC Admin implementation: - -* Leverages the `__check_auth` function to handle authentication and authorization logic -* Maintains a unified interface for both user-facing and admin functions -* Allows for injecting any custom authorization logic -* Requires a more sophisticated authorization mechanism - -=== Example Implementation - -Here's a simplified example of a SAC Admin Generic contract: - -[source,rust] ----- -#[contracterror] -#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] -#[repr(u32)] -pub enum SACAdminGenericError { - Unauthorized = 1, - InvalidContext = 2, - MintingLimitExceeded = 3, -} - -#[contracttype] -#[derive(Clone)] -pub struct Signature { - pub public_key: BytesN<32>, - pub signature: BytesN<64>, -} - -#[contracttype] -pub enum SacDataKey { - Chief, - Operator(BytesN<32>), // -> true/false - MintingLimit(BytesN<32>), // -> (max_limit, curr) -} - -#[contract] -pub struct SacAdminExampleContract; - -#[contractimpl] -impl SacAdminExampleContract { - pub fn __constructor(e: Env, sac: Address, chief: BytesN<32>, operator: BytesN<32>) { - set_sac_address(&e, &sac); - e.storage().instance().set(&SacDataKey::Chief, &chief); - e.storage().instance().set(&SacDataKey::Operator(operator.clone()), &true); - e.storage() - .instance() - .set(&SacDataKey::MintingLimit(operator), &(1_000_000_000i128, 0i128)); - } - - pub fn get_sac_address(e: &Env) -> Address { - get_sac_address(e) - } -} ----- - -=== Custom Authorization Logic - -The key feature of the Generic approach is the ability to implement custom authorization logic in the `__check_auth` -function: - -[source,rust] ----- -use soroban_sdk::{ - auth::{Context, CustomAccountInterface}, - contract, contracterror, contractimpl, contracttype, - crypto::Hash, - Address, BytesN, Env, IntoVal, Val, Vec, -}; - -#[contractimpl] -impl CustomAccountInterface for SacAdminExampleContract { - type Error = SACAdminGenericError; - type Signature = Signature; - - fn __check_auth( - e: Env, - payload: Hash<32>, - signature: Self::Signature, - auth_context: Vec, - ) -> Result<(), SACAdminGenericError> { - // authenticate - e.crypto().ed25519_verify( - &signature.public_key, - &payload.clone().into(), - &signature.signature, - ); - let caller = signature.public_key.clone(); - - // extract from context and check required permissions for every function - for ctx in auth_context.iter() { - let context = match ctx { - Context::Contract(c) => c, - _ => return Err(SACAdminGenericError::InvalidContext), - }; - - match extract_sac_contract_context(&e, &context) { - SacFn::Mint(amount) => { - // ensure caller has required permissions - ensure_caller_operator(&e, &SacDataKey::Operator(caller.clone()))?; - // ensure operator has minting limit - ensure_minting_limit(&e, &caller, amount)?; - } - SacFn::Clawback(_amount) => { - // ensure caller has required permissions - ensure_caller_operator(&e, &SacDataKey::Operator(caller.clone()))?; - } - SacFn::SetAuthorized(_) => { - // ensure caller has required permissions - ensure_caller_operator(&e, &SacDataKey::Operator(caller.clone()))?; - } - SacFn::SetAdmin => { - // ensure caller has required permissions - ensure_caller_chief(&e, &caller, &SacDataKey::Chief)?; - } - SacFn::Unknown => { - // ensure only chief can call other functions - ensure_caller_chief(&e, &caller, &SacDataKey::Chief)? - } - } - } - - Ok(()) - } -} - -// Helper functions -fn ensure_caller_chief>( - e: &Env, - caller: &BytesN<32>, - key: &K, -) -> Result<(), SACAdminGenericError> { - let operator: BytesN<32> = e.storage().instance().get(key).expect("chief or operator not set"); - if *caller != operator { - return Err(SACAdminGenericError::Unauthorized); - } - Ok(()) -} - -fn ensure_caller_operator>( - e: &Env, - key: &K, -) -> Result<(), SACAdminGenericError> { - match e.storage().instance().get::<_, bool>(key) { - Some(is_op) if is_op => Ok(()), - _ => Err(SACAdminGenericError::Unauthorized), - } -} ----- - -== Benefits and Trade-offs - -=== Benefits - -* Maintains a unified interface for both user-facing and admin functions -* Allows for complex authorization logic -* Provides flexibility in implementing custom rules - -=== Trade-offs - -* Requires a more sophisticated authorization mechanism -* More complex to implement compared to the wrapper approach -* Requires understanding of the Soroban authorization system - -== Full Example - -A complete example implementation can be found in the -https://github.com/OpenZeppelin/stellar-contracts/tree/main/examples/sac-admin-generic[sac-admin-generic example]. - -== See Also - -* xref:tokens/fungible/sac-admin-wrapper.adoc[SAC Admin Wrapper] -* xref:tokens/fungible/fungible.adoc[Fungible Token] diff --git a/docs/modules/ROOT/pages/tokens/fungible/sac-admin-wrapper.adoc b/docs/modules/ROOT/pages/tokens/fungible/sac-admin-wrapper.adoc deleted file mode 100644 index f82fb628..00000000 --- a/docs/modules/ROOT/pages/tokens/fungible/sac-admin-wrapper.adoc +++ /dev/null @@ -1,125 +0,0 @@ -:source-highlighter: highlight.js -:highlightjs-languages: rust -:github-icon: pass:[] -= SAC Admin Wrapper - -https://github.com/OpenZeppelin/stellar-contracts/tree/main/packages/tokens/src/fungible/utils/sac_admin_wrapper[Source Code] - -== Overview - -The Stellar Asset Contract (SAC) Admin Wrapper module provides a way to implement custom administrative functionality for Stellar Asset Contracts (SACs) using the wrapper approach. This approach defines specific entry points for each admin function and forwards calls to the corresponding SAC functions, providing a straightforward and modular design. - -== Key Concepts - -When a classic Stellar asset is ported to Soroban, it is represented by a SAC - a smart contract that provides both user-facing and administrative functions for asset management. SACs expose standard functions for handling fungible tokens, such as `transfer`, `approve`, `burn`, etc. Additionally, they include administrative functions (`mint`, `clawback`, `set_admin`, `set_authorized`) that are initially restricted to the issuer (a G-account). - -The `set_admin` function enables transferring administrative control to a custom contract, allowing for more complex authorization logic. This flexibility opens up possibilities for implementing custom rules, such as role-based access control, two-step admin transfers, mint rate limits, and upgradeability. - -== Wrapper Approach - -The Wrapper approach to SAC Admin implementation: - -* Acts as a middleware, defining specific entry points for each admin function -* Forwards calls to the corresponding SAC functions -* Applies custom logic before forwarding the call -* Provides a straightforward and modular design -* Separates user-facing and admin interfaces - -=== SACAdminWrapper Trait - -The `SACAdminWrapper` trait defines the interface for the wrapper approach: - -[source,rust] ----- -pub trait SACAdminWrapper { - fn set_admin(e: Env, new_admin: Address, operator: Address); - fn set_authorized(e: Env, id: Address, authorize: bool, operator: Address); - fn mint(e: Env, to: Address, amount: i128, operator: Address); - fn clawback(e: Env, from: Address, amount: i128, operator: Address); -} ----- - -=== Example Implementation - -Here's a simplified example of a SAC Admin Wrapper contract using the OpenZeppelin access control library: - -[source,rust] ----- -#[contract] -pub struct ExampleContract; - -#[contractimpl] -impl ExampleContract { - pub fn __constructor( - e: &Env, - default_admin: Address, - manager1: Address, - manager2: Address, - sac: Address, - ) { - access_control::set_admin(e, &default_admin); - - // create a role "manager" and grant it to `manager1` - access_control::grant_role_no_auth(e, &default_admin, &manager1, &symbol_short!("manager")); - - // grant it to `manager2` - access_control::grant_role_no_auth(e, &default_admin, &manager2, &symbol_short!("manager")); - - fungible::sac_admin_wrapper::set_sac_address(e, &sac); - } -} - -#[contractimpl] -impl SACAdminWrapper for ExampleContract { - #[only_admin] - fn set_admin(e: Env, new_admin: Address, _operator: Address) { - fungible::sac_admin_wrapper::set_admin(&e, &new_admin); - } - - #[only_role(operator, "manager")] - fn set_authorized(e: Env, id: Address, authorize: bool, operator: Address) { - fungible::sac_admin_wrapper::set_authorized(&e, &id, authorize); - } - - #[only_role(operator, "manager")] - fn mint(e: Env, to: Address, amount: i128, operator: Address) { - fungible::sac_admin_wrapper::mint(&e, &to, amount); - } - - #[only_role(operator, "manager")] - fn clawback(e: Env, from: Address, amount: i128, operator: Address) { - fungible::sac_admin_wrapper::clawback(&e, &from, amount); - } -} ----- - -=== Integration with Access Control - -The wrapper approach works particularly well with the OpenZeppelin access control library, allowing for role-based access control to be applied to each admin function: - -* `#[only_admin]`: Restricts the function to be called only by the admin -* `#[only_role(operator, "manager")]`: Restricts the function to be called only by addresses with the "manager" role - -== Benefits and Trade-offs - -=== Benefits - -* Simpler to implement compared to the generic approach -* More flexible in terms of function-specific authorization -* Works well with role-based access control -* Clear separation of concerns - -=== Trade-offs - -* Requires additional entry points for each admin function -* Splits user-facing and admin interfaces -* May require more code for complex authorization scenarios - -== Full Example - -A complete example implementation can be found in the https://github.com/OpenZeppelin/stellar-contracts/tree/main/examples/sac-admin-wrapper[sac-admin-wrapper example]. - -== See Also - -* xref:tokens/fungible/sac-admin-generic.adoc[SAC Admin Generic] -* xref:tokens/fungible/fungible.adoc[Fungible Token] diff --git a/docs/modules/ROOT/pages/tokens/non-fungible/nft-consecutive.adoc b/docs/modules/ROOT/pages/tokens/non-fungible/nft-consecutive.adoc deleted file mode 100644 index 77f0d043..00000000 --- a/docs/modules/ROOT/pages/tokens/non-fungible/nft-consecutive.adoc +++ /dev/null @@ -1,61 +0,0 @@ -:source-highlighter: highlight.js -:highlightjs-languages: rust -:github-icon: pass:[] -= Non-Fungible Consecutive - -https://github.com/OpenZeppelin/stellar-contracts/tree/main/packages/tokens/src/non-fungible/extensions/consecutive[Source Code] - -Consecutive extension for xref:tokens/non-fungible/non-fungible.adoc[Non-Fungible Token] is useful -for efficiently minting multiple tokens in a single transaction. This can significantly -reduce costs and improve performance when creating a large number of tokens at once. - -== Usage - -We'll continue with the xref:tokens/non-fungible/non-fungible.adoc#usage[example] from *Non-Fungible Token* -and modify the contract so that now batches of tokens can be minted with each call -to `award_items`. Please note any account can call `award_items` and we might want to -implement access control to restrict who can mint. - - -[source,rust] ----- -use soroban_sdk::{contract, contractimpl, Address, Env, String}; -use stellar_macros::default_impl; -use stellar_tokens::non_fungible::{ - consecutive::{Consecutive, NonFungibleConsecutive}, - Base, ContractOverrides, NonFungibleToken, -}; - -#[contract] -pub struct GameItem; - -#[contractimpl] -impl GameItem { - pub fn __constructor(e: &Env) { - Base::set_metadata( - e, - String::from_str(e, "www.mygame.com"), - String::from_str(e, "My Game Items Collection"), - String::from_str(e, "MGMC"), - ); - } - - pub fn award_items(e: &Env, to: Address, amount: u32) -> u32 { - // access control might be needed - Consecutive::batch_mint(e, &to, amount) - } - - pub fn burn(e: &Env, from: Address, token_id: u32) { - Consecutive::burn(e, &from, token_id); - } -} - -#[default_impl] -#[contractimpl] -impl NonFungibleToken for GameItem { - type ContractType = Consecutive; -} - -// no entry-point functions required, marker impl -impl NonFungibleConsecutive for GameItem {} ----- diff --git a/docs/modules/ROOT/pages/tokens/non-fungible/nft-enumerable.adoc b/docs/modules/ROOT/pages/tokens/non-fungible/nft-enumerable.adoc deleted file mode 100644 index 28fe9ee0..00000000 --- a/docs/modules/ROOT/pages/tokens/non-fungible/nft-enumerable.adoc +++ /dev/null @@ -1,69 +0,0 @@ -:source-highlighter: highlight.js -:highlightjs-languages: rust -:github-icon: pass:[] -= Non-Fungible Enumerable - -https://github.com/OpenZeppelin/stellar-contracts/tree/main/packages/tokens/src/non-fungible/extensions/enumerable[Source Code] - -Enumerable extension for xref:tokens/non-fungible/non-fungible.adoc[Non-Fungible Token] allows for enumeration -of all the token IDs in the contract as well as all the token IDs owned by each account. This is -useful for applications that need to list or iterate over tokens, such as marketplaces or wallets. - -== Usage - -We'll build on the xref:tokens/non-fungible/non-fungible.adoc#usage[example] from *Non-Fungible Token* -and modify the contract so that all tokens an address own can be listed. Please note any account -can call `award_item` and we might want to implement access control to restrict who can mint. - -[source,rust] ----- -use soroban_sdk::{contract, contractimpl, Address, Env, String}; -use stellar_macros::default_impl; -use stellar_tokens::non_fungible::{ - enumerable::{Enumerable, NonFungibleEnumerable}, - Base, ContractOverrides, NonFungibleToken, -}; - -#[contract] -pub struct GameItem; - -#[contractimpl] -impl GameItem { - pub fn __constructor(e: &Env) { - Base::set_metadata( - e, - String::from_str(e, "www.mygame.com"), - String::from_str(e, "My Game Items Collection"), - String::from_str(e, "MGMC"), - ); - } - - pub fn award_item(e: &Env, to: Address) -> u32 { - // access control might be needed - Enumerable::sequential_mint(e, &to) - } - - pub fn burn(e: &Env, from: Address, token_id: u32) { - Enumerable::sequential_burn(e, &from, token_id); - } -} - -#[default_impl] -#[contractimpl] -impl NonFungibleToken for GameItem { - type ContractType = Enumerable; -} - -#[default_impl] -#[contractimpl] -impl NonFungibleEnumerable for GameItem {} ----- - -The extension exposes additionally the following entry-point functions, automatically implemented by `#[default_impl]`: - -[source,rust] ----- -fn total_supply(e: &Env) -> u32; -fn get_owner_token_id(e: &Env, owner: Address, index: u32) -> u32; -fn get_token_id(e: &Env, index: u32) -> u32; ----- diff --git a/docs/modules/ROOT/pages/tokens/non-fungible/non-fungible.adoc b/docs/modules/ROOT/pages/tokens/non-fungible/non-fungible.adoc deleted file mode 100644 index 026d73b6..00000000 --- a/docs/modules/ROOT/pages/tokens/non-fungible/non-fungible.adoc +++ /dev/null @@ -1,110 +0,0 @@ -:source-highlighter: highlight.js -:highlightjs-languages: rust -:github-icon: pass:[] -= Non-Fungible Token - -https://github.com/OpenZeppelin/stellar-contracts/tree/main/packages/tokens/src/non-fungible[Source Code] - -In the world of digital assets, not all tokens are alike. This becomes important in situations -like *real estate*, *voting rights*, or *collectibles*, where some items are valued more than -others due to their usefulness, rarity, etc. -On Stellar, you can create non-fungible tokens (NFTs), where each token is unique and -represents something distinct, with ownership tracked through Soroban smart contracts. - -== Overview - -The https://github.com/OpenZeppelin/stellar-contracts/tree/main/packages/tokens/src/non-fungible[non-fungible] module -provides three different NFT variants that differ in how certain features like ownership tracking, -token creation and destruction are handled: - -1. *Base*: Contract variant that implements the base logic for the NonFungibleToken interface. Suitable for most use cases. -2. *Consecutive*: Contract variant for optimized minting of batches of tokens. Builds on top of the base variant, and overrides the necessary functions from the `Base` variant. -3. *Enumerable*: Contract variant that allows enumerating the tokens on-chain. Builds on top of the base variant, and overrides the necessary functions from the `Base` variant. - -These three variants share core functionality and a common interface, exposing identical contract functions as -entry-points. However, composing custom flows must be handled with extra caution. That is required because of the -incompatible nature between the business logic of the different NFT variants or the need to wrap the base -functionality with additional logic. - -== Usage - -We'll use an NFT to track game items, each having their own unique attributes. Whenever one is to be -awarded to a player, it will be minted and sent to them. Players are free to keep or burn their token or -trade it with other people as they see fit. Please note any account can call `award_item` and we might -want to implement access control to restrict who can mint. - -Here's what a contract for tokenized items might look like: - -[source,rust] ----- -use soroban_sdk::{contract, contractimpl, Address, Env, String}; -use stellar_macros::default_impl; -use stellar_tokens::non_fungible::{ - burnable::NonFungibleBurnable, - Base, ContractOverrides, NonFungibleToken, -}; - -#[contract] -pub struct GameItem; - -#[contractimpl] -impl GameItem { - pub fn __constructor(e: &Env) { - Base::set_metadata( - e, - String::from_str(e, "www.mygame.com"), - String::from_str(e, "My Game Items Collection"), - String::from_str(e, "MGMC"), - ); - } - - pub fn award_item(e: &Env, to: Address) -> u32 { - // access control might be needed - Base::sequential_mint(e, &to) - } -} - -#[default_impl] -#[contractimpl] -impl NonFungibleToken for GameItem { - type ContractType = Base; -} - -#[default_impl] -#[contractimpl] -impl NonFungibleBurnable for GameItem {} ----- - -== Extensions - -The following optional extensions are provided to enhance capabilities: - -=== - Burnable -https://github.com/OpenZeppelin/stellar-contracts/tree/main/packages/tokens/src/non-fungible/extensions/burnable[Source Code] - -The `NonFungibleBurnable` trait extends the `NonFungibleToken` trait to provide the capability to burn tokens. - -=== - Consecutive -https://github.com/OpenZeppelin/stellar-contracts/tree/main/packages/tokens/src/non-fungible/extensions/consecutive[Source Code] - -The `NonFungibleConsecutive` extension is optimized for batch minting of tokens with consecutive IDs. This approach drastically reduces storage writes during minting by storing ownership only at boundaries and inferring ownership for other tokens. See xref:tokens/non-fungible/nft-consecutive.adoc[Non-Fungible Consecutive] for detailed documentation. - -This extension is build around the contract variant `Consecutive`. Here is an example usage: - -* xref:tokens/non-fungible/nft-consecutive.adoc[Non-Fungible Consecutive] - -=== - Enumerable -https://github.com/OpenZeppelin/stellar-contracts/tree/main/packages/tokens/src/non-fungible/extensions/enumerable[Source Code] - -The `NonFungibleEnumerable` extension enables on-chain enumeration of tokens owned by an address. See xref:tokens/non-fungible/nft-enumerable.adoc[Non-Fungible Enumerable] for detailed documentation. - -This extension is build around the contract variant `Enumerable`. Here is an example usage: - -* xref:tokens/non-fungible/nft-enumerable.adoc[Non-Fungible Enumerable] - -=== - Royalties -https://github.com/OpenZeppelin/stellar-contracts/tree/main/packages/tokens/src/non-fungible/extensions/royalties[Source Code] - -The `NonFungibleRoyalties` trait extends the `NonFungibleToken` trait to provide royalty information for tokens, similar to ERC-2981 standard. This allows marketplaces to query royalty information and pay appropriate fees to creators. - -Note: The royalties extension allows both collection-wide default royalties and per-token royalty settings. diff --git a/docs/modules/ROOT/pages/utils/crypto.adoc b/docs/modules/ROOT/pages/utils/crypto.adoc deleted file mode 100644 index 5b0c0514..00000000 --- a/docs/modules/ROOT/pages/utils/crypto.adoc +++ /dev/null @@ -1,209 +0,0 @@ -:source-highlighter: highlight.js -:highlightjs-languages: rust -:github-icon: pass:[] -= Cryptography Utilities - -https://github.com/OpenZeppelin/stellar-contracts/tree/main/packages/contract-utils/src/crypto[Crypto Source Code] | -https://github.com/OpenZeppelin/stellar-contracts/tree/main/packages/contract-utils/src/merkle-distributor[Merkle Distributor Source Code] - -== Overview - -The Cryptography Utilities provide a set of cryptographic tools for Soroban smart contracts, -including hash functions, Merkle tree verification, and Merkle-based distribution systems. -These utilities enable secure data verification and efficient token distribution mechanisms. -The Cryptography Utilities consist of two main packages: - -* Crypto: A set of cryptographic primitives and utilities for Soroban contracts. -* Merkle Distributor: A system for distributing tokens or other assets using Merkle proofs for verification. - -== Crypto Package - -The crypto package provides fundamental cryptographic primitives and utilities for Soroban contracts, -with a focus on hashing and Merkle tree operations. - -=== Key Components - -==== Hashers - -Provides a generic `Hasher` trait and implementations for common hash functions: - -* `Sha256`: Implementation of the SHA-256 hash function -* `Keccak256`: Implementation of the Keccak-256 hash function (used in Ethereum) - -Each hasher follows the same interface: - -[source,rust] ----- -pub trait Hasher { - type Output; - - fn new(e: &Env) -> Self; - fn update(&mut self, input: Bytes); - fn finalize(self) -> Self::Output; -} ----- - -==== Hashable - -The `Hashable` trait allows types to be hashed with any `Hasher` implementation: - -[source,rust] ----- -pub trait Hashable { - fn hash(&self, hasher: &mut H); -} ----- - -Built-in implementations are provided for `BytesN<32>` and `Bytes`. - -==== Utility Functions - -* `hash_pair`: Hashes two values together -* `commutative_hash_pair`: Hashes two values in a deterministic order (important for Merkle trees) - -==== Merkle Tree Verification - -The `Verifier` struct provides functionality to verify Merkle proofs: - -[source,rust] ----- -impl Verifier -where - H: Hasher, -{ - pub fn verify(e: &Env, proof: Vec, root: Bytes32, leaf: Bytes32) -> bool { - // Implementation verifies that the leaf is part of the tree defined by root - } -} ----- - -=== Usage Examples - -==== Hashing Data - -[source,rust] ----- -use soroban_sdk::{Bytes, Env}; -use stellar_contract_utils::crypto::keccak::Keccak256; -use stellar_contract_utils::crypto::hasher::Hasher; - -// Hash some data with Keccak256 -let e = Env::default(); -let data = Bytes::from_slice(&e, "Hello, world!".as_bytes()); - -let mut hasher = Keccak256::new(&e); -hasher.update(data); -let hash = hasher.finalize(); ----- - -==== Verifying a Merkle Proof - -[source,rust] ----- -use soroban_sdk::{BytesN, Env, Vec}; -use stellar_crypto::keccak::Keccak256; -use stellar_crypto::merkle::Verifier; - -// Verify that a leaf is part of a Merkle tree -let e = Env::default(); -let root = /* merkle root as BytesN<32> */; -let leaf = /* leaf to verify as BytesN<32> */; -let proof = /* proof as Vec> */; - -let is_valid = Verifier::::verify(&e, proof, root, leaf); ----- - -== Merkle Distributor - -The Merkle Distributor package builds on the crypto package to provide a system for distributing tokens or -other assets using Merkle proofs for verification. - -=== Key Concepts - -==== IndexableLeaf - -The `IndexableLeaf` trait defines the structure for nodes in the Merkle tree: - -[source,rust] ----- -pub trait IndexableLeaf { - fn index(&self) -> u32; -} ----- - -Each node must include a unique index that identifies its position in the Merkle tree. - -==== MerkleDistributor - -The `MerkleDistributor` struct provides functionality for: - -* Setting a Merkle root -* Checking if an index has been claimed -* Verifying proofs and marking indices as claimed - -=== Usage Example - -[source,rust] ----- -use soroban_sdk::{contract, contractimpl, contracttype, Address, BytesN, Env, Vec}; -use stellar_contract_utils::crypto::keccak::Keccak256; -use stellar_contract_utils::merkle_distributor::{IndexableLeaf, MerkleDistributor}; - -// Define a leaf node structure -#[contracttype] -struct LeafData { - pub index: u32, - pub address: Address, - pub amount: i128, -} - -// Implement IndexableLeaf for the leaf structure -impl IndexableLeaf for LeafData { - fn index(&self) -> u32 { - self.index - } -} - -#[contract] -pub struct TokenDistributor; - -#[contractimpl] -impl TokenDistributor { - // Initialize the distributor with a Merkle root - pub fn initialize(e: &Env, root: BytesN<32>) { - MerkleDistributor::::set_root(e, root); - } - - // Claim tokens by providing a proof - pub fn claim(e: &Env, leaf: LeafData, proof: Vec>) { - // Verify the proof and mark as claimed - MerkleDistributor::::verify_and_set_claimed(e, leaf.clone(), proof); - - // Transfer tokens or perform other actions based on leaf data - // ... - } - - // Check if an index has been claimed - pub fn is_claimed(e: &Env, index: u32) -> bool { - MerkleDistributor::::is_claimed(e, index) - } -} ----- - -== Use Cases - -=== Token Airdrops - -Efficiently distribute tokens to a large number of recipients without requiring individual transactions for each recipient. - -=== NFT Distributions - -Distribute NFTs to a whitelist of addresses, with each address potentially receiving different NFTs. - -=== Off-chain Allowlists - -Maintain a list of eligible addresses off-chain and allow them to claim tokens or other assets on-chain. - -=== Snapshot-based Voting - -Create a snapshot of token holders at a specific block and allow them to vote based on their holdings. diff --git a/docs/modules/ROOT/pages/utils/pausable.adoc b/docs/modules/ROOT/pages/utils/pausable.adoc deleted file mode 100644 index 8d8c04f8..00000000 --- a/docs/modules/ROOT/pages/utils/pausable.adoc +++ /dev/null @@ -1,35 +0,0 @@ -:source-highlighter: highlight.js -:highlightjs-languages: rust -:github-icon: pass:[] -= Pausable - -https://github.com/OpenZeppelin/stellar-contracts/tree/main/packages/contract-utils/src/pausable[Source Code] - -== Purpose - -Allows contracts to be paused and unpaused by authorized accounts. - -This utility contract can be used with any token standard (fungible, non-fungible, multi-token). - -== Design - -To make it easier to spot when inspecting the code, we turned this simple functionality into a macro that can annotate your smart contract functions. - - -An example: -```rust -#[when_paused] -pub fn emergency_reset(e: &Env) { - e.storage().instance().set(&DataKey::Counter, &0); -} -``` - -Which will expand into the below code: - -```rust -pub fn emergency_reset(e: &Env) { - when_paused(e); - - e.storage().instance().set(&DataKey::Counter, &0); -} -``` diff --git a/docs/modules/ROOT/pages/utils/upgradeable.adoc b/docs/modules/ROOT/pages/utils/upgradeable.adoc deleted file mode 100644 index ee813ad0..00000000 --- a/docs/modules/ROOT/pages/utils/upgradeable.adoc +++ /dev/null @@ -1,188 +0,0 @@ -:source-highlighter: highlight.js -:highlightjs-languages: rust -:github-icon: pass:[] -= Upgrades and Migrations - -https://github.com/OpenZeppelin/stellar-contracts/tree/main/packages/contract-utils/src/upgradeable[Source code] - -Soroban contracts are mutable by default. Mutability in the context of Stellar Soroban refers to the ability of a smart -contract to modify its WASM bytecode, thereby altering its function interface, execution logic, or metadata. - -Soroban provides a built-in, protocol-level defined mechanism for contract upgrades, allowing contracts to upgrade -themselves if they are explicitly designed to do so. One of the advantages of it is the flexibility it offers to -contract developers who can choose to make the contract immutable by simply not provisioning upgradability mechanics. On -the other hand, providing upgradability on a protocol level significantly reduces the risk surface, compared to other -smart contract platforms, which lack native support for upgradability. - -While Soroban’s built-in upgradability eliminates many of the challenges, related to managing smart contract upgrades -and migrations, certain caveats must still be considered. - -== Overview - -The https://github.com/OpenZeppelin/stellar-contracts/tree/main/packages/contract-utils/src/upgradeable[upgradeable] module -provides a lightweight upgradeability framework with additional support for structured and safe migrations. - -It consists of two main components: - -1. **xref:utils/upgradeable.adoc#upgrade_only[`Upgradeable`]** for cases where only the WASM binary needs to be updated. - -2. **xref:utils/upgradeable.adoc#upgrade_and_migrate[`UpgradeableMigratable`]** for more advanced scenarios where, in addition to the WASM binary, specific storage entries -must be modified (migrated) during the upgrade process. - -The recommended way to use this module is through the `\#[derive(Upgradeable)]` and `#[derive(UpgradeableMigratable)]` -macros. - -They handle the implementation of the necessary functions, allowing developers to focus solely on managing authorizations -and access control. These derive macros also leverage the crate version from the contract’s `Cargo.toml` and set it as -the binary version in the WASM metadata, aligning with the guidelines outlined in -https://github.com/stellar/stellar-protocol/blob/master/ecosystem%2Fsep-0049.md[SEP-49]. - -[WARNING] -==== -While the framework structures the upgrade flow, it does NOT perform deeper checks and verifications such as: - -- Ensuring that the new contract does not include a constructor, as it will not be invoked. -- Verifying that the new contract includes an upgradability mechanism, preventing an unintended loss of further - upgradability capacity. -- Checking for storage consistency, ensuring that the new contract does not inadvertently introduce storage mismatches. -==== - -== Usage - -=== Upgrade Only -==== `Upgradeable` - -When only the WASM binary needs to be upgraded and no additional migration logic is required, developers should implement -the `UpgradeableInternal` trait. This trait is where authorization and custom access control logic are defined, -specifying who can perform the upgrade. This minimal implementation keeps the focus solely on controlling upgrade -permissions. - -[source,rust] ----- -use soroban_sdk::{ - contract, contracterror, contractimpl, panic_with_error, symbol_short, Address, Env, -}; -use stellar_contract_utils::upgradeable::UpgradeableInternal; -use stellar_macros::Upgradeable; - -#[contracterror] -#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] -#[repr(u32)] -pub enum ExampleContractError { - Unauthorized = 1, -} - -#[derive(Upgradeable)] -#[contract] -pub struct ExampleContract; - -#[contractimpl] -impl ExampleContract { - pub fn __constructor(e: &Env, admin: Address) { - e.storage().instance().set(&symbol_short!("OWNER"), &admin); - } -} - -impl UpgradeableInternal for ExampleContract { - fn _require_auth(e: &Env, operator: &Address) { - operator.require_auth(); - // `operator` is the invoker of the upgrade function and can be used - // to perform a role-based access control if implemented - let owner: Address = e.storage().instance().get(&symbol_short!("OWNER")).unwrap(); - if *operator != owner { - panic_with_error!(e, ExampleContractError::Unauthorized) - } - } -} ----- - -=== Upgrade and Migrate -==== `UpgradeableMigratable` - -When both the WASM binary and specific storage entries need to be modified as part of the upgrade process, the -`UpgradeableMigratableInternal` trait should be implemented. In addition to defining access control and migration -logic, the developer must specify an associated type that represents the data required for the migration. - -The `#[derive(UpgradeableMigratable)]` macro manages the sequencing of operations, ensuring that the migration can -only be invoked after a successful upgrade, preventing potential state inconsistencies and storage corruption. - -[source,rust] ----- -use soroban_sdk::{ - contract, contracterror, contracttype, panic_with_error, symbol_short, Address, Env, -}; -use stellar_contract_utils::upgradeable::UpgradeableMigratableInternal; -use stellar_macros::UpgradeableMigratable; - -#[contracterror] -#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] -#[repr(u32)] -pub enum ExampleContractError { - Unauthorized = 1, -} - -#[contracttype] -pub struct Data { - pub num1: u32, - pub num2: u32, -} - -#[derive(UpgradeableMigratable)] -#[contract] -pub struct ExampleContract; - -impl UpgradeableMigratableInternal for ExampleContract { - type MigrationData = Data; - - fn _require_auth(e: &Env, operator: &Address) { - operator.require_auth(); - let owner: Address = e.storage().instance().get(&symbol_short!("OWNER")).unwrap(); - if *operator != owner { - panic_with_error!(e, ExampleContractError::Unauthorized) - } - } - - fn _migrate(e: &Env, data: &Self::MigrationData) { - e.storage().instance().set(&symbol_short!("DATA_KEY"), data); - } -} ----- - -NOTE: If a rollback is required, the contract can be upgraded to a newer version where the rollback-specific logic -is defined and performed as a migration. - -==== Atomic upgrade and migration - -When performing an upgrade, the new implementation only becomes effective after the current invocation completes. -This means that if migration logic is included in the new implementation, it cannot be executed within the same -call. To address this, an auxiliary contract called `Upgrader` can be used to wrap both invocations, enabling an -atomic upgrade-and-migrate process. This approach ensures that the migration logic is executed immediately after the -upgrade without requiring a separate transaction. - -[source,rust] ----- -use soroban_sdk::{contract, contractimpl, symbol_short, Address, BytesN, Env, Val}; -use stellar_contract_utils::upgradeable::UpgradeableClient; - -#[contract] -pub struct Upgrader; - -#[contractimpl] -impl Upgrader { - pub fn upgrade_and_migrate( - env: Env, - contract_address: Address, - operator: Address, - wasm_hash: BytesN<32>, - migration_data: soroban_sdk::Vec, - ) { - operator.require_auth(); - let contract_client = UpgradeableClient::new(&env, &contract_address); - - contract_client.upgrade(&wasm_hash, &operator); - // The types of the arguments to the migrate function are unknown to this - // contract, so we need to call it with invoke_contract. - env.invoke_contract::<()>(&contract_address, &symbol_short!("migrate"), migration_data); - } -} ----- diff --git a/docs/modules/ROOT/templates/token_template.adoc b/docs/modules/ROOT/templates/token_template.adoc deleted file mode 100644 index da70afc2..00000000 --- a/docs/modules/ROOT/templates/token_template.adoc +++ /dev/null @@ -1,8 +0,0 @@ -:source-highlighter: highlight.js -:highlightjs-languages: rust -:github-icon: pass:[] -= XXX Token Standard - -== Purpose - -== Extensions diff --git a/docs/package-lock.json b/docs/package-lock.json deleted file mode 100644 index 64d3e184..00000000 --- a/docs/package-lock.json +++ /dev/null @@ -1,749 +0,0 @@ -{ - "name": "docs", - "version": "0.0.1", - "lockfileVersion": 2, - "requires": true, - "packages": { - "": { - "name": "docs", - "version": "0.0.1", - "license": "ISC", - "devDependencies": { - "@openzeppelin/docs-utils": "^0.1.2" - } - }, - "node_modules/@frangio/servbot": { - "version": "0.3.0-1", - "resolved": "https://registry.npmjs.org/@frangio/servbot/-/servbot-0.3.0-1.tgz", - "integrity": "sha512-eKXRqt8Zh3aqtVYoyayuLiktcW6vnYCuwlcqg91cv3HSdM5foTmIECJEJiwI+GBmSLY37mzczpt/ZfvYqzrQWQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.x", - "pnpm": "10.x" - } - }, - "node_modules/@openzeppelin/docs-utils": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/@openzeppelin/docs-utils/-/docs-utils-0.1.6.tgz", - "integrity": "sha512-cVLtDPrCdVgnLV9QRK9D1jrTB8ezQ8tCLTM4g6PHe9TIK3DbO6lSizLF98DhncK2bk6uodOLRT3LO1WtNzei1Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@frangio/servbot": "^0.3.0-1", - "chalk": "^3.0.0", - "chokidar": "^3.5.3", - "env-paths": "^2.2.0", - "find-up": "^4.1.0", - "is-port-reachable": "^3.0.0", - "js-yaml": "^3.13.1", - "lodash.startcase": "^4.4.0", - "minimist": "^1.2.0" - }, - "bin": { - "oz-docs": "oz-docs.js" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, - "dependencies": { - "sprintf-js": "~1.0.2" - } - }, - "node_modules/binary-extensions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", - "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "dependencies": { - "fill-range": "^7.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/chokidar": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", - "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/env-paths": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", - "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true, - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-port-reachable": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-port-reachable/-/is-port-reachable-3.1.0.tgz", - "integrity": "sha512-vjc0SSRNZ32s9SbZBzGaiP6YVB+xglLShhgZD/FHMZUXBvQWaV9CtzgeVhjccFJrI6RAMV+LX7NYxueW/A8W5A==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", - "dev": true, - "dependencies": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/lodash.startcase": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.startcase/-/lodash.startcase-4.4.0.tgz", - "integrity": "sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==", - "dev": true - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", - "dev": true - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - } - }, - "dependencies": { - "@frangio/servbot": { - "version": "0.3.0-1", - "resolved": "https://registry.npmjs.org/@frangio/servbot/-/servbot-0.3.0-1.tgz", - "integrity": "sha512-eKXRqt8Zh3aqtVYoyayuLiktcW6vnYCuwlcqg91cv3HSdM5foTmIECJEJiwI+GBmSLY37mzczpt/ZfvYqzrQWQ==", - "dev": true - }, - "@openzeppelin/docs-utils": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/@openzeppelin/docs-utils/-/docs-utils-0.1.6.tgz", - "integrity": "sha512-cVLtDPrCdVgnLV9QRK9D1jrTB8ezQ8tCLTM4g6PHe9TIK3DbO6lSizLF98DhncK2bk6uodOLRT3LO1WtNzei1Q==", - "dev": true, - "requires": { - "@frangio/servbot": "^0.3.0-1", - "chalk": "^3.0.0", - "chokidar": "^3.5.3", - "env-paths": "^2.2.0", - "find-up": "^4.1.0", - "is-port-reachable": "^3.0.0", - "js-yaml": "^3.13.1", - "lodash.startcase": "^4.4.0", - "minimist": "^1.2.0" - } - }, - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, - "requires": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - } - }, - "argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, - "requires": { - "sprintf-js": "~1.0.2" - } - }, - "binary-extensions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", - "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", - "dev": true - }, - "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "requires": { - "fill-range": "^7.0.1" - } - }, - "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "chokidar": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", - "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", - "dev": true, - "requires": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "fsevents": "~2.3.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "env-paths": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", - "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", - "dev": true - }, - "esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true - }, - "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "requires": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - } - }, - "fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "optional": true - }, - "glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "requires": { - "is-glob": "^4.0.1" - } - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, - "requires": { - "binary-extensions": "^2.0.0" - } - }, - "is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true - }, - "is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "requires": { - "is-extglob": "^2.1.1" - } - }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true - }, - "is-port-reachable": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-port-reachable/-/is-port-reachable-3.1.0.tgz", - "integrity": "sha512-vjc0SSRNZ32s9SbZBzGaiP6YVB+xglLShhgZD/FHMZUXBvQWaV9CtzgeVhjccFJrI6RAMV+LX7NYxueW/A8W5A==", - "dev": true - }, - "js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", - "dev": true, - "requires": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - } - }, - "locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "requires": { - "p-locate": "^4.1.0" - } - }, - "lodash.startcase": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.startcase/-/lodash.startcase-4.4.0.tgz", - "integrity": "sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==", - "dev": true - }, - "minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true - }, - "normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true - }, - "p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "requires": { - "p-try": "^2.0.0" - } - }, - "p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "requires": { - "p-limit": "^2.2.0" - } - }, - "p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true - }, - "path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true - }, - "picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true - }, - "readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, - "requires": { - "picomatch": "^2.2.1" - } - }, - "sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - }, - "to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "requires": { - "is-number": "^7.0.0" - } - } - } -} diff --git a/docs/package.json b/docs/package.json deleted file mode 100644 index 649eb044..00000000 --- a/docs/package.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "name": "docs", - "version": "0.0.1", - "scripts": { - "docs": "oz-docs -c .", - "docs:watch": "npm run docs watch", - "prepare-docs": "" - }, - "keywords": [], - "author": "", - "license": "ISC", - "devDependencies": { - "@openzeppelin/docs-utils": "^0.1.2" - } -} \ No newline at end of file diff --git a/examples/fungible-allowlist/Cargo.toml b/examples/fungible-allowlist/Cargo.toml index 4cf40c51..4d76d65c 100644 --- a/examples/fungible-allowlist/Cargo.toml +++ b/examples/fungible-allowlist/Cargo.toml @@ -17,4 +17,4 @@ stellar-macros = { workspace = true } stellar-tokens = { workspace = true } [dev-dependencies] -soroban-sdk = { workspace = true, features = ["testutils"] } +soroban-sdk = { workspace = true, features = ["testutils"] } \ No newline at end of file diff --git a/examples/fungible-blocklist/Cargo.toml b/examples/fungible-blocklist/Cargo.toml index e50d9116..e997e4b8 100644 --- a/examples/fungible-blocklist/Cargo.toml +++ b/examples/fungible-blocklist/Cargo.toml @@ -18,3 +18,7 @@ stellar-tokens = { workspace = true } [dev-dependencies] soroban-sdk = { workspace = true, features = ["testutils"] } + +[features] +default = ["certora"] +certora = [] \ No newline at end of file diff --git a/examples/fungible-vault/src/contract.rs b/examples/fungible-vault/src/contract.rs index 5b6c1696..588121dd 100644 --- a/examples/fungible-vault/src/contract.rs +++ b/examples/fungible-vault/src/contract.rs @@ -2,9 +2,9 @@ use soroban_sdk::{contract, contractimpl, Address, Env, String}; use stellar_macros::default_impl; -use stellar_tokens::fungible::{ +use stellar_tokens::{ + fungible::{Base, FungibleToken}, vault::{FungibleVault, Vault}, - Base, FungibleToken, }; #[contract] diff --git a/examples/fungible-vault/src/test.rs b/examples/fungible-vault/src/test.rs index 153f0dc4..a6e7b6d9 100644 --- a/examples/fungible-vault/src/test.rs +++ b/examples/fungible-vault/src/test.rs @@ -347,7 +347,7 @@ fn test_deposit_max_validation() { } #[test] -#[should_panic(expected = "Error(Contract, #122)")] +#[should_panic(expected = "Error(Contract, #407)")] fn test_withdraw_exceeds_max() { let e = Env::default(); let admin = Address::generate(&e); @@ -373,7 +373,7 @@ fn test_withdraw_exceeds_max() { } #[test] -#[should_panic(expected = "Error(Contract, #123)")] +#[should_panic(expected = "Error(Contract, #408)")] fn test_redeem_exceeds_max() { let e = Env::default(); let admin = Address::generate(&e); diff --git a/examples/nft-access-control/src/test.rs b/examples/nft-access-control/src/test.rs index 1820a7c7..8f085fb4 100644 --- a/examples/nft-access-control/src/test.rs +++ b/examples/nft-access-control/src/test.rs @@ -64,7 +64,7 @@ fn minters_can_mint() { } #[test] -#[should_panic(expected = "Error(Contract, #1210)")] +#[should_panic(expected = "Error(Contract, #2000)")] fn non_minters_cannot_mint() { let e = Env::default(); let admin = Address::generate(&e); @@ -92,7 +92,7 @@ fn burners_can_burn() { } #[test] -#[should_panic(expected = "Error(Contract, #1210)")] +#[should_panic(expected = "Error(Contract, #2000)")] fn non_burners_cannot_burn() { let e = Env::default(); let admin = Address::generate(&e); @@ -125,7 +125,7 @@ fn burners_can_burn_from() { } #[test] -#[should_panic(expected = "Error(Contract, #1210)")] +#[should_panic(expected = "Error(Contract, #2000)")] fn non_burners_cannot_burn_from() { let e = Env::default(); let admin = Address::generate(&e); @@ -160,7 +160,7 @@ fn minter_admin_can_grant_role() { } #[test] -#[should_panic(expected = "Error(Contract, #1210)")] +#[should_panic(expected = "Error(Contract, #2000)")] fn burner_admin_can_revoke_role() { let e = Env::default(); let admin = Address::generate(&e); @@ -178,7 +178,7 @@ fn burner_admin_can_revoke_role() { } #[test] -#[should_panic(expected = "Error(Contract, #1210)")] +#[should_panic(expected = "Error(Contract, #2000)")] fn non_admin_cannot_grant_role() { let e = Env::default(); let admin = Address::generate(&e); @@ -193,7 +193,7 @@ fn non_admin_cannot_grant_role() { } #[test] -#[should_panic(expected = "Error(Contract, #1210)")] +#[should_panic(expected = "Error(Contract, #2000)")] fn non_admin_cannot_revoke_role() { let e = Env::default(); let admin = Address::generate(&e); @@ -256,7 +256,7 @@ fn admin_transfer_works() { } #[test] -#[should_panic(expected = "Error(Contract, #1200)")] +#[should_panic(expected = "Error(Contract, #2200)")] fn cannot_accept_after_admin_transfer_cancelled() { let e = Env::default(); let admin = Address::generate(&e); @@ -331,7 +331,7 @@ fn non_recipient_cannot_accept_transfer() { } #[test] -#[should_panic(expected = "Error(Contract, #1200)")] +#[should_panic(expected = "Error(Contract, #2200)")] fn expired_admin_transfer_panics() { let e = Env::default(); let admin = Address::generate(&e); diff --git a/examples/ownable/Cargo.toml b/examples/ownable/Cargo.toml index bf0f8a6e..ded3e5d8 100644 --- a/examples/ownable/Cargo.toml +++ b/examples/ownable/Cargo.toml @@ -12,8 +12,11 @@ doctest = false [dependencies] soroban-sdk = { workspace = true } -stellar-access = { workspace = true } +stellar-access = { workspace = true} stellar-macros = { workspace = true } [dev-dependencies] soroban-sdk = { workspace = true, features = ["testutils"] } + +[features] +certora = ["stellar-access/certora"] \ No newline at end of file diff --git a/netlify.toml b/netlify.toml deleted file mode 100644 index 581c6636..00000000 --- a/netlify.toml +++ /dev/null @@ -1,4 +0,0 @@ -[build] -base = "docs/" -command = "npm run docs" -publish = "build/site" diff --git a/packages/access/.gitignore b/packages/access/.gitignore new file mode 100644 index 00000000..c8b4b07c --- /dev/null +++ b/packages/access/.gitignore @@ -0,0 +1 @@ +emv-* diff --git a/packages/access/Cargo.toml b/packages/access/Cargo.toml index b534cf9b..08002f14 100644 --- a/packages/access/Cargo.toml +++ b/packages/access/Cargo.toml @@ -13,11 +13,12 @@ crate-type = ["lib", "cdylib"] doctest = false [dependencies] -soroban-sdk = { workspace = true } +soroban-sdk = { workspace = true} cvlr = { workspace = true, default-features = false } cvlr-soroban = { workspace = true } cvlr-soroban-macros = { workspace = true } cvlr-soroban-derive = { workspace = true } +base64ct = { workspace = true } stellar-macros = { workspace = true } [dev-dependencies] @@ -25,4 +26,4 @@ soroban-sdk = { workspace = true, features = ["testutils"] } stellar-event-assertion = { workspace = true } [features] -certora = [] +certora = [] \ No newline at end of file diff --git a/packages/access/README.md b/packages/access/README.md index 6ae91eea..844bfcce 100644 --- a/packages/access/README.md +++ b/packages/access/README.md @@ -137,9 +137,9 @@ Add this to your `Cargo.toml`: ```toml [dependencies] # We recommend pinning to a specific version, because rapid iterations are expected as the library is in an active development phase. -stellar-access = "=0.4.0" +stellar-access = "=0.5.1" # Add this if you want to use macros -stellar-macros = "=0.4.0" +stellar-macros = "=0.5.1" ``` ## Examples diff --git a/packages/access/certora_build.py b/packages/access/certora_build.py index 42c2d4dd..03dd4919 100755 --- a/packages/access/certora_build.py +++ b/packages/access/certora_build.py @@ -13,8 +13,8 @@ # JSON FIELDS PROJECT_DIR = (SCRIPT_DIR / "../").resolve() -SOURCES = ["access/src/**/*.rs"] -EXECUTABLES = "../target/wasm32v1-none/release/stellar_access.wasm" +SOURCES = ["../packages/**/*.rs"] +EXECUTABLES = "../target/wasm32-unknown-unknown/release/stellar_access.wasm" VERBOSE = False diff --git a/packages/access/confs/access_control_integrity.conf b/packages/access/confs/access_control_integrity.conf new file mode 100644 index 00000000..dd7dbd47 --- /dev/null +++ b/packages/access/confs/access_control_integrity.conf @@ -0,0 +1,17 @@ +{ + "build_script": "../certora_build.py", + "msg": "Integrity Rules Access Control", + "rule": [ + "access_control_constructor_integrity", + "grant_role_integrity", + "revoke_role_integrity", + "renounce_role_integrity", + "transfer_admin_role_integrity", + "remove_transfer_admin_role_integrity", + "accept_admin_transfer_integrity", + "set_role_admin_integrity", + "renounce_admin_integrity", + ], + "server": "production", + "prover_version": "stellar-oz-changes" +} \ No newline at end of file diff --git a/packages/access/confs/access_control_invariants.conf b/packages/access/confs/access_control_invariants.conf new file mode 100644 index 00000000..5e04c116 --- /dev/null +++ b/packages/access/confs/access_control_invariants.conf @@ -0,0 +1,62 @@ +{ + "build_script": "../certora_build.py", + "msg": "Invariants Access Control", + "rule": [ + "after_constructor_admin_is_set", + "after_constructor_admin_is_set_sanity", + "after_grant_role_admin_is_set", + "after_grant_role_admin_is_set_sanity", + "after_revoke_role_admin_is_set", + "after_revoke_role_admin_is_set_sanity", + "after_renounce_role_admin_is_set", + "after_renounce_role_admin_is_set_sanity", + "after_transfer_admin_role_admin_is_set", + "after_transfer_admin_role_admin_is_set_sanity", + "after_accept_admin_transfer_admin_is_set", + "after_accept_admin_transfer_admin_is_set_sanity", + "after_set_role_admin_admin_is_set", + "after_set_role_admin_admin_is_set_sanity", + "after_constructor_pending_admin_implies_admin", + "after_constructor_pending_admin_implies_admin_sanity", + "after_grant_role_pending_admin_implies_admin", + "after_grant_role_pending_admin_implies_admin_sanity", + "after_revoke_role_pending_admin_implies_admin", + "after_revoke_role_pending_admin_implies_admin_sanity", + "after_renounce_role_pending_admin_implies_admin", + "after_renounce_role_pending_admin_implies_admin_sanity", + "after_transfer_admin_role_pending_admin_implies_admin", + "after_transfer_admin_role_pending_admin_implies_admin_sanity", + "after_accept_admin_transfer_pending_admin_implies_admin", + "after_accept_admin_transfer_pending_admin_implies_admin_sanity", + "after_set_role_admin_pending_admin_implies_admin", + "after_set_role_admin_pending_admin_implies_admin_sanity", + "after_renounce_admin_pending_admin_implies_admin", + "after_renounce_admin_pending_admin_implies_admin_sanity", + "after_constructor_unique_indices_for_role", + "after_grant_role_unique_indices_for_role", + "after_revoke_role_unique_indices_for_role", + "after_renounce_role_unique_indices_for_role", + "after_transfer_admin_role_unique_indices_for_role", + "after_accept_admin_transfer_unique_indices_for_role", + "after_set_role_admin_unique_indices_for_role", + "after_renounce_admin_unique_indices_for_role", + "after_constructor_role_count_minus_one_geq_index", + "after_grant_role_role_count_minus_one_geq_index", + "after_revoke_role_role_count_minus_one_geq_index", + "after_renounce_role_role_count_minus_one_geq_index", + "after_transfer_admin_role_role_count_minus_one_geq_index", + "after_accept_admin_transfer_role_count_minus_one_geq_index", + "after_set_role_admin_role_count_minus_one_geq_index", + "after_renounce_admin_role_count_minus_one_geq_index", + "after_constructor_has_role_index_implies_get_role_account", + "after_grant_role_has_role_index_implies_get_role_account", + "after_revoke_role_has_role_index_implies_get_role_account", + "after_renounce_role_has_role_index_implies_get_role_account", + "after_transfer_admin_role_has_role_index_implies_get_role_account", + "after_accept_admin_transfer_has_role_index_implies_get_role_account", + "after_set_role_admin_has_role_index_implies_get_role_account", + "after_renounce_admin_has_role_index_implies_get_role_account", + ], + "server": "production", + "prover_version": "stellar-oz-changes" +} \ No newline at end of file diff --git a/packages/access/confs/access_control_non_panics.conf b/packages/access/confs/access_control_non_panics.conf new file mode 100644 index 00000000..71f01f52 --- /dev/null +++ b/packages/access/confs/access_control_non_panics.conf @@ -0,0 +1,24 @@ +{ + "build_script": "../certora_build.py", + "msg": "Panic Rules Access Control", + "rule": [ + "grant_role_non_panic", + "grant_role_non_panic_sanity", + "renounce_role_non_panic", + "renounce_role_non_panic_sanity", + "transfer_admin_role_non_panic", + "transfer_admin_role_non_panic_sanity", + "accept_admin_transfer_non_panic", + "accept_admin_transfer_non_panic_sanity", + "set_role_admin_non_panic", + "set_role_admin_non_panic_sanity", + "renounce_admin_non_panic", + "renounce_admin_non_panic_sanity", + ], + "prover_args": [ + "-trapAsAssert", + "true", + ], + "server": "production", + "prover_version": "stellar-oz-changes" +} \ No newline at end of file diff --git a/packages/access/confs/access_control_panics.conf b/packages/access/confs/access_control_panics.conf new file mode 100644 index 00000000..8638fbcb --- /dev/null +++ b/packages/access/confs/access_control_panics.conf @@ -0,0 +1,40 @@ +{ + "build_script": "../certora_build.py", + "msg": "Panic Rules Access Control", + "optimistic_loop": true, + "rule": [ + "grant_role_panics_if_caller_unauth", + "grant_role_panics_if_caller_not_admin_nor_admin_role", + "revoke_role_panics_if_caller_unauth", + "revoke_role_panics_if_caller_not_admin_nor_admin_role", + "revoke_role_panics_if_account_does_not_have_role", + "revoke_role_panics_if_role_is_empty", + "renounce_role_panics_if_caller_unauth", + "renounce_role_panics_if_caller_does_not_have_role", + "renounce_role_panics_if_role_is_empty", + "transfer_admin_role_panics_if_unauth_by_admin", + "transfer_admin_role_panics_if_admin_not_set", + "transfer_admin_role_panics_if_live_until_ledger_0_and_pending_admin_none", + "transfer_admin_role_panics_if_live_until_ledger_0_and_diff_pending_admin", + "transfer_admin_role_panics_if_invalid_live_until_ledger", + "accept_admin_transfer_panics_if_unauth_by_pending_admin", + "accept_admin_transfer_panics_if_pending_admin_not_set", + "set_role_admin_panics_if_unauth_by_admin", + "set_role_admin_panics_if_admin_not_set", + "renounce_admin_panics_if_unauth_by_admin", + "renounce_admin_panics_if_admin_not_set", + "renounce_admin_panics_if_pending_adminship_transfer", + "admin_function_panics_if_unauth_by_admin", + "admin_function_panics_if_admin_not_set", + "role1_func_panics_if_caller_does_not_have_role", + "role1_auth_func_panics_if_caller_does_not_have_role", + "role1_auth_func_panics_if_caller_does_not_authorize", + "role1_or_role2_func_panics_if_caller_does_not_have_role", + "role1_or_role2_auth_func_panics_if_caller_does_not_have_role", + "role1_or_role2_auth_func_panics_if_caller_does_not_authorize", + "role1_and_role2_func_panics_if_caller1_does_not_have_role", + "role1_and_role2_func_panics_if_caller2_does_not_have_role", + ], + "server": "production", + "prover_version": "stellar-oz-changes" +} \ No newline at end of file diff --git a/packages/access/confs/access_control_revoke_role_non_panic.conf b/packages/access/confs/access_control_revoke_role_non_panic.conf new file mode 100644 index 00000000..40a22b25 --- /dev/null +++ b/packages/access/confs/access_control_revoke_role_non_panic.conf @@ -0,0 +1,16 @@ +{ + "build_script": "../certora_build.py", + "msg": "Panic Rules Access Control", + "rule": [ + "revoke_role_non_panic", + "revoke_role_non_panic_sanity", + ], + "prover_args": [ + "-trapAsAssert", + "true", + "-split", + "false", + ], + "server": "production", + "prover_version": "stellar-oz-changes" +} \ No newline at end of file diff --git a/packages/access/confs/access_control_sanity.conf b/packages/access/confs/access_control_sanity.conf index 9d753412..afc52b6d 100644 --- a/packages/access/confs/access_control_sanity.conf +++ b/packages/access/confs/access_control_sanity.conf @@ -1,7 +1,6 @@ { "build_script": "../certora_build.py", - "optimistic_loop": true, - "msg": "Sanity Rules for OZ access crate, access_control", + "msg": "Sanity Rules Access Control", "rule": [ "has_role_sanity", "get_admin_sanity", @@ -10,23 +9,14 @@ "get_role_admin_sanity", "set_admin_sanity", "grant_role_sanity", - "grant_role_no_auth_sanity", "revoke_role_sanity", - "revoke_role_no_auth_sanity", "renounce_role_sanity", "transfer_admin_role_sanity", "accept_admin_transfer_sanity", "set_role_admin_sanity", - "renounce_admin_sanity", - "set_role_admin_no_auth_sanity", - "remove_role_admin_no_auth_sanity", - "remove_role_accounts_count_no_auth_sanity", - "ensure_if_admin_or_admin_role_sanity", - "ensure_role_sanity", - "enforce_admin_auth_sanity", - "add_to_role_enumeration_sanity", - "remove_from_role_enumeration_sanity" + "renounce_admin_sanity" ], - "server": "prover", - "prover_version": "abakst/soroban-new-summaries" -} \ No newline at end of file + "server": "production", + "prover_version": "stellar-oz-changes" +} + diff --git a/packages/access/confs/ownable_integrity.conf b/packages/access/confs/ownable_integrity.conf new file mode 100644 index 00000000..3a9ba11d --- /dev/null +++ b/packages/access/confs/ownable_integrity.conf @@ -0,0 +1,14 @@ +{ + "build_script": "../certora_build.py", + "msg": "Integrity Rules Ownable", + "rule": [ + "ownable_constructor_integrity", + "transfer_ownership_integrity", + "remove_transfer_ownership_integrity", + "accept_ownership_integrity", + "renounce_ownership_integrity", + ], + "precise_bitwise_ops": true, + "server": "production", + "prover_version": "stellar-oz-changes" +} \ No newline at end of file diff --git a/packages/access/confs/ownable_invariants.conf b/packages/access/confs/ownable_invariants.conf new file mode 100644 index 00000000..d722d66a --- /dev/null +++ b/packages/access/confs/ownable_invariants.conf @@ -0,0 +1,26 @@ +{ + "build_script": "../certora_build.py", + "msg": "Invariants Ownable", + "prover_version": "stellar-oz-changes", + "rule": [ + "after_constructor_owner_is_set", + "after_constructor_owner_is_set_sanity", + "after_transfer_ownership_pending_owner_is_set", + "after_transfer_ownership_pending_owner_is_set_sanity", + "after_accept_ownership_owner_is_set", + "after_accept_ownership_owner_is_set_sanity", + "after_owner_restricted_function_owner_is_set", + "after_owner_restricted_function_owner_is_set_sanity", + "after_constructor_pending_owner_implies_owner", + "after_constructor_pending_owner_implies_owner_sanity", + "after_transfer_ownership_pending_owner_implies_owner", + "after_transfer_ownership_pending_owner_implies_owner_sanity", + "after_accept_ownership_pending_owner_implies_owner", + "after_accept_ownership_pending_owner_implies_owner_sanity", + "after_renounce_ownership_pending_owner_implies_owner", + "after_renounce_ownership_pending_owner_implies_owner_sanity", + "after_owner_restricted_function_pending_owner_implies_owner", + "after_owner_restricted_function_pending_owner_implies_owner_sanity" + ], + "server": "production" +} diff --git a/packages/access/confs/ownable_non_panics.conf b/packages/access/confs/ownable_non_panics.conf new file mode 100644 index 00000000..29f0bb81 --- /dev/null +++ b/packages/access/confs/ownable_non_panics.conf @@ -0,0 +1,17 @@ +{ + "build_script": "../certora_build.py", + "msg": "Non-Panic Rules Ownable", + "rule": [ + "transfer_ownership_non_panic", + "accept_ownership_non_panic", + "renounce_ownership_non_panic", + "owner_restricted_function_non_panic", + "transfer_ownership_non_panic_sanity", + "accept_ownership_non_panic_sanity", + "renounce_ownership_non_panic_sanity", + "owner_restricted_function_non_panic_sanity", + ], + "prover_args": ["-trapAsAssert true"], + "server": "production", + "prover_version": "stellar-oz-changes" +} \ No newline at end of file diff --git a/packages/access/confs/ownable_panics.conf b/packages/access/confs/ownable_panics.conf new file mode 100644 index 00000000..864146d1 --- /dev/null +++ b/packages/access/confs/ownable_panics.conf @@ -0,0 +1,21 @@ +{ + "build_script": "../certora_build.py", + "msg": "Panic Rules Ownable", + "precise_bitwise_ops": true, + "rule": [ + "transfer_ownership_panics_if_unauth_by_owner", + "transfer_ownership_panics_if_owner_not_set", + "transfer_ownership_panics_if_live_until_ledger_0_and_pending_owner_none", + "transfer_ownership_panics_if_live_until_ledger_0_and_diff_pending_owner", + "transfer_ownership_panics_if_invalid_live_until_ledger", + "accept_ownership_panics_if_unauth_by_pending_owner", + "accept_ownership_panics_if_pending_owner_not_set", + "renounce_ownership_panics_if_unauth_by_owner", + "renounce_ownership_panics_if_owner_not_set", + "renounce_ownership_panics_if_pending_ownership_transfer", + "owner_restricted_function_panics_if_unauth_by_owner", + "owner_restricted_function_panics_if_owner_not_set", + ], + "server": "production", + "prover_version": "stellar-oz-changes" +} \ No newline at end of file diff --git a/packages/access/confs/ownable_sanity.conf b/packages/access/confs/ownable_sanity.conf index 8725387c..c0309f9c 100644 --- a/packages/access/confs/ownable_sanity.conf +++ b/packages/access/confs/ownable_sanity.conf @@ -1,16 +1,13 @@ { "build_script": "../certora_build.py", - "optimistic_loop": true, - "msg": "Sanity Rules for OZ access crate, ownable", + "msg": "Sanity Rules Ownable", "rule": [ "get_owner_sanity", "set_owner_sanity", "transfer_ownership_sanity", "accept_ownership_sanity", - "renounce_ownership_sanity", - "enforce_owner_auth_sanity", - "fv_harness_contract_sanity_dummy" + "renounce_ownership_sanity" ], - "server": "prover", - "prover_version": "abakst/soroban-new-summaries" + "server": "production", + "prover_version": "stellar-oz-changes" } \ No newline at end of file diff --git a/packages/access/justfile b/packages/access/justfile index 36fa02a9..8d41ec4b 100644 --- a/packages/access/justfile +++ b/packages/access/justfile @@ -3,7 +3,10 @@ export RUSTFLAGS := "-C strip=none" target := "../../target" build: - cargo build --target=wasm32v1-none --release --features certora + cargo +nightly-2024-11-22 build --target=wasm32-unknown-unknown --release --features certora + +expand: + cargo +nightly-2024-11-22 expand --target=wasm32-unknown-unknown --release --features certora clean: rm -rf {{target}} \ No newline at end of file diff --git a/packages/access/src/access_control/mod.rs b/packages/access/src/access_control/mod.rs index 4bfc2e48..c0da1e73 100644 --- a/packages/access/src/access_control/mod.rs +++ b/packages/access/src/access_control/mod.rs @@ -86,15 +86,21 @@ //! 1. When accounts were assigned to a role but later all were removed. //! 2. When a role never existed in the first place. -#[cfg(feature = "certora")] -pub mod spec; - mod storage; #[cfg(test)] mod test; -use soroban_sdk::{contracterror, contractevent, Address, Env, Symbol}; +#[cfg(feature = "certora")] +pub mod specs; + +use soroban_sdk::{contracterror, Address, Env, Symbol}; + +#[cfg(not(feature = "certora"))] +use soroban_sdk::{contractevent}; + +#[cfg(feature = "certora")] +use cvlr_soroban_derive::contractevent; pub use crate::access_control::storage::{ accept_admin_transfer, add_to_role_enumeration, enforce_admin_auth, @@ -105,6 +111,7 @@ pub use crate::access_control::storage::{ transfer_admin_role, AccessControlStorageKey, }; + pub trait AccessControl { /// Returns `Some(index)` if the account has the specified role, /// where `index` is the position of the account for that role, @@ -339,15 +346,15 @@ pub trait AccessControl { #[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] #[repr(u32)] pub enum AccessControlError { - Unauthorized = 1210, - AdminNotSet = 1211, - IndexOutOfBounds = 1212, - AdminRoleNotFound = 1213, - RoleCountIsNotZero = 1214, - RoleNotFound = 1215, - AdminAlreadySet = 1216, - RoleNotHeld = 1217, - RoleIsEmpty = 1218, + Unauthorized = 2000, + AdminNotSet = 2001, + IndexOutOfBounds = 2002, + AdminRoleNotFound = 2003, + RoleCountIsNotZero = 2004, + RoleNotFound = 2005, + AdminAlreadySet = 2006, + RoleNotHeld = 2007, + RoleIsEmpty = 2008, } // ################## CONSTANTS ################## @@ -377,6 +384,7 @@ pub struct RoleGranted { /// * `role` - The role that was granted. /// * `account` - The account that received the role. /// * `caller` - The account that granted the role. +#[cfg(not(feature = "certora"))] pub fn emit_role_granted(e: &Env, role: &Symbol, account: &Address, caller: &Address) { RoleGranted { role: role.clone(), account: account.clone(), caller: caller.clone() }.publish(e); } @@ -401,6 +409,7 @@ pub struct RoleRevoked { /// * `account` - The account that lost the role. /// * `caller` - The account that revoked the role (either the admin or the /// account itself). +#[cfg(not(feature = "certora"))] pub fn emit_role_revoked(e: &Env, role: &Symbol, account: &Address, caller: &Address) { RoleRevoked { role: role.clone(), account: account.clone(), caller: caller.clone() }.publish(e); } @@ -423,6 +432,7 @@ pub struct RoleAdminChanged { /// * `role` - The role whose admin is changing. /// * `previous_admin_role` - The previous admin role. /// * `new_admin_role` - The new admin role. +#[cfg(not(feature = "certora"))] pub fn emit_role_admin_changed( e: &Env, role: &Symbol, @@ -456,6 +466,7 @@ pub struct AdminTransferInitiated { /// * `new_admin` - The proposed new admin. /// * `live_until_ledger` - The ledger number at which the pending transfer will /// expire. If this value is `0`, it means the pending transfer is cancelled. +#[cfg(not(feature = "certora"))] pub fn emit_admin_transfer_initiated( e: &Env, current_admin: &Address, @@ -486,6 +497,7 @@ pub struct AdminTransferCompleted { /// * `e` - Access to Soroban environment. /// * `previous_admin` - The previous admin. /// * `new_admin` - The new admin who accepted the transfer. +#[cfg(not(feature = "certora"))] pub fn emit_admin_transfer_completed(e: &Env, previous_admin: &Address, new_admin: &Address) { AdminTransferCompleted { new_admin: new_admin.clone(), previous_admin: previous_admin.clone() } .publish(e); @@ -505,6 +517,7 @@ pub struct AdminRenounced { /// /// * `e` - Access to Soroban environment. /// * `admin` - The admin that renounced the role. +#[cfg(not(feature = "certora"))] pub fn emit_admin_renounced(e: &Env, admin: &Address) { AdminRenounced { admin: admin.clone() }.publish(e); } \ No newline at end of file diff --git a/packages/access/src/access_control/spec/access_control_sanity_rules.rs b/packages/access/src/access_control/spec/access_control_sanity_rules.rs deleted file mode 100644 index 777ef7c8..00000000 --- a/packages/access/src/access_control/spec/access_control_sanity_rules.rs +++ /dev/null @@ -1,168 +0,0 @@ - -use cvlr::{cvlr_assert}; -use cvlr_soroban::{nondet_address}; -use cvlr_soroban_derive::rule; -use cvlr::nondet::Nondet; - -use soroban_sdk::{Env, Symbol}; - -use crate::access_control::*; - -// TODO: need nondet for Symbol -// until then pass it as argument - -#[rule] -pub fn has_role_sanity(e: Env, role: Symbol) { - let account = nondet_address(); - has_role(&e, &account, &role); - cvlr_assert!(false); -} - -#[rule] -pub fn get_admin_sanity(e: Env) { - get_admin(&e); - cvlr_assert!(false); -} - -#[rule] -pub fn get_role_member_count_sanity(e: Env, role: Symbol) { - let _ = get_role_member_count(&e, &role); - cvlr_assert!(false); -} - -#[rule] -pub fn get_role_member_sanity(e: Env, role: Symbol) { - let i = u32::nondet(); - let _ = get_role_member(&e, &role, i); - cvlr_assert!(false); -} - -#[rule] -pub fn get_role_admin_sanity(e: Env, role: Symbol) { - let _ = get_role_admin(&e, &role); - cvlr_assert!(false); -} - -#[rule] -pub fn set_admin_sanity(e: Env) { - let admin = nondet_address(); - set_admin(&e, &admin); - cvlr_assert!(false); -} - -#[rule] -pub fn grant_role_sanity(e: Env, role: Symbol) { - let caller = nondet_address(); - let account = nondet_address(); - grant_role(&e, &caller, &account, &role); - cvlr_assert!(false); -} - -#[rule] -pub fn grant_role_no_auth_sanity(e: Env, role: Symbol) { - let caller = nondet_address(); - let account = nondet_address(); - grant_role_no_auth(&e, &caller, &account, &role); - cvlr_assert!(false); -} - -#[rule] -pub fn revoke_role_sanity(e: Env, role: Symbol) { - let caller = nondet_address(); - let account = nondet_address(); - revoke_role(&e, &caller, &account, &role); - cvlr_assert!(false); -} - -#[rule] -pub fn revoke_role_no_auth_sanity(e: Env, role: Symbol) { - let caller = nondet_address(); - let account = nondet_address(); - revoke_role_no_auth(&e, &caller, &account, &role); - cvlr_assert!(false); -} - -#[rule] -pub fn renounce_role_sanity(e: Env, role: Symbol) { - let caller = nondet_address(); - renounce_role(&e, &caller, &role); - cvlr_assert!(false); -} - -#[rule] -pub fn transfer_admin_role_sanity(e: Env) { - let new_admin = nondet_address(); - let live_until_ledger = u32::nondet(); - transfer_admin_role(&e, &new_admin, live_until_ledger); - cvlr_assert!(false); -} - -#[rule] -pub fn accept_admin_transfer_sanity(e: Env) { - accept_admin_transfer(&e); - cvlr_assert!(false); -} - -#[rule] -pub fn set_role_admin_sanity(e: Env, role: Symbol, admin_role: Symbol) { - set_role_admin(&e, &role, &admin_role); - cvlr_assert!(false); -} - -#[rule] -pub fn renounce_admin_sanity(e: Env) { - renounce_admin(&e); - cvlr_assert!(false); -} - -#[rule] -pub fn set_role_admin_no_auth_sanity(e: Env, role: Symbol, admin_role: Symbol) { - set_role_admin_no_auth(&e, &role, &admin_role); - cvlr_assert!(false); -} - -#[rule] -pub fn remove_role_admin_no_auth_sanity(e: Env, role: Symbol) { - remove_role_admin_no_auth(&e, &role); - cvlr_assert!(false); -} - -#[rule] -pub fn remove_role_accounts_count_no_auth_sanity(e: Env, role: Symbol) { - remove_role_accounts_count_no_auth(&e, &role); - cvlr_assert!(false); -} - -#[rule] -pub fn ensure_if_admin_or_admin_role_sanity(e: Env, role: Symbol) { - let caller = nondet_address(); - ensure_if_admin_or_admin_role(&e, &caller, &role); - cvlr_assert!(false); -} - -#[rule] -pub fn ensure_role_sanity(e: Env, role: Symbol) { - let caller = nondet_address(); - ensure_role(&e, &caller, &role); - cvlr_assert!(false); -} - -#[rule] -pub fn enforce_admin_auth_sanity(e: Env) { - let _ = enforce_admin_auth(&e); - cvlr_assert!(false); -} - -#[rule] -pub fn add_to_role_enumeration_sanity(e: Env, role: &Symbol) { - let account = nondet_address(); - add_to_role_enumeration(&e, &account, &role); - cvlr_assert!(false); -} - -#[rule] -pub fn remove_from_role_enumeration_sanity(e: Env, role: &Symbol) { - let account = nondet_address(); - remove_from_role_enumeration(&e, &account, &role); - cvlr_assert!(false); -} \ No newline at end of file diff --git a/packages/access/src/access_control/spec/mod.rs b/packages/access/src/access_control/spec/mod.rs deleted file mode 100644 index d05d8dba..00000000 --- a/packages/access/src/access_control/spec/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod access_control_sanity_rules; \ No newline at end of file diff --git a/packages/access/src/access_control/specs/access_control_contract.rs b/packages/access/src/access_control/specs/access_control_contract.rs new file mode 100644 index 00000000..b3c63a32 --- /dev/null +++ b/packages/access/src/access_control/specs/access_control_contract.rs @@ -0,0 +1,43 @@ +use crate::access_control::{AccessControl, *}; +use soroban_sdk::{contract, contractimpl, Address, Env}; +use stellar_macros::{default_impl, has_any_role, has_role, only_admin, only_any_role, only_role}; + +use crate as stellar_access; + +#[contract] +pub struct AccessControlContract; + +#[contractimpl] +impl AccessControlContract { + pub fn access_control_constructor(e: &Env, admin: Address) { + set_admin(e, &admin); + } + #[only_admin] + pub fn admin_function(e: &Env) { + } + + #[has_role(caller, "role1")] + pub fn role1_func(e: &Env, caller: Address) { + } + + #[only_role(caller, "role1")] + pub fn role1_auth_func(e: &Env, caller: Address) { + } + + #[has_any_role(caller, ["role1", "role2"])] + pub fn role1_or_role2_func(e: &Env, caller: Address) { + } + + #[only_any_role(caller, ["role1", "role2"])] + pub fn role1_or_role2_auth_func(e: &Env, caller: Address) { + } + + #[has_role(caller1, "role1")] + #[has_role(caller2, "role2")] + pub fn role1_and_role2_func(e: &Env, caller1: Address, caller2: Address) { + } +} + +#[default_impl] +#[contractimpl] +impl AccessControl for AccessControlContract {} \ No newline at end of file diff --git a/packages/access/src/access_control/specs/access_control_integrity.rs b/packages/access/src/access_control/specs/access_control_integrity.rs new file mode 100644 index 00000000..548fc56b --- /dev/null +++ b/packages/access/src/access_control/specs/access_control_integrity.rs @@ -0,0 +1,119 @@ +use cvlr::{cvlr_assert, cvlr_assume,cvlr_satisfy}; +use cvlr_soroban::{nondet_address, nondet_symbol}; +use cvlr::nondet::Nondet; +use cvlr_soroban_derive::rule; +use cvlr::clog; + +use soroban_sdk::{Env}; +use crate::access_control::{AccessControl, specs::{access_control_contract::AccessControlContract, helper::get_pending_admin}}; + + +#[rule] +// after call to constructor the admin is set +// status: verified +pub fn access_control_constructor_integrity(e: Env) { + let admin = nondet_address(); + clog!(cvlr_soroban::Addr(&admin)); + AccessControlContract::access_control_constructor(&e, admin.clone()); + let admin_post = AccessControlContract::get_admin(&e); + if let Some(admin_post_internal) = &admin_post { + clog!(cvlr_soroban::Addr(&admin_post_internal)); + } + cvlr_assert!(admin_post.unwrap() == admin); +} + +#[rule] +// after call to grant_role the account has the role +// status: verified +pub fn grant_role_integrity(e: Env) { + let caller = nondet_address(); + let account = nondet_address(); + let role = nondet_symbol(); + crate::access_control::grant_role(&e, &caller, &account, &role); + let account_has_role = crate::access_control::has_role(&e, &account, &role); + cvlr_assert!(account_has_role.is_some()); +} + +#[rule] +// after call to revoke_role the account does not have the role +// status: verified +pub fn revoke_role_integrity(e: Env) { + let caller = nondet_address(); + let account = nondet_address(); + let role = nondet_symbol(); + AccessControlContract::revoke_role(&e, caller.clone(), account.clone(), role.clone()); + let account_has_role = AccessControlContract::has_role(&e, account.clone(), role.clone()); + cvlr_assert!(account_has_role.is_none()); +} + +#[rule] +// after call to renounce_role the account does not have the role +// status: verified +pub fn renounce_role_integrity(e: Env) { + let caller = nondet_address(); + let role = nondet_symbol(); + AccessControlContract::renounce_role(&e, caller.clone(), role.clone()); + let account_has_role = AccessControlContract::has_role(&e, caller.clone(), role.clone()); + cvlr_assert!(account_has_role.is_none()); +} + +#[rule] +// after call to transfer_admin_role with live_until_ledger > current_ledger the pending admin is set to the new admin +// status: verified +pub fn transfer_admin_role_integrity(e: Env) { + let new_admin = nondet_address(); + let live_until_ledger = u32::nondet(); + let current_ledger = e.ledger().sequence(); + cvlr_assume!(live_until_ledger > current_ledger); // proper admin transfer + AccessControlContract::transfer_admin_role(&e, new_admin.clone(), live_until_ledger); + let pending_admin = get_pending_admin(&e); + cvlr_assert!(pending_admin == Some(new_admin.clone())); +} + +#[rule] +// after call to accept_admin_transfer with live_until_ledger = 0 the pending admin is none +// status: verified +pub fn remove_transfer_admin_role_integrity(e: Env) { + let new_admin = nondet_address(); + let live_until_ledger = 0; + AccessControlContract::transfer_admin_role(&e, new_admin.clone(), live_until_ledger); + let pending_admin = get_pending_admin(&e); + cvlr_assert!(pending_admin.is_none()); +} + +#[rule] +// after call to accept_admin_transfer the admin is set to the previous pending admin, which is not none, and the pending admin is set to none +// status: verified +pub fn accept_admin_transfer_integrity(e: Env) { + let pending_admin_pre = get_pending_admin(&e); + cvlr_assume!(!pending_admin_pre.is_none()); + AccessControlContract::accept_admin_transfer(&e); + let admin = AccessControlContract::get_admin(&e); + if let Some(admin_internal) = admin.clone() { + clog!(cvlr_soroban::Addr(&admin_internal)); + } + cvlr_assert!(admin == pending_admin_pre); + cvlr_assert!(!admin.is_none()); + let pending_admin_post = get_pending_admin(&e); + cvlr_assert!(pending_admin_post.is_none()); +} + +#[rule] +// after call to set_role_admin the role admin of the given role is the given admin_role +// status: verified +pub fn set_role_admin_integrity(e: Env) { + let role = nondet_symbol(); + let admin_role = nondet_symbol(); + AccessControlContract::set_role_admin(&e, role.clone(), admin_role.clone()); + let role_admin = AccessControlContract::get_role_admin(&e, role.clone()); + cvlr_assert!(role_admin.is_some() && role_admin.unwrap().to_val().get_payload() == admin_role.to_val().get_payload()); +} + +#[rule] +// after call to renounce_admin the admin is none +// status: verified +pub fn renounce_admin_integrity(e: Env) { + AccessControlContract::renounce_admin(&e); + let admin = AccessControlContract::get_admin(&e); + cvlr_assert!(admin.is_none()); +} \ No newline at end of file diff --git a/packages/access/src/access_control/specs/access_control_invariants.rs b/packages/access/src/access_control/specs/access_control_invariants.rs new file mode 100644 index 00000000..a7e42409 --- /dev/null +++ b/packages/access/src/access_control/specs/access_control_invariants.rs @@ -0,0 +1,753 @@ +use cvlr::{cvlr_assert, cvlr_assume,cvlr_satisfy}; +use cvlr_soroban::{nondet_address, nondet_symbol}; +use cvlr::nondet::Nondet; +use cvlr_soroban_derive::rule; +use cvlr::clog; + +use soroban_sdk::{Env, Address, Symbol}; +use crate::access_control::{AccessControl, specs::{access_control_contract::AccessControlContract, helper::{get_pending_admin, get_role_account}}}; +use crate::access_control::specs::constructor_helper::{before_constructor_no_has_role, before_constructor_no_role_accounts, before_constructor_no_role_count}; + +// invariant: admin != None -> holds in all cases except for renounce_admin + +// helpers +pub fn assume_pre_admin_is_set(e: Env) { + let admin_pre = AccessControlContract::get_admin(&e); + cvlr_assume!(admin_pre.is_some()); +} + +pub fn assert_post_admin_is_set(e: Env) { + let admin_post = AccessControlContract::get_admin(&e); + cvlr_assert!(admin_post.is_some()); +} + +#[rule] +// status: verified +pub fn after_constructor_admin_is_set(e: Env) { + let admin = nondet_address(); + AccessControlContract::access_control_constructor(&e, admin); + assert_post_admin_is_set(e); +} + +#[rule] +// status: verified +pub fn after_constructor_admin_is_set_sanity(e: Env) { + let admin = nondet_address(); + AccessControlContract::access_control_constructor(&e, admin); + cvlr_satisfy!(true); +} + +#[rule] +// status: verified +pub fn after_grant_role_admin_is_set(e: Env) { + assume_pre_admin_is_set(e.clone()); + let caller = nondet_address(); + let account = nondet_address(); + let role = nondet_symbol(); + AccessControlContract::grant_role(&e, caller, account, role); + assert_post_admin_is_set(e); +} + +#[rule] +// status: verified +pub fn after_grant_role_admin_is_set_sanity(e: Env) { + assume_pre_admin_is_set(e.clone()); + let caller = nondet_address(); + let account = nondet_address(); + let role = nondet_symbol(); + AccessControlContract::grant_role(&e, caller, account, role); + cvlr_satisfy!(true); +} + +#[rule] +// status: verified +pub fn after_revoke_role_admin_is_set(e: Env) { + assume_pre_admin_is_set(e.clone()); + let caller = nondet_address(); + let account = nondet_address(); + let role = nondet_symbol(); + AccessControlContract::revoke_role(&e, caller, account, role); + assert_post_admin_is_set(e); +} + +#[rule] +// status: verified +pub fn after_revoke_role_admin_is_set_sanity(e: Env) { + assume_pre_admin_is_set(e.clone()); + let caller = nondet_address(); + let account = nondet_address(); + let role = nondet_symbol(); + AccessControlContract::revoke_role(&e, caller, account, role); + cvlr_satisfy!(true); +} + +#[rule] +// status: verified +pub fn after_renounce_role_admin_is_set(e: Env) { + assume_pre_admin_is_set(e.clone()); + let caller = nondet_address(); + let role = nondet_symbol(); + AccessControlContract::renounce_role(&e, caller, role); + assert_post_admin_is_set(e); +} + +#[rule] +// status: verified +pub fn after_renounce_role_admin_is_set_sanity(e: Env) { + assume_pre_admin_is_set(e.clone()); + let caller = nondet_address(); + let role = nondet_symbol(); + AccessControlContract::renounce_role(&e, caller, role); + cvlr_satisfy!(true); +} + +#[rule] +// status: verified +pub fn after_transfer_admin_role_admin_is_set(e: Env) { + assume_pre_admin_is_set(e.clone()); + let new_admin = nondet_address(); + let live_until_ledger = u32::nondet(); + AccessControlContract::transfer_admin_role(&e, new_admin, live_until_ledger); + assert_post_admin_is_set(e); +} + +#[rule] +// status: verified +pub fn after_transfer_admin_role_admin_is_set_sanity(e: Env) { + assume_pre_admin_is_set(e.clone()); + let new_admin = nondet_address(); + let live_until_ledger = u32::nondet(); + AccessControlContract::transfer_admin_role(&e, new_admin, live_until_ledger); + cvlr_satisfy!(true); +} + +#[rule] +// status: verified +pub fn after_accept_admin_transfer_admin_is_set(e: Env) { + assume_pre_admin_is_set(e.clone()); + AccessControlContract::accept_admin_transfer(&e); + assert_post_admin_is_set(e); +} + +#[rule] +// status: verified +pub fn after_accept_admin_transfer_admin_is_set_sanity(e: Env) { + assume_pre_admin_is_set(e.clone()); + AccessControlContract::accept_admin_transfer(&e); + cvlr_satisfy!(true); +} + +#[rule] +// status: verified +pub fn after_set_role_admin_admin_is_set(e: Env) { + assume_pre_admin_is_set(e.clone()); + let role = nondet_symbol(); + let admin_role = nondet_symbol(); + AccessControlContract::set_role_admin(&e, role, admin_role); + assert_post_admin_is_set(e); +} + +#[rule] +// status: verified +pub fn after_set_role_admin_admin_is_set_sanity(e: Env) { + assume_pre_admin_is_set(e.clone()); + let role = nondet_symbol(); + let admin_role = nondet_symbol(); + AccessControlContract::set_role_admin(&e, role, admin_role); + cvlr_satisfy!(true); +} + +// for the case renonuce_admin it's obviously true - and expected + +// invariant: pending_admin != none implies admin != none + +// helpers +pub fn assume_pre_pending_admin_implies_admin(e: &Env) { + let pending_admin_pre = get_pending_admin(&e); + let admin = AccessControlContract::get_admin(&e); + if pending_admin_pre.is_some() { + cvlr_assume!(admin.is_some()); + } +} + +pub fn assert_post_pending_admin_implies_admin(e: &Env) { + let pending_admin_post = get_pending_admin(&e); + let admin = AccessControlContract::get_admin(&e); + if pending_admin_post.is_some() { + cvlr_assert!(admin.is_some()); + } +} + +#[rule] +// status: verified +pub fn after_constructor_pending_admin_implies_admin(e: Env) { + let admin = nondet_address(); + AccessControlContract::access_control_constructor(&e, admin); + assert_post_pending_admin_implies_admin(&e); +} + +#[rule] +// status: verified +pub fn after_constructor_pending_admin_implies_admin_sanity(e: Env) { + let admin = nondet_address(); + AccessControlContract::access_control_constructor(&e, admin); + cvlr_satisfy!(true); +} + +#[rule] +// status: verified +pub fn after_grant_role_pending_admin_implies_admin(e: Env) { + assume_pre_pending_admin_implies_admin(&e); + let caller = nondet_address(); + let account = nondet_address(); + let role = nondet_symbol(); + AccessControlContract::grant_role(&e, caller, account, role); + assert_post_pending_admin_implies_admin(&e); +} + +#[rule] +// status: verified +pub fn after_grant_role_pending_admin_implies_admin_sanity(e: Env) { + assume_pre_pending_admin_implies_admin(&e); + let caller = nondet_address(); + let account = nondet_address(); + let role = nondet_symbol(); + AccessControlContract::grant_role(&e, caller, account, role); + cvlr_satisfy!(true); +} + +#[rule] +// status: verified +pub fn after_revoke_role_pending_admin_implies_admin(e: Env) { + assume_pre_pending_admin_implies_admin(&e); + let caller = nondet_address(); + let account = nondet_address(); + let role = nondet_symbol(); + AccessControlContract::revoke_role(&e, caller, account, role); + assert_post_pending_admin_implies_admin(&e); +} + +#[rule] +// status: verified +pub fn after_revoke_role_pending_admin_implies_admin_sanity(e: Env) { + assume_pre_pending_admin_implies_admin(&e); + let caller = nondet_address(); + let account = nondet_address(); + let role = nondet_symbol(); + AccessControlContract::revoke_role(&e, caller, account, role); + cvlr_satisfy!(true); +} + +#[rule] +// status: verified +pub fn after_renounce_role_pending_admin_implies_admin(e: Env) { + assume_pre_pending_admin_implies_admin(&e); + let caller = nondet_address(); + let role = nondet_symbol(); + AccessControlContract::renounce_role(&e, caller, role); + assert_post_pending_admin_implies_admin(&e); +} + +#[rule] +// status: verified +pub fn after_renounce_role_pending_admin_implies_admin_sanity(e: Env) { + assume_pre_pending_admin_implies_admin(&e); + let caller = nondet_address(); + let role = nondet_symbol(); + AccessControlContract::renounce_role(&e, caller, role); + cvlr_satisfy!(true); +} + +#[rule] +// status: verified +pub fn after_transfer_admin_role_pending_admin_implies_admin(e: Env) { + assume_pre_pending_admin_implies_admin(&e); + let new_admin = nondet_address(); + let live_until_ledger = u32::nondet(); + AccessControlContract::transfer_admin_role(&e, new_admin, live_until_ledger); + assert_post_pending_admin_implies_admin(&e); +} + +#[rule] +// status: verified +pub fn after_transfer_admin_role_pending_admin_implies_admin_sanity(e: Env) { + assume_pre_pending_admin_implies_admin(&e); + let new_admin = nondet_address(); + let live_until_ledger = u32::nondet(); + AccessControlContract::transfer_admin_role(&e, new_admin, live_until_ledger); + cvlr_satisfy!(true); +} + +#[rule] +// status: verified +pub fn after_accept_admin_transfer_pending_admin_implies_admin(e: Env) { + assume_pre_pending_admin_implies_admin(&e); + AccessControlContract::accept_admin_transfer(&e); + assert_post_pending_admin_implies_admin(&e); +} + +#[rule] +// status: verified +pub fn after_accept_admin_transfer_pending_admin_implies_admin_sanity(e: Env) { + assume_pre_pending_admin_implies_admin(&e); + AccessControlContract::accept_admin_transfer(&e); + cvlr_satisfy!(true); +} + +#[rule] +// status: verified +pub fn after_set_role_admin_pending_admin_implies_admin(e: Env) { + assume_pre_pending_admin_implies_admin(&e); + let role = nondet_symbol(); + let admin_role = nondet_symbol(); + AccessControlContract::set_role_admin(&e, role, admin_role); + assert_post_pending_admin_implies_admin(&e); +} + +#[rule] +// status: verified +pub fn after_set_role_admin_pending_admin_implies_admin_sanity(e: Env) { + assume_pre_pending_admin_implies_admin(&e); + let role = nondet_symbol(); + let admin_role = nondet_symbol(); + AccessControlContract::set_role_admin(&e, role, admin_role); + cvlr_satisfy!(true); +} + +#[rule] +// status: bug +pub fn after_renounce_admin_pending_admin_implies_admin(e: Env) { + assume_pre_pending_admin_implies_admin(&e); + AccessControlContract::renounce_admin(&e); + assert_post_pending_admin_implies_admin(&e); +} + +#[rule] +// status: verified +pub fn after_renounce_admin_pending_admin_implies_admin_sanity(e: Env) { + assume_pre_pending_admin_implies_admin(&e); + AccessControlContract::renounce_admin(&e); + cvlr_satisfy!(true); +} + +// invariant: the index of two different accounts with the same role is different + +// helpers +pub fn assume_pre_unique_indices_for_role( + e: &Env, account1: Address, account2: Address, role: Symbol +) { + clog!(cvlr_soroban::Addr(&account1)); + clog!(cvlr_soroban::Addr(&account2)); + let index1 = AccessControlContract::has_role(&e, account1.clone(), role.clone()); + let index2 = AccessControlContract::has_role(&e, account2.clone(), role.clone()); + clog!(index1); + clog!(index2); + if index1.is_some() && index2.is_some() && account1 != account2 { + cvlr_assume!(index1.unwrap() != index2.unwrap()); + } +} + +pub fn assert_post_unique_indices_for_role( + e: &Env, account1: Address, account2: Address, role: Symbol +) { + clog!(cvlr_soroban::Addr(&account1)); + clog!(cvlr_soroban::Addr(&account2)); + let index1 = AccessControlContract::has_role(&e, account1.clone(), role.clone()); + let index2 = AccessControlContract::has_role(&e, account2.clone(), role.clone()); + clog!(index1); + clog!(index2); + if index1.is_some() && index2.is_some() && account1 != account2 { + cvlr_assert!(index1.unwrap() != index2.unwrap()); + } +} + +// didn't do sanity for these. + +#[rule] +// status: verified +pub fn after_constructor_unique_indices_for_role( + e: Env, account1: Address, account2: Address, role: Symbol +) { + before_constructor_no_has_role(&e, account1.clone(), role.clone()); + before_constructor_no_has_role(&e, account2.clone(), role.clone()); + let admin: Address = nondet_address(); + clog!(cvlr_soroban::Addr(&admin)); + AccessControlContract::access_control_constructor(&e, admin); + assert_post_unique_indices_for_role(&e, account1.clone(), account2.clone(), role.clone()); +} + +#[rule] +// status: verified +pub fn after_grant_role_unique_indices_for_role( + e: Env, account1: Address, account2: Address, role: Symbol +) { + assume_pre_unique_indices_for_role(&e, account1.clone(), account2.clone(), role.clone()); + assume_pre_role_count_minus_one_geq_index(&e, account1.clone(), role.clone()); + assume_pre_role_count_minus_one_geq_index(&e, account2.clone(), role.clone()); + let caller: Address = nondet_address(); + clog!(cvlr_soroban::Addr(&caller)); + let account = nondet_address(); + clog!(cvlr_soroban::Addr(&account)); + let role_granted = nondet_symbol(); + AccessControlContract::grant_role(&e, caller, account, role_granted); + assert_post_unique_indices_for_role(&e, account1.clone(), account2.clone(), role.clone()); +} + +#[rule] +// status: verified +pub fn after_revoke_role_unique_indices_for_role( + e: Env, account1: Address, account2: Address, role: Symbol +) { + assume_pre_unique_indices_for_role(&e, account1.clone(), account2.clone(), role.clone()); + let caller: Address = nondet_address(); + clog!(cvlr_soroban::Addr(&caller)); + let account = nondet_address(); + clog!(cvlr_soroban::Addr(&account)); + let role_revoked = nondet_symbol(); + assume_pre_unique_indices_for_role(&e, account1.clone(), account.clone(), role.clone()); + assume_pre_unique_indices_for_role(&e, account2.clone(), account.clone(), role.clone()); + assume_pre_role_count_minus_one_geq_index(&e, account.clone(), role.clone()); + AccessControlContract::revoke_role(&e, caller, account, role_revoked); + assert_post_unique_indices_for_role(&e, account1.clone(), account2.clone(), role.clone()); +} + +#[rule] +// status: verified +pub fn after_renounce_role_unique_indices_for_role( + e: Env, account1: Address, account2: Address, role: Symbol +) { + assume_pre_unique_indices_for_role(&e, account1.clone(), account2.clone(), role.clone()); + let caller = nondet_address(); + clog!(cvlr_soroban::Addr(&caller)); + let role_renounced = nondet_symbol(); + assume_pre_unique_indices_for_role(&e, account1.clone(), caller.clone(), role.clone()); + assume_pre_unique_indices_for_role(&e, account2.clone(), caller.clone(), role.clone()); + assume_pre_role_count_minus_one_geq_index(&e, caller.clone(), role.clone()); + AccessControlContract::renounce_role(&e, caller, role_renounced); + assert_post_unique_indices_for_role(&e, account1.clone(), account2.clone(), role.clone()); +} + +#[rule] +// status: verified +pub fn after_transfer_admin_role_unique_indices_for_role( + e: Env, account1: Address, account2: Address, role: Symbol +) { + assume_pre_unique_indices_for_role(&e, account1.clone(), account2.clone(), role.clone()); + let new_admin = nondet_address(); + clog!(cvlr_soroban::Addr(&new_admin)); + let live_until_ledger = u32::nondet(); + clog!(live_until_ledger); + AccessControlContract::transfer_admin_role(&e, new_admin, live_until_ledger); + assert_post_unique_indices_for_role(&e, account1.clone(), account2.clone(), role.clone()); +} + +#[rule] +// status: verified +pub fn after_accept_admin_transfer_unique_indices_for_role( + e: Env, account1: Address, account2: Address, role: Symbol +) { + assume_pre_unique_indices_for_role(&e, account1.clone(), account2.clone(), role.clone()); + AccessControlContract::accept_admin_transfer(&e); + assert_post_unique_indices_for_role(&e, account1.clone(), account2.clone(), role.clone()); +} + +#[rule] +// status: verified +pub fn after_set_role_admin_unique_indices_for_role( + e: Env, account1: Address, account2: Address, role: Symbol +) { + assume_pre_unique_indices_for_role(&e, account1.clone(), account2.clone(), role.clone()); + let role_admin = nondet_symbol(); + let role_treated = nondet_symbol(); + AccessControlContract::set_role_admin(&e, role_treated, role_admin); + assert_post_unique_indices_for_role(&e, account1.clone(), account2.clone(), role.clone()); +} + +#[rule] +// status: verified +pub fn after_renounce_admin_unique_indices_for_role( + e: Env, account1: Address, account2: Address, role: Symbol +) { + assume_pre_unique_indices_for_role(&e, account1.clone(), account2.clone(), role.clone()); + AccessControlContract::renounce_admin(&e); + assert_post_unique_indices_for_role(&e, account1.clone(), account2.clone(), role.clone()); +} + +// invariant role_count - 1 >= has_role(address) (for any address) + +// helpers +pub fn assume_pre_role_count_minus_one_geq_index( + e: &Env, account: Address, role: Symbol +) { + clog!(cvlr_soroban::Addr(&account)); + let role_count = AccessControlContract::get_role_member_count(&e, role.clone()); + clog!(role_count); + let index = AccessControlContract::has_role(&e, account.clone(), role.clone()); + clog!(index); + if index.is_some() { + cvlr_assume!(role_count - 1 >= index.unwrap()); + } +} + +pub fn assert_post_role_count_minus_one_geq_index( + e: &Env, account: Address, role: Symbol +) { + clog!(cvlr_soroban::Addr(&account)); + let role_count = AccessControlContract::get_role_member_count(&e, role.clone()); + clog!(role_count); + let index = AccessControlContract::has_role(&e, account.clone(), role.clone()); + clog!(index); + if index.is_some() { + cvlr_assert!(role_count - 1 >= index.unwrap()); + } +} + +#[rule] +// status: verified +pub fn after_constructor_role_count_minus_one_geq_index( + e: Env, account: Address, role: Symbol +) { + before_constructor_no_has_role(&e, account.clone(), role.clone()); + before_constructor_no_role_count(&e, &role); + let admin = nondet_address(); + clog!(cvlr_soroban::Addr(&admin)); + AccessControlContract::access_control_constructor(&e, admin); + assert_post_role_count_minus_one_geq_index(&e, account.clone(), role.clone()); +} + +#[rule] +// status: verified +pub fn after_grant_role_role_count_minus_one_geq_index( + e: Env, account: Address, role: Symbol +) { + assume_pre_role_count_minus_one_geq_index(&e, account.clone(), role.clone()); + let caller = nondet_address(); + clog!(cvlr_soroban::Addr(&caller)); + let account_granted = nondet_address(); + clog!(cvlr_soroban::Addr(&account)); + let role_granted = nondet_symbol(); + AccessControlContract::grant_role(&e, caller, account_granted, role_granted); + assert_post_role_count_minus_one_geq_index(&e, account.clone(), role.clone()); +} + +#[rule] +// status: violated - spurious (i think) +// see below for renounce_role +pub fn after_revoke_role_role_count_minus_one_geq_index( + e: Env, account: Address, role: Symbol +) { + assume_pre_role_count_minus_one_geq_index(&e, account.clone(), role.clone()); + let caller = nondet_address(); + clog!(cvlr_soroban::Addr(&caller)); + let account_revoked = nondet_address(); + clog!(cvlr_soroban::Addr(&account_revoked)); + let role_revoked = nondet_symbol(); + assume_pre_role_count_minus_one_geq_index(&e, account_revoked.clone(), role_revoked.clone()); // like requireInvariant in CVL + AccessControlContract::revoke_role(&e, caller, account_revoked, role_revoked); + assert_post_role_count_minus_one_geq_index(&e, account.clone(), role.clone()); +} + +#[rule] +// status: violated - not sure +// https://prover.certora.com/output/5771024/b3daf131f5ea41d69ebbd2684ce3520b/?anonymousKey=94c303fa729eabe38df7e5ffca4f30e736a2aba2¶ms=%7B%2225%22%3A%7B%22index%22%3A0%2C%22ruleCounterExamples%22%3A%5B%7B%22name%22%3A%22rule_output_19.json%22%2C%22selectedRepresentation%22%3A%7B%22label%22%3A%22PRETTY%22%2C%22value%22%3A0%7D%2C%22callResolutionSingleFilter%22%3A%22%22%2C%22variablesFilter%22%3A%22%22%2C%22callTraceFilter%22%3A%22%22%2C%22variablesOpenItems%22%3A%5Btrue%2Ctrue%5D%2C%22callTraceCollapsed%22%3Atrue%2C%22rightSidePanelCollapsed%22%3Afalse%2C%22rightSideTab%22%3A%22%22%2C%22callResolutionSingleCollapsed%22%3Atrue%2C%22viewStorage%22%3Atrue%2C%22variablesExpandedArray%22%3A%22%22%2C%22expandedArray%22%3A%22509-10-12-186-1-1-1248-1-1319_320-1360_361-1-1508%22%2C%22orderVars%22%3A%5B%22%22%2C%22%22%2C0%5D%2C%22orderParams%22%3A%5B%22%22%2C%22%22%2C0%5D%2C%22scrollNode%22%3A%2288%22%2C%22currentPoint%22%3A0%2C%22trackingChildren%22%3A%5B%5D%2C%22trackingParents%22%3A%5B%5D%2C%22trackingOnly%22%3Afalse%2C%22highlightOnly%22%3Afalse%2C%22filterPosition%22%3A0%2C%22singleCallResolutionOpen%22%3A%5B%5D%2C%22snap_drop_1%22%3Anull%2C%22snap_drop_2%22%3Anull%2C%22snap_filter%22%3A%22%22%7D%5D%7D%7D&generalState=%7B%22fileViewOpen%22%3Afalse%2C%22fileViewCollapsed%22%3Atrue%2C%22mainTreeViewCollapsed%22%3Atrue%2C%22callTraceClosed%22%3Afalse%2C%22mainSideNavItem%22%3A%22rules%22%2C%22globalResSelected%22%3Afalse%2C%22isSideBarCollapsed%22%3Afalse%2C%22isRightSideBarCollapsed%22%3Atrue%2C%22selectedFile%22%3A%7B%7D%2C%22fileViewFilter%22%3A%22%22%2C%22mainTreeViewFilter%22%3A%22%22%2C%22contractsFilter%22%3A%22%22%2C%22globalCallResolutionFilter%22%3A%22%22%2C%22currentRuleUiId%22%3A25%2C%22counterExamplePos%22%3A1%2C%22expandedKeysState%22%3A%2224-10-1-1-1-1-1-116-118-123-1-1%22%2C%22expandedFilesState%22%3A%5B%5D%2C%22outlinedfilterShared%22%3A%22000000000%22%7D +pub fn after_renounce_role_role_count_minus_one_geq_index( + e: Env, account: Address, role: Symbol +) { + assume_pre_role_count_minus_one_geq_index(&e, account.clone(), role.clone()); + let caller = nondet_address(); + clog!(cvlr_soroban::Addr(&caller)); + let role_renounced = nondet_symbol(); + assume_pre_role_count_minus_one_geq_index(&e, caller.clone(), role_renounced.clone()); // like requireInvariant in CVL + assume_pre_unique_indices_for_role(&e, caller.clone(), account.clone(), role.clone()); + assume_pre_has_role_index_implies_get_role_account(&e, caller.clone(), role.clone()); + AccessControlContract::renounce_role(&e, caller, role_renounced); + assert_post_role_count_minus_one_geq_index(&e, account.clone(), role.clone()); +} + +#[rule] +// status: verified +pub fn after_transfer_admin_role_role_count_minus_one_geq_index( + e: Env, account: Address, role: Symbol +) { + assume_pre_role_count_minus_one_geq_index(&e, account.clone(), role.clone()); + let new_admin = nondet_address(); + clog!(cvlr_soroban::Addr(&new_admin)); + let live_until_ledger = u32::nondet(); + clog!(live_until_ledger); + AccessControlContract::transfer_admin_role(&e, new_admin, live_until_ledger); + assert_post_role_count_minus_one_geq_index(&e, account.clone(), role.clone()); +} + +#[rule] +// status: verified +pub fn after_accept_admin_transfer_role_count_minus_one_geq_index( + e: Env, account: Address, role: Symbol +) { + assume_pre_role_count_minus_one_geq_index(&e, account.clone(), role.clone()); + AccessControlContract::accept_admin_transfer(&e); + assert_post_role_count_minus_one_geq_index(&e, account.clone(), role.clone()); +} + +#[rule] +// status: verified +pub fn after_set_role_admin_role_count_minus_one_geq_index( + e: Env, account: Address, role: Symbol +) { + assume_pre_role_count_minus_one_geq_index(&e, account.clone(), role.clone()); + let role_admin = nondet_symbol(); + let role_treated = nondet_symbol(); + AccessControlContract::set_role_admin(&e, role_treated, role_admin); + assert_post_role_count_minus_one_geq_index(&e, account.clone(), role.clone()); +} + +#[rule] +// status: verified +pub fn after_renounce_admin_role_count_minus_one_geq_index( + e: Env, account: Address, role: Symbol +) { + assume_pre_role_count_minus_one_geq_index(&e, account.clone(), role.clone()); + AccessControlContract::renounce_admin(&e); + assert_post_role_count_minus_one_geq_index(&e, account.clone(), role.clone()); +} + +// you would also want Exists(address). has_role(address) = role_count - 1 but not supported. + +// invariant: if has_role(account,role) = index then get_account_role(role,index) = account + +// helpers +pub fn assume_pre_has_role_index_implies_get_role_account( + e: &Env, account: Address, role: Symbol +) { + clog!(cvlr_soroban::Addr(&account)); + let index = AccessControlContract::has_role(&e, account.clone(), role.clone()); + clog!(index); + if index.is_some() { + let account_with_index = get_role_account(&e, &role, index.unwrap()); + if let Some(account_with_index_internal) = account_with_index.clone() { + clog!(cvlr_soroban::Addr(&account_with_index_internal)); + } + cvlr_assume!(account_with_index == Some(account)); + } +} + +pub fn assert_post_has_role_index_implies_get_role_account( + e: &Env, account: Address, role: Symbol +) { + clog!(cvlr_soroban::Addr(&account)); + let index = AccessControlContract::has_role(&e, account.clone(), role.clone()); + clog!(index); + if index.is_some() { + let account_with_index = get_role_account(&e, &role, index.unwrap()); + if let Some(account_with_index_internal) = account_with_index.clone() { + clog!(cvlr_soroban::Addr(&account_with_index_internal)); + } + cvlr_assert!(account_with_index == Some(account)); + } +} + + +#[rule] +// status: verified +pub fn after_constructor_has_role_index_implies_get_role_account( + e: Env, account: Address, role: Symbol +) { + before_constructor_no_has_role(&e, account.clone(), role.clone()); + before_constructor_no_role_accounts(&e, role.clone(), 0); + let admin = nondet_address(); + clog!(cvlr_soroban::Addr(&admin)); + AccessControlContract::access_control_constructor(&e, admin); + assert_post_role_count_minus_one_geq_index(&e, account.clone(), role.clone()); +} + +#[rule] +// status: spurious - prover bug ? Some(0) = Some(11) +// https://prover.certora.com/output/5771024/6e270c43416b4137b3fc221758f6cf47/?anonymousKey=cc464cb85345dd06596fdecb09d7f7414369e434¶ms=%7B%2212%22%3A%7B%22index%22%3A0%2C%22ruleCounterExamples%22%3A%5B%7B%22name%22%3A%22rule_output_3.json%22%2C%22selectedRepresentation%22%3A%7B%22label%22%3A%22PRETTY%22%2C%22value%22%3A0%7D%2C%22callResolutionSingleFilter%22%3A%22%22%2C%22variablesFilter%22%3A%22%22%2C%22callTraceFilter%22%3A%22%22%2C%22variablesOpenItems%22%3A%5Btrue%2Ctrue%5D%2C%22callTraceCollapsed%22%3Atrue%2C%22rightSidePanelCollapsed%22%3Afalse%2C%22rightSideTab%22%3A%22%22%2C%22callResolutionSingleCollapsed%22%3Atrue%2C%22viewStorage%22%3Atrue%2C%22variablesExpandedArray%22%3A%22%22%2C%22expandedArray%22%3A%22208-10-12-1-1-1-1-1-1-1-1-1207%22%2C%22orderVars%22%3A%5B%22%22%2C%22%22%2C0%5D%2C%22orderParams%22%3A%5B%22%22%2C%22%22%2C0%5D%2C%22scrollNode%22%3A%221%22%2C%22currentPoint%22%3A0%2C%22trackingChildren%22%3A%5B%5D%2C%22trackingParents%22%3A%5B%5D%2C%22trackingOnly%22%3Afalse%2C%22highlightOnly%22%3Afalse%2C%22filterPosition%22%3A0%2C%22singleCallResolutionOpen%22%3A%5B%5D%2C%22snap_drop_1%22%3Anull%2C%22snap_drop_2%22%3Anull%2C%22snap_filter%22%3A%22%22%7D%5D%7D%7D&generalState=%7B%22fileViewOpen%22%3Afalse%2C%22fileViewCollapsed%22%3Atrue%2C%22mainTreeViewCollapsed%22%3Atrue%2C%22callTraceClosed%22%3Afalse%2C%22mainSideNavItem%22%3A%22rules%22%2C%22globalResSelected%22%3Afalse%2C%22isSideBarCollapsed%22%3Afalse%2C%22isRightSideBarCollapsed%22%3Atrue%2C%22selectedFile%22%3A%7B%7D%2C%22fileViewFilter%22%3A%22%22%2C%22mainTreeViewFilter%22%3A%22%22%2C%22contractsFilter%22%3A%22%22%2C%22globalCallResolutionFilter%22%3A%22%22%2C%22currentRuleUiId%22%3A12%2C%22counterExamplePos%22%3A1%2C%22expandedKeysState%22%3A%228-10-1-02-03-04-1-1-1-08-1-1%22%2C%22expandedFilesState%22%3A%5B%5D%2C%22outlinedfilterShared%22%3A%22000000000%22%7D +pub fn after_grant_role_has_role_index_implies_get_role_account( + e: Env, account: Address, role: Symbol +) { + assume_pre_has_role_index_implies_get_role_account(&e, account.clone(), role.clone()); + let caller = nondet_address(); + clog!(cvlr_soroban::Addr(&caller)); + let account_granted = nondet_address(); + clog!(cvlr_soroban::Addr(&account)); + let role_granted = nondet_symbol(); + AccessControlContract::grant_role(&e, caller, account_granted, role_granted); + assert_post_has_role_index_implies_get_role_account(&e, account.clone(), role.clone()); +} + +#[rule] +// status: verified +// 9 minute timeout +pub fn after_revoke_role_has_role_index_implies_get_role_account( + e: Env, account: Address, role: Symbol +) { + assume_pre_has_role_index_implies_get_role_account(&e, account.clone(), role.clone()); + let caller = nondet_address(); + clog!(cvlr_soroban::Addr(&caller)); + let account_revoked = nondet_address(); + clog!(cvlr_soroban::Addr(&account_revoked)); + let role_revoked = nondet_symbol(); + assume_pre_unique_indices_for_role(&e, account.clone(), account_revoked.clone(), role.clone()); // like requireInvariant in CVL + AccessControlContract::revoke_role(&e, caller, account_revoked, role_revoked); + assert_post_has_role_index_implies_get_role_account(&e, account.clone(), role.clone()); +} + +#[rule] +// status: verified +pub fn after_renounce_role_has_role_index_implies_get_role_account( + e: Env, account: Address, role: Symbol +) { + assume_pre_has_role_index_implies_get_role_account(&e, account.clone(), role.clone()); + let caller = nondet_address(); + clog!(cvlr_soroban::Addr(&caller)); + let role_renounced = nondet_symbol(); + assume_pre_unique_indices_for_role(&e, account.clone(), caller.clone(), role.clone()); // like requireInvariant in CVL + AccessControlContract::renounce_role(&e, caller, role_renounced); + assert_post_has_role_index_implies_get_role_account(&e, account.clone(), role.clone()); +} + +#[rule] +// status: verified +pub fn after_transfer_admin_role_has_role_index_implies_get_role_account( + e: Env, account: Address, role: Symbol +) { + assume_pre_has_role_index_implies_get_role_account(&e, account.clone(), role.clone()); + let new_admin = nondet_address(); + clog!(cvlr_soroban::Addr(&new_admin)); + let live_until_ledger = u32::nondet(); + clog!(live_until_ledger); + AccessControlContract::transfer_admin_role(&e, new_admin, live_until_ledger); + assert_post_has_role_index_implies_get_role_account(&e, account.clone(), role.clone()); +} + +#[rule] +// status: verified +pub fn after_accept_admin_transfer_has_role_index_implies_get_role_account( + e: Env, account: Address, role: Symbol +) { + assume_pre_has_role_index_implies_get_role_account(&e, account.clone(), role.clone()); + AccessControlContract::accept_admin_transfer(&e); + assert_post_has_role_index_implies_get_role_account(&e, account.clone(), role.clone()); +} + +#[rule] +// status: verified +pub fn after_set_role_admin_has_role_index_implies_get_role_account( + e: Env, account: Address, role: Symbol +) { + assume_pre_has_role_index_implies_get_role_account(&e, account.clone(), role.clone()); + let role_admin = nondet_symbol(); + let role_treated = nondet_symbol(); + AccessControlContract::set_role_admin(&e, role_treated, role_admin); + assert_post_has_role_index_implies_get_role_account(&e, account.clone(), role.clone()); +} + +#[rule] +// status: verified +pub fn after_renounce_admin_has_role_index_implies_get_role_account( + e: Env, account: Address, role: Symbol +) { + assume_pre_has_role_index_implies_get_role_account(&e, account.clone(), role.clone()); + AccessControlContract::renounce_admin(&e); + assert_post_has_role_index_implies_get_role_account(&e, account.clone(), role.clone()); +} diff --git a/packages/access/src/access_control/specs/access_control_non_panics.rs b/packages/access/src/access_control/specs/access_control_non_panics.rs new file mode 100644 index 00000000..0a0869f6 --- /dev/null +++ b/packages/access/src/access_control/specs/access_control_non_panics.rs @@ -0,0 +1,432 @@ + +use cvlr::{cvlr_assert, cvlr_assume,cvlr_satisfy}; +use cvlr_soroban::{nondet_address, nondet_symbol, is_auth}; +use cvlr::nondet::{Nondet, nondet}; +use cvlr_soroban_derive::rule; +use cvlr::clog; + +use crate::access_control::storage::{AccessControlStorageKey, RoleAccountKey}; +use soroban_sdk::{Env, Address, Symbol}; +use crate::access_control::{AccessControl, specs::{access_control_contract::AccessControlContract, helper::get_pending_admin}}; + +// These rules require the prover arg "prover_args": ["-trapAsAssert true"] to consider also panicking paths. + +// storage setup + +// im a bit unsure about storage setup in cases where there are options, +// is the case of None ignored the way we do this? + +pub fn storage_setup_admin(e: Env) { + let admin = nondet_address(); + e.storage().instance().set(&AccessControlStorageKey::Admin, &admin); +} + +pub fn storage_setup_pending_admin(e: Env) { + let pending_admin = nondet_address(); + e.storage().temporary().set(&AccessControlStorageKey::PendingAdmin, &pending_admin); +} + +pub fn storage_setup_pending_admin_none(e: Env) { + let pending_admin: Option
= None::
; + e.storage().temporary().set(&AccessControlStorageKey::PendingAdmin, &pending_admin.clone()); +} + +pub fn storage_setup_role_admin(e: Env, role: Symbol) { + let role_admin_key: AccessControlStorageKey = AccessControlStorageKey::RoleAdmin(role.clone()); + let symbol = nondet_symbol(); + e.storage().persistent().set(&role_admin_key, &symbol); +} + +pub fn storage_setup_role_counts(e: Env, role: Symbol) { + let role_accounts_count_key: AccessControlStorageKey = AccessControlStorageKey::RoleAccountsCount(role.clone()); + let nondet_count : u32 = nondet(); + e.storage().persistent().set(&role_accounts_count_key, &nondet_count); +} + +pub fn storage_setup_account_has_role(e: Env, account: Address, role: Symbol) { + let has_role_key = AccessControlStorageKey::HasRole(account.clone(), role.clone()); + let nondet_index_account : u32 = nondet(); + e.storage().persistent().set(&has_role_key, &nondet_index_account); +} + +pub fn storage_setup_caller_has_role_admin(e: Env, caller: Address, role: Symbol) { + let role_admin = AccessControlContract::get_role_admin(&e, role.clone()); + if let Some(role_admin_internal) = role_admin { + let caller_has_role_admin_key = AccessControlStorageKey::HasRole(caller.clone(), role_admin_internal.clone()); + let nondet_index : u32 = nondet(); + e.storage().persistent().set(&caller_has_role_admin_key, &nondet_index); + } +} + +pub fn storage_setup_last_account(e: Env, role: Symbol) { + let count = AccessControlContract::get_role_member_count(&e, role.clone()); + let last_index = count - 1; + let last_key = AccessControlStorageKey::RoleAccounts(RoleAccountKey { + role: role.clone(), + index: last_index, + }); + let last_account = nondet_address(); + e.storage().persistent().set(&last_key, &last_account); +} + +// package functions + +#[rule] +// requires +// storage setup +// caller auth +// caller is admin or has admin role +// status: verified (13 minutes) +pub fn grant_role_non_panic(e: Env) { + let caller = nondet_address(); + let account = nondet_address(); + let role = nondet_symbol(); + + storage_setup_admin(e.clone()); + storage_setup_role_admin(e.clone(), role.clone()); + storage_setup_account_has_role(e.clone(), account.clone(), role.clone()); + storage_setup_caller_has_role_admin(e.clone(), caller.clone(), role.clone()); + + cvlr_assume!(is_auth(caller.clone())); + let admin = AccessControlContract::get_admin(&e); + let mut caller_equals_admin = false; + if let Some(admin_internal) = admin { + caller_equals_admin = caller.clone() == admin_internal; + } + let mut caller_has_role_admin = false; + let role_admin = AccessControlContract::get_role_admin(&e, role.clone()); + if let Some(role_admin_internal) = role_admin { + caller_has_role_admin = AccessControlContract::has_role(&e, caller.clone(), role_admin_internal).is_some(); + } + cvlr_assume!(caller_equals_admin || caller_has_role_admin); + AccessControlContract::grant_role(&e, caller, account, role); + cvlr_assert!(true); +} + +#[rule] +// sanity +// status: verified +pub fn grant_role_non_panic_sanity(e: Env) { + let caller = nondet_address(); + let account = nondet_address(); + let role = nondet_symbol(); + + storage_setup_admin(e.clone()); + storage_setup_role_admin(e.clone(), role.clone()); + storage_setup_role_counts(e.clone(), role.clone()); + storage_setup_account_has_role(e.clone(), account.clone(), role.clone()); + storage_setup_caller_has_role_admin(e.clone(), caller.clone(), role.clone()); + + cvlr_assume!(is_auth(caller.clone())); + let admin = AccessControlContract::get_admin(&e); + let mut caller_equals_admin = false; + if let Some(admin_internal) = admin { + caller_equals_admin = caller.clone() == admin_internal; + } + let mut caller_has_role_admin = false; + let role_admin = AccessControlContract::get_role_admin(&e, role.clone()); + if let Some(role_admin_internal) = role_admin { + caller_has_role_admin = AccessControlContract::has_role(&e, caller.clone(), role_admin_internal).is_some(); + } + cvlr_assume!(caller_equals_admin || caller_has_role_admin); + AccessControlContract::grant_role(&e, caller, account, role); + cvlr_satisfy!(true); +} + +#[rule] +// requires +// storage setup +// auth by caller +// caller is admin or has admin_role +// account has the role +// role is not empty +// status: verified +// when using -split false +pub fn revoke_role_non_panic(e: Env) { + let caller = nondet_address(); + let account = nondet_address(); + let role = nondet_symbol(); + + storage_setup_admin(e.clone()); + storage_setup_role_admin(e.clone(), role.clone()); + storage_setup_role_counts(e.clone(), role.clone()); + storage_setup_account_has_role(e.clone(), account.clone(), role.clone()); + storage_setup_caller_has_role_admin(e.clone(), caller.clone(), role.clone()); + storage_setup_last_account(e.clone(), role.clone()); + + cvlr_assume!(is_auth(caller.clone())); + let admin = AccessControlContract::get_admin(&e); + let mut caller_equals_admin = false; + if let Some(admin_internal) = admin { + caller_equals_admin = caller.clone() == admin_internal; + } + let mut caller_has_role_admin = false; + let role_admin = AccessControlContract::get_role_admin(&e, role.clone()); + if let Some(role_admin_internal) = role_admin { + caller_has_role_admin = AccessControlContract::has_role(&e, caller.clone(), role_admin_internal).is_some(); + } + cvlr_assume!(caller_equals_admin || caller_has_role_admin); + let account_has_role = AccessControlContract::has_role(&e, account.clone(), role.clone()); + cvlr_assume!(account_has_role.is_some()); + let role_member_count = AccessControlContract::get_role_member_count(&e, role.clone()); + cvlr_assume!(role_member_count > 0); + AccessControlContract::revoke_role(&e, caller, account, role); + cvlr_assert!(true); +} + +#[rule] +// sanity +// status: verified +pub fn revoke_role_non_panic_sanity(e: Env) { + let caller = nondet_address(); + let account = nondet_address(); + let role = nondet_symbol(); + + storage_setup_admin(e.clone()); + storage_setup_role_admin(e.clone(), role.clone()); + storage_setup_role_counts(e.clone(), role.clone()); + storage_setup_account_has_role(e.clone(), account.clone(), role.clone()); + storage_setup_caller_has_role_admin(e.clone(), caller.clone(), role.clone()); + storage_setup_last_account(e.clone(), role.clone()); + + cvlr_assume!(is_auth(caller.clone())); + let admin = AccessControlContract::get_admin(&e); + let mut caller_equals_admin = false; + if let Some(admin_internal) = admin { + caller_equals_admin = caller.clone() == admin_internal; + } + let mut caller_has_role_admin = false; + let role_admin = AccessControlContract::get_role_admin(&e, role.clone()); + if let Some(role_admin_internal) = role_admin { + caller_has_role_admin = AccessControlContract::has_role(&e, caller.clone(), role_admin_internal).is_some(); + } + cvlr_assume!(caller_equals_admin || caller_has_role_admin); + let account_has_role = AccessControlContract::has_role(&e, account.clone(), role.clone()); + cvlr_assume!(account_has_role.is_some()); + let role_member_count = AccessControlContract::get_role_member_count(&e, role.clone()); + cvlr_assume!(role_member_count > 0); + AccessControlContract::revoke_role(&e, caller, account, role); + cvlr_satisfy!(true); +} + +#[rule] +// requires +// storage setup +// auth by caller +// caller has the role +// role is not empty +// status: verified +pub fn renounce_role_non_panic(e: Env) { + let caller = nondet_address(); + let role = nondet_symbol(); + + storage_setup_role_counts(e.clone(), role.clone()); + storage_setup_account_has_role(e.clone(), caller.clone(), role.clone()); + storage_setup_last_account(e.clone(), role.clone()); + + cvlr_assume!(is_auth(caller.clone())); + let caller_has_role = AccessControlContract::has_role(&e, caller.clone(), role.clone()); + cvlr_assume!(caller_has_role.is_some()); + let role_member_count = AccessControlContract::get_role_member_count(&e, role.clone()); + cvlr_assume!(role_member_count > 0); + AccessControlContract::renounce_role(&e, caller, role); + cvlr_assert!(true); +} + +#[rule] +// sanity +// status: verified +pub fn renounce_role_non_panic_sanity(e: Env) { + let caller = nondet_address(); + let role = nondet_symbol(); + + storage_setup_role_counts(e.clone(), role.clone()); + storage_setup_account_has_role(e.clone(), caller.clone(), role.clone()); + storage_setup_last_account(e.clone(), role.clone()); + + cvlr_assume!(is_auth(caller.clone())); + let caller_has_role = AccessControlContract::has_role(&e, caller.clone(), role.clone()); + cvlr_assume!(caller_has_role.is_some()); + let role_member_count = AccessControlContract::get_role_member_count(&e, role.clone()); + cvlr_assume!(role_member_count > 0); + AccessControlContract::renounce_role(&e, caller, role); + cvlr_satisfy!(true); +} + +#[rule] +// requires +// storage setup +// admin exists +// admin auth +// if there is a pending owner they are the same +// live until ledger is appropriate +// status: verified +pub fn transfer_admin_role_non_panic(e: Env) { + let new_admin = nondet_address().clone(); + let live_until_ledger = u32::nondet(); + + storage_setup_pending_admin(e.clone()); + storage_setup_admin(e.clone()); + + let admin = AccessControlContract::get_admin(&e); + cvlr_assume!(admin.is_some()); + if let Some(admin_internal) = admin.clone() { + cvlr_assume!(is_auth(admin_internal)); + } + + let pending_admin = get_pending_admin(&e); + if let Some(pending_admin_internal) = pending_admin.clone() { + cvlr_assume!(pending_admin_internal == new_admin); + } + + if live_until_ledger == 0 { + cvlr_assume!(pending_admin.is_some()); + } + else { + cvlr_assume!(live_until_ledger >= e.ledger().sequence()); + cvlr_assume!(live_until_ledger <= e.ledger().max_live_until_ledger()); + } + + AccessControlContract::transfer_admin_role(&e, new_admin, live_until_ledger); + cvlr_assert!(true); +} + +#[rule] +// sanity +// status: verified +pub fn transfer_admin_role_non_panic_sanity(e: Env) { + let new_admin = nondet_address().clone(); + let live_until_ledger = u32::nondet(); + + storage_setup_pending_admin(e.clone()); + storage_setup_admin(e.clone()); + + let admin = AccessControlContract::get_admin(&e); + cvlr_assume!(admin.is_some()); + if let Some(admin_internal) = admin.clone() { + cvlr_assume!(is_auth(admin_internal)); + } + + let pending_admin = get_pending_admin(&e); + if let Some(pending_admin_internal) = pending_admin.clone() { + cvlr_assume!(pending_admin_internal == new_admin); + } + + if live_until_ledger == 0 { + cvlr_assume!(pending_admin.is_some()); + } + else { + cvlr_assume!(live_until_ledger >= e.ledger().sequence()); + cvlr_assume!(live_until_ledger <= e.ledger().max_live_until_ledger()); + } + + AccessControlContract::transfer_admin_role(&e, new_admin, live_until_ledger); + cvlr_satisfy!(true); +} + +#[rule] +// requires +// storage setup +// pending admin exists +// pending admin auth +// status: verified +pub fn accept_admin_transfer_non_panic(e: Env) { + + storage_setup_pending_admin(e.clone()); + storage_setup_admin(e.clone()); + + let pending_admin = get_pending_admin(&e); + cvlr_assume!(pending_admin.is_some()); + if let Some(pending_admin_internal) = pending_admin.clone() { + cvlr_assume!(is_auth(pending_admin_internal)); + } + AccessControlContract::accept_admin_transfer(&e); + cvlr_assert!(true); +} + +#[rule] +// sanity +// status: verified +pub fn accept_admin_transfer_non_panic_sanity(e: Env) { + storage_setup_pending_admin(e.clone()); + storage_setup_admin(e.clone()); + + let pending_admin = get_pending_admin(&e); + cvlr_assume!(pending_admin.is_some()); + if let Some(pending_admin_internal) = pending_admin.clone() { + cvlr_assume!(is_auth(pending_admin_internal)); + } + AccessControlContract::accept_admin_transfer(&e); + cvlr_satisfy!(true); +} + +#[rule] +// requires +// storage setup +// admin exists +// admin auth +// status: verified +pub fn set_role_admin_non_panic(e: Env) { + let role = nondet_symbol(); + let admin_role = nondet_symbol(); + storage_setup_admin(e.clone()); + storage_setup_role_admin(e.clone(), role.clone()); + let admin = AccessControlContract::get_admin(&e); + cvlr_assume!(admin.is_some()); + if let Some(admin_internal) = admin.clone() { + cvlr_assume!(is_auth(admin_internal)); + } + AccessControlContract::set_role_admin(&e, role.clone(), admin_role.clone()); + cvlr_assert!(true); +} + +#[rule] +// sanity +// status: verified +pub fn set_role_admin_non_panic_sanity(e: Env) { + let role = nondet_symbol(); + let admin_role = nondet_symbol(); + storage_setup_admin(e.clone()); + storage_setup_role_admin(e.clone(), role.clone()); + let admin = AccessControlContract::get_admin(&e); + cvlr_assume!(admin.is_some()); + if let Some(admin_internal) = admin.clone() { + cvlr_assume!(is_auth(admin_internal)); + } + AccessControlContract::set_role_admin(&e, role.clone(), admin_role.clone()); + cvlr_satisfy!(true); +} + +#[rule] +// requires +// storage setup +// admin exists +// admin auth +// no pending admin +// status: verified +pub fn renounce_admin_non_panic(e: Env) { + storage_setup_admin(e.clone()); + storage_setup_pending_admin_none(e.clone()); + let admin = AccessControlContract::get_admin(&e); + cvlr_assume!(admin.is_some()); + if let Some(admin_internal) = admin.clone() { + cvlr_assume!(is_auth(admin_internal)); + } + AccessControlContract::renounce_admin(&e); + cvlr_assert!(true); +} + +#[rule] +// sanity +// status: verified +pub fn renounce_admin_non_panic_sanity(e: Env) { + storage_setup_admin(e.clone()); + storage_setup_pending_admin_none(e.clone()); + let admin = AccessControlContract::get_admin(&e); + cvlr_assume!(admin.is_some()); + if let Some(admin_internal) = admin.clone() { + cvlr_assume!(is_auth(admin_internal)); + } + AccessControlContract::renounce_admin(&e); + cvlr_satisfy!(true); +} \ No newline at end of file diff --git a/packages/access/src/access_control/specs/access_control_panics.rs b/packages/access/src/access_control/specs/access_control_panics.rs new file mode 100644 index 00000000..15503e8d --- /dev/null +++ b/packages/access/src/access_control/specs/access_control_panics.rs @@ -0,0 +1,415 @@ + +use cvlr::{cvlr_assert, cvlr_assume,cvlr_satisfy}; +use cvlr_soroban::{nondet_address, nondet_symbol, is_auth}; +use cvlr::nondet::Nondet; +use cvlr_soroban_derive::rule; +use cvlr::clog; + +use soroban_sdk::{Env}; +use crate::access_control::{AccessControl, ensure_role, specs::{access_control_contract::AccessControlContract, helper::get_pending_admin}}; + + +// package functions + +#[rule] +// grant role panic if unauthorized by caller +// status: verified +pub fn grant_role_panics_if_caller_unauth(e: Env) { + let caller = nondet_address(); + let account = nondet_address(); + let role = nondet_symbol(); + cvlr_assume!(!is_auth(caller.clone())); + AccessControlContract::grant_role(&e, caller, account, role); + cvlr_assert!(false); +} + +#[rule] +// grant role panics if caller is not admin and not admin_role +// status: verified +pub fn grant_role_panics_if_caller_not_admin_nor_admin_role(e: Env) { + let caller = nondet_address(); + let account = nondet_address(); + let role = nondet_symbol(); + let role_admin = AccessControlContract::get_role_admin(&e, role.clone()); + if let Some(role_admin_internal) = role_admin { + let caller_has_role_admin = AccessControlContract::has_role(&e, caller.clone(), role_admin_internal); + cvlr_assume!(caller_has_role_admin.is_none()); + } + let admin = AccessControlContract::get_admin(&e); + if let Some(admin_internal) = admin { + cvlr_assume!(caller.clone()!= admin_internal); + } + AccessControlContract::grant_role(&e, caller.clone(), account, role.clone()); + cvlr_assert!(false); +} + +#[rule] +// revoke_role panics if unauthorized by caller +// status: verified +pub fn revoke_role_panics_if_caller_unauth(e: Env) { + let caller = nondet_address(); + let account = nondet_address(); + let role = nondet_symbol(); + cvlr_assume!(!is_auth(caller.clone())); + AccessControlContract::revoke_role(&e, caller, account, role); + cvlr_assert!(false); +} + +#[rule] +// revoke_role panics if caller is not admin and not admin_role +// status: verified +pub fn revoke_role_panics_if_caller_not_admin_nor_admin_role(e: Env) { + let caller = nondet_address(); + clog!(cvlr_soroban::Addr(&caller)); + let account = nondet_address(); + clog!(cvlr_soroban::Addr(&account)); + let role = nondet_symbol(); + let role_admin = AccessControlContract::get_role_admin(&e, role.clone()); + if let Some(role_admin_internal) = role_admin { + let caller_has_role_admin = AccessControlContract::has_role(&e, caller.clone(), role_admin_internal); + cvlr_assume!(caller_has_role_admin.is_none()); + clog!(caller_has_role_admin.unwrap()); + } + let admin = AccessControlContract::get_admin(&e); + if let Some(admin_internal) = admin { + cvlr_assume!(caller.clone()!= admin_internal); + clog!(cvlr_soroban::Addr(&admin_internal)); + } + AccessControlContract::revoke_role(&e, caller.clone(), account, role.clone()); + cvlr_assert!(false); +} + + +#[rule] +// revoke_role panics if account does not have the role +// status: verified +pub fn revoke_role_panics_if_account_does_not_have_role(e: Env) { + let caller = nondet_address(); + let account = nondet_address(); + let role = nondet_symbol(); + let account_has_role = AccessControlContract::has_role(&e, account.clone(), role.clone()); + cvlr_assume!(account_has_role.is_none()); + AccessControlContract::revoke_role(&e, caller.clone(), account, role.clone()); + cvlr_assert!(false); +} + + +#[rule] +// revoke_role panics if role is empty +// status: verified +pub fn revoke_role_panics_if_role_is_empty(e: Env) { + let caller = nondet_address(); + let account = nondet_address(); + let role = nondet_symbol(); + let role_member_count = AccessControlContract::get_role_member_count(&e, role.clone()); + cvlr_assume!(role_member_count == 0); + AccessControlContract::revoke_role(&e, caller.clone(), account, role.clone()); + cvlr_assert!(false); +} + +#[rule] +// renounce_role panics if unauthorized by caller +// status: verified +pub fn renounce_role_panics_if_caller_unauth(e: Env) { + let caller = nondet_address(); + let role = nondet_symbol(); + cvlr_assume!(!is_auth(caller.clone())); + AccessControlContract::renounce_role(&e, caller, role); + cvlr_assert!(false); +} + +#[rule] +// renounce_role panics if caller does not have the role +// status: verified +pub fn renounce_role_panics_if_caller_does_not_have_role(e: Env) { + let caller = nondet_address(); + clog!(cvlr_soroban::Addr(&caller)); + let role: soroban_sdk::Symbol = nondet_symbol(); + let caller_has_role: Option = AccessControlContract::has_role(&e, caller.clone(), role.clone()); + cvlr_assume!(caller_has_role.is_none()); + AccessControlContract::renounce_role(&e, caller.clone(), role.clone()); + cvlr_assert!(false); +} + +#[rule] +// renounce_role panics if role is empty +// status: verified +pub fn renounce_role_panics_if_role_is_empty(e: Env) { + let caller = nondet_address(); + let role = nondet_symbol(); + let role_member_count = AccessControlContract::get_role_member_count(&e, role.clone()); + cvlr_assume!(role_member_count == 0); + AccessControlContract::renounce_role(&e, caller.clone(), role.clone()); + cvlr_assert!(false); +} + +#[rule] +// transfer_admin_role panics if the not authorized by the admin. +// status: verified +pub fn transfer_admin_role_panics_if_unauth_by_admin(e: Env) { + let new_admin = nondet_address(); + clog!(cvlr_soroban::Addr(&new_admin)); + let live_until_ledger = u32::nondet(); + clog!(live_until_ledger); + let admin = AccessControlContract::get_admin(&e); + if let Some(admin_internal) = admin.clone() { + clog!(cvlr_soroban::Addr(&admin_internal)); + cvlr_assume!(!is_auth(admin_internal)); + } + AccessControlContract::transfer_admin_role(&e, new_admin.clone(), live_until_ledger); + cvlr_assert!(false); +} + +#[rule] +// transfer_admin_role panics if the admin is not set. +// status: verified +pub fn transfer_admin_role_panics_if_admin_not_set(e: Env) { + let new_admin = nondet_address(); + let live_until_ledger = u32::nondet(); + let admin = AccessControlContract::get_admin(&e); + cvlr_assume!(admin.is_none()); + AccessControlContract::transfer_admin_role(&e, new_admin, live_until_ledger); + cvlr_assert!(false); +} + +#[rule] +// transfer_admin_role panics if live_until_ledger = 0 and PendingAdmin = None +// status: verified +pub fn transfer_admin_role_panics_if_live_until_ledger_0_and_pending_admin_none(e: Env) { + let new_admin = nondet_address(); + let live_until_ledger = 0; + let pending_admin = get_pending_admin(&e); + cvlr_assume!(pending_admin.is_none()); + AccessControlContract::transfer_admin_role(&e, new_admin, live_until_ledger); + cvlr_assert!(false); +} + +#[rule] +// transfer_admin_role panics if live_until_ledger = 0 and PendingAdmin != new_admin +// status: verified +pub fn transfer_admin_role_panics_if_live_until_ledger_0_and_diff_pending_admin(e: Env) { + let new_admin = nondet_address(); + let live_until_ledger = 0; + let pending_admin = get_pending_admin(&e); + if let Some(pending_admin_internal) = pending_admin.clone() { + cvlr_assume!(pending_admin_internal != new_admin); + } + AccessControlContract::transfer_admin_role(&e, new_admin, live_until_ledger); + cvlr_assert!(false); +} + +#[rule] +// transfer_admin_role panics if the live_until_ledger is in the past. +// status: verified +pub fn transfer_admin_role_panics_if_invalid_live_until_ledger(e: Env) { + let new_admin = nondet_address(); + let live_until_ledger = u32::nondet(); + cvlr_assume!(live_until_ledger < e.ledger().sequence() || live_until_ledger > e.ledger().max_live_until_ledger()); + cvlr_assume!(live_until_ledger > 0); + AccessControlContract::transfer_admin_role(&e, new_admin, live_until_ledger); + cvlr_assert!(false); +} + +#[rule] +// accept_admin_transfer panics if the not authorized by the pending admin. +// status: verified +pub fn accept_admin_transfer_panics_if_unauth_by_pending_admin(e: Env) { + let pending_admin = get_pending_admin(&e); + if let Some(pending_admin_internal) = pending_admin.clone() { + cvlr_assume!(!is_auth(pending_admin_internal)); + } + AccessControlContract::accept_admin_transfer(&e); + cvlr_assert!(false); +} + +#[rule] +// accept_admin_transfer panics if the pending admin is not set. +// status: verified +pub fn accept_admin_transfer_panics_if_pending_admin_not_set(e: Env) { + let pending_admin = get_pending_admin(&e); + cvlr_assume!(pending_admin.is_none()); + AccessControlContract::accept_admin_transfer(&e); + cvlr_assert!(false); +} + +#[rule] +// set_role_admin panics if not authorized by the admin +// status: verified +pub fn set_role_admin_panics_if_unauth_by_admin(e: Env) { + let role = nondet_symbol(); + let admin_role = nondet_symbol(); + let admin = AccessControlContract::get_admin(&e); + if let Some(admin_internal) = admin.clone() { + cvlr_assume!(!is_auth(admin_internal)); + } + AccessControlContract::set_role_admin(&e, role.clone(), admin_role.clone()); + cvlr_assert!(false); +} + +#[rule] +// set_role_admin panics if the admin is not set +// status: verified +pub fn set_role_admin_panics_if_admin_not_set(e: Env) { + let role = nondet_symbol(); + let admin_role = nondet_symbol(); + let admin = AccessControlContract::get_admin(&e); + cvlr_assume!(admin.is_none()); + AccessControlContract::set_role_admin(&e, role.clone(), admin_role.clone()); + cvlr_assert!(false); +} + +#[rule] +// renounce_admin panics if not authorized by the admin. +// status: verified +pub fn renounce_admin_panics_if_unauth_by_admin(e: Env) { + let admin = AccessControlContract::get_admin(&e); + if let Some(admin_internal) = admin.clone() { + clog!(cvlr_soroban::Addr(&admin_internal)); + cvlr_assume!(!is_auth(admin_internal)); + } + AccessControlContract::renounce_admin(&e); + cvlr_assert!(false); +} + +#[rule] +// renounce_admin panics if the admin is not set. +// status: verified +pub fn renounce_admin_panics_if_admin_not_set(e: Env) { + let admin = AccessControlContract::get_admin(&e); + cvlr_assume!(admin.is_none()); + AccessControlContract::renounce_admin(&e); + cvlr_assert!(false); +} + +#[rule] +// renounce_admin panics if there is a pending adminship transfer. +// status: bug +pub fn renounce_admin_panics_if_pending_adminship_transfer(e: Env) { + let pending_admin = get_pending_admin(&e); + cvlr_assume!(pending_admin.is_some()); + AccessControlContract::renounce_admin(&e); + cvlr_assert!(false); +} + +// harness functions + +#[rule] +// admin_function panics if not authorized by the admin. +// status: verified +pub fn admin_function_panics_if_unauth_by_admin(e: Env) { + let admin = AccessControlContract::get_admin(&e); + if let Some(admin_internal) = admin.clone() { + clog!(cvlr_soroban::Addr(&admin_internal)); + cvlr_assume!(!is_auth(admin_internal)); + } + AccessControlContract::admin_function(&e); + cvlr_assert!(false); +} + +#[rule] +// admin_function panics if admin not set +// status: verified +pub fn admin_function_panics_if_admin_not_set(e: Env) { + let admin = AccessControlContract::get_admin(&e); + cvlr_assume!(admin.is_none()); + AccessControlContract::admin_function(&e); + cvlr_assert!(false); +} + +#[rule] +// role1_func panics if caller doesn't have role +// status: violated - symbol issue +pub fn role1_func_panics_if_caller_does_not_have_role(e: Env) { + let caller = nondet_address(); + let role1 = soroban_sdk::Symbol::new(&e, "role1"); + let caller_has_role = AccessControlContract::has_role(&e, caller.clone(), role1); + cvlr_assume!(caller_has_role.is_none()); + AccessControlContract::role1_func(&e, caller); + cvlr_assert!(false); +} + +#[rule] +// role1_auth_func panics if caller doesn't have role +// status: violated - symbol issue +pub fn role1_auth_func_panics_if_caller_does_not_have_role(e: Env) { + let caller = nondet_address(); + let role1 = soroban_sdk::Symbol::new(&e, "role1"); + let caller_has_role = AccessControlContract::has_role(&e, caller.clone(), role1.clone()); + cvlr_assume!(caller_has_role.is_none()); + AccessControlContract::role1_auth_func(&e, caller.clone()); + cvlr_assert!(false); +} + +#[rule] +// role1_auth_func panics if caller does not authorize +// status: verified +pub fn role1_auth_func_panics_if_caller_does_not_authorize(e: Env) { + let caller = nondet_address(); + cvlr_assume!(!is_auth(caller.clone())); + AccessControlContract::role1_auth_func(&e, caller.clone()); + cvlr_assert!(false); +} + +#[rule] +// role1_or_role2_func panics if caller doesn't have role +// status: violated - symbol issue +pub fn role1_or_role2_func_panics_if_caller_does_not_have_role(e: Env) { + let caller = nondet_address(); + let role1 = soroban_sdk::Symbol::new(&e, "role1"); + let role2 = soroban_sdk::Symbol::new(&e, "role2"); + let caller_has_role1 = AccessControlContract::has_role(&e, caller.clone(), role1); + let caller_has_role2 = AccessControlContract::has_role(&e, caller.clone(), role2); + cvlr_assume!(caller_has_role1.is_none() && caller_has_role2.is_none()); + AccessControlContract::role1_or_role2_func(&e, caller.clone()); + cvlr_assert!(false); +} + +#[rule] +// role1_or_role2_auth_func panics if caller doesn't have role +// status: violated - symbol issue +pub fn role1_or_role2_auth_func_panics_if_caller_does_not_have_role(e: Env) { + let caller = nondet_address(); + let role1 = soroban_sdk::Symbol::new(&e, "role1"); + let role2 = soroban_sdk::Symbol::new(&e, "role2"); + let caller_has_role1 = AccessControlContract::has_role(&e, caller.clone(), role1); + let caller_has_role2 = AccessControlContract::has_role(&e, caller.clone(), role2); + cvlr_assume!(caller_has_role1.is_none() && caller_has_role2.is_none()); + AccessControlContract::role1_or_role2_auth_func(&e, caller.clone()); + cvlr_assert!(false); +} + +#[rule] +// role1_or_role2_auth_func panics if caller doesn't authorize +// status: verified +pub fn role1_or_role2_auth_func_panics_if_caller_does_not_authorize(e: Env) { + let caller = nondet_address(); + cvlr_assume!(!is_auth(caller.clone())); + AccessControlContract::role1_or_role2_auth_func(&e, caller.clone()); + cvlr_assert!(false); +} + +#[rule] +// role1_and_role2_func panics if caller1 doesn't have role +// status: violated - symbol issue +pub fn role1_and_role2_func_panics_if_caller1_does_not_have_role(e: Env) { + let caller1 = nondet_address(); + let caller2 = nondet_address(); + let role1 = soroban_sdk::Symbol::new(&e, "role1"); + let caller1_has_role = AccessControlContract::has_role(&e, caller1.clone(), role1); + cvlr_assume!(caller1_has_role.is_none()); + AccessControlContract::role1_and_role2_func(&e, caller1.clone(), caller2.clone()); + cvlr_assert!(false); +} + +#[rule] +// role1_and_role2_func panics if caller2 doesn't have role +// status: violated - symbol issue +pub fn role1_and_role2_func_panics_if_caller2_does_not_have_role(e: Env) { + let caller1 = nondet_address(); + let caller2 = nondet_address(); + let role2 = soroban_sdk::Symbol::new(&e, "role2"); + let caller2_has_role = AccessControlContract::has_role(&e, caller2.clone(), role2); + cvlr_assume!(caller2_has_role.is_none()); + AccessControlContract::role1_and_role2_func(&e, caller1.clone(), caller2.clone()); + cvlr_assert!(false); +} diff --git a/packages/access/src/access_control/specs/access_control_sanity.rs b/packages/access/src/access_control/specs/access_control_sanity.rs new file mode 100644 index 00000000..272c24d5 --- /dev/null +++ b/packages/access/src/access_control/specs/access_control_sanity.rs @@ -0,0 +1,130 @@ + +use cvlr::{cvlr_assert,cvlr_satisfy};use cvlr_soroban::{nondet_address, nondet_symbol}; +use cvlr_soroban_derive::rule; +use cvlr::nondet::Nondet; + +use soroban_sdk::{Env}; + +use crate::access_control::{AccessControl, specs::access_control_contract::AccessControlContract}; + +#[rule] +pub fn has_role_sanity(e: Env) { + let role = nondet_symbol(); + let account = nondet_address(); + let admin = nondet_address(); + AccessControlContract::access_control_constructor(&e, admin); + AccessControlContract::has_role(&e, account, role); + cvlr_satisfy!(true); +} + +#[rule] +pub fn get_admin_sanity(e: Env) { + let admin = nondet_address(); + AccessControlContract::access_control_constructor(&e, admin); + AccessControlContract::get_admin(&e); + cvlr_satisfy!(true); +} + +#[rule] +pub fn get_role_member_count_sanity(e: Env) { + let role = nondet_symbol(); + let admin = nondet_address(); + AccessControlContract::access_control_constructor(&e, admin); + let _ = AccessControlContract::get_role_member_count(&e, role); + cvlr_satisfy!(true); +} + +#[rule] +pub fn get_role_member_sanity(e: Env) { + let role = nondet_symbol(); + let i = u32::nondet(); + let admin = nondet_address(); + AccessControlContract::access_control_constructor(&e, admin); + let _ = AccessControlContract::get_role_member(&e, role, i); + cvlr_satisfy!(true); +} + +#[rule] +pub fn get_role_admin_sanity(e: Env) { + let role = nondet_symbol(); + let admin = nondet_address(); + AccessControlContract::access_control_constructor(&e, admin); + let _ = AccessControlContract::get_role_admin(&e, role); + cvlr_satisfy!(true); +} + +#[rule] +pub fn set_admin_sanity(e: Env) { + let admin = nondet_address(); + AccessControlContract::access_control_constructor(&e, admin); + cvlr_satisfy!(true); +} + +#[rule] +pub fn grant_role_sanity(e: Env) { + let role = nondet_symbol(); + let caller = nondet_address(); + let account = nondet_address(); + let admin = nondet_address(); + AccessControlContract::access_control_constructor(&e, admin); + AccessControlContract::grant_role(&e, caller, account, role); + cvlr_satisfy!(true); +} + +#[rule] +pub fn revoke_role_sanity(e: Env) { + let role = nondet_symbol(); + let caller = nondet_address(); + let account = nondet_address(); + let admin = nondet_address(); + AccessControlContract::access_control_constructor(&e, admin); + AccessControlContract::revoke_role(&e, caller, account, role); + cvlr_satisfy!(true); +} + + +#[rule] +pub fn renounce_role_sanity(e: Env) { + let role = nondet_symbol(); + let caller = nondet_address(); + let admin = nondet_address(); + AccessControlContract::access_control_constructor(&e, admin); + AccessControlContract::renounce_role(&e, caller, role); + cvlr_satisfy!(true); +} + +#[rule] +pub fn transfer_admin_role_sanity(e: Env) { + let new_admin = nondet_address(); + let live_until_ledger = u32::nondet(); + let admin = nondet_address(); + AccessControlContract::access_control_constructor(&e, admin); + AccessControlContract::transfer_admin_role(&e, new_admin, live_until_ledger); + cvlr_satisfy!(true); +} + +#[rule] +pub fn accept_admin_transfer_sanity(e: Env) { + let admin = nondet_address(); + AccessControlContract::access_control_constructor(&e, admin); + AccessControlContract::accept_admin_transfer(&e); + cvlr_satisfy!(true); +} + +#[rule] +pub fn set_role_admin_sanity(e: Env) { + let role = nondet_symbol(); + let admin_role = nondet_symbol(); + let admin = nondet_address(); + AccessControlContract::access_control_constructor(&e, admin); + AccessControlContract::set_role_admin(&e, role, admin_role); + cvlr_satisfy!(true); +} + +#[rule] +pub fn renounce_admin_sanity(e: Env) { + let admin = nondet_address(); + AccessControlContract::access_control_constructor(&e, admin); + AccessControlContract::renounce_admin(&e); + cvlr_satisfy!(true); +} \ No newline at end of file diff --git a/packages/access/src/access_control/specs/constructor_helper.rs b/packages/access/src/access_control/specs/constructor_helper.rs new file mode 100644 index 00000000..38efe7b7 --- /dev/null +++ b/packages/access/src/access_control/specs/constructor_helper.rs @@ -0,0 +1,46 @@ +use cvlr::cvlr_assume; +use soroban_sdk::{Env, Address, Symbol}; + +use crate::access_control::{AccessControl, specs::access_control_contract::AccessControlContract}; + +use crate::access_control::storage::{AccessControlStorageKey, RoleAccountKey}; + +pub fn before_constructor_no_admin(e: &Env) { + let key = AccessControlStorageKey::Admin; + let admin = e.storage().persistent().get::<_, Address>(&key); + cvlr_assume!(admin.is_none()); +} + +pub fn before_constructor_no_pending_admin(e: &Env) { + let key = AccessControlStorageKey::PendingAdmin; + let pending_admin = e.storage().temporary().get::<_, Address>(&key); + cvlr_assume!(pending_admin.is_none()); +} + +pub fn before_constructor_no_role_count(e: &Env, role: &Symbol) { + let key = AccessControlStorageKey::RoleAccountsCount(role.clone()); + let count = e.storage().persistent().get::<_, u32>(&key); + cvlr_assume!(count.is_none()); +} + +pub fn before_constructor_no_role_admin(e: &Env, role: &Symbol) { + let key = AccessControlStorageKey::RoleAdmin(role.clone()); + let admin = e.storage().persistent().get::<_, Symbol>(&key); + cvlr_assume!(admin.is_none()); +} + +pub fn before_constructor_no_has_role(e: &Env, account: Address, role: Symbol) { + let key = AccessControlStorageKey::HasRole(account.clone(), role.clone()); + let has_role = e.storage().persistent().get::<_, u32>(&key); + cvlr_assume!(has_role.is_none()); +} + +pub fn before_constructor_no_role_accounts(e: &Env, role: Symbol, index: u32) { + let key = AccessControlStorageKey::RoleAccounts(RoleAccountKey { + role, + index, + }); + let account = e.storage().persistent().get::<_, Address>(&key); + cvlr_assume!(account.is_none()); +} + diff --git a/packages/access/src/access_control/specs/helper.rs b/packages/access/src/access_control/specs/helper.rs new file mode 100644 index 00000000..55283282 --- /dev/null +++ b/packages/access/src/access_control/specs/helper.rs @@ -0,0 +1,41 @@ +use soroban_sdk::{Env, Address, Symbol}; +use crate::access_control::{AccessControlStorageKey, storage::RoleAccountKey}; +use cvlr::clog; + +/// Returns `Some(Address)` if a pending owner is set, or `None` if there is no pending ownership transfer. +/// +/// # Arguments +/// +/// * `e` - Access to the Soroban environment. +pub fn get_pending_admin(e: &Env) -> Option
{ + let pending_admin = e.storage().temporary().get::<_, Address>(&AccessControlStorageKey::PendingAdmin); + if let Some(pending_admin_internal) = pending_admin.clone() { + clog!(cvlr_soroban::Addr(&pending_admin_internal)); + } + pending_admin +} + +/// Returns `Some(Address)` if an account exists at the specified index for the given role, +/// or `None` if there is no account at that index. +/// +/// # Arguments +/// +/// * `e` - Access to the Soroban environment. +/// * `role` - The role to query. +/// * `index` - The index of the account to retrieve. +pub fn get_role_account(e: &Env, role: &Symbol, index: u32) -> Option
{ + let key = AccessControlStorageKey::RoleAccounts(RoleAccountKey { + role: role.clone(), + index, + }); + let account = e + .storage() + .persistent() + .get::<_, Address>(&key); + if let Some(account_internal) = account.clone() { + clog!(cvlr_soroban::Addr(&account_internal)); + } + account +} + + diff --git a/packages/access/src/access_control/specs/mod.rs b/packages/access/src/access_control/specs/mod.rs new file mode 100644 index 00000000..d3732c4e --- /dev/null +++ b/packages/access/src/access_control/specs/mod.rs @@ -0,0 +1,8 @@ +pub mod access_control_sanity; +pub mod access_control_non_panics; +pub mod access_control_panics; +pub mod access_control_invariants; +pub mod access_control_integrity; +pub mod access_control_contract; +pub mod helper; +pub mod constructor_helper; \ No newline at end of file diff --git a/packages/access/src/access_control/storage.rs b/packages/access/src/access_control/storage.rs index bb4f9c95..d7b63c5b 100644 --- a/packages/access/src/access_control/storage.rs +++ b/packages/access/src/access_control/storage.rs @@ -1,15 +1,20 @@ use soroban_sdk::{contracttype, panic_with_error, Address, Env, Symbol}; +#[cfg(not(feature = "certora"))] use crate::{ access_control::{ emit_admin_renounced, emit_admin_transfer_completed, emit_admin_transfer_initiated, - emit_role_admin_changed, emit_role_granted, emit_role_revoked, AccessControlError, - ROLE_EXTEND_AMOUNT, ROLE_TTL_THRESHOLD, + emit_role_admin_changed, emit_role_granted, emit_role_revoked, + } +}; + +use crate::{ + access_control::{ + AccessControlError, ROLE_EXTEND_AMOUNT, ROLE_TTL_THRESHOLD, }, role_transfer::{accept_transfer, transfer_role}, }; - /// Storage key for enumeration of accounts per role. #[contracttype] pub struct RoleAccountKey { diff --git a/packages/access/src/access_control/test.rs b/packages/access/src/access_control/test.rs index b0c93499..81aedcfc 100644 --- a/packages/access/src/access_control/test.rs +++ b/packages/access/src/access_control/test.rs @@ -260,7 +260,7 @@ fn admin_transfer_cancel_works() { } #[test] -#[should_panic(expected = "Error(Contract, #1210)")] +#[should_panic(expected = "Error(Contract, #2000)")] fn unauthorized_role_grant_panics() { let e = Env::default(); e.mock_all_auths(); @@ -278,7 +278,7 @@ fn unauthorized_role_grant_panics() { } #[test] -#[should_panic(expected = "Error(Contract, #1210)")] +#[should_panic(expected = "Error(Contract, #2000)")] fn unauthorized_role_revoke_panics() { let e = Env::default(); e.mock_all_auths(); @@ -299,7 +299,7 @@ fn unauthorized_role_revoke_panics() { } #[test] -#[should_panic(expected = "Error(Contract, #1217)")] +#[should_panic(expected = "Error(Contract, #2007)")] fn renounce_nonexistent_role_panics() { let e = Env::default(); e.mock_all_auths(); @@ -328,7 +328,7 @@ fn get_admin_with_no_admin_set_works() { } #[test] -#[should_panic(expected = "Error(Contract, #1212)")] +#[should_panic(expected = "Error(Contract, #2002)")] fn get_role_member_with_out_of_bounds_index_panics() { let e = Env::default(); e.mock_all_auths(); @@ -351,7 +351,7 @@ fn get_role_member_with_out_of_bounds_index_panics() { } #[test] -#[should_panic(expected = "Error(Contract, #1211)")] +#[should_panic(expected = "Error(Contract, #2001)")] fn admin_transfer_fails_when_no_admin_set() { let e = Env::default(); e.mock_all_auths(); @@ -460,7 +460,7 @@ fn remove_from_role_enumeration_for_last_account_works() { } #[test] -#[should_panic(expected = "Error(Contract, #1218)")] +#[should_panic(expected = "Error(Contract, #2008)")] fn remove_from_role_enumeration_with_nonexistent_role_panics() { let e = Env::default(); e.mock_all_auths(); @@ -475,7 +475,7 @@ fn remove_from_role_enumeration_with_nonexistent_role_panics() { } #[test] -#[should_panic(expected = "Error(Contract, #1217)")] +#[should_panic(expected = "Error(Contract, #2007)")] fn remove_from_role_enumeration_with_account_not_in_role_panics() { let e = Env::default(); e.mock_all_auths(); @@ -539,7 +539,7 @@ fn remove_role_admin_no_auth_works() { } #[test] -#[should_panic(expected = "Error(Contract, #1216)")] +#[should_panic(expected = "Error(Contract, #2006)")] fn set_admin_when_already_set_panics() { let e = Env::default(); e.mock_all_auths(); @@ -561,7 +561,7 @@ fn set_admin_when_already_set_panics() { } #[test] -#[should_panic(expected = "Error(Contract, #1213)")] +#[should_panic(expected = "Error(Contract, #2003)")] fn remove_role_admin_no_auth_panics_with_nonexistent_role() { let e = Env::default(); e.mock_all_auths(); @@ -596,7 +596,7 @@ fn remove_role_accounts_count_no_auth_works() { } #[test] -#[should_panic(expected = "Error(Contract, #1214)")] +#[should_panic(expected = "Error(Contract, #2004)")] fn remove_role_accounts_count_no_auth_does_not_remove_nonzero_count() { let e = Env::default(); e.mock_all_auths(); @@ -617,7 +617,7 @@ fn remove_role_accounts_count_no_auth_does_not_remove_nonzero_count() { } #[test] -#[should_panic(expected = "Error(Contract, #1215)")] +#[should_panic(expected = "Error(Contract, #2005)")] fn remove_role_accounts_count_no_auth_panics_with_nonexistent_role() { let e = Env::default(); e.mock_all_auths(); @@ -650,7 +650,7 @@ fn renounce_admin_works() { } #[test] -#[should_panic(expected = "Error(Contract, #1211)")] +#[should_panic(expected = "Error(Contract, #2001)")] fn renounce_admin_fails_when_no_admin_set() { let e = Env::default(); e.mock_all_auths(); diff --git a/packages/access/src/ownable/mod.rs b/packages/access/src/ownable/mod.rs index efab5c6f..70af4987 100644 --- a/packages/access/src/ownable/mod.rs +++ b/packages/access/src/ownable/mod.rs @@ -30,15 +30,22 @@ //! Not providing a direct ownership transfer is a deliberate design decision to //! help avoid mistakes by transferring to a wrong address. -#[cfg(feature = "certora")] -pub mod spec; - mod storage; #[cfg(test)] mod test; -use soroban_sdk::{contracterror, contractevent, Address, Env}; +#[cfg(feature = "certora")] +pub mod specs; + +use soroban_sdk::{contracterror, Address, Env}; + +#[cfg(not(feature = "certora"))] +use soroban_sdk::{contractevent}; + +#[cfg(feature = "certora")] +use cvlr_soroban_derive::{contractevent}; + pub use crate::ownable::storage::{ accept_ownership, enforce_owner_auth, get_owner, renounce_ownership, set_owner, @@ -130,9 +137,9 @@ pub trait Ownable { #[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] #[repr(u32)] pub enum OwnableError { - OwnerNotSet = 1220, - TransferInProgress = 1221, - OwnerAlreadySet = 1222, + OwnerNotSet = 2100, + TransferInProgress = 2101, + OwnerAlreadySet = 2102, } // ################## EVENTS ################## @@ -155,6 +162,7 @@ pub struct OwnershipTransfer { /// * `new_owner` - The address of the proposed new owner. /// * `live_until_ledger` - The ledger number until which the new owner can /// accept the transfer. +#[cfg(not(feature = "certora"))] pub fn emit_ownership_transfer( e: &Env, old_owner: &Address, @@ -182,6 +190,7 @@ pub struct OwnershipTransferCompleted { /// /// * `e` - Access to the Soroban environment. /// * `new_owner` - The address of the new owner. +#[cfg(not(feature = "certora"))] pub fn emit_ownership_transfer_completed(e: &Env, new_owner: &Address) { OwnershipTransferCompleted { new_owner: new_owner.clone() }.publish(e); } @@ -199,6 +208,7 @@ pub struct OwnershipRenounced { /// /// * `e` - Access to the Soroban environment. /// * `old_owner` - The address of the owner who renounced ownership. +#[cfg(not(feature = "certora"))] pub fn emit_ownership_renounced(e: &Env, old_owner: &Address) { OwnershipRenounced { old_owner: old_owner.clone() }.publish(e); } diff --git a/packages/access/src/ownable/spec/contract.rs b/packages/access/src/ownable/spec/contract.rs deleted file mode 100644 index d67c6379..00000000 --- a/packages/access/src/ownable/spec/contract.rs +++ /dev/null @@ -1,38 +0,0 @@ -use crate::ownable::{ - accept_ownership, get_owner, renounce_ownership, set_owner, transfer_ownership, Ownable, -}; -use soroban_sdk::{contract, contractimpl, contracttype, Address, Env}; -use stellar_macros::{default_impl, only_owner}; - -#[contract] -pub struct FVHarnessContract; - -#[contractimpl] -impl FVHarnessContract { - pub fn __constructor(e: &Env, owner: Address) { - set_owner(e, &owner); - } -} - -#[contractimpl] -impl Ownable for FVHarnessContract { - fn get_owner(e: &Env) -> Option
{ - get_owner(e) - } - - fn transfer_ownership( - e: &Env, - new_owner: Address, - live_until_ledger: u32, - ) { - transfer_ownership(e, &new_owner, live_until_ledger); - } - - fn accept_ownership(e: &Env) { - accept_ownership(e); - } - - fn renounce_ownership(e: &Env) { - renounce_ownership(e); - } -} diff --git a/packages/access/src/ownable/spec/mod.rs b/packages/access/src/ownable/spec/mod.rs deleted file mode 100644 index fbfc2460..00000000 --- a/packages/access/src/ownable/spec/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod contract; -pub mod ownable_sanity_rules; \ No newline at end of file diff --git a/packages/access/src/ownable/spec/ownable_sanity_rules.rs b/packages/access/src/ownable/spec/ownable_sanity_rules.rs deleted file mode 100644 index 6ad048ae..00000000 --- a/packages/access/src/ownable/spec/ownable_sanity_rules.rs +++ /dev/null @@ -1,57 +0,0 @@ -use crate::ownable::spec::contract::FVHarnessContract; -use cvlr::{cvlr_assert}; -use cvlr_soroban::{nondet_address}; -use cvlr_soroban_derive::rule; -use cvlr::nondet::Nondet; -use cvlr::clog; - -use soroban_sdk::{Env}; - -use crate::ownable::*; - -#[rule] -pub fn get_owner_sanity(e: Env) { - let _ = get_owner(&e); - cvlr_assert!(false); -} - -#[rule] -pub fn set_owner_sanity(e: Env) { - let owner = nondet_address(); - set_owner(&e, &owner); - cvlr_assert!(false); -} - -#[rule] -pub fn transfer_ownership_sanity(e: Env) { - let new_owner = nondet_address(); - let live_until_ledger = u32::nondet(); - transfer_ownership(&e, &new_owner, live_until_ledger); - cvlr_assert!(false); -} - -#[rule] -pub fn accept_ownership_sanity(e: Env) { - accept_ownership(&e); - cvlr_assert!(false); -} - -#[rule] -pub fn renounce_ownership_sanity(e: Env) { - renounce_ownership(&e); - cvlr_assert!(false); -} - -#[rule] -pub fn enforce_owner_auth_sanity(e: Env) { - enforce_owner_auth(&e); - cvlr_assert!(false); -} - -#[rule] -pub fn fv_harness_contract_sanity_dummy(e: Env) { - let owner = nondet_address(); - FVHarnessContract::__constructor(&e, owner); - FVHarnessContract::get_owner(&e); - cvlr_assert!(false); -} \ No newline at end of file diff --git a/packages/access/src/ownable/specs/helper.rs b/packages/access/src/ownable/specs/helper.rs new file mode 100644 index 00000000..68772afe --- /dev/null +++ b/packages/access/src/ownable/specs/helper.rs @@ -0,0 +1,18 @@ +use cvlr::clog; +use soroban_sdk::{Address, Env}; + +use crate::ownable::OwnableStorageKey; + +/// Returns `Some(Address)` if a pending owner is set, or `None` if there is no +/// pending ownership transfer. +/// +/// # Arguments +/// +/// * `e` - Access to the Soroban environment. +pub fn get_pending_owner(e: &Env) -> Option
{ + let pending_owner = e.storage().temporary().get::<_, Address>(&OwnableStorageKey::PendingOwner); + if let Some(pending_owner_internal) = pending_owner.clone() { + clog!(cvlr_soroban::Addr(&pending_owner_internal)); + } + pending_owner +} diff --git a/packages/access/src/ownable/specs/mod.rs b/packages/access/src/ownable/specs/mod.rs new file mode 100644 index 00000000..48015d38 --- /dev/null +++ b/packages/access/src/ownable/specs/mod.rs @@ -0,0 +1,7 @@ +pub mod ownable_integrity; +pub mod ownable_sanity; +pub mod ownable_non_panics; +pub mod ownable_panics; +pub mod ownable_invariants; +pub mod helper; +pub mod ownable_contract; \ No newline at end of file diff --git a/packages/access/src/ownable/specs/ownable_contract.rs b/packages/access/src/ownable/specs/ownable_contract.rs new file mode 100644 index 00000000..c4d5e4d9 --- /dev/null +++ b/packages/access/src/ownable/specs/ownable_contract.rs @@ -0,0 +1,25 @@ +use crate::ownable::{ + set_owner, Ownable, +}; +use soroban_sdk::{contract, contractimpl, Address, Env}; +use stellar_macros::{default_impl, only_owner}; + +use crate as stellar_access; + +#[contract] +pub struct OwnableContract; + +#[contractimpl] +impl OwnableContract { + pub fn ownable_constructor(e: &Env, owner: Address) { + set_owner(e, &owner); + } + + #[only_owner] + pub fn owner_restricted_function(e: &Env) { + } +} + +#[contractimpl] +#[default_impl] +impl Ownable for OwnableContract {} \ No newline at end of file diff --git a/packages/access/src/ownable/specs/ownable_integrity.rs b/packages/access/src/ownable/specs/ownable_integrity.rs new file mode 100644 index 00000000..4882175b --- /dev/null +++ b/packages/access/src/ownable/specs/ownable_integrity.rs @@ -0,0 +1,113 @@ +use cvlr::{cvlr_assert, cvlr_assume,cvlr_satisfy}; +use cvlr_soroban::{nondet_address}; +use cvlr_soroban_derive::rule; +use cvlr::clog; +use cvlr::nondet::Nondet; + +use soroban_sdk::{Env}; + +use crate::ownable::{specs::{helper::get_pending_owner, ownable_contract::OwnableContract}, *}; + +#[rule] +// after the constructor the owner is set. +// status: verified +pub fn ownable_constructor_integrity(e: Env) { + let new_owner = nondet_address(); + clog!(cvlr_soroban::Addr(&new_owner)); + + OwnableContract::ownable_constructor(&e, new_owner.clone()); + let owner_post = OwnableContract::get_owner(&e); + + if let Some(owner_post_internal) = owner_post.clone() { + clog!(cvlr_soroban::Addr(&owner_post_internal)); + } + cvlr_assert!(owner_post == Some(new_owner)); +} + +#[rule] +// transfer_ownership with live_until_ledger > current_ledger +// sets the pending owner to new_owner and does not change the owner +// status: verified +pub fn transfer_ownership_integrity(e: Env) { + let new_owner = nondet_address(); + clog!(cvlr_soroban::Addr(&new_owner)); + let live_until_ledger = u32::nondet(); + clog!(live_until_ledger); + let current_ledger = e.ledger().sequence(); + clog!(current_ledger); + cvlr_assume!(live_until_ledger > current_ledger); // assuming a proper transfer + let owner_pre = OwnableContract::get_owner(&e); + if let Some(owner_pre_internal) = owner_pre.clone() { + clog!(cvlr_soroban::Addr(&owner_pre_internal)); + } + OwnableContract::transfer_ownership(&e, new_owner.clone(), live_until_ledger); + + let pending_owner = get_pending_owner(&e); + if let Some(pending_owner_internal) = pending_owner.clone() { + clog!(cvlr_soroban::Addr(&pending_owner_internal)); + } + let owner_post = OwnableContract::get_owner(&e); + if let Some(owner_post_internal) = owner_post.clone() { + clog!(cvlr_soroban::Addr(&owner_post_internal)); + } + cvlr_assert!(owner_post == owner_pre); + cvlr_assert!(pending_owner == Some(new_owner)); + // TODO : assert about TTL. +} + + +#[rule] +// transfer_ownership with a live ledger 0 removes the pending owner. +// status: verified +pub fn remove_transfer_ownership_integrity(e: Env) { + let new_owner = nondet_address(); + clog!(cvlr_soroban::Addr(&new_owner)); + let live_until_ledger = 0; + clog!(live_until_ledger); + let current_ledger = e.ledger().sequence(); + clog!(current_ledger); + + OwnableContract::transfer_ownership(&e, new_owner.clone(), live_until_ledger); + + let pending_owner = get_pending_owner(&e); + cvlr_assert!(pending_owner.is_none()); +} + +#[rule] +// accept_ownership sets the owner to the pending owner and removes the pending owner. +// status: verified +pub fn accept_ownership_integrity(e: Env) { + + let pending_owner_pre = get_pending_owner(&e); + if let Some(pending_owner_internal) = pending_owner_pre.clone() { + clog!(cvlr_soroban::Addr(&pending_owner_internal)); + } + + OwnableContract::accept_ownership(&e); + + let owner = OwnableContract::get_owner(&e); + if let Some(owner_internal) = owner.clone() { + clog!(cvlr_soroban::Addr(&owner_internal)); + } + + cvlr_assert!(owner == pending_owner_pre); + cvlr_assert!(!owner.is_none()); + + let pending_owner_post = get_pending_owner(&e); + if let Some(pending_owner_internal) = pending_owner_post.clone() { + clog!(cvlr_soroban::Addr(&pending_owner_internal)); + } + cvlr_assert!(pending_owner_post.is_none()); +} + +#[rule] +// renounce_ownership removes the owner. +// status: verified +pub fn renounce_ownership_integrity(e: Env) { + + OwnableContract::renounce_ownership(&e); + + let owner = OwnableContract::get_owner(&e); + + cvlr_assert!(owner.is_none()); +} \ No newline at end of file diff --git a/packages/access/src/ownable/specs/ownable_invariants.rs b/packages/access/src/ownable/specs/ownable_invariants.rs new file mode 100644 index 00000000..9e28fcad --- /dev/null +++ b/packages/access/src/ownable/specs/ownable_invariants.rs @@ -0,0 +1,217 @@ +use cvlr::{cvlr_assert, cvlr_assume,cvlr_satisfy}; +use cvlr_soroban::{nondet_address}; +use cvlr::nondet::Nondet; +use cvlr_soroban_derive::rule; + + +use soroban_sdk::{Env}; +use cvlr::clog; +use crate::ownable::{specs::{helper::get_pending_owner, ownable_contract::OwnableContract}, *}; + +// invariant: owner != None -> holds in all cases except for renounce_ownership + +// helpers +pub fn assume_pre_owner_is_set(e: Env) { + let owner_pre = OwnableContract::get_owner(&e); + cvlr_assume!(owner_pre.is_some()); +} + +pub fn assert_post_owner_is_set(e: Env) { + let owner_post = OwnableContract::get_owner(&e); + cvlr_assert!(owner_post.is_some()); +} + +///////// +#[rule] +// status: verified +pub fn after_constructor_owner_is_set(e: Env) { + let new_owner = nondet_address(); + OwnableContract::ownable_constructor(&e, new_owner); + assert_post_owner_is_set(e); +} + +#[rule] +// status: verified +pub fn after_constructor_owner_is_set_sanity(e: Env) { + let new_owner = nondet_address(); + OwnableContract::ownable_constructor(&e, new_owner); + cvlr_satisfy!(true); +} + +///////// +#[rule] +// status: verified +pub fn after_transfer_ownership_pending_owner_is_set(e: Env) { + assume_pre_owner_is_set(e.clone()); + let new_owner = nondet_address(); + let live_until_ledger = u32::nondet(); + OwnableContract::transfer_ownership(&e, new_owner, live_until_ledger); + assert_post_owner_is_set(e); +} + +#[rule] +// status: verified +pub fn after_transfer_ownership_pending_owner_is_set_sanity(e: Env) { + assume_pre_owner_is_set(e.clone()); + let new_owner = nondet_address(); + let live_until_ledger = u32::nondet(); + OwnableContract::transfer_ownership(&e, new_owner, live_until_ledger); + cvlr_satisfy!(true); +} + +///////// +#[rule] +// status: verified +pub fn after_accept_ownership_owner_is_set(e: Env) { + assume_pre_owner_is_set(e.clone()); + OwnableContract::accept_ownership(&e); + assert_post_owner_is_set(e); +} + +#[rule] +// status: verified +pub fn after_accept_ownership_owner_is_set_sanity(e: Env) { + assume_pre_owner_is_set(e.clone()); + OwnableContract::accept_ownership(&e); + cvlr_satisfy!(true) +} + +// for the case renounce_ownership it's obviously true - and expected + +///////// +#[rule] +// status: verified +pub fn after_owner_restricted_function_owner_is_set(e: Env) { + assume_pre_owner_is_set(e.clone()); + OwnableContract::owner_restricted_function(&e); + assert_post_owner_is_set(e); +} + +#[rule] +// status: verified +pub fn after_owner_restricted_function_owner_is_set_sanity(e: Env) { + assume_pre_owner_is_set(e.clone()); + OwnableContract::owner_restricted_function(&e); + cvlr_satisfy!(true) +} + +// invariant: pending_owner != none implies owner != none + +// helpers +pub fn assume_pre_pending_owner_implies_owner(e: &Env) { + let pending_owner_pre = get_pending_owner(e); + if let Some(pend_pre) = pending_owner_pre.clone() { + clog!(cvlr_soroban::Addr(&pend_pre)); + } + let owner = OwnableContract::get_owner(&e); + if let Some(owner_internal_pre) = owner.clone() { + clog!(cvlr_soroban::Addr(&owner_internal_pre)); + } + if pending_owner_pre.is_some() { + cvlr_assume!(owner.is_some()); + } +} + +pub fn assert_post_pending_owner_implies_owner(e: &Env) { + let pending_owner_post = get_pending_owner(&e); + if let Some(pend_post) = pending_owner_post.clone() { + clog!(cvlr_soroban::Addr(&pend_post)); + } + let owner = OwnableContract::get_owner(&e); + if let Some(owner_internal_post) = owner.clone() { + clog!(cvlr_soroban::Addr(&owner_internal_post)); + } + if pending_owner_post.is_some() { + cvlr_assert!(owner.is_some()); + } +} + +///////// +#[rule] +// status: verified +pub fn after_constructor_pending_owner_implies_owner(e: Env) { + let new_owner = nondet_address(); + OwnableContract::ownable_constructor(&e, new_owner); + assert_post_pending_owner_implies_owner(&e); +} + +#[rule] +// status: verified +pub fn after_constructor_pending_owner_implies_owner_sanity(e: Env) { + let new_owner = nondet_address(); + OwnableContract::ownable_constructor(&e, new_owner); + cvlr_satisfy!(true); +} + + +///////// +#[rule] +// status: verified +pub fn after_transfer_ownership_pending_owner_implies_owner(e: Env) { + assume_pre_pending_owner_implies_owner(&e); + let new_owner = nondet_address(); + let live_until_ledger = u32::nondet(); + OwnableContract::transfer_ownership(&e, new_owner, live_until_ledger); + assert_post_pending_owner_implies_owner(&e); +} + +#[rule] +// status: verified +pub fn after_transfer_ownership_pending_owner_implies_owner_sanity(e: Env) { + assume_pre_pending_owner_implies_owner(&e); + let new_owner = nondet_address(); + let live_until_ledger = u32::nondet(); + OwnableContract::transfer_ownership(&e, new_owner, live_until_ledger); + cvlr_satisfy!(true); +} + +///////// +#[rule] +// status: verified +pub fn after_accept_ownership_pending_owner_implies_owner(e: Env) { + assume_pre_pending_owner_implies_owner(&e); + OwnableContract::accept_ownership(&e); + assert_post_pending_owner_implies_owner(&e); +} + +#[rule] +// status: verified +pub fn after_accept_ownership_pending_owner_implies_owner_sanity(e: Env) { + assume_pre_pending_owner_implies_owner(&e); + OwnableContract::accept_ownership(&e); + cvlr_satisfy!(true); +} + +///////// +#[rule] +// status: verified +pub fn after_renounce_ownership_pending_owner_implies_owner(e: Env) { + assume_pre_pending_owner_implies_owner(&e); + OwnableContract::renounce_ownership(&e); + assert_post_pending_owner_implies_owner(&e); +} + +#[rule] +// status: verified +pub fn after_renounce_ownership_pending_owner_implies_owner_sanity(e: Env) { + assume_pre_pending_owner_implies_owner(&e); + OwnableContract::renounce_ownership(&e); + cvlr_satisfy!(true) +} + +///////// +#[rule] +// status: verified +pub fn after_owner_restricted_function_pending_owner_implies_owner(e: Env) { + assume_pre_pending_owner_implies_owner(&e); + OwnableContract::owner_restricted_function(&e); + assert_post_pending_owner_implies_owner(&e); +} + +#[rule] +// status: verified +pub fn after_owner_restricted_function_pending_owner_implies_owner_sanity(e: Env) { + assume_pre_pending_owner_implies_owner(&e); + OwnableContract::owner_restricted_function(&e); + cvlr_satisfy!(true); +} \ No newline at end of file diff --git a/packages/access/src/ownable/specs/ownable_non_panics.rs b/packages/access/src/ownable/specs/ownable_non_panics.rs new file mode 100644 index 00000000..05175eb0 --- /dev/null +++ b/packages/access/src/ownable/specs/ownable_non_panics.rs @@ -0,0 +1,207 @@ +use cvlr::{cvlr_assert, cvlr_assume,cvlr_satisfy}; +use cvlr_soroban::{nondet_address, is_auth}; +use cvlr_soroban_derive::rule; +use cvlr::nondet::Nondet; + +use soroban_sdk::{Address, Env}; + +use crate::ownable::specs::ownable_contract::OwnableContract; +use crate::ownable::*; +use crate::{ownable::{OwnableStorageKey, specs::helper::get_pending_owner}}; + +// These rules require the prover arg "prover_args": ["-trapAsAssert true"] to consider also panicking paths. + +#[rule] +// requires +// storage setup +// owner exists +// owner auth +// if there is a pending owner they are the same +// live until ledger is appropriate +// status: verified +pub fn transfer_ownership_non_panic(e: Env) { + let address1 = nondet_address(); + e.storage().temporary().set(&OwnableStorageKey::PendingOwner, &address1); + + let address2 = nondet_address(); + e.storage().instance().set(&OwnableStorageKey::Owner, &address2); + + let new_owner = nondet_address().clone(); + let live_until_ledger = u32::nondet(); + + let owner = OwnableContract::get_owner(&e); + cvlr_assume!(owner.is_some()); + if let Some(owner_internal) = owner.clone() { + cvlr_assume!(is_auth(owner_internal)); + } + + let pending_owner = get_pending_owner(&e); + if let Some(pending_owner_internal) = pending_owner.clone() { + cvlr_assume!(pending_owner_internal == new_owner); + } + + if live_until_ledger == 0 { + cvlr_assume!(pending_owner.is_some()); + } + else { + cvlr_assume!(live_until_ledger >= e.ledger().sequence()); + cvlr_assume!(live_until_ledger <= e.ledger().max_live_until_ledger()); + } + + OwnableContract::transfer_ownership(&e, new_owner, live_until_ledger); + cvlr_assert!(true); +} + +#[rule] +// sanity +// status: verified +pub fn transfer_ownership_non_panic_sanity(e: Env) { + let address1 = nondet_address(); + e.storage().temporary().set(&OwnableStorageKey::PendingOwner, &address1); + + let address2 = nondet_address(); + e.storage().instance().set(&OwnableStorageKey::Owner, &address2); + + let new_owner = nondet_address().clone(); + let live_until_ledger = u32::nondet(); + + let owner = OwnableContract::get_owner(&e); + cvlr_assume!(owner.is_some()); + if let Some(owner_internal) = owner.clone() { + cvlr_assume!(is_auth(owner_internal)); + } + + let pending_owner = get_pending_owner(&e); + if let Some(pending_owner_internal) = pending_owner.clone() { + cvlr_assume!(pending_owner_internal == new_owner); + } + + if live_until_ledger == 0 { + cvlr_assume!(pending_owner.is_some()); + } + else { + cvlr_assume!(live_until_ledger >= e.ledger().sequence()); + cvlr_assume!(live_until_ledger <= e.ledger().max_live_until_ledger()); + } + + OwnableContract::transfer_ownership(&e, new_owner, live_until_ledger); + cvlr_satisfy!(true); +} + +#[rule] +// requires +// storage setup +// pending_owner is some +// pending_owner auth +// status: verified +pub fn accept_ownership_non_panic(e: Env) { + let address1 = nondet_address(); + e.storage().temporary().set(&OwnableStorageKey::PendingOwner, &address1); + + let address2 = nondet_address(); + e.storage().instance().set(&OwnableStorageKey::Owner, &address2); + + let pending_owner = get_pending_owner(&e); + cvlr_assume!(pending_owner.is_some() && is_auth(pending_owner.unwrap())); + OwnableContract::accept_ownership(&e); + cvlr_assert!(true); +} + +#[rule] +// sanity +// status: verified +pub fn accept_ownership_non_panic_sanity(e: Env) { + let address1 = nondet_address(); + e.storage().temporary().set(&OwnableStorageKey::PendingOwner, &address1); + + let address2 = nondet_address(); + e.storage().instance().set(&OwnableStorageKey::Owner, &address2); + + let pending_owner = get_pending_owner(&e); + cvlr_assume!(pending_owner.is_some()); + if let Some(pending_owner_internal) = pending_owner.clone() { + cvlr_assume!(is_auth(pending_owner_internal)); + } + OwnableContract::accept_ownership(&e); + cvlr_satisfy!(true); +} + +#[rule] +// requires +// storage setup +// pending_owner is none +// status: verified +pub fn renounce_ownership_non_panic(e: Env) { + // // setup storage: needed for now. + // // WIP: will have this macro for setting storage up automatically. + // // require_storage_tag(OwnableStorageKey::PendingOwner.into_val(&e), 77); + + e.storage().temporary().set(&OwnableStorageKey::PendingOwner, &nondet_address()); + e.storage().temporary().remove(&OwnableStorageKey::PendingOwner); + + e.storage().instance().set(&OwnableStorageKey::Owner, &nondet_address()); + let owner = OwnableContract::get_owner(&e).unwrap(); + + cvlr_assume!(is_auth(owner)); + + OwnableContract::renounce_ownership(&e); + cvlr_assert!(true); +} + +#[rule] +// sanity +// status: verified +pub fn renounce_ownership_non_panic_sanity(e: Env) { + let key = OwnableStorageKey::PendingOwner; + e.storage().temporary().set(&key, &nondet_address()); + e.storage().temporary().remove(&key); + + let owner = OwnableContract::get_owner(&e); + cvlr_assume!(owner.is_some()); + if let Some(owner_internal) = owner.clone() { + cvlr_assume!(is_auth(owner_internal)); + } + OwnableContract::renounce_ownership(&e); + cvlr_satisfy!(true); +} + +#[rule] +// requires +// storage setup +// owner exists +// owner auth +// status: verified +pub fn owner_restricted_function_non_panic(e: Env) { + let address1 = nondet_address(); + e.storage().temporary().set(&OwnableStorageKey::PendingOwner, &address1); + + let address2 = nondet_address(); + e.storage().instance().set(&OwnableStorageKey::Owner, &address2); + + let owner = OwnableContract::get_owner(&e); + cvlr_assume!(owner.is_some()); + if let Some(owner_internal) = owner.clone() { + cvlr_assume!(is_auth(owner_internal)); + } + OwnableContract::owner_restricted_function(&e); + cvlr_assert!(true); +} + +#[rule] +// sanity +// status: verified +pub fn owner_restricted_function_non_panic_sanity(e: Env) { + let address1 = nondet_address(); + e.storage().temporary().set(&OwnableStorageKey::PendingOwner, &address1); + + let address2 = nondet_address(); + e.storage().instance().set(&OwnableStorageKey::Owner, &address2); + + let owner = OwnableContract::get_owner(&e); + cvlr_assume!(owner.is_some()); + if let Some(owner_internal) = owner.clone() { + cvlr_assume!(is_auth(owner_internal)); + } + OwnableContract::owner_restricted_function(&e); + cvlr_satisfy!(true); +} \ No newline at end of file diff --git a/packages/access/src/ownable/specs/ownable_panics.rs b/packages/access/src/ownable/specs/ownable_panics.rs new file mode 100644 index 00000000..ff7387e2 --- /dev/null +++ b/packages/access/src/ownable/specs/ownable_panics.rs @@ -0,0 +1,163 @@ +use cvlr::{cvlr_assert, cvlr_assume,cvlr_satisfy}; +use cvlr_soroban::{nondet_address,is_auth}; +use cvlr_soroban_derive::rule; +use cvlr::nondet::Nondet; +use cvlr::clog; + +use soroban_sdk::{Env, Address}; + +use crate::ownable::specs::ownable_contract::OwnableContract; +use crate::ownable::*; + +use crate::ownable::specs::helper::get_pending_owner; + +// panic rules should all "pass" to be considered verified, even though they assert false. + +// package functions + +#[rule] +// transfer_ownership panics if the not authorized by the owner. +// status: verified +pub fn transfer_ownership_panics_if_unauth_by_owner(e: Env) { + let new_owner = nondet_address(); + clog!(cvlr_soroban::Addr(&new_owner)); + let live_until_ledger = u32::nondet(); + clog!(live_until_ledger); + let owner = OwnableContract::get_owner(&e); + if let Some(owner_internal) = owner.clone() { + clog!(cvlr_soroban::Addr(&owner_internal)); + cvlr_assume!(!is_auth(owner_internal)); + } + OwnableContract::transfer_ownership(&e, new_owner.clone(), live_until_ledger); + cvlr_assert!(false); +} + +#[rule] +// transfer_ownership panics if the owner is not set. +// status: verified +pub fn transfer_ownership_panics_if_owner_not_set(e: Env) { + let new_owner = nondet_address(); + let live_until_ledger = u32::nondet(); + let owner = OwnableContract::get_owner(&e); + cvlr_assume!(owner.is_none()); + OwnableContract::transfer_ownership(&e, new_owner, live_until_ledger); + cvlr_assert!(false); +} + +#[rule] +// transfer_ownership panics if live_until_ledger = 0 and PendingOwner = None +// status: verified +pub fn transfer_ownership_panics_if_live_until_ledger_0_and_pending_owner_none(e: Env) { + let new_owner = nondet_address(); + let live_until_ledger = 0; + let pending_owner = get_pending_owner(&e); + cvlr_assume!(pending_owner.is_none()); + OwnableContract::transfer_ownership(&e, new_owner, live_until_ledger); + cvlr_assert!(false); +} + +#[rule] +// transfer_ownership panics if live_until_ledger = 0 and PendingOwner != new_owner +// status: verified +pub fn transfer_ownership_panics_if_live_until_ledger_0_and_diff_pending_owner(e: Env) { + let new_owner = nondet_address(); + let live_until_ledger = 0; + let pending_owner = get_pending_owner(&e); + if let Some(pending_owner_internal) = pending_owner.clone() { + cvlr_assume!(pending_owner_internal != new_owner); + } + OwnableContract::transfer_ownership(&e, new_owner, live_until_ledger); + cvlr_assert!(false); +} + +#[rule] +// transfer_ownership panics if the live_until_ledger is in the past. +// status: verified +pub fn transfer_ownership_panics_if_invalid_live_until_ledger(e: Env) { + let new_owner = nondet_address(); + let live_until_ledger = u32::nondet(); + cvlr_assume!(live_until_ledger < e.ledger().sequence() || live_until_ledger > e.ledger().max_live_until_ledger()); + cvlr_assume!(live_until_ledger > 0); + OwnableContract::transfer_ownership(&e, new_owner, live_until_ledger); + cvlr_assert!(false); +} + +#[rule] +// accept_ownership panics if the not authorized by the pending owner. +// status: verified +pub fn accept_ownership_panics_if_unauth_by_pending_owner(e: Env) { + let pending_owner = get_pending_owner(&e); + if let Some(pending_owner_internal) = pending_owner.clone() { + cvlr_assume!(!is_auth(pending_owner_internal)); + } + OwnableContract::accept_ownership(&e); + cvlr_assert!(false); +} + +#[rule] +// accept_ownership panics if the pending owner is not set. +// status: verified +pub fn accept_ownership_panics_if_pending_owner_not_set(e: Env) { + let pending_owner = get_pending_owner(&e); + cvlr_assume!(pending_owner.is_none()); + OwnableContract::accept_ownership(&e); + cvlr_assert!(false); +} + +#[rule] +// renounce_ownership panics if not authorized by the owner. +// status: verified +pub fn renounce_ownership_panics_if_unauth_by_owner(e: Env) { + let owner = OwnableContract::get_owner(&e); + if let Some(owner_internal) = owner.clone() { + clog!(cvlr_soroban::Addr(&owner_internal)); + cvlr_assume!(!is_auth(owner_internal)); + } + OwnableContract::renounce_ownership(&e); + cvlr_assert!(false); +} + +#[rule] +// renounce_ownership panics if the owner is not set. +// status: verified +pub fn renounce_ownership_panics_if_owner_not_set(e: Env) { + let owner: Option
= OwnableContract::get_owner(&e); + cvlr_assume!(owner.is_none()); + OwnableContract::renounce_ownership(&e); + cvlr_assert!(false); +} + +#[rule] +// renounce_ownership panics if there is a pending ownership transfer. +// status: verified +pub fn renounce_ownership_panics_if_pending_ownership_transfer(e: Env) { + let pending_owner = e.storage().temporary().get::<_, Address>(&OwnableStorageKey::PendingOwner); + cvlr_assume!(pending_owner.is_some()); + OwnableContract::renounce_ownership(&e); + cvlr_assert!(false); +} + +// harness functions + +#[rule] +// owner_restricted_function panics if not authorized by owner. +// status: verified +pub fn owner_restricted_function_panics_if_unauth_by_owner(e: Env) { + let owner = OwnableContract::get_owner(&e); + if let Some(owner_internal) = owner.clone() { + clog!(cvlr_soroban::Addr(&owner_internal)); + cvlr_assume!(!is_auth(owner_internal)); + } + OwnableContract::owner_restricted_function(&e); + cvlr_assert!(false); +} + +#[rule] +// owner_restricted_function panics if the owner is not set. +// status: verified +pub fn owner_restricted_function_panics_if_owner_not_set(e: Env) { + let owner = OwnableContract::get_owner(&e); + cvlr_assume!(owner.is_none()); + OwnableContract::owner_restricted_function(&e); + cvlr_assert!(false); +} diff --git a/packages/access/src/ownable/specs/ownable_sanity.rs b/packages/access/src/ownable/specs/ownable_sanity.rs new file mode 100644 index 00000000..24f12b4e --- /dev/null +++ b/packages/access/src/ownable/specs/ownable_sanity.rs @@ -0,0 +1,50 @@ +use cvlr::{cvlr_assert,cvlr_satisfy};use cvlr_soroban::{nondet_address}; +use cvlr_soroban_derive::rule; +use cvlr::nondet::Nondet; + +use soroban_sdk::{Env}; + +use crate::ownable::*; + +use crate::ownable::specs::ownable_contract::OwnableContract; + +#[rule] +pub fn get_owner_sanity(e: Env) { + let owner = nondet_address(); + OwnableContract::ownable_constructor(&e, owner); + let _ =OwnableContract:: get_owner(&e); + cvlr_satisfy!(true); +} + +#[rule] +pub fn set_owner_sanity(e: Env) { + let owner = nondet_address(); + OwnableContract::ownable_constructor(&e, owner); + cvlr_satisfy!(true); +} + +#[rule] +pub fn transfer_ownership_sanity(e: Env) { + let owner = nondet_address(); + OwnableContract::ownable_constructor(&e, owner); + let new_owner = nondet_address(); + let live_until_ledger = u32::nondet(); + OwnableContract::transfer_ownership(&e, new_owner, live_until_ledger); + cvlr_satisfy!(true); +} + +#[rule] +pub fn accept_ownership_sanity(e: Env) { + let owner = nondet_address(); + OwnableContract::ownable_constructor(&e, owner); + OwnableContract::accept_ownership(&e); + cvlr_satisfy!(true); +} + +#[rule] +pub fn renounce_ownership_sanity(e: Env) { + let owner = nondet_address(); + OwnableContract::ownable_constructor(&e, owner); + OwnableContract::renounce_ownership(&e); + cvlr_satisfy!(true); +} \ No newline at end of file diff --git a/packages/access/src/ownable/storage.rs b/packages/access/src/ownable/storage.rs index 99d7bfae..886f6f7d 100644 --- a/packages/access/src/ownable/storage.rs +++ b/packages/access/src/ownable/storage.rs @@ -2,12 +2,18 @@ use soroban_sdk::{contracttype, panic_with_error, Address, Env}; use crate::{ ownable::{ - emit_ownership_renounced, emit_ownership_transfer, emit_ownership_transfer_completed, OwnableError, }, role_transfer::{accept_transfer, transfer_role}, }; +#[cfg(not(feature = "certora"))] +use crate::{ + ownable::{ + emit_ownership_renounced, emit_ownership_transfer, emit_ownership_transfer_completed + } +}; + /// Storage keys for `Ownable` utility. #[contracttype] pub enum OwnableStorageKey { @@ -131,11 +137,16 @@ pub fn accept_ownership(e: &Env) { /// * Authorization for the current owner is required. pub fn renounce_ownership(e: &Env) { let owner = enforce_owner_auth(e); + #[cfg(not(feature = "certora"))] let key = OwnableStorageKey::PendingOwner; - + #[cfg(not(feature = "certora"))] if e.storage().temporary().get::<_, Address>(&key).is_some() { panic_with_error!(e, OwnableError::TransferInProgress); } + #[cfg(feature = "certora")] + if e.storage().temporary().get::<_, Address>(&OwnableStorageKey::PendingOwner).is_some() { + panic_with_error!(e, OwnableError::TransferInProgress); + } e.storage().instance().remove(&OwnableStorageKey::Owner); #[cfg(not(feature = "certora"))] diff --git a/packages/access/src/ownable/test.rs b/packages/access/src/ownable/test.rs index 8ccffedb..1724bb7d 100644 --- a/packages/access/src/ownable/test.rs +++ b/packages/access/src/ownable/test.rs @@ -98,7 +98,7 @@ fn enforce_owner_auth_works() { } #[test] -#[should_panic(expected = "Error(Contract, #1220)")] +#[should_panic(expected = "Error(Contract, #2100)")] fn enforce_owner_auth_panics_if_renounced() { let e = Env::default(); let owner = Address::generate(&e); @@ -122,7 +122,7 @@ fn enforce_owner_auth_panics_if_renounced() { } #[test] -#[should_panic(expected = "Error(Contract, #1221)")] +#[should_panic(expected = "Error(Contract, #2101)")] fn renounce_fails_if_pending_transfer_exists() { let e = Env::default(); let owner = Address::generate(&e); @@ -142,7 +142,7 @@ fn renounce_fails_if_pending_transfer_exists() { } #[test] -#[should_panic(expected = "Error(Contract, #1222)")] +#[should_panic(expected = "Error(Contract, #2102)")] fn set_owner_when_already_set_panics() { let e = Env::default(); e.mock_all_auths(); diff --git a/packages/access/src/role_transfer/mod.rs b/packages/access/src/role_transfer/mod.rs index ade94514..f27857ce 100644 --- a/packages/access/src/role_transfer/mod.rs +++ b/packages/access/src/role_transfer/mod.rs @@ -11,9 +11,9 @@ pub use storage::{accept_transfer, transfer_role}; #[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] #[repr(u32)] pub enum RoleTransferError { - NoPendingTransfer = 1200, - InvalidLiveUntilLedger = 1201, - InvalidPendingAccount = 1202, + NoPendingTransfer = 2200, + InvalidLiveUntilLedger = 2201, + InvalidPendingAccount = 2202, } #[cfg(test)] diff --git a/packages/access/src/role_transfer/test.rs b/packages/access/src/role_transfer/test.rs index 37e4db03..a8a3da0b 100644 --- a/packages/access/src/role_transfer/test.rs +++ b/packages/access/src/role_transfer/test.rs @@ -69,7 +69,7 @@ fn role_transfer_cancel_works() { } #[test] -#[should_panic(expected = "Error(Contract, #1200)")] +#[should_panic(expected = "Error(Contract, #2200)")] fn accept_transfer_with_no_pending_transfer_panics() { let e = Env::default(); e.mock_all_auths(); @@ -88,7 +88,7 @@ fn accept_transfer_with_no_pending_transfer_panics() { } #[test] -#[should_panic(expected = "Error(Contract, #1202)")] +#[should_panic(expected = "Error(Contract, #2202)")] fn cannot_cancel_with_invalid_pending_address() { let e = Env::default(); e.mock_all_auths(); @@ -113,7 +113,7 @@ fn cannot_cancel_with_invalid_pending_address() { } #[test] -#[should_panic(expected = "Error(Contract, #1201)")] +#[should_panic(expected = "Error(Contract, #2201)")] fn transfer_with_invalid_live_until_ledger_panics() { let e = Env::default(); e.mock_all_auths(); @@ -134,7 +134,7 @@ fn transfer_with_invalid_live_until_ledger_panics() { } #[test] -#[should_panic(expected = "Error(Contract, #1200)")] +#[should_panic(expected = "Error(Contract, #2200)")] fn cancel_transfer_when_there_is_no_pending_transfer_panics() { let e = Env::default(); e.mock_all_auths(); diff --git a/packages/accounts/Cargo.toml b/packages/accounts/Cargo.toml index e196a5a4..9e4dcca0 100644 --- a/packages/accounts/Cargo.toml +++ b/packages/accounts/Cargo.toml @@ -16,6 +16,12 @@ soroban-sdk = { workspace = true } serde = { workspace = true, features = ["derive"] } serde-json-core = { workspace = true } +cvlr = { workspace = true, default-features = false } +cvlr-soroban = { workspace = true } +cvlr-soroban-macros = { workspace = true } +cvlr-soroban-derive = { workspace = true } +base64ct = { workspace = true } + [dev-dependencies] soroban-sdk = { workspace = true, features = ["testutils"] } soroban-test-helpers = { workspace = true } @@ -23,3 +29,7 @@ stellar-event-assertion = { workspace = true } ed25519-dalek = { workspace = true } p256 = { workspace = true, features = ["ecdsa"] } hex-literal = { workspace = true } + +[features] +certora = [] +default = ["certora"] \ No newline at end of file diff --git a/packages/accounts/README.md b/packages/accounts/README.md index 3e6dbf30..9d9f38f7 100644 --- a/packages/accounts/README.md +++ b/packages/accounts/README.md @@ -132,7 +132,7 @@ Policies follow a well-defined lifecycle that integrates with context rule manag **Enforcement** is triggered when a context rule successfully matches. Once all policies in the matched rule pass their `can_enforce()` checks, the smart account calls `enforce()` on each policy. This state-changing hook allows policies to update counters, emit events, record timestamps, or perform other mutations that track authorization activity. For example, a spending limit policy might deduct from the available balance and emit an event documenting the transaction. -**Uninstallation** occurs when a context rule is removed from the smart account. The account calls `uninstall()` on each attached policy, allowing them to clean up any stored data associated with that specific account and context rule pairing. This ensures that policies do not leave orphaned state in storage. +**Uninstallation** occurs when a context rule is removed from the smart account. The account calls `uninstall()` on each attached policy, allowing them to clean up any stored data associated with that specific account and context rule pairing. This ensures that policies do not leave orphaned state in storage. #### Policy Examples @@ -223,7 +223,7 @@ create_context_rule( name: "DeFi Session", valid_until: Some(current_ledger + 24_hours), signers: vec![&e, ed25519_key], - policies: map![&E, (spending_limit_policy, spending_params)] + policies: map![&e, (spending_limit_policy, spending_params)] ) ``` @@ -252,7 +252,7 @@ create_context_rule( context_type: Default, name: "Portfolio AI", valid_until: Some(current_ledger + 7_days), - signers: vec![ai_agent_key], + signers: vec![&e, ai_agent_key], policies: map![ &e, (whitelist_policy, allowed_functions), @@ -270,6 +270,7 @@ create_context_rule( name: "Treasury Operations", valid_until: None, signers: vec![ + &e, Signer::External(ed25519_verifier, alice_pubkey), Signer::External(secp256k1_verifier, bob_pubkey), Signer::Delegated(carol_contract) @@ -280,7 +281,17 @@ create_context_rule( ## Getting Started -### 1. Implement the Smart Account Trait +### 1. Installation + +Add this to your `Cargo.toml`: + +```toml +[dependencies] +# We recommend pinning to a specific version, because rapid iterations are expected as the library is in an active development phase. +stellar-accounts = "=0.5.1" +``` + +### 2. Implement the Smart Account Trait ```rust use stellar_accounts::smart_account::{ @@ -302,7 +313,7 @@ impl SmartAccount for MySmartAccount { ) -> ContextRule { e.current_contract_address().require_auth(); - add_context_rule(e, &context_type, name, valid_until, signers, policies) + add_context_rule(e, &context_type, &name, &valid_until, &signers, &policies) } // Implement all other methods } @@ -312,31 +323,33 @@ impl CustomAccountInterface for MySmartAccount { type Signature = Signatures; fn __check_auth( - env: Env, + e: Env, signature_payload: Hash<32>, signatures: Signatures, auth_context: Vec, ) -> Result<(), SmartAccountError> { do_check_auth(e, signature_payload, signatures, auth_contexts) + + Ok(()) } } ``` -### 2. Create Context Rules +### 3. Create Context Rules ```rust // Create an admin rule add_context_rule( - &env, + &e, ContextRuleType::Default, - String::from_str(&env, "Admin Access"), + String::from_str(&e, "Admin Access"), None, // No expiration - vec![&env, admin_signer], - Map::new(&env) + vec![&e, admin_signer], + map![&e] ); ``` -### 3. Add Policies (Optional) +### 4. Add Policies (Optional) For policies, there are two options: @@ -351,14 +364,14 @@ For policies, there are two options: ```rust // Add a spending limit policy add_policy( - &env, + &e, admin_rule.id, spending_policy_address, spending_limit_params ); ``` -### 4. Choose or Deploy Verifier Contracts (For External Signers) +### 5. Choose or Deploy Verifier Contracts (For External Signers) For external signers, there are two options: diff --git a/packages/accounts/certora_build.py b/packages/accounts/certora_build.py new file mode 100755 index 00000000..87e6690e --- /dev/null +++ b/packages/accounts/certora_build.py @@ -0,0 +1,101 @@ +#!/usr/bin/env python3 +import argparse +import json +import subprocess +import tempfile +import sys +from pathlib import Path + +SCRIPT_DIR = Path(__file__).resolve().parent + +# Command to run for compiling the rust project. +COMMAND = "just build" + +# JSON FIELDS +PROJECT_DIR = (SCRIPT_DIR / "../").resolve() +SOURCES = ["accounts/src/**/*.rs"] +EXECUTABLES = "../target/wasm32-unknown-unknown/release/stellar_accounts.wasm" + +VERBOSE = False + +def log(msg): + if VERBOSE: + print(msg, file=sys.stderr) + +def run_command(command, to_stdout=False): + """Runs the build command and dumps output to temporary files.""" + log(f"Running '{command}'") + try: + if to_stdout: + result = subprocess.run( + command, + shell=True, + text=True + ) + return None, None, result.returncode + else: + with tempfile.NamedTemporaryFile(delete=False, mode='w', prefix="certora_build_", suffix='.stdout') as stdout_file, \ + tempfile.NamedTemporaryFile(delete=False, mode='w', prefix="certora_build_", suffix='.stderr') as stderr_file: + # Compile rust project and redirect stdout and stderr to a temp file + result = subprocess.run( + command, + shell=True, + stdout=stdout_file, + stderr=stderr_file, + text=True + ) + return stdout_file.name, stderr_file.name, result.returncode + except Exception as e: + log(f"Error running command '{command}': {e}") + return None, None -1 + +def write_output(output_data, output_file=None): + """Writes the JSON output either to a file or dumps it to the console.""" + if output_file: + with open(output_file, 'w') as f: + json.dump(output_data, f, indent=4) + log(f"Output written to {output_file}") + else: + print(json.dumps(output_data, indent=4), file=sys.stdout) + +def main(): + parser = argparse.ArgumentParser(description="Compile rust projects and generate JSON output to be used by Certora Prover.") + parser.add_argument("-o", "--output", metavar="FILE", help="Path to output JSON to a file.") + parser.add_argument("--json", action="store_true", help="Dump JSON output to the console.") + parser.add_argument("-l", "--log", action="store_true", help="Show log outputs from cargo build on standard out.") + parser.add_argument("-v", "--verbose", action="store_true", help="Be verbose.") + + args = parser.parse_args() + global VERBOSE + VERBOSE = args.verbose + + to_stdout = args.log + + # Compile rust project and dump the logs to tmp files + stdout_log, stderr_log, return_code = run_command(COMMAND, to_stdout) + + if stdout_log is not None: + log(f"Temporary log file located at:\n\t{stdout_log}\nand\n\t{stderr_log}") + + # JSON template + output_data = { + "project_directory": str(PROJECT_DIR), + "sources": SOURCES, + "executables": EXECUTABLES, + "success": True if return_code == 0 else False, + "return_code": return_code, + "log" : {"stdout": stdout_log, "stderr": stderr_log} + } + + # Handle output based on the provided argument + if args.output: + write_output(output_data, args.output) + + if args.json: + write_output(output_data) + + # Needed for mutations: if you run _this_ script inside another script, you can check this returncode and decide what to do + sys.exit(0 if return_code == 0 else 1) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/packages/accounts/confs/simple_threshold_contract_sanity.conf b/packages/accounts/confs/simple_threshold_contract_sanity.conf new file mode 100644 index 00000000..e53ca668 --- /dev/null +++ b/packages/accounts/confs/simple_threshold_contract_sanity.conf @@ -0,0 +1,16 @@ +{ + "build_script": "../certora_build.py", + "loop_iter": 2, + "msg": "Sanity Rules Simple Threshold Contract", + "rule": [ + "get_simple_threshold_sanity", + "can_enforce_simple_threshold_sanity", + "enforce_simple_threshold_sanity", + "set_simple_threshold_sanity", + "install_simple_threshold_sanity", + "uninstall_simple_threshold_sanity" + ], + "server": "production", + "prover_version": "stellar-oz-changes" +} + diff --git a/packages/accounts/confs/simple_threshold_integrity.conf b/packages/accounts/confs/simple_threshold_integrity.conf new file mode 100644 index 00000000..8e6d3b67 --- /dev/null +++ b/packages/accounts/confs/simple_threshold_integrity.conf @@ -0,0 +1,14 @@ +{ + "build_script": "../certora_build.py", + "msg": "Integrity Rules Simple Threshold", + "loop_iter": 3, + "optimistic_loop": true, + "rule": [ + "st_set_threshold_integrity", + "st_can_enforce_integrity", + "st_install_integrity", + "st_uninstall_integrity" + ], + "server": "production", + "prover_version": "stellar-oz-changes" +} \ No newline at end of file diff --git a/packages/accounts/confs/simple_threshold_non_panics.conf b/packages/accounts/confs/simple_threshold_non_panics.conf new file mode 100644 index 00000000..fcc0d3f5 --- /dev/null +++ b/packages/accounts/confs/simple_threshold_non_panics.conf @@ -0,0 +1,18 @@ +{ + "build_script": "../certora_build.py", + "msg": "Non-Panic Rules Simple Threshold", + "rule": [ + "set_threshold_non_panic", + "get_threshold_non_panic", + "can_enforce_non_panic", + "enforce_non_panic", + "install_non_panic", + "uninstall_non_panic" + ], + "prover_args": ["-trapAsAssert", "true"], + "server": "production", + "prover_version": "stellar-oz-changes", + "loop_iter" : 2, + "optimistic_loop" : true, +} + diff --git a/packages/accounts/confs/simple_threshold_panics.conf b/packages/accounts/confs/simple_threshold_panics.conf new file mode 100644 index 00000000..305dd822 --- /dev/null +++ b/packages/accounts/confs/simple_threshold_panics.conf @@ -0,0 +1,18 @@ +{ + "build_script": "../certora_build.py", + "msg": "Panic Rules Simple Threshold", + "loop_iter": 3, + "optimistic_loop": true, + "rule": [ + "set_threshold_panics_if_invalid_threshold", + "set_threshold_panics_if_unauth", + "get_threshold_panics_if_no_threshold", + "enforce_panics_if_can_enforce_returns_false", + "install_panics_if_invalid_threshold", + "install_panics_if_unauth", + "uninstall_panics_if_unauth" + ], + "server": "production", + "prover_version": "stellar-oz-changes" +} + diff --git a/packages/accounts/confs/simple_threshold_sanity.conf b/packages/accounts/confs/simple_threshold_sanity.conf new file mode 100644 index 00000000..7cd02d54 --- /dev/null +++ b/packages/accounts/confs/simple_threshold_sanity.conf @@ -0,0 +1,15 @@ +{ + "build_script": "../certora_build.py", + "loop_iter": 2, + "msg": "Sanity Rules Simple Threshold", + "rule": [ + "get_simple_threshold_sanity", + "can_enforce_simple_threshold_sanity", + "enforce_simple_threshold_sanity", + "set_simple_threshold_sanity", + "install_simple_threshold_sanity", + "uninstall_simple_threshold_sanity" + ], + "server": "production", + "prover_version": "stellar-oz-changes" +} \ No newline at end of file diff --git a/packages/accounts/confs/smart_account_sanity.conf b/packages/accounts/confs/smart_account_sanity.conf new file mode 100644 index 00000000..3bd454f1 --- /dev/null +++ b/packages/accounts/confs/smart_account_sanity.conf @@ -0,0 +1,19 @@ +{ + "build_script": "../certora_build.py", + "loop_iter": 3, + "msg": "Sanity Rules Smart Account", + "rule": [ + "get_context_rule_sanity", + "get_context_rules_sanity", + "add_context_rule_sanity", + "update_context_rule_name_sanity", + "update_context_rule_valid_until_sanity", + "remove_context_rule_sanity", + "add_signer_sanity", + "remove_signer_sanity", + "add_policy_sanity", + "remove_policy_sanity" + ], + "server": "production", + "prover_version": "stellar-oz-changes" +} \ No newline at end of file diff --git a/packages/accounts/confs/spending_limit_contract_sanity.conf b/packages/accounts/confs/spending_limit_contract_sanity.conf new file mode 100644 index 00000000..12205e78 --- /dev/null +++ b/packages/accounts/confs/spending_limit_contract_sanity.conf @@ -0,0 +1,17 @@ +{ + "build_script": "../certora_build.py", + "msg": "Sanity Rules Spending Limit Contract", + "optimistic_loop": true, + "loop_iter": 1, + "rule": [ + "get_spending_limit_data_sanity", + "can_enforce_spending_limit_sanity", + "enforce_spending_limit_sanity", + "set_spending_limit_sanity", + "install_spending_limit_sanity", + "uninstall_spending_limit_sanity" + ], + "server": "production", + "prover_version": "stellar-oz-changes" +} + diff --git a/packages/accounts/confs/spending_limit_integrity.conf b/packages/accounts/confs/spending_limit_integrity.conf new file mode 100644 index 00000000..90300cf3 --- /dev/null +++ b/packages/accounts/confs/spending_limit_integrity.conf @@ -0,0 +1,15 @@ +{ + "build_script": "../certora_build.py", + "loop_iter": 1, + "msg": "Integrity Rules Spending Limit", + "rule": [ + "sl_set_spending_limit_integrity", + "sl_install_integrity", + "sl_uninstall_integrity", + "no_previous_transfer_succeeds", + ], + "server": "production", + "optimistic_loop": true, + "prover_version": "stellar-oz-changes" +} + diff --git a/packages/accounts/confs/spending_limit_sanity.conf b/packages/accounts/confs/spending_limit_sanity.conf new file mode 100644 index 00000000..d0515284 --- /dev/null +++ b/packages/accounts/confs/spending_limit_sanity.conf @@ -0,0 +1,15 @@ +{ + "build_script": "../certora_build.py", + "loop_iter": 2, + "msg": "Sanity Rules Spending Limit", + "rule": [ + "get_spending_limit_sanity", + "can_enforce_spending_limit_sanity", + "enforce_spending_limit_sanity", + "set_spending_limit_sanity", + "install_spending_limit_sanity", + "uninstall_spending_limit_sanity" + ], + "server": "production", + "prover_version": "stellar-oz-changes" +} \ No newline at end of file diff --git a/packages/accounts/confs/weighted_threshold_contract_sanity.conf b/packages/accounts/confs/weighted_threshold_contract_sanity.conf new file mode 100644 index 00000000..50265a84 --- /dev/null +++ b/packages/accounts/confs/weighted_threshold_contract_sanity.conf @@ -0,0 +1,19 @@ +{ + "build_script": "../certora_build.py", + "loop_iter": 2, + "msg": "Sanity Rules Weighted Threshold Contract", + "rule": [ + "get_weighted_threshold_sanity", + "get_signer_weights_weighted_threshold_sanity", + "can_enforce_weighted_threshold_sanity", + "enforce_weighted_threshold_sanity", + "set_weighted_threshold_sanity", + "set_signer_weight_weighted_threshold_sanity", + "install_weighted_threshold_sanity", + "uninstall_weighted_threshold_sanity" + ], + "server": "production", + "prover_version": "stellar-oz-changes" +} + + diff --git a/packages/accounts/confs/weighted_threshold_integrity.conf b/packages/accounts/confs/weighted_threshold_integrity.conf new file mode 100644 index 00000000..7d87f09a --- /dev/null +++ b/packages/accounts/confs/weighted_threshold_integrity.conf @@ -0,0 +1,17 @@ +{ + "build_script": "../certora_build.py", + "loop_iter": 1, + "msg": "Integrity Rules Weighted Threshold", + "rule": [ + "wt_can_enforce_integrity", + "wt_set_threshold_integrity", + "wt_set_signer_weight_integrity", + "wt_install_integrity", + "wt_uninstall_integrity" + ], + "server": "production", + "optimistic_loop": true, + "prover_version": "stellar-oz-changes" +} + + diff --git a/packages/accounts/confs/weighted_threshold_sanity.conf b/packages/accounts/confs/weighted_threshold_sanity.conf new file mode 100644 index 00000000..209c62fd --- /dev/null +++ b/packages/accounts/confs/weighted_threshold_sanity.conf @@ -0,0 +1,18 @@ +{ + "build_script": "../certora_build.py", + "loop_iter": 2, + "msg": "Sanity Rules Weighted Threshold", + "rule": [ + "get_weighted_threshold_sanity", + "get_signer_weights_sanity", + "calculate_weight_sanity", + "can_enforce_weighted_threshold_sanity", + "enforce_weighted_threshold_sanity", + "set_weighted_threshold_sanity", + "set_signer_weight_sanity", + "install_weighted_threshold_sanity", + "uninstall_weighted_threshold_sanity" + ], + "server": "production", + "prover_version": "stellar-oz-changes" +} \ No newline at end of file diff --git a/packages/accounts/justfile b/packages/accounts/justfile new file mode 100644 index 00000000..4d26eddf --- /dev/null +++ b/packages/accounts/justfile @@ -0,0 +1,9 @@ +export RUSTFLAGS := "-C strip=none" + +target := "../../target" + +build: + cargo +nightly-2024-11-22 build --target=wasm32-unknown-unknown --release --features certora + +clean: + rm -rf {{target}} \ No newline at end of file diff --git a/packages/accounts/src/lib.rs b/packages/accounts/src/lib.rs index 25b6a2b5..9ef75ed7 100644 --- a/packages/accounts/src/lib.rs +++ b/packages/accounts/src/lib.rs @@ -4,7 +4,8 @@ //! advanced authentication and authorization patterns through composable rules, //! signers, and policies. #![no_std] +#![cfg_attr(feature = "certora", allow(unused_variables,unused_imports))] pub mod policies; pub mod smart_account; -pub mod verifiers; +pub mod verifiers; \ No newline at end of file diff --git a/packages/accounts/src/policies/mod.rs b/packages/accounts/src/policies/mod.rs index d0606270..e9a94f6d 100644 --- a/packages/accounts/src/policies/mod.rs +++ b/packages/accounts/src/policies/mod.rs @@ -9,10 +9,14 @@ use soroban_sdk::{auth::Context, contractclient, Address, Env, FromVal, Val, Vec use crate::smart_account::{ContextRule, Signer}; +#[cfg(feature = "certora")] +pub mod specs; + pub mod simple_threshold; pub mod spending_limit; #[cfg(test)] mod test; + pub mod weighted_threshold; /// Core trait for authorization policies in smart accounts. @@ -179,7 +183,7 @@ pub trait Policy { // with the public `Policy` trait above for their implementations. #[allow(unused)] #[contractclient(name = "PolicyClient")] -trait PolicyClientInterface { +pub trait PolicyClientInterface { fn can_enforce( e: &Env, context: Context, diff --git a/packages/accounts/src/policies/simple_threshold.rs b/packages/accounts/src/policies/simple_threshold.rs index baf929c9..cc11a6e9 100644 --- a/packages/accounts/src/policies/simple_threshold.rs +++ b/packages/accounts/src/policies/simple_threshold.rs @@ -45,10 +45,17 @@ //! **Failure to follow this process may result in permanent DoS or silent //! security degradation.** +use cvlr::nondet::Nondet; use soroban_sdk::{ - auth::Context, contracterror, contractevent, contracttype, panic_with_error, Address, Env, Vec, + auth::Context, contracterror, contracttype, panic_with_error, Address, Env, Vec, }; +#[cfg(not(feature = "certora"))] +use soroban_sdk::{contractevent}; + +#[cfg(feature = "certora")] +use cvlr_soroban_derive::contractevent; + use crate::smart_account::ContextRule; // re-export pub use crate::smart_account::Signer; @@ -72,17 +79,25 @@ pub struct SimpleThresholdAccountParams { pub threshold: u32, } +impl Nondet for SimpleThresholdAccountParams { + fn nondet() -> Self { + SimpleThresholdAccountParams { + threshold: u32::nondet(), + } + } +} + /// Error codes for simple threshold policy operations. #[contracterror] #[derive(Copy, Clone, Debug, PartialEq)] #[repr(u32)] pub enum SimpleThresholdError { /// The smart account does not have a simple threshold policy installed. - SmartAccountNotInstalled = 2200, + SmartAccountNotInstalled = 3200, /// When threshold is 0 or exceeds the number of available signers. - InvalidThreshold = 2201, + InvalidThreshold = 3201, /// The transaction is not allowed by this policy. - NotAllowed = 2202, + NotAllowed = 3202, } /// Storage keys for simple threshold policy data. @@ -198,6 +213,7 @@ pub fn enforce( if authenticated_signers.len() >= threshold { // emit event + #[cfg(not(feature = "certora"))] SimplePolicyEnforced { smart_account: smart_account.clone(), context: context.clone(), diff --git a/packages/accounts/src/policies/specs/mod.rs b/packages/accounts/src/policies/specs/mod.rs new file mode 100644 index 00000000..94daa5a0 --- /dev/null +++ b/packages/accounts/src/policies/specs/mod.rs @@ -0,0 +1,16 @@ +// pub mod simple_threshold_sanity; +pub mod simple_threshold_contract; +pub mod simple_threshold_contract_sanity; +pub mod simple_threshold_integrity; +pub mod simple_threshold_panics; +pub mod simple_threshold_non_panics; + +// pub mod weighted_threshold_sanity; +pub mod weighted_threshold_contract; +pub mod weighted_threshold_contract_sanity; +pub mod weighted_threshold_integrity; + +// pub mod spending_limit_sanity; +pub mod spending_limit_contract; +pub mod spending_limit_contract_sanity; +pub mod spending_limit_integrity; \ No newline at end of file diff --git a/packages/accounts/src/policies/specs/simple_threshold_contract.rs b/packages/accounts/src/policies/specs/simple_threshold_contract.rs new file mode 100644 index 00000000..feed5fc1 --- /dev/null +++ b/packages/accounts/src/policies/specs/simple_threshold_contract.rs @@ -0,0 +1,41 @@ +use soroban_sdk::{contract, contractimpl, Env}; +use crate::policies::Policy; +use crate::policies::simple_threshold::SimpleThresholdAccountParams; +use crate::smart_account::{ContextRule, Signer}; +use soroban_sdk::{auth::Context, Address, Vec}; +use crate::policies::simple_threshold; + +#[contract] +pub struct SimpleThresholdPolicy; + +impl SimpleThresholdPolicy { + pub fn get_threshold(e: &Env, context_rule_id: u32, smart_account: Address) -> u32 { + crate::policies::simple_threshold::get_threshold(e, context_rule_id, &smart_account) + } + + pub fn set_threshold(e: &Env, threshold: u32, context_rule: ContextRule, smart_account: Address) { + crate::policies::simple_threshold::set_threshold(e, threshold, &context_rule, &smart_account) + } +} + +#[contractimpl] +impl Policy for SimpleThresholdPolicy { + type AccountParams = SimpleThresholdAccountParams; + + fn can_enforce(e: &Env, context: Context, authenticated_signers: Vec, context_rule: ContextRule, smart_account: Address) -> bool { + crate::policies::simple_threshold::can_enforce(e, &context, &authenticated_signers, &context_rule, &smart_account) + } + + fn enforce(e: &Env, context: Context, authenticated_signers: Vec, context_rule: ContextRule, smart_account: Address) { + crate::policies::simple_threshold::enforce(e, &context, &authenticated_signers, &context_rule, &smart_account) + } + + fn install(e: &Env, install_params: Self::AccountParams, context_rule: ContextRule, smart_account: Address) { + crate::policies::simple_threshold::install(e, &install_params, &context_rule, &smart_account) + } + + fn uninstall(e: &Env, context_rule: ContextRule, smart_account: Address) { + crate::policies::simple_threshold::uninstall(e, &context_rule, &smart_account) + } +} + diff --git a/packages/accounts/src/policies/specs/simple_threshold_contract_sanity.rs b/packages/accounts/src/policies/specs/simple_threshold_contract_sanity.rs new file mode 100644 index 00000000..4f76762d --- /dev/null +++ b/packages/accounts/src/policies/specs/simple_threshold_contract_sanity.rs @@ -0,0 +1,79 @@ +use core::task::Context; + +use cvlr::{ + cvlr_assert, + nondet::{self, Nondet}, + cvlr_satisfy, +}; +use cvlr_soroban::{nondet_address}; +use cvlr_soroban_derive::rule; +use soroban_sdk::{Address, Env, Vec}; + +use crate::{ + policies::{Policy, simple_threshold::SimpleThresholdAccountParams, specs::simple_threshold_contract::SimpleThresholdPolicy}, + smart_account::{ContextRule, Signer, specs::nondet::nondet_signers_vec}, +}; + +#[rule] +pub fn get_simple_threshold_sanity(e: Env) { + let ctx_rule_id: u32 = u32::nondet(); + let account_id = nondet_address(); + let _ = SimpleThresholdPolicy::get_threshold(&e, ctx_rule_id, account_id); + cvlr_satisfy!(true); +} + +#[rule] +pub fn can_enforce_simple_threshold_sanity(e: Env, context: soroban_sdk::auth::Context) { + let auth_signers: Vec = nondet_signers_vec(); + let ctx_rule: ContextRule = ContextRule::nondet(); + let account: Address = nondet_address(); + let _ = SimpleThresholdPolicy::can_enforce( + &e, + context, + auth_signers, + ctx_rule, + account, + ); + cvlr_satisfy!(true); +} + +#[rule] +pub fn enforce_simple_threshold_sanity(e: Env, context: soroban_sdk::auth::Context) { + let auth_signers = nondet_signers_vec(); + let ctx_rule: ContextRule = ContextRule::nondet(); + let account: Address = nondet_address(); + let _ = SimpleThresholdPolicy::enforce( + &e, + context, + auth_signers, + ctx_rule, + account, + ); + cvlr_satisfy!(true); +} + +#[rule] +pub fn set_simple_threshold_sanity(e: Env) { + let threshold: u32 = u32::nondet(); + let ctx_rule: ContextRule = ContextRule::nondet(); + let account_id = nondet_address(); + SimpleThresholdPolicy::set_threshold(&e, threshold, ctx_rule, account_id); + cvlr_satisfy!(true); +} + +#[rule] +pub fn install_simple_threshold_sanity(e: Env) { + let params = SimpleThresholdAccountParams::nondet(); + let ctx_rule: ContextRule = ContextRule::nondet(); + let account_id = nondet_address(); + SimpleThresholdPolicy::install(&e, params, ctx_rule, account_id); + cvlr_satisfy!(true); +} + +#[rule] +pub fn uninstall_simple_threshold_sanity(e: Env) { + let ctx_rule: ContextRule = ContextRule::nondet(); + let account_id = nondet_address(); + SimpleThresholdPolicy::uninstall(&e, ctx_rule, account_id); + cvlr_satisfy!(true); +} diff --git a/packages/accounts/src/policies/specs/simple_threshold_integrity.rs b/packages/accounts/src/policies/specs/simple_threshold_integrity.rs new file mode 100644 index 00000000..4e4057e9 --- /dev/null +++ b/packages/accounts/src/policies/specs/simple_threshold_integrity.rs @@ -0,0 +1,68 @@ +use core::task::Context; + +use cvlr::{ + cvlr_assert, + nondet::{self, Nondet}, + cvlr_satisfy, +}; +use cvlr_soroban::{nondet_address}; +use cvlr_soroban_derive::rule; +use soroban_sdk::{Address, Env, Vec}; + +use crate::{ + policies::{Policy, simple_threshold::SimpleThresholdAccountParams, specs::simple_threshold_contract::SimpleThresholdPolicy}, + smart_account::{ContextRule, Signer, specs::nondet::nondet_signers_vec}, +}; + + +#[rule] +// after set_threshold the threshold is set to input +// status: verified +pub fn st_set_threshold_integrity(e: Env) { + let threshold: u32 = u32::nondet(); + let ctx_rule: ContextRule = ContextRule::nondet(); + let account_id = nondet_address(); + SimpleThresholdPolicy::set_threshold(&e, threshold, ctx_rule.clone(), account_id.clone()); + let threshold_post = SimpleThresholdPolicy::get_threshold(&e, ctx_rule.id, account_id); + cvlr_assert!(threshold_post == threshold); +} + +#[rule] +// can_enforce returns the expected auth_signers.len() >= threshold_pre; +// not really an intgerity rule because this is a view function +// status: verified +pub fn st_can_enforce_integrity(e: Env, context: soroban_sdk::auth::Context) { + let auth_signers: Vec = nondet_signers_vec(); + let ctx_rule: ContextRule = ContextRule::nondet(); + let account_id = nondet_address(); + let threshold_pre = SimpleThresholdPolicy::get_threshold(&e, ctx_rule.id, account_id.clone()); + let can_enforce = SimpleThresholdPolicy::can_enforce(&e, context, auth_signers.clone(), ctx_rule.clone(), account_id.clone()); + let expected_result = auth_signers.len() >= threshold_pre; + cvlr_assert!(can_enforce == expected_result); +} + +// can't write an integrity rule for enforce because it panics if can_enforce returns false. + +#[rule] +// after install the threshold is set to input +// status: verified +pub fn st_install_integrity(e: Env) { + let params: SimpleThresholdAccountParams = SimpleThresholdAccountParams::nondet(); + let ctx_rule: ContextRule = ContextRule::nondet(); + let account_id = nondet_address(); + SimpleThresholdPolicy::install(&e, params.clone(), ctx_rule.clone(), account_id.clone()); + let threshold_post = SimpleThresholdPolicy::get_threshold(&e, ctx_rule.id, account_id.clone()); + cvlr_assert!(threshold_post == params.threshold); +} + +#[rule] +// after uninstall the account ctx is removed +// status: verified +pub fn st_uninstall_integrity(e: Env) { + let ctx_rule: ContextRule = ContextRule::nondet(); + let account_id = nondet_address(); + SimpleThresholdPolicy::uninstall(&e, ctx_rule.clone(), account_id.clone()); + let key = crate::policies::simple_threshold::SimpleThresholdStorageKey::AccountContext(account_id.clone(), ctx_rule.id); + let account_ctx_opt: Option = e.storage().persistent().get(&key); + cvlr_assert!(account_ctx_opt.is_none()); +} \ No newline at end of file diff --git a/packages/accounts/src/policies/specs/simple_threshold_invariants.rs b/packages/accounts/src/policies/specs/simple_threshold_invariants.rs new file mode 100644 index 00000000..ee0b98ed --- /dev/null +++ b/packages/accounts/src/policies/specs/simple_threshold_invariants.rs @@ -0,0 +1,2 @@ +// threshold != 0 +// one invariant you would expect is that threshold <= ctx_rule.signers() but this is noted in the docs to be violated. \ No newline at end of file diff --git a/packages/accounts/src/policies/specs/simple_threshold_non_panics.rs b/packages/accounts/src/policies/specs/simple_threshold_non_panics.rs new file mode 100644 index 00000000..4dbb6bab --- /dev/null +++ b/packages/accounts/src/policies/specs/simple_threshold_non_panics.rs @@ -0,0 +1,116 @@ +use core::task::Context; + +use cvlr::{ + cvlr_assert, + cvlr_assume, + nondet::{self, Nondet}, + cvlr_satisfy, +}; +use cvlr_soroban::{nondet_address, is_auth}; +use cvlr::clog; +use cvlr_soroban_derive::rule; +use soroban_sdk::{Address, Env, Vec}; + +use crate::{ + policies::{Policy, simple_threshold::SimpleThresholdAccountParams, specs::simple_threshold_contract::SimpleThresholdPolicy}, + smart_account::{ContextRule, Signer, specs::nondet::nondet_signers_vec}, +}; + +fn storage_setup_threshold(e: Env, ctx_rule_id: u32, account_id: Address) { + let threshold: u32 = u32::nondet(); + let key = crate::policies::simple_threshold::SimpleThresholdStorageKey::AccountContext(account_id.clone(), ctx_rule_id); + e.storage().persistent().set(&key, &threshold); + clog!(threshold); +} + +// These rules require the prover arg "prover_args": ["-trapAsAssert true"] to consider also panicking paths. + +#[rule] +// requires +// valid threshold +// account_id auth +// status: violated - Expected sym to be a valid Val, in vec/vec_len +pub fn set_threshold_non_panic(e: Env) { + let threshold: u32 = u32::nondet(); + let ctx_rule: ContextRule = ContextRule::nondet(); + let account_id = nondet_address(); + cvlr_assume!(is_auth(account_id.clone())); + storage_setup_threshold(e.clone(), ctx_rule.id, account_id.clone()); + cvlr_assume!(threshold != 0 && threshold <= ctx_rule.signers.len()); + clog!(threshold); + clog!(ctx_rule.signers.len()); + SimpleThresholdPolicy::set_threshold(&e, threshold, ctx_rule.clone(), account_id.clone()); + cvlr_assert!(true); +} + +#[rule] +// requires +// threshold exists +// status: verified +pub fn get_threshold_non_panic(e: Env) { + let ctx_rule_id: u32 = u32::nondet(); + let account_id = nondet_address(); + storage_setup_threshold(e.clone(), ctx_rule_id, account_id.clone()); + let key = crate::policies::simple_threshold::SimpleThresholdStorageKey::AccountContext(account_id.clone(), ctx_rule_id); + let threshold_opt: Option = e.storage().persistent().get(&key); + cvlr_assume!(threshold_opt.is_some()); + SimpleThresholdPolicy::get_threshold(&e, ctx_rule_id, account_id); + cvlr_assert!(true); +} + +#[rule] +// requires nothing +// status: violated - Expected sym to be a valid Val, in vec/vec_len +pub fn can_enforce_non_panic(e: Env, context: soroban_sdk::auth::Context) { + let authenticated_signers: Vec = nondet_signers_vec(); + let ctx_rule: ContextRule = ContextRule::nondet(); + let account_id = nondet_address(); + storage_setup_threshold(e.clone(), ctx_rule.id, account_id.clone()); + SimpleThresholdPolicy::can_enforce(&e, context, authenticated_signers, ctx_rule, account_id); + cvlr_assert!(true); +} + +#[rule] +// requires +// can_enforce returns true +// status: violated - unreachable - unwrap_failed in nondet_bytes_n +pub fn enforce_non_panic(e: Env, context: soroban_sdk::auth::Context) { + let authenticated_signers: Vec = nondet_signers_vec(); + let ctx_rule: ContextRule = ContextRule::nondet(); + let account_id = nondet_address(); + storage_setup_threshold(e.clone(), ctx_rule.id, account_id.clone()); + let can_enforce = SimpleThresholdPolicy::can_enforce(&e, context.clone(), authenticated_signers.clone(), ctx_rule.clone(), account_id.clone()); + cvlr_assume!(can_enforce == true); + SimpleThresholdPolicy::enforce(&e, context, authenticated_signers, ctx_rule, account_id); + cvlr_assert!(true); +} + +#[rule] +// requires +// account_id auth +// valid threshold +// status: violated - Expected sym to be a valid Val, in vec/vec_len +pub fn install_non_panic(e: Env) { + let params: SimpleThresholdAccountParams = SimpleThresholdAccountParams::nondet(); + let ctx_rule: ContextRule = ContextRule::nondet(); + let account_id = nondet_address(); + storage_setup_threshold(e.clone(), ctx_rule.id, account_id.clone()); + cvlr_assume!(is_auth(account_id.clone())); + let threshold = params.threshold; + cvlr_assume!(threshold != 0 && threshold <= ctx_rule.signers.len()); + SimpleThresholdPolicy::install(&e, params, ctx_rule.clone(), account_id.clone()); + cvlr_assert!(true); +} + +#[rule] +// requires +// account_id auth +// status: verified +pub fn uninstall_non_panic(e: Env) { + let ctx_rule: ContextRule = ContextRule::nondet(); + let account_id = nondet_address(); + storage_setup_threshold(e.clone(), ctx_rule.id, account_id.clone()); + cvlr_assume!(is_auth(account_id.clone())); + SimpleThresholdPolicy::uninstall(&e, ctx_rule.clone(), account_id.clone()); + cvlr_assert!(true); +} \ No newline at end of file diff --git a/packages/accounts/src/policies/specs/simple_threshold_panics.rs b/packages/accounts/src/policies/specs/simple_threshold_panics.rs new file mode 100644 index 00000000..e09f07f3 --- /dev/null +++ b/packages/accounts/src/policies/specs/simple_threshold_panics.rs @@ -0,0 +1,105 @@ +use core::task::Context; + +use cvlr::{ + cvlr_assert, + cvlr_assume, + nondet::{self, Nondet}, + cvlr_satisfy, +}; +use cvlr_soroban::{nondet_address, nondet_vec, is_auth}; +use cvlr_soroban_derive::rule; +use soroban_sdk::{Address, Env, Vec}; + +use crate::{ + policies::{Policy, simple_threshold::SimpleThresholdAccountParams, specs::simple_threshold_contract::SimpleThresholdPolicy}, + smart_account::{ContextRule, Signer, specs::nondet::nondet_signers_vec}, +}; + +#[rule] +// set_threshold_panics if invalid threshold +// status: verified +pub fn set_threshold_panics_if_invalid_threshold(e: Env) { + let threshold: u32 = u32::nondet(); + let ctx_rule: ContextRule = ContextRule::nondet(); + let account_id = nondet_address(); + cvlr_assume!(threshold == 0 || threshold > ctx_rule.signers.len()); + SimpleThresholdPolicy::set_threshold(&e, threshold, ctx_rule.clone(), account_id.clone()); + cvlr_assert!(false); +} + +#[rule] +// set_threshold_panics if unauth +// status: verified +pub fn set_threshold_panics_if_unauth(e: Env) { + let threshold: u32 = u32::nondet(); + let ctx_rule: ContextRule = ContextRule::nondet(); + let account_id = nondet_address(); + cvlr_assume!(!is_auth(account_id.clone())); + SimpleThresholdPolicy::set_threshold(&e, threshold, ctx_rule.clone(), account_id.clone()); + cvlr_assert!(false); +} + +#[rule] +// get_threshold_panics if no threshold +// status: verified +pub fn get_threshold_panics_if_no_threshold(e: Env) { + let ctx_rule_id: u32 = u32::nondet(); + let account_id = nondet_address(); + let key = crate::policies::simple_threshold::SimpleThresholdStorageKey::AccountContext(account_id.clone(), ctx_rule_id); + let threshold_opt: Option = e.storage().persistent().get(&key); + cvlr_assume!(threshold_opt.is_none()); + SimpleThresholdPolicy::get_threshold(&e, ctx_rule_id, account_id); + cvlr_assert!(false); +} + +// can_enforce should never panic + +#[rule] +// enforce panics if can_enforce returns false +// status: verified +pub fn enforce_panics_if_can_enforce_returns_false(e: Env, context: soroban_sdk::auth::Context) { + let authenticated_signers: Vec = nondet_signers_vec(); + let ctx_rule: ContextRule = ContextRule::nondet(); + let account_id = nondet_address(); + let can_enforce = SimpleThresholdPolicy::can_enforce(&e, context.clone(), authenticated_signers.clone(), ctx_rule.clone(), account_id.clone()); + cvlr_assume!(can_enforce == false); + SimpleThresholdPolicy::enforce(&e, context, authenticated_signers, ctx_rule, account_id); + cvlr_assert!(false); +} + +#[rule] +// install panics if invalid threshold +// status: verified +pub fn install_panics_if_invalid_threshold(e: Env) { + let params: SimpleThresholdAccountParams = SimpleThresholdAccountParams::nondet(); + let ctx_rule: ContextRule = ContextRule::nondet(); + let account_id = nondet_address(); + let threshold = params.threshold; + cvlr_assume!(threshold == 0 || threshold > ctx_rule.signers.len()); + SimpleThresholdPolicy::install(&e, params, ctx_rule.clone(), account_id.clone()); + cvlr_assert!(false); +} + +#[rule] +// install panics if unauth +// status: verified +pub fn install_panics_if_unauth(e: Env) { + let params: SimpleThresholdAccountParams = SimpleThresholdAccountParams::nondet(); + let ctx_rule: ContextRule = ContextRule::nondet(); + let account_id = nondet_address(); + cvlr_assume!(!is_auth(account_id.clone())); + SimpleThresholdPolicy::install(&e, params, ctx_rule.clone(), account_id.clone()); + cvlr_assert!(false); +} + + +#[rule] +// uninstall panics if unauth +// status: verified +pub fn uninstall_panics_if_unauth(e: Env) { + let ctx_rule: ContextRule = ContextRule::nondet(); + let account_id = nondet_address(); + cvlr_assume!(!is_auth(account_id.clone())); + SimpleThresholdPolicy::uninstall(&e, ctx_rule.clone(), account_id.clone()); + cvlr_assert!(false); +} \ No newline at end of file diff --git a/packages/accounts/src/policies/specs/simple_threshold_sanity.rs b/packages/accounts/src/policies/specs/simple_threshold_sanity.rs new file mode 100644 index 00000000..c0fe3a17 --- /dev/null +++ b/packages/accounts/src/policies/specs/simple_threshold_sanity.rs @@ -0,0 +1,79 @@ +use core::task::Context; + +use cvlr::{ + cvlr_assert, + nondet::{self, Nondet}, + cvlr_satisfy, +}; +use cvlr_soroban::{nondet_address}; +use cvlr_soroban_derive::rule; +use soroban_sdk::{Address, Env, Vec}; + +use crate::{ + policies::simple_threshold::SimpleThresholdAccountParams, + smart_account::{ContextRule, Signer, specs::nondet::nondet_signers_vec}, +}; + +#[rule] +pub fn get_simple_threshold_sanity(e: Env) { + let ctx_rule_id: u32 = u32::nondet(); + let account_id = nondet_address(); + let _ = crate::policies::simple_threshold::get_threshold(&e, ctx_rule_id, &account_id); + cvlr_satisfy!(true); +} + +#[rule] +pub fn can_enforce_simple_threshold_sanity(e: Env, context: soroban_sdk::auth::Context) { + let auth_signers: Vec = nondet_signers_vec(); + let ctx_rule: ContextRule = ContextRule::nondet(); + let account: Address = nondet_address(); + let _ = crate::policies::simple_threshold::can_enforce( + &e, + &context, + &auth_signers, + &ctx_rule, + &account, + ); + cvlr_satisfy!(true); +} + +#[rule] +pub fn enforce_simple_threshold_sanity(e: Env, context: soroban_sdk::auth::Context) { + let auth_signers = nondet_signers_vec(); + let ctx_rule: ContextRule = ContextRule::nondet(); + let account: Address = nondet_address(); + let _ = crate::policies::simple_threshold::enforce( + &e, + &context, + &auth_signers, + &ctx_rule, + &account, + ); + cvlr_satisfy!(true); +} + +#[rule] +pub fn set_simple_threshold_sanity(e: Env) { + let threshold: u32 = u32::nondet(); + let ctx_rule: ContextRule = ContextRule::nondet(); + let account_id = nondet_address(); + crate::policies::simple_threshold::set_threshold(&e, threshold, &ctx_rule, &account_id); + cvlr_satisfy!(true); +} + +#[rule] +pub fn install_simple_threshold_sanity(e: Env) { + let params = SimpleThresholdAccountParams::nondet(); + let ctx_rule: ContextRule = ContextRule::nondet(); + let account_id = nondet_address(); + crate::policies::simple_threshold::install(&e, ¶ms, &ctx_rule, &account_id); + cvlr_satisfy!(true); +} + +#[rule] +pub fn uninstall_simple_threshold_sanity(e: Env) { + let ctx_rule: ContextRule = ContextRule::nondet(); + let account_id = nondet_address(); + crate::policies::simple_threshold::uninstall(&e, &ctx_rule, &account_id); + cvlr_satisfy!(true); +} diff --git a/packages/accounts/src/policies/specs/spending_limit_contract.rs b/packages/accounts/src/policies/specs/spending_limit_contract.rs new file mode 100644 index 00000000..b7a80068 --- /dev/null +++ b/packages/accounts/src/policies/specs/spending_limit_contract.rs @@ -0,0 +1,42 @@ +use soroban_sdk::{contract, contractimpl, Env}; +use crate::policies::Policy; +use crate::policies::spending_limit::SpendingLimitAccountParams; +use crate::policies::spending_limit::SpendingLimitData; +use crate::smart_account::{ContextRule, Signer}; +use soroban_sdk::{auth::Context, Address, Vec}; +use crate::policies::spending_limit; + +#[contract] +pub struct SpendingLimitPolicy; + +impl SpendingLimitPolicy { + pub fn get_spending_limit_data(e: &Env, context_rule_id: u32, smart_account: Address) -> SpendingLimitData { + crate::policies::spending_limit::get_spending_limit_data(e, context_rule_id, &smart_account) + } + + pub fn set_spending_limit(e: &Env, spending_limit: i128, context_rule: ContextRule, smart_account: Address) { + crate::policies::spending_limit::set_spending_limit(e, spending_limit, &context_rule, &smart_account) + } +} + +// #[contractimpl] -- doesn't compile with this because of duplicate names of contract functions in the same crate. +impl Policy for SpendingLimitPolicy { + type AccountParams = SpendingLimitAccountParams; + + fn can_enforce(e: &Env, context: Context, authenticated_signers: Vec, context_rule: ContextRule, smart_account: Address) -> bool { + crate::policies::spending_limit::can_enforce(e, &context, &authenticated_signers, &context_rule, &smart_account) + } + + fn enforce(e: &Env, context: Context, authenticated_signers: Vec, context_rule: ContextRule, smart_account: Address) { + crate::policies::spending_limit::enforce(e, &context, &authenticated_signers, &context_rule, &smart_account) + } + + fn install(e: &Env, install_params: Self::AccountParams, context_rule: ContextRule, smart_account: Address) { + crate::policies::spending_limit::install(e, &install_params, &context_rule, &smart_account) + } + + fn uninstall(e: &Env, context_rule: ContextRule, smart_account: Address) { + crate::policies::spending_limit::uninstall(e, &context_rule, &smart_account) + } +} + diff --git a/packages/accounts/src/policies/specs/spending_limit_contract_sanity.rs b/packages/accounts/src/policies/specs/spending_limit_contract_sanity.rs new file mode 100644 index 00000000..265d37ca --- /dev/null +++ b/packages/accounts/src/policies/specs/spending_limit_contract_sanity.rs @@ -0,0 +1,78 @@ +use cvlr::{ + cvlr_assert, + nondet::{self, Nondet}, + cvlr_satisfy, +}; +use cvlr_soroban::{nondet_address}; +use cvlr_soroban_derive::rule; +use soroban_sdk::{Address, Env, Vec}; + +use crate::{ + policies::{Policy, specs::spending_limit_contract::SpendingLimitPolicy, spending_limit::SpendingLimitAccountParams}, + smart_account::{ContextRule, Signer, specs::nondet::nondet_signers_vec}, +}; + +#[rule] +pub fn get_spending_limit_data_sanity(e: Env) { + let ctx_rule_id: u32 = u32::nondet(); + let account_id = nondet_address(); + let _ = SpendingLimitPolicy::get_spending_limit_data(&e, ctx_rule_id, account_id); + cvlr_satisfy!(true); +} + +#[rule] +pub fn can_enforce_spending_limit_sanity(e: Env, context: soroban_sdk::auth::Context) { + let auth_signers: Vec = nondet_signers_vec(); + let ctx_rule: ContextRule = ContextRule::nondet(); + let account: Address = nondet_address(); + let _ = SpendingLimitPolicy::can_enforce( + &e, + context, + auth_signers, + ctx_rule, + account, + ); + cvlr_satisfy!(true); +} + +#[rule] +pub fn enforce_spending_limit_sanity(e: Env, context: soroban_sdk::auth::Context) { + let auth_signers = nondet_signers_vec(); + let ctx_rule: ContextRule = ContextRule::nondet(); + let account: Address = nondet_address(); + let _ = SpendingLimitPolicy::enforce( + &e, + context, + auth_signers, + ctx_rule, + account, + ); + cvlr_satisfy!(true); +} + +#[rule] +pub fn set_spending_limit_sanity(e: Env) { + let spending_limit: i128 = i128::nondet(); + let ctx_rule: ContextRule = ContextRule::nondet(); + let account_id = nondet_address(); + SpendingLimitPolicy::set_spending_limit(&e, spending_limit, ctx_rule, account_id); + cvlr_satisfy!(true); +} + +#[rule] +pub fn install_spending_limit_sanity(e: Env) { + let params = SpendingLimitAccountParams::nondet(); + let ctx_rule: ContextRule = ContextRule::nondet(); + let account_id = nondet_address(); + SpendingLimitPolicy::install(&e, params, ctx_rule, account_id); + cvlr_satisfy!(true); +} + +#[rule] +pub fn uninstall_spending_limit_sanity(e: Env) { + let ctx_rule: ContextRule = ContextRule::nondet(); + let account_id = nondet_address(); + SpendingLimitPolicy::uninstall(&e, ctx_rule, account_id); + cvlr_satisfy!(true); +} + diff --git a/packages/accounts/src/policies/specs/spending_limit_integrity.rs b/packages/accounts/src/policies/specs/spending_limit_integrity.rs new file mode 100644 index 00000000..acbb31af --- /dev/null +++ b/packages/accounts/src/policies/specs/spending_limit_integrity.rs @@ -0,0 +1,108 @@ +// use core::task::Context; + +use cvlr::{ + cvlr_assert, + nondet::{self, Nondet}, + cvlr_satisfy, + cvlr_assume, +}; +use cvlr::clog; +use cvlr_soroban::{nondet_address}; +use cvlr_soroban_derive::rule; +use soroban_sdk::{Address, Env, IntoVal, Vec}; +use soroban_sdk::auth::{Context, ContractContext}; +use soroban_sdk::symbol_short; +use crate::{ + policies::{Policy, specs::spending_limit_contract::SpendingLimitPolicy, spending_limit::{SpendingLimitAccountParams, SpendingLimitData, SpendingLimitStorageKey}}, + smart_account::{ContextRule, Signer, specs::nondet::nondet_signers_vec}, +}; + +// note we verify the rules in this file with: +// "loop_iter": 1 or 2 +// "optimistic_loop": true +// meaning we consider only runs where the loops are iterated at most 1/2 times. + + +#[rule] +// after set_spending_limit the spending_limit is set to the input +// status: verified +pub fn sl_set_spending_limit_integrity(e: Env) { + let spending_limit: i128 = i128::nondet(); + let ctx_rule: ContextRule = ContextRule::nondet(); + let account_id = nondet_address(); + SpendingLimitPolicy::set_spending_limit(&e, spending_limit, ctx_rule.clone(), account_id.clone()); + let spending_limit_data_post = SpendingLimitPolicy::get_spending_limit_data(&e, ctx_rule.id, account_id); + let spending_limit_post = spending_limit_data_post.spending_limit; + cvlr_assert!(spending_limit_post == spending_limit); +} + +#[rule] +// status: violated - spurious. +// trying to describe some path where can_enforce returns true. +// should separate these out to a different file +// and describe all the different possible paths with loop_iter <= 2 +// and the trivial paths that return 0. +// possibly we need an invariant that connects the different parameters of the spending limit +pub fn no_previous_transfer_succeeds(e: Env, context: soroban_sdk::auth::Context) { + let auth_signers: Vec = nondet_signers_vec(); + cvlr_assume!(auth_signers.len() > 0); + let ctx_rule: ContextRule = ContextRule::nondet(); + let from = nondet_address(); + clog!(cvlr_soroban::Addr(&from)); + let to = nondet_address(); + clog!(cvlr_soroban::Addr(&to)); + let amount = i128::nondet(); + clog!(amount); + let mut args = Vec::new(&e); + args.push_back(from.into_val(&e)); + args.push_back(to.into_val(&e)); + args.push_back(amount.into_val(&e)); + let contract_context = Context::Contract(ContractContext{ + fn_name: symbol_short!("transfer"), + args, + contract: nondet_address(), + }); + let account_id = nondet_address(); + clog!(cvlr_soroban::Addr(&account_id)); + let spending_limit_data = SpendingLimitPolicy::get_spending_limit_data(&e, ctx_rule.id, account_id.clone()); + let spending_limit = spending_limit_data.spending_limit; + clog!(spending_limit); + let total_spent = spending_limit_data.cached_total_spent; + clog!(total_spent); + cvlr_assume!(total_spent == 0); + cvlr_assume!(amount <= spending_limit); + let result = SpendingLimitPolicy::can_enforce(&e, context, auth_signers.clone(), ctx_rule, account_id); + cvlr_assert!(result == true); +} + + +// can't write an integrity rule for enforce because it panics if can_enforce returns false. + +#[rule] +// after install the spending_limit_data is set to the input +// status: verified +pub fn sl_install_integrity(e: Env) { + let params: SpendingLimitAccountParams = SpendingLimitAccountParams::nondet(); + let params_spending_limit = params.spending_limit; + let params_period_ledgers = params.period_ledgers; + let ctx_rule: ContextRule = ContextRule::nondet(); + let account_id = nondet_address(); + SpendingLimitPolicy::install(&e, params.clone(), ctx_rule.clone(), account_id.clone()); + let spending_limit_data_post = SpendingLimitPolicy::get_spending_limit_data(&e, ctx_rule.id, account_id); + let spending_limit_data_post_spending_limit = spending_limit_data_post.spending_limit; + let spending_limit_data_post_period_ledgers = spending_limit_data_post.period_ledgers; + cvlr_assert!(spending_limit_data_post_spending_limit == params_spending_limit); + cvlr_assert!(spending_limit_data_post_period_ledgers == params_period_ledgers); +} + +#[rule] +// after uninstall the spending_limit_data is removed +// status: verified +pub fn sl_uninstall_integrity(e: Env) { + let ctx_rule: ContextRule = ContextRule::nondet(); + let account_id = nondet_address(); + SpendingLimitPolicy::uninstall(&e, ctx_rule.clone(), account_id.clone()); + let key: SpendingLimitStorageKey = SpendingLimitStorageKey::AccountContext(account_id.clone(), ctx_rule.id); + let account_ctx_opt: Option = e.storage().persistent().get(&key); + cvlr_assert!(account_ctx_opt.is_none()); +} \ No newline at end of file diff --git a/packages/accounts/src/policies/specs/spending_limit_non_panics.rs b/packages/accounts/src/policies/specs/spending_limit_non_panics.rs new file mode 100644 index 00000000..e69de29b diff --git a/packages/accounts/src/policies/specs/spending_limit_panics.rs b/packages/accounts/src/policies/specs/spending_limit_panics.rs new file mode 100644 index 00000000..e69de29b diff --git a/packages/accounts/src/policies/specs/spending_limit_sanity.rs b/packages/accounts/src/policies/specs/spending_limit_sanity.rs new file mode 100644 index 00000000..1965c061 --- /dev/null +++ b/packages/accounts/src/policies/specs/spending_limit_sanity.rs @@ -0,0 +1,63 @@ +use cvlr::{cvlr_assert, nondet::*, cvlr_satisfy}; +use cvlr_soroban::{nondet_address, nondet_bytes}; +use cvlr_soroban_derive::rule; +use soroban_sdk::Env; + +use crate::{ + policies::spending_limit::{ + SpendingLimitAccountParams, can_enforce, enforce, get_spending_limit_data, install, set_spending_limit, uninstall + }, + smart_account::{ContextRule, specs::nondet::nondet_signers_vec}, +}; + +#[rule] +pub fn get_spending_limit_sanity(e: Env) { + let ctx_rule_id: u32 = nondet(); + let account = nondet_address(); + let _ = get_spending_limit_data(&e, ctx_rule_id, &account); + cvlr_satisfy!(true); +} + +#[rule] +pub fn can_enforce_spending_limit_sanity(e: Env, context: soroban_sdk::auth::Context) { + let auth_signers = nondet_signers_vec(); + let ctx_rule = ContextRule::nondet(); + let account = nondet_address(); + let _ = can_enforce(&e, &context, &auth_signers, &ctx_rule, &account); + cvlr_satisfy!(true); +} + +#[rule] +pub fn enforce_spending_limit_sanity(e: Env, context: soroban_sdk::auth::Context) { + let auth_signers = nondet_signers_vec(); + let ctx_rule = ContextRule::nondet(); + let account = nondet_address(); + let _ = enforce(&e, &context, &auth_signers, &ctx_rule, &account); + cvlr_satisfy!(true); +} + +#[rule] +pub fn set_spending_limit_sanity(e: Env) { + let spending_limit_data = nondet(); + let ctx_rule = ContextRule::nondet(); + let account = nondet_address(); + set_spending_limit(&e, spending_limit_data, &ctx_rule, &account); + cvlr_satisfy!(true); +} + +#[rule] +pub fn install_spending_limit_sanity(e: Env) { + let params = SpendingLimitAccountParams::nondet(); + let ctx_rule = ContextRule::nondet(); + let account = nondet_address(); + install(&e, ¶ms, &ctx_rule, &account); + cvlr_satisfy!(true); +} + +#[rule] +pub fn uninstall_spending_limit_sanity(e: Env) { + let ctx_rule = ContextRule::nondet(); + let account = nondet_address(); + uninstall(&e, &ctx_rule, &account); + cvlr_satisfy!(true); +} diff --git a/packages/accounts/src/policies/specs/weighted_threshold_contract.rs b/packages/accounts/src/policies/specs/weighted_threshold_contract.rs new file mode 100644 index 00000000..81f78fe9 --- /dev/null +++ b/packages/accounts/src/policies/specs/weighted_threshold_contract.rs @@ -0,0 +1,49 @@ +use soroban_sdk::{contract, contractimpl, Env}; +use crate::policies::Policy; +use crate::policies::weighted_threshold::WeightedThresholdAccountParams; +use crate::smart_account::{ContextRule, Signer}; +use soroban_sdk::{auth::Context, Address, Vec, Map}; +use crate::policies::weighted_threshold; + +#[contract] +pub struct WeightedThresholdPolicy; + +impl WeightedThresholdPolicy { + pub fn get_threshold(e: &Env, context_rule_id: u32, smart_account: Address) -> u32 { + crate::policies::weighted_threshold::get_threshold(e, context_rule_id, &smart_account) + } + + pub fn get_signer_weights(e: &Env, context_rule: ContextRule, smart_account: Address) -> Map { + crate::policies::weighted_threshold::get_signer_weights(e, &context_rule, &smart_account) + } + + pub fn set_threshold(e: &Env, threshold: u32, context_rule: ContextRule, smart_account: Address) { + crate::policies::weighted_threshold::set_threshold(e, threshold, &context_rule, &smart_account) + } + + pub fn set_signer_weight(e: &Env, signer: Signer, weight: u32, context_rule: ContextRule, smart_account: Address) { + crate::policies::weighted_threshold::set_signer_weight(e, &signer, weight, &context_rule, &smart_account) + } +} + +// #[contractimpl] -- doesn't compile with this because of duplicate names of contract functions in the same crate. +impl Policy for WeightedThresholdPolicy { + type AccountParams = WeightedThresholdAccountParams; + + fn can_enforce(e: &Env, context: Context, authenticated_signers: Vec, context_rule: ContextRule, smart_account: Address) -> bool { + crate::policies::weighted_threshold::can_enforce(e, &context, &authenticated_signers, &context_rule, &smart_account) + } + + fn enforce(e: &Env, context: Context, authenticated_signers: Vec, context_rule: ContextRule, smart_account: Address) { + crate::policies::weighted_threshold::enforce(e, &context, &authenticated_signers, &context_rule, &smart_account) + } + + fn install(e: &Env, install_params: Self::AccountParams, context_rule: ContextRule, smart_account: Address) { + crate::policies::weighted_threshold::install(e, &install_params, &context_rule, &smart_account) + } + + fn uninstall(e: &Env, context_rule: ContextRule, smart_account: Address) { + crate::policies::weighted_threshold::uninstall(e, &context_rule, &smart_account) + } +} + diff --git a/packages/accounts/src/policies/specs/weighted_threshold_contract_sanity.rs b/packages/accounts/src/policies/specs/weighted_threshold_contract_sanity.rs new file mode 100644 index 00000000..8b2d2cfa --- /dev/null +++ b/packages/accounts/src/policies/specs/weighted_threshold_contract_sanity.rs @@ -0,0 +1,96 @@ +use cvlr::{ + cvlr_assert, + nondet::{self, Nondet}, + cvlr_satisfy, +}; +use cvlr_soroban::{nondet_address}; +use cvlr_soroban_derive::rule; +use soroban_sdk::{Address, Env, Vec}; + +use crate::{ + policies::{Policy, specs::weighted_threshold_contract::WeightedThresholdPolicy, weighted_threshold::WeightedThresholdAccountParams}, + smart_account::{ContextRule, Signer, specs::nondet::nondet_signers_vec}, +}; + +#[rule] +pub fn get_weighted_threshold_sanity(e: Env) { + let ctx_rule_id: u32 = u32::nondet(); + let account_id = nondet_address(); + let _ = WeightedThresholdPolicy::get_threshold(&e, ctx_rule_id, account_id); + cvlr_satisfy!(true); +} + +#[rule] +pub fn get_signer_weights_weighted_threshold_sanity(e: Env) { + let ctx_rule: ContextRule = ContextRule::nondet(); + let account_id = nondet_address(); + let _ = WeightedThresholdPolicy::get_signer_weights(&e, ctx_rule, account_id); + cvlr_satisfy!(true); +} + +#[rule] +pub fn can_enforce_weighted_threshold_sanity(e: Env, context: soroban_sdk::auth::Context) { + let auth_signers: Vec = nondet_signers_vec(); + let ctx_rule: ContextRule = ContextRule::nondet(); + let account: Address = nondet_address(); + let _ = WeightedThresholdPolicy::can_enforce( + &e, + context, + auth_signers, + ctx_rule, + account, + ); + cvlr_satisfy!(true); +} + +#[rule] +pub fn enforce_weighted_threshold_sanity(e: Env, context: soroban_sdk::auth::Context) { + let auth_signers = nondet_signers_vec(); + let ctx_rule: ContextRule = ContextRule::nondet(); + let account: Address = nondet_address(); + let _ = WeightedThresholdPolicy::enforce( + &e, + context, + auth_signers, + ctx_rule, + account, + ); + cvlr_satisfy!(true); +} + +#[rule] +pub fn set_weighted_threshold_sanity(e: Env) { + let threshold: u32 = u32::nondet(); + let ctx_rule: ContextRule = ContextRule::nondet(); + let account_id = nondet_address(); + WeightedThresholdPolicy::set_threshold(&e, threshold, ctx_rule, account_id); + cvlr_satisfy!(true); +} + +#[rule] +pub fn set_signer_weight_weighted_threshold_sanity(e: Env) { + let signer: Signer = Signer::nondet(); + let weight: u32 = u32::nondet(); + let ctx_rule: ContextRule = ContextRule::nondet(); + let account_id = nondet_address(); + WeightedThresholdPolicy::set_signer_weight(&e, signer, weight, ctx_rule, account_id); + cvlr_satisfy!(true); +} + +#[rule] +pub fn install_weighted_threshold_sanity(e: Env) { + let params = WeightedThresholdAccountParams::nondet(); + let ctx_rule: ContextRule = ContextRule::nondet(); + let account_id = nondet_address(); + WeightedThresholdPolicy::install(&e, params, ctx_rule, account_id); + cvlr_satisfy!(true); +} + +#[rule] +pub fn uninstall_weighted_threshold_sanity(e: Env) { + let ctx_rule: ContextRule = ContextRule::nondet(); + let account_id = nondet_address(); + WeightedThresholdPolicy::uninstall(&e, ctx_rule, account_id); + cvlr_satisfy!(true); +} + diff --git a/packages/accounts/src/policies/specs/weighted_threshold_integrity.rs b/packages/accounts/src/policies/specs/weighted_threshold_integrity.rs new file mode 100644 index 00000000..5cd0ad9d --- /dev/null +++ b/packages/accounts/src/policies/specs/weighted_threshold_integrity.rs @@ -0,0 +1,119 @@ +use cvlr::{cvlr_assert, clog, nondet::*}; +use cvlr_soroban::{nondet_address, nondet_vec}; +use cvlr_soroban_derive::rule; +use soroban_sdk::{Address, Env, Vec}; + +use crate::{ + policies::{Policy, specs::weighted_threshold_contract::WeightedThresholdPolicy}, + smart_account::{ContextRule, Signer}, +}; + +use crate::policies::weighted_threshold::WeightedThresholdAccountParams; +use crate::policies::weighted_threshold::WeightedThresholdStorageKey; + +// note we verify the rules in this file with: +// "loop_iter": 1 or 2 +// "optimistic_loop": true +// meaning we consider only runs where the loops are iterated at most 1/2 times. + +#[rule] +// can_enforce returns the expected result: total_weight >= threshold_pre where +// total_weight is the sum of the weights of the authenticated signers +// status: violated - constant propagation issue? +// https://prover.certora.com/output/5771024/b7b57f5270c744cb8a996fddacf1190d?anonymousKey=adbd322ae0dbe71e6f75367c03e67d963d586358¶ms=%7B%226%22%3A%7B%22index%22%3A0%2C%22ruleCounterExamples%22%3A%5B%7B%22name%22%3A%22rule_output_5.json%22%2C%22selectedRepresentation%22%3A%7B%22label%22%3A%22PRETTY%22%2C%22value%22%3A0%7D%2C%22callResolutionSingleFilter%22%3A%22%22%2C%22variablesFilter%22%3A%22%22%2C%22callTraceFilter%22%3A%22%22%2C%22variablesOpenItems%22%3A%5Btrue%2Ctrue%5D%2C%22callTraceCollapsed%22%3Atrue%2C%22rightSidePanelCollapsed%22%3Afalse%2C%22rightSideTab%22%3A%22%22%2C%22callResolutionSingleCollapsed%22%3Atrue%2C%22viewStorage%22%3Atrue%2C%22variablesExpandedArray%22%3A%22%22%2C%22expandedArray%22%3A%2247-10-12_3-1-1-1-1-1-1-1-1-145_46%22%2C%22orderVars%22%3A%5B%22%22%2C%22%22%2C0%5D%2C%22orderParams%22%3A%5B%22%22%2C%22%22%2C0%5D%2C%22scrollNode%22%3A%2244%22%2C%22currentPoint%22%3A0%2C%22trackingChildren%22%3A%5B%5D%2C%22trackingParents%22%3A%5B%5D%2C%22trackingOnly%22%3Afalse%2C%22highlightOnly%22%3Afalse%2C%22filterPosition%22%3A0%2C%22singleCallResolutionOpen%22%3A%5B%5D%2C%22snap_drop_1%22%3Anull%2C%22snap_drop_2%22%3Anull%2C%22snap_filter%22%3A%22%22%7D%5D%7D%7D&generalState=%7B%22fileViewOpen%22%3Afalse%2C%22fileViewCollapsed%22%3Atrue%2C%22mainTreeViewCollapsed%22%3Atrue%2C%22callTraceClosed%22%3Afalse%2C%22mainSideNavItem%22%3A%22rules%22%2C%22globalResSelected%22%3Afalse%2C%22isSideBarCollapsed%22%3Afalse%2C%22isRightSideBarCollapsed%22%3Atrue%2C%22selectedFile%22%3A%7B%7D%2C%22fileViewFilter%22%3A%22%22%2C%22mainTreeViewFilter%22%3A%22%22%2C%22contractsFilter%22%3A%22%22%2C%22globalCallResolutionFilter%22%3A%22%22%2C%22currentRuleUiId%22%3A6%2C%22counterExamplePos%22%3A1%2C%22expandedKeysState%22%3A%223-10-1-1-03-1-1-1-1-1-1-1%22%2C%22expandedFilesState%22%3A%5B%5D%2C%22outlinedfilterShared%22%3A%22000000000%22%7D +pub fn wt_can_enforce_integrity( + e: Env, + context: soroban_sdk::auth::Context, + auth_signers: Vec, + ctx_rule: ContextRule, + account_id: Address, +) { + let threshold_pre = WeightedThresholdPolicy::get_threshold(&e, ctx_rule.id, account_id.clone()); + let can_enforce = WeightedThresholdPolicy::can_enforce(&e, context, auth_signers.clone(), ctx_rule.clone(), account_id.clone()); + clog!(threshold_pre); + clog!(can_enforce); + clog!(ctx_rule.id); + clog!(cvlr_soroban::Addr(&account_id)); + let signer_weights = WeightedThresholdPolicy::get_signer_weights(&e, ctx_rule.clone(), account_id.clone()); + let mut total_weight: u32 = 0; + for signer in auth_signers.iter() { + if let Some(weight) = signer_weights.get(signer.clone()) { + clog!(weight); + total_weight = total_weight + .checked_add(weight) + .unwrap(); + } + } + clog!(total_weight); + clog!(threshold_pre); + let expected_result = total_weight >= threshold_pre; + clog!(expected_result); + cvlr_assert!(can_enforce == expected_result); +} + +// can't write an integrity rule for enforce because it panics if can_enforce returns false. + +#[rule] +// set_threshold sets the threshold +// status: verified +pub fn wt_set_threshold_integrity(e: Env) { + let threshold: u32 = u32::nondet(); + clog!(threshold); + let ctx_rule: ContextRule = ContextRule::nondet(); + clog!(ctx_rule.id); + let account_id = nondet_address(); + clog!(cvlr_soroban::Addr(&account_id)); + WeightedThresholdPolicy::set_threshold(&e, threshold, ctx_rule.clone(), account_id.clone()); + let threshold_post = WeightedThresholdPolicy::get_threshold(&e, ctx_rule.id, account_id); + clog!(threshold_post); + cvlr_assert!(threshold_post == threshold); +} + +#[rule] +// set_signer_weight sets the weight for a signer +// status: verified +pub fn wt_set_signer_weight_integrity(e: Env) { + let signer: Signer = Signer::nondet(); + let weight: u32 = u32::nondet(); + clog!(weight); + let ctx_rule: ContextRule = ContextRule::nondet(); + clog!(ctx_rule.id); + let account_id = nondet_address(); + clog!(cvlr_soroban::Addr(&account_id)); + WeightedThresholdPolicy::set_signer_weight(&e, signer.clone(), weight, ctx_rule.clone(), account_id.clone()); + let signer_weights = WeightedThresholdPolicy::get_signer_weights(&e, ctx_rule.clone(), account_id.clone()); + let signer_weight_post = signer_weights.get(signer.clone()); + clog!(signer_weight_post); + cvlr_assert!(signer_weight_post == Some(weight)); +} + +#[rule] +// install sets the signer weights and threshold +// status: verified +pub fn wt_install_integrity(e: Env) { + let params: WeightedThresholdAccountParams = WeightedThresholdAccountParams::nondet(); + let ctx_rule: ContextRule = ContextRule::nondet(); + clog!(ctx_rule.id); + let account_id = nondet_address(); + clog!(cvlr_soroban::Addr(&account_id)); + WeightedThresholdPolicy::install(&e, params.clone(), ctx_rule.clone(), account_id.clone()); + let threshold_post = WeightedThresholdPolicy::get_threshold(&e, ctx_rule.id, account_id.clone()); + clog!(threshold_post); + cvlr_assert!(threshold_post == params.threshold); + let signer_weights = WeightedThresholdPolicy::get_signer_weights(&e, ctx_rule.clone(), account_id.clone()); + cvlr_assert!(signer_weights == params.signer_weights); +} + +#[rule] +// after uninstall the account ctx is removed +// status: verified +pub fn wt_uninstall_integrity(e: Env) { + let ctx_rule: ContextRule = ContextRule::nondet(); + clog!(ctx_rule.id); + let account_id = nondet_address(); + clog!(cvlr_soroban::Addr(&account_id)); + WeightedThresholdPolicy::uninstall(&e, ctx_rule.clone(), account_id.clone()); + let key = WeightedThresholdStorageKey::AccountContext(account_id.clone(), ctx_rule.id); + let account_ctx_opt: Option = e.storage().persistent().get(&key); + cvlr_assert!(account_ctx_opt.is_none()); +} diff --git a/packages/accounts/src/policies/specs/weighted_threshold_non_panics.rs b/packages/accounts/src/policies/specs/weighted_threshold_non_panics.rs new file mode 100644 index 00000000..e69de29b diff --git a/packages/accounts/src/policies/specs/weighted_threshold_panics.rs b/packages/accounts/src/policies/specs/weighted_threshold_panics.rs new file mode 100644 index 00000000..e69de29b diff --git a/packages/accounts/src/policies/specs/weighted_threshold_sanity.rs b/packages/accounts/src/policies/specs/weighted_threshold_sanity.rs new file mode 100644 index 00000000..a8076e82 --- /dev/null +++ b/packages/accounts/src/policies/specs/weighted_threshold_sanity.rs @@ -0,0 +1,106 @@ +use core::task::Context; + +use cvlr::{cvlr_assert, nondet::*, cvlr_satisfy}; +use cvlr_soroban::{nondet_address, nondet_map}; +use cvlr_soroban_derive::rule; +use soroban_sdk::{Address, Env, Vec}; + +use crate::{policies::weighted_threshold::*, smart_account::{ContextRule, specs::nondet::nondet_signers_vec}}; + +#[rule] +pub fn get_weighted_threshold_sanity(e: Env) { + let ctx_rule_id: u32 = u32::nondet(); + let account_id = nondet_address(); + let _ = crate::policies::weighted_threshold::get_threshold(&e, ctx_rule_id, &account_id); + cvlr_satisfy!(true); +} + +#[rule] +pub fn get_signer_weights_sanity(e: Env) { + let ctx_rule: ContextRule = ContextRule::nondet(); + let account_id = nondet_address(); + let _ = crate::policies::weighted_threshold::get_signer_weights(&e, &ctx_rule, &account_id); + cvlr_satisfy!(true); +} + +#[rule] +pub fn calculate_weight_sanity(e: Env) { + let signers = nondet_signers_vec(); + let ctx_rule: ContextRule = ContextRule::nondet(); + let account_id = nondet_address(); + let _ = + crate::policies::weighted_threshold::calculate_weight(&e, &signers, &ctx_rule, &account_id); + cvlr_satisfy!(true); +} + +#[rule] +pub fn can_enforce_weighted_threshold_sanity(e: Env, context: soroban_sdk::auth::Context) { + let auth_signers = nondet_signers_vec(); + let ctx_rule: ContextRule = ContextRule::nondet(); + let account: Address = nondet_address(); + let _ = crate::policies::weighted_threshold::can_enforce( + &e, + &context, + &auth_signers, + &ctx_rule, + &account, + ); + cvlr_satisfy!(true); +} + +#[rule] +pub fn enforce_weighted_threshold_sanity(e: Env, context: soroban_sdk::auth::Context) { + let auth_signers = nondet_signers_vec(); + let ctx_rule: ContextRule = ContextRule::nondet(); + let account: Address = nondet_address(); + let _ = crate::policies::weighted_threshold::enforce( + &e, + &context, + &auth_signers, + &ctx_rule, + &account, + ); + cvlr_satisfy!(true); +} + +#[rule] +pub fn set_weighted_threshold_sanity(e: Env) { + let threshold: u32 = nondet(); + let ctx_rule: ContextRule = ContextRule::nondet(); + let account_id = nondet_address(); + crate::policies::weighted_threshold::set_threshold(&e, threshold, &ctx_rule, &account_id); + cvlr_satisfy!(true); +} + +#[rule] +pub fn set_signer_weight_sanity(e: Env) { + let signer = nondet(); + let weight: u32 = nondet(); + let ctx_rule: ContextRule = ContextRule::nondet(); + let account_id = nondet_address(); + crate::policies::weighted_threshold::set_signer_weight( + &e, + &signer, + weight, + &ctx_rule, + &account_id, + ); + cvlr_satisfy!(true); +} + +#[rule] +pub fn install_weighted_threshold_sanity(e: Env) { + let ctx_rule: ContextRule = ContextRule::nondet(); + let params = WeightedThresholdAccountParams::nondet(); + let account_id = nondet_address(); + crate::policies::weighted_threshold::install(&e, ¶ms, &ctx_rule, &account_id); + cvlr_satisfy!(true); +} + +#[rule] +pub fn uninstall_weighted_threshold_sanity(e: Env) { + let ctx_rule: ContextRule = ContextRule::nondet(); + let account_id = nondet_address(); + crate::policies::weighted_threshold::uninstall(&e, &ctx_rule, &account_id); + cvlr_satisfy!(true); +} diff --git a/packages/accounts/src/policies/spending_limit.rs b/packages/accounts/src/policies/spending_limit.rs index 00a77c15..22b95b72 100644 --- a/packages/accounts/src/policies/spending_limit.rs +++ b/packages/accounts/src/policies/spending_limit.rs @@ -13,12 +13,19 @@ //! period_ledgers: 17280, // ~1 day in ledgers //! } //! ``` +use cvlr::nondet::Nondet; use soroban_sdk::{ auth::{Context, ContractContext}, - contracterror, contractevent, contracttype, panic_with_error, symbol_short, Address, Env, + contracterror, contracttype, panic_with_error, symbol_short, Address, Env, TryFromVal, Vec, }; +#[cfg(not(feature = "certora"))] +use soroban_sdk::{contractevent}; + +#[cfg(feature = "certora")] +use cvlr_soroban_derive::{contractevent}; + use crate::smart_account::{ContextRule, Signer}; /// Event emitted when a spending limit policy is enforced. @@ -44,6 +51,15 @@ pub struct SpendingLimitAccountParams { pub period_ledgers: u32, } +impl Nondet for SpendingLimitAccountParams { + fn nondet() -> Self { + SpendingLimitAccountParams { + spending_limit: i128::nondet(), + period_ledgers: u32::nondet(), + } + } +} + /// Internal storage structure for spending limit tracking. #[contracttype] #[derive(Clone, Debug, PartialEq)] @@ -74,15 +90,15 @@ pub struct SpendingEntry { #[repr(u32)] pub enum SpendingLimitError { /// The smart account does not have a spending limit policy installed. - SmartAccountNotInstalled = 2220, + SmartAccountNotInstalled = 3220, /// The spending limit has been exceeded. - SpendingLimitExceeded = 2221, + SpendingLimitExceeded = 3221, /// The spending limit or period is invalid. - InvalidLimitOrPeriod = 2222, + InvalidLimitOrPeriod = 3222, /// The transaction is not allowed by this policy. - NotAllowed = 2223, + NotAllowed = 3223, /// The spending history has reached maximum capacity. - HistoryCapacityExceeded = 2224, + HistoryCapacityExceeded = 3224, } /// Storage keys for spending limit policy data. @@ -289,6 +305,7 @@ pub fn enforce( e.storage().persistent().set(&key, &data); + #[cfg(not(feature = "certora"))] SpendingLimitPolicyEnforced { smart_account: smart_account.clone(), context: context.clone(), diff --git a/packages/accounts/src/policies/test/simple_threshold.rs b/packages/accounts/src/policies/test/simple_threshold.rs index 1858ab8e..a1b0a845 100644 --- a/packages/accounts/src/policies/test/simple_threshold.rs +++ b/packages/accounts/src/policies/test/simple_threshold.rs @@ -60,7 +60,7 @@ fn install_success() { } #[test] -#[should_panic(expected = "Error(Contract, #2201)")] +#[should_panic(expected = "Error(Contract, #3201)")] fn install_zero_threshold_fails() { let e = Env::default(); let address = e.register(MockContract, ()); @@ -77,7 +77,7 @@ fn install_zero_threshold_fails() { } #[test] -#[should_panic(expected = "Error(Contract, #2200)")] +#[should_panic(expected = "Error(Contract, #3200)")] fn smart_account_get_threshold_fails() { let e = Env::default(); let address = e.register(MockContract, ()); @@ -234,7 +234,7 @@ fn set_threshold_success() { } #[test] -#[should_panic(expected = "Error(Contract, #2201)")] +#[should_panic(expected = "Error(Contract, #3201)")] fn set_threshold_zero_fails() { let e = Env::default(); let address = e.register(MockContract, ()); @@ -282,7 +282,7 @@ fn uninstall_success() { } #[test] -#[should_panic(expected = "Error(Contract, #2200)")] +#[should_panic(expected = "Error(Contract, #3200)")] fn enforce_not_installed_fails() { let e = Env::default(); let address = e.register(MockContract, ()); @@ -308,7 +308,7 @@ fn enforce_not_installed_fails() { } #[test] -#[should_panic(expected = "Error(Contract, #2202)")] +#[should_panic(expected = "Error(Contract, #3202)")] fn enforce_threshold_not_met_fails() { let e = Env::default(); let address = e.register(MockContract, ()); diff --git a/packages/accounts/src/policies/test/spending_limit.rs b/packages/accounts/src/policies/test/spending_limit.rs index d6d5b467..b0bded68 100644 --- a/packages/accounts/src/policies/test/spending_limit.rs +++ b/packages/accounts/src/policies/test/spending_limit.rs @@ -80,7 +80,7 @@ fn install_success() { } #[test] -#[should_panic(expected = "Error(Contract, #2222)")] +#[should_panic(expected = "Error(Contract, #3222)")] fn install_invalid_spending_limit() { let e = Env::default(); let address = e.register(MockContract, ()); @@ -100,7 +100,7 @@ fn install_invalid_spending_limit() { } #[test] -#[should_panic(expected = "Error(Contract, #2222)")] +#[should_panic(expected = "Error(Contract, #3222)")] fn install_invalid_period() { let e = Env::default(); let address = e.register(MockContract, ()); @@ -240,7 +240,7 @@ fn enforce_within_limit() { } #[test] -#[should_panic(expected = "Error(Contract, #2221)")] +#[should_panic(expected = "Error(Contract, #3221)")] fn enforce_exceeds_limit() { let e = Env::default(); let address = e.register(MockContract, ()); @@ -262,7 +262,7 @@ fn enforce_exceeds_limit() { } #[test] -#[should_panic(expected = "Error(Contract, #2223)")] +#[should_panic(expected = "Error(Contract, #3223)")] fn enforce_no_singers() { let e = Env::default(); let address = e.register(MockContract, ()); @@ -422,7 +422,7 @@ fn set_spending_limit_success() { } #[test] -#[should_panic(expected = "Error(Contract, #2222)")] +#[should_panic(expected = "Error(Contract, #3222)")] fn set_invalid_spending_limit() { let e = Env::default(); let address = e.register(MockContract, ()); @@ -473,7 +473,7 @@ fn uninstall_success() { } #[test] -#[should_panic(expected = "Error(Contract, #2220)")] +#[should_panic(expected = "Error(Contract, #3220)")] fn get_spending_limit_data_not_installed() { let e = Env::default(); let address = e.register(MockContract, ()); @@ -591,7 +591,7 @@ fn can_enforce_on_non_contract_context() { } #[test] -#[should_panic(expected = "Error(Contract, #2223)")] +#[should_panic(expected = "Error(Contract, #3223)")] fn enforce_non_transfer_context_errors() { let e = Env::default(); let address = e.register(MockContract, ()); @@ -621,7 +621,7 @@ fn enforce_non_transfer_context_errors() { } #[test] -#[should_panic(expected = "Error(Contract, #2223)")] +#[should_panic(expected = "Error(Contract, #3223)")] fn enforce_on_non_contract_context_errors() { let e = Env::default(); let address = e.register(MockContract, ()); @@ -646,7 +646,7 @@ fn enforce_on_non_contract_context_errors() { } #[test] -#[should_panic(expected = "Error(Contract, #2223)")] +#[should_panic(expected = "Error(Contract, #3223)")] fn enforce_invalid_amount_arg_errors() { let e = Env::default(); let address = e.register(MockContract, ()); @@ -682,7 +682,7 @@ fn enforce_invalid_amount_arg_errors() { } #[test] -#[should_panic(expected = "Error(Contract, #2223)")] +#[should_panic(expected = "Error(Contract, #3223)")] fn enforce_missing_amount_arg_errors() { let e = Env::default(); let address = e.register(MockContract, ()); @@ -717,7 +717,7 @@ fn enforce_missing_amount_arg_errors() { } #[test] -#[should_panic(expected = "Error(Contract, #2224)")] +#[should_panic(expected = "Error(Contract, #3224)")] fn enforce_history_capacity_exceeded() { let e = Env::default(); let address = e.register(MockContract, ()); diff --git a/packages/accounts/src/policies/test/weighted_threshold.rs b/packages/accounts/src/policies/test/weighted_threshold.rs index 7df815df..d239fd02 100644 --- a/packages/accounts/src/policies/test/weighted_threshold.rs +++ b/packages/accounts/src/policies/test/weighted_threshold.rs @@ -64,7 +64,7 @@ fn install_success() { } #[test] -#[should_panic(expected = "Error(Contract, #2211)")] +#[should_panic(expected = "Error(Contract, #3211)")] fn install_zero_threshold_fails() { let e = Env::default(); let address = e.register(MockContract, ()); @@ -85,7 +85,7 @@ fn install_zero_threshold_fails() { } #[test] -#[should_panic(expected = "Error(Contract, #2211)")] +#[should_panic(expected = "Error(Contract, #3211)")] fn install_threshold_exceeds_total_weight_fails() { let e = Env::default(); let address = e.register(MockContract, ()); @@ -182,7 +182,7 @@ fn calculate_weight_signer_without_weight() { } #[test] -#[should_panic(expected = "Error(Contract, #2210)")] +#[should_panic(expected = "Error(Contract, #3210)")] fn calculate_weight_not_installed_fails() { let e = Env::default(); let address = e.register(MockContract, ()); @@ -196,7 +196,7 @@ fn calculate_weight_not_installed_fails() { } #[test] -#[should_panic(expected = "Error(Contract, #2212)")] +#[should_panic(expected = "Error(Contract, #3212)")] fn calculate_weight_overflow_fails() { let e = Env::default(); let address = e.register(MockContract, ()); @@ -374,7 +374,7 @@ fn set_threshold_success() { } #[test] -#[should_panic(expected = "Error(Contract, #2211)")] +#[should_panic(expected = "Error(Contract, #3211)")] fn set_threshold_zero_fails() { let e = Env::default(); let address = e.register(MockContract, ()); @@ -397,7 +397,7 @@ fn set_threshold_zero_fails() { } #[test] -#[should_panic(expected = "Error(Contract, #2212)")] +#[should_panic(expected = "Error(Contract, #3212)")] fn install_math_overflow_fails() { let e = Env::default(); let address = e.register(MockContract, ()); @@ -445,7 +445,7 @@ fn set_signer_weight_success() { } #[test] -#[should_panic(expected = "Error(Contract, #2210)")] +#[should_panic(expected = "Error(Contract, #3210)")] fn set_threshold_not_installed_fails() { let e = Env::default(); let address = e.register(MockContract, ()); @@ -462,7 +462,7 @@ fn set_threshold_not_installed_fails() { } #[test] -#[should_panic(expected = "Error(Contract, #2210)")] +#[should_panic(expected = "Error(Contract, #3210)")] fn set_signer_weight_not_installed_fails() { let e = Env::default(); let address = e.register(MockContract, ()); @@ -505,7 +505,7 @@ fn uninstall_success() { } #[test] -#[should_panic(expected = "Error(Contract, #2211)")] +#[should_panic(expected = "Error(Contract, #3211)")] fn set_threshold_unreachable_fails() { let e = Env::default(); e.mock_all_auths(); @@ -528,7 +528,7 @@ fn set_threshold_unreachable_fails() { } #[test] -#[should_panic(expected = "Error(Contract, #2211)")] +#[should_panic(expected = "Error(Contract, #3211)")] fn set_signer_weight_makes_threshold_unreachable_fails() { let e = Env::default(); e.mock_all_auths(); @@ -553,7 +553,7 @@ fn set_signer_weight_makes_threshold_unreachable_fails() { } #[test] -#[should_panic(expected = "Error(Contract, #2210)")] +#[should_panic(expected = "Error(Contract, #3210)")] fn enforce_not_installed_fails() { let e = Env::default(); let address = e.register(MockContract, ()); @@ -579,7 +579,7 @@ fn enforce_not_installed_fails() { } #[test] -#[should_panic(expected = "Error(Contract, #2213)")] +#[should_panic(expected = "Error(Contract, #3213)")] fn enforce_threshold_not_met_fails() { let e = Env::default(); let address = e.register(MockContract, ()); diff --git a/packages/accounts/src/policies/weighted_threshold.rs b/packages/accounts/src/policies/weighted_threshold.rs index 66dbbaf8..47b70ac9 100644 --- a/packages/accounts/src/policies/weighted_threshold.rs +++ b/packages/accounts/src/policies/weighted_threshold.rs @@ -61,11 +61,20 @@ //! } //! ``` +use cvlr::nondet::*; +use cvlr::clog; +use cvlr_soroban::nondet_map; use soroban_sdk::{ - auth::Context, contracterror, contractevent, contracttype, panic_with_error, Address, Env, Map, + auth::Context, contracterror, contracttype, panic_with_error, Address, Env, Map, Vec, }; +#[cfg(not(feature = "certora"))] +use soroban_sdk::{contractevent}; + +#[cfg(feature = "certora")] +use cvlr_soroban_derive::{contractevent}; + // re-export use crate::smart_account::{ContextRule, Signer}; @@ -90,19 +99,29 @@ pub struct WeightedThresholdAccountParams { pub threshold: u32, } +impl Nondet for WeightedThresholdAccountParams { + fn nondet() -> Self { + WeightedThresholdAccountParams { + signer_weights: nondet_map(), + threshold: nondet(), + } + } + +} + /// Error codes for weighted threshold policy operations. #[contracterror] #[derive(Copy, Clone, Debug, PartialEq)] #[repr(u32)] pub enum WeightedThresholdError { /// The smart account does not have a weighted threshold policy installed. - SmartAccountNotInstalled = 2210, + SmartAccountNotInstalled = 3210, /// The threshold value is invalid. - InvalidThreshold = 2211, + InvalidThreshold = 3211, /// A mathematical operation would overflow. - MathOverflow = 2212, + MathOverflow = 3212, /// The transaction is not allowed by this policy. - NotAllowed = 2213, + NotAllowed = 3213, } /// Storage keys for weighted threshold policy data. @@ -145,6 +164,7 @@ pub fn get_threshold(e: &Env, context_rule_id: u32, smart_account: &Address) -> WEIGHTED_THRESHOLD_EXTEND_AMOUNT, ); }); + clog!(params.is_some()); params .map(|p| p.threshold) @@ -242,7 +262,7 @@ pub fn can_enforce( ) -> bool { let key = WeightedThresholdStorageKey::AccountContext(smart_account.clone(), context_rule.id); let params: Option = e.storage().persistent().get(&key); - + clog!(params.is_some()); if let Some(params) = params { e.storage().persistent().extend_ttl( &key, @@ -300,6 +320,7 @@ pub fn enforce( if total_weight >= params.threshold { // emit event + #[cfg(not(feature = "certora"))] WeightedPolicyEnforced { smart_account: smart_account.clone(), context: context.clone(), diff --git a/packages/accounts/src/smart_account/mod.rs b/packages/accounts/src/smart_account/mod.rs index 4c20f2f9..94be6922 100644 --- a/packages/accounts/src/smart_account/mod.rs +++ b/packages/accounts/src/smart_account/mod.rs @@ -1,10 +1,17 @@ -mod storage; +pub(crate) mod storage; #[cfg(test)] mod test; use soroban_sdk::{ - auth::CustomAccountInterface, contractclient, contracterror, contractevent, Address, Env, Map, + auth::CustomAccountInterface, contractclient, contracterror, Address, Env, Map, String, Symbol, Val, Vec, }; + +#[cfg(not(feature = "certora"))] +use soroban_sdk::{contractevent}; + +#[cfg(feature = "certora")] +use cvlr_soroban_derive::{contractevent}; + pub use storage::{ add_context_rule, add_policy, add_signer, authenticate, do_check_auth, get_context_rule, get_context_rules, get_validated_context, remove_context_rule, remove_policy, remove_signer, @@ -12,6 +19,11 @@ pub use storage::{ Signatures, Signer, }; + +#[cfg(feature = "certora")] +pub mod specs; + + /// Core trait for smart account functionality, extending Soroban's /// CustomAccountInterface with context rule management capabilities. /// @@ -310,31 +322,31 @@ pub const MAX_CONTEXT_RULES: u32 = 15; #[repr(u32)] pub enum SmartAccountError { /// The specified context rule does not exist. - ContextRuleNotFound = 2000, + ContextRuleNotFound = 3000, /// A duplicate context rule already exists. - DuplicateContextRule = 2001, + DuplicateContextRule = 3001, /// The provided context cannot be validated against any rule. - UnvalidatedContext = 2002, + UnvalidatedContext = 3002, /// External signature verification failed. - ExternalVerificationFailed = 2003, + ExternalVerificationFailed = 3003, /// Context rule must have at least one signer or policy. - NoSignersAndPolicies = 2004, + NoSignersAndPolicies = 3004, /// The valid_until timestamp is in the past. - PastValidUntil = 2005, + PastValidUntil = 3005, /// The specified signer was not found. - SignerNotFound = 2006, + SignerNotFound = 3006, /// The signer already exists in the context rule. - DuplicateSigner = 2007, + DuplicateSigner = 3007, /// The specified policy was not found. - PolicyNotFound = 2008, + PolicyNotFound = 3008, /// The policy already exists in the context rule. - DuplicatePolicy = 2009, + DuplicatePolicy = 3009, /// Too many signers in the context rule. - TooManySigners = 2010, + TooManySigners = 3010, /// Too many policies in the context rule. - TooManyPolicies = 2011, + TooManyPolicies = 3011, /// Too many context rules in the smart account. - TooManyContextRules = 2012, + TooManyContextRules = 3012, } // ################## EVENTS ################## @@ -364,6 +376,7 @@ pub struct ContextRuleAdded { /// * topics - `["context_rule_added", context_rule_id: u32]` /// * data - `[name: String, context_type: ContextRuleType, valid_until: /// Option, signers: Vec, policies: Vec
]` +#[cfg(not(feature = "certora"))] pub fn emit_context_rule_added(e: &Env, context_rule: &ContextRule) { ContextRuleAdded { context_rule_id: context_rule.id, @@ -400,6 +413,7 @@ pub struct ContextRuleUpdated { /// * topics - `["context_rule_updated", context_rule_id: u32]` /// * data - `[name: String, context_type: ContextRuleType, valid_until: /// Option]` +#[cfg(not(feature = "certora"))] pub fn emit_context_rule_updated(e: &Env, context_rule_id: u32, meta: &Meta) { ContextRuleUpdated { context_rule_id, @@ -429,6 +443,7 @@ pub struct ContextRuleRemoved { /// /// * topics - `["context_rule_removed", context_rule_id: u32]` /// * data - `[]` +#[cfg(not(feature = "certora"))] pub fn emit_context_rule_removed(e: &Env, context_rule_id: u32) { ContextRuleRemoved { context_rule_id }.publish(e); } @@ -454,6 +469,7 @@ pub struct SignerAdded { /// /// * topics - `["signer_added", context_rule_id: u32]` /// * data - `[signer: Signer]` +#[cfg(not(feature = "certora"))] pub fn emit_signer_added(e: &Env, context_rule_id: u32, signer: &Signer) { SignerAdded { context_rule_id, signer: signer.clone() }.publish(e); } @@ -479,6 +495,7 @@ pub struct SignerRemoved { /// /// * topics - `["signer_removed", context_rule_id: u32]` /// * data - `[signer: Signer]` +#[cfg(not(feature = "certora"))] pub fn emit_signer_removed(e: &Env, context_rule_id: u32, signer: &Signer) { SignerRemoved { context_rule_id, signer: signer.clone() }.publish(e); } @@ -506,6 +523,7 @@ pub struct PolicyAdded { /// /// * topics - `["policy_added", context_rule_id: u32]` /// * data - `[policy: Address, install_param: Val]` +#[cfg(not(feature = "certora"))] pub fn emit_policy_added(e: &Env, context_rule_id: u32, policy: &Address, install_param: Val) { PolicyAdded { context_rule_id, policy: policy.clone(), install_param }.publish(e); } @@ -531,6 +549,9 @@ pub struct PolicyRemoved { /// /// * topics - `["policy_removed", context_rule_id: u32]` /// * data - `[policy: Address]` +#[cfg(not(feature = "certora"))] pub fn emit_policy_removed(e: &Env, context_rule_id: u32, policy: &Address) { PolicyRemoved { context_rule_id, policy: policy.clone() }.publish(e); } + + diff --git a/packages/accounts/src/smart_account/specs/mod.rs b/packages/accounts/src/smart_account/specs/mod.rs new file mode 100644 index 00000000..affd7a43 --- /dev/null +++ b/packages/accounts/src/smart_account/specs/mod.rs @@ -0,0 +1,4 @@ +pub mod smart_account_sanity; +pub mod smart_account_contract; +pub mod policy; +pub mod nondet; \ No newline at end of file diff --git a/packages/accounts/src/smart_account/specs/nondet.rs b/packages/accounts/src/smart_account/specs/nondet.rs new file mode 100644 index 00000000..bad6897c --- /dev/null +++ b/packages/accounts/src/smart_account/specs/nondet.rs @@ -0,0 +1,71 @@ +use cvlr_soroban::nondet_address; +use soroban_sdk::{Address, Env, Map, Val, Vec}; +use cvlr::nondet::{self, Nondet}; +use crate::smart_account::Signer; + +pub fn nondet_signers_vec() -> Vec +where + Signer: soroban_sdk::IntoVal + + soroban_sdk::TryFromVal, +{ + let env = Env::default(); + + // Choose an arbitrary length (but keep it bounded so verification doesn't explode). + // Adjust MAX as needed. + const MAX: u32 = 5; + let mut n: u32 = u32::nondet(); + if n > MAX { + n = n % (MAX + 1); + } + + let mut out: Vec = Vec::new(&env); + let mut i = 0u32; + while i < n { + out.push_back(Signer::nondet()); + i += 1; + } + + out +} + +pub fn nondet_policy_vec() -> Vec
{ + let env = Env::default(); + + // Choose an arbitrary length (but keep it bounded so verification doesn't explode). + // Adjust MAX as needed. + const MAX: u32 = 5; + let mut n: u32 = u32::nondet(); + if n > MAX { + n = n % (MAX + 1); + } + + let mut out: Vec
= Vec::new(&env); + let mut i = 0u32; + while i < n { + out.push_back(nondet_address()); + i += 1; + } + + out +} + +pub fn nondet_policy_map() -> Map { + let env = Env::default(); + + // Choose an arbitrary length (but keep it bounded so verification doesn't explode). + // Adjust MAX as needed. + const MAX: u32 = 5; + let mut n: u32 = u32::nondet(); + if n > MAX { + n = n % (MAX + 1); + } + + let mut out: Map = Map::new(&env); + let mut i = 0u32; + while i < n { + out.set(nondet_address(), Val::from_payload(u64::nondet())); + i += 1; + } + + out +} \ No newline at end of file diff --git a/packages/accounts/src/smart_account/specs/policy.rs b/packages/accounts/src/smart_account/specs/policy.rs new file mode 100644 index 00000000..a90aa8c8 --- /dev/null +++ b/packages/accounts/src/smart_account/specs/policy.rs @@ -0,0 +1,47 @@ +use soroban_sdk::{Address, Env, Val, Vec, auth::Context, contracttype}; + +use crate::{ + policies::{ + simple_threshold::{self, SimpleThresholdAccountParams}, + Policy, + PolicyClientInterface, + }, + smart_account::{ContextRule, Signer}, +}; + +// NOTE: using a Simple Threshold policy for FV. Ideally we will want to verify with many different policies. +// Using a concrete one because we do need implementations of the methods. + + +pub struct SimpleThresholdPolicyContract; + +impl SimpleThresholdPolicyContract { + pub fn get_threshold(e: &Env, context_rule_id: u32, smart_account: Address) -> u32 { + crate::policies::simple_threshold::get_threshold(e, context_rule_id, &smart_account) + } + + pub fn set_threshold(e: &Env, threshold: u32, context_rule: ContextRule, smart_account: Address) { + crate::policies::simple_threshold::set_threshold(e, threshold, &context_rule, &smart_account) + } +} + +impl Policy for SimpleThresholdPolicyContract { + type AccountParams = SimpleThresholdAccountParams; + + fn can_enforce(e: &Env, context: Context, authenticated_signers: Vec, context_rule: ContextRule, smart_account: Address) -> bool { + crate::policies::simple_threshold::can_enforce(e, &context, &authenticated_signers, &context_rule, &smart_account) + } + + fn enforce(e: &Env, context: Context, authenticated_signers: Vec, context_rule: ContextRule, smart_account: Address) { + crate::policies::simple_threshold::enforce(e, &context, &authenticated_signers, &context_rule, &smart_account) + } + + fn install(e: &Env, install_params: Self::AccountParams, context_rule: ContextRule, smart_account: Address) { + crate::policies::simple_threshold::install(e, &install_params, &context_rule, &smart_account) + } + + fn uninstall(e: &Env, context_rule: ContextRule, smart_account: Address) { + crate::policies::simple_threshold::uninstall(e, &context_rule, &smart_account) + } +} + diff --git a/packages/accounts/src/smart_account/specs/smart_account_contract.rs b/packages/accounts/src/smart_account/specs/smart_account_contract.rs new file mode 100644 index 00000000..0b12ea4b --- /dev/null +++ b/packages/accounts/src/smart_account/specs/smart_account_contract.rs @@ -0,0 +1,100 @@ +use soroban_sdk::{ + auth::{Context, CustomAccountInterface}, + contract, contractimpl, + crypto::Hash, + Env, Vec, +}; + +use crate::smart_account::*; + +// TODO: please implement the methods as needed. This is similar to the multisig +// example but not identical. + +#[contract] +pub struct SmartAccountContract; + +#[contractimpl] +impl CustomAccountInterface for SmartAccountContract { + type Error = SmartAccountError; + type Signature = Signatures; + + fn __check_auth( + e: Env, + signature_payload: Hash<32>, + signatures: Signatures, + auth_contexts: Vec, + ) -> Result<(), Self::Error> { + // TODO: sanity will not work for this right now. DO NOT TRY. + do_check_auth(&e, &signature_payload, &signatures, &auth_contexts) + } +} + +#[contractimpl] +impl SmartAccount for SmartAccountContract { + fn get_context_rule(e: &Env, context_rule_id: u32) -> ContextRule { + get_context_rule(e, context_rule_id) + } + + fn get_context_rules(e: &Env, context_rule_type: ContextRuleType) -> Vec { + get_context_rules(e, &context_rule_type) + } + + fn add_context_rule( + e: &Env, + context_type: ContextRuleType, + name: String, + valid_until: Option, + signers: Vec, + policies: Map, + ) -> ContextRule { + e.current_contract_address().require_auth(); + + add_context_rule(e, &context_type, &name, valid_until, &signers, &policies) + } + + fn update_context_rule_name(e: &Env, context_rule_id: u32, name: String) -> ContextRule { + e.current_contract_address().require_auth(); + + update_context_rule_name(e, context_rule_id, &name) + } + + fn update_context_rule_valid_until( + e: &Env, + context_rule_id: u32, + valid_until: Option, + ) -> ContextRule { + e.current_contract_address().require_auth(); + + update_context_rule_valid_until(e, context_rule_id, valid_until) + } + + fn remove_context_rule(e: &Env, context_rule_id: u32) { + e.current_contract_address().require_auth(); + + remove_context_rule(e, context_rule_id); + } + + fn add_signer(e: &Env, context_rule_id: u32, signer: Signer) { + e.current_contract_address().require_auth(); + + add_signer(e, context_rule_id, &signer); + } + + fn remove_signer(e: &Env, context_rule_id: u32, signer: Signer) { + e.current_contract_address().require_auth(); + + remove_signer(e, context_rule_id, &signer); + } + + fn add_policy(e: &Env, context_rule_id: u32, policy: Address, install_param: Val) { + e.current_contract_address().require_auth(); + + add_policy(e, context_rule_id, &policy, install_param); + } + + fn remove_policy(e: &Env, context_rule_id: u32, policy: Address) { + e.current_contract_address().require_auth(); + + remove_policy(e, context_rule_id, &policy); + } +} diff --git a/packages/accounts/src/smart_account/specs/smart_account_sanity.rs b/packages/accounts/src/smart_account/specs/smart_account_sanity.rs new file mode 100644 index 00000000..feed0de6 --- /dev/null +++ b/packages/accounts/src/smart_account/specs/smart_account_sanity.rs @@ -0,0 +1,94 @@ + +use cvlr::{cvlr_assert, cvlr_assume, cvlr_satisfy, nondet::*}; +use cvlr_soroban::{nondet_address, nondet_map, nondet_string}; +use cvlr_soroban_derive::rule; +use soroban_sdk::{Env, String, Val, Vec, map, panic_with_error, vec}; + +use crate::smart_account::{ + ContextRuleType, Meta, Signer, SmartAccount, SmartAccountError, specs::{ + nondet::{nondet_policy_map, nondet_signers_vec}, + smart_account_contract::SmartAccountContract, + }, storage::{self, SmartAccountStorageKey, get_persistent_entry} +}; + +#[rule] +pub fn get_context_rule_sanity(e: Env) { + let id: u32 = nondet(); + let _ = SmartAccountContract::get_context_rule(&e, id); + cvlr_satisfy!(true); +} + +#[rule] +pub fn get_context_rules_sanity(e: Env) { + let ctx_typ = ContextRuleType::nondet(); + let _ = SmartAccountContract::get_context_rules(&e, ctx_typ); + cvlr_satisfy!(true); +} + +#[rule] +pub fn add_context_rule_sanity(e: Env) { + let ctx_typ = ContextRuleType::nondet(); + let name = nondet_string(); + let valid_until = Option::::nondet(); + let signers = nondet_signers_vec(); + let policies = nondet_policy_map(); + let _ = + SmartAccountContract::add_context_rule(&e, ctx_typ, name, valid_until, signers, policies); + cvlr_satisfy!(true); +} + +#[rule] +pub fn update_context_rule_name_sanity(e: Env) { + let id: u32 = nondet(); + let name = nondet_string(); + let _ = SmartAccountContract::update_context_rule_name(&e, id, name); + cvlr_satisfy!(true); +} + +#[rule] +pub fn update_context_rule_valid_until_sanity(e: Env) { + let id: u32 = nondet(); + let valid_until = Option::::nondet(); + let _ = SmartAccountContract::update_context_rule_valid_until(&e, id, valid_until); + cvlr_satisfy!(true); +} + +#[rule] +pub fn remove_context_rule_sanity(e: Env) { + let id: u32 = nondet(); + SmartAccountContract::remove_context_rule(&e, id); + cvlr_satisfy!(true); +} + +#[rule] +pub fn add_signer_sanity(e: Env) { + let id: u32 = nondet(); + let signer = Signer::nondet(); + SmartAccountContract::add_signer(&e, id, signer); + cvlr_satisfy!(true); +} + +#[rule] +pub fn remove_signer_sanity(e: Env) { + let id: u32 = nondet(); + let signer = Signer::nondet(); + SmartAccountContract::remove_signer(&e, id, signer); + cvlr_satisfy!(true); +} + +#[rule] +pub fn add_policy_sanity(e: Env) { + let id: u32 = nondet(); + let policy = nondet_address(); + let install_param = soroban_sdk::Val::from_payload(u64::nondet()); + SmartAccountContract::add_policy(&e, id, policy, install_param); + cvlr_satisfy!(true); +} + +#[rule] +pub fn remove_policy_sanity(e: Env) { + let id: u32 = nondet(); + let policy = nondet_address(); + SmartAccountContract::remove_policy(&e, id, policy); + cvlr_satisfy!(true); +} diff --git a/packages/accounts/src/smart_account/storage.rs b/packages/accounts/src/smart_account/storage.rs index 54c2312a..b5fb916a 100644 --- a/packages/accounts/src/smart_account/storage.rs +++ b/packages/accounts/src/smart_account/storage.rs @@ -83,7 +83,10 @@ //! policies: [threshold_policy, spending_limit_policy], //! } //! ``` - +use cvlr::nondet::{self, Nondet}; +use cvlr_soroban::{nondet_address, nondet_bytes, nondet_bytes_n, nondet_string}; +#[cfg(feature = "certora")] +use soroban_sdk::FromVal; use soroban_sdk::{ auth::{ Context, ContractContext, ContractExecutable, CreateContractHostFnContext, @@ -96,13 +99,20 @@ use soroban_sdk::{ Address, Bytes, BytesN, Env, IntoVal, Map, String, TryFromVal, Val, Vec, }; +#[cfg(not(feature = "certora"))] +use crate::smart_account::{ + emit_context_rule_added, emit_context_rule_removed, emit_context_rule_updated, + emit_policy_added, emit_policy_removed, emit_signer_added, emit_signer_removed, +}; +#[cfg(feature = "certora")] +use crate::{ + policies::{Policy, simple_threshold::SimpleThresholdAccountParams, spending_limit::SpendingLimitAccountParams}, + smart_account::specs::policy::SimpleThresholdPolicyContract, +}; use crate::{ - policies::PolicyClient, + policies::{self, PolicyClient}, smart_account::{ - emit_context_rule_added, emit_context_rule_removed, emit_context_rule_updated, - emit_policy_added, emit_policy_removed, emit_signer_added, emit_signer_removed, - SmartAccountError, MAX_CONTEXT_RULES, MAX_POLICIES, MAX_SIGNERS, - SMART_ACCOUNT_EXTEND_AMOUNT, SMART_ACCOUNT_TTL_THRESHOLD, + MAX_CONTEXT_RULES, MAX_POLICIES, MAX_SIGNERS, SMART_ACCOUNT_EXTEND_AMOUNT, SMART_ACCOUNT_TTL_THRESHOLD, SmartAccountError, specs::nondet::nondet_policy_vec }, verifiers::VerifierClient, }; @@ -142,6 +152,16 @@ pub enum Signer { External(Address, Bytes), } +impl Nondet for Signer { + fn nondet() -> Self { + if bool::nondet() { + Signer::Delegated(nondet_address()) + } else { + Signer::External(nondet_address(), nondet_bytes()) + } + } +} + /// A collection of signatures mapped to their respective signers. #[contracttype] #[derive(Clone, Debug, PartialEq)] @@ -159,6 +179,17 @@ pub enum ContextRuleType { CreateContract(BytesN<32>), } +impl Nondet for ContextRuleType { + fn nondet() -> Self { + match u8::nondet() % 3 { + 0 => ContextRuleType::Default, + 1 => ContextRuleType::CallContract(nondet_address()), + 2 => ContextRuleType::CreateContract(nondet_bytes_n()), + _ => panic!("unreachable"), + } + } +} + /// Metadata for a context rule. #[contracttype] #[derive(Clone, Debug, PartialEq)] @@ -171,6 +202,16 @@ pub struct Meta { pub valid_until: Option, } +impl Nondet for Meta { + fn nondet() -> Self { + Meta { + name: nondet_string(), + context_type: ContextRuleType::nondet(), + valid_until: Option::nondet(), + } + } +} + /// A complete context rule defining authorization requirements. #[contracttype] #[derive(Clone, Debug, PartialEq)] @@ -189,6 +230,20 @@ pub struct ContextRule { pub valid_until: Option, } +impl Nondet for ContextRule { + fn nondet() -> Self { + use crate::smart_account::specs::nondet::nondet_signers_vec; + ContextRule { + id: u32::nondet(), + context_type: ContextRuleType::nondet(), + name: nondet_string(), + signers: nondet_signers_vec(), + policies: nondet_policy_vec(), + valid_until: Option::::nondet(), + } + } +} + // ################## QUERY STATE ################## /// Retrieves a complete context rule by its ID. @@ -220,7 +275,7 @@ pub fn get_context_rule(e: &Env, id: u32) -> ContextRule { name: meta.name, signers, policies, - valid_until: meta.valid_until, + valid_until: Option::nondet(), } } @@ -235,7 +290,19 @@ pub fn get_context_rules(e: &Env, context_rule_type: &ContextRuleType) -> Vec = get_persistent_entry(e, &ids_key).unwrap_or_else(|| Vec::new(e)); - Vec::from_iter(e, ids.iter().map(|id| get_context_rule(e, id))) + #[cfg(not(feature = "certora"))] + { + Vec::from_iter(e, ids.iter().map(|id| get_context_rule(e, id))) + } + + #[cfg(feature = "certora")] + { + let mut v: Vec = Vec::new(e); + for id in ids.iter() { + v.push_back(get_context_rule(e, id)); + } + return v; + } } /// Retrieves all valid (non-expired) context rules for a specific context type, @@ -405,6 +472,7 @@ pub fn can_enforce_all_policies( ) -> bool { for policy in context_rule.policies.iter() { // policies are all or nothing + #[cfg(not(feature = "certora"))] if !PolicyClient::new(e, &policy).can_enforce( context, matched_signers, @@ -413,6 +481,16 @@ pub fn can_enforce_all_policies( ) { return false; } + #[cfg(feature = "certora")] + if SimpleThresholdPolicyContract::can_enforce( + e, + context.clone(), + matched_signers.clone(), + context_rule.clone(), + e.current_contract_address(), + ) { + return false; + } } true } @@ -477,6 +555,7 @@ pub fn do_check_auth( ) -> Result<(), SmartAccountError> { authenticate(e, signature_payload, &signatures.0); + #[cfg(not(feature = "certora"))] let validated_contexts = Vec::from_iter( e, auth_contexts @@ -484,18 +563,36 @@ pub fn do_check_auth( .map(|context| get_validated_context(e, &context, &signatures.0.keys())), ); + #[cfg(feature = "certora")] + let validated_contexts = { + let mut tmp = Vec::new(e); + for context in auth_contexts { + tmp.push_back(get_validated_context(e, &context, &signatures.0.keys())); + } + tmp + }; + // After collecting validated context rules and authenticated signers, call for // every policy `PolicyClient::enforce` to trigger the state-changing // effects if any. for (rule, context, authenticated_signers) in validated_contexts.iter() { let ContextRule { policies, .. } = rule.clone(); for policy in policies.iter() { + #[cfg(not(feature = "certora"))] PolicyClient::new(e, &policy).enforce( &context, &authenticated_signers, &rule, &e.current_contract_address(), ); + #[cfg(feature = "certora")] + SimpleThresholdPolicyContract::enforce( + e, + context.clone(), + authenticated_signers.clone(), + rule.clone(), + e.current_contract_address(), + ); } } @@ -524,6 +621,7 @@ pub fn do_check_auth( /// during sorting. /// * [`SmartAccountError::DuplicatePolicy`] - When duplicate policies are found /// during sorting. +#[cfg(not(feature = "certora"))] pub fn compute_fingerprint( e: &Env, context_type: &ContextRuleType, @@ -546,13 +644,28 @@ pub fn compute_fingerprint( } } + #[cfg(not(feature = "certora"))] let mut rule_data = context_type.to_xdr(e); + #[cfg(feature = "certora")] + let mut rule_data = context_type.clone().to_xdr(e); rule_data.append(&sorted_signers.to_xdr(e)); rule_data.append(&sorted_policies.to_xdr(e)); e.crypto().sha256(&rule_data).to_bytes() } + +#[cfg(feature = "certora")] +pub fn compute_fingerprint( + e: &Env, + context_type: &ContextRuleType, + signers: &Vec, + policies: &Vec
, +) -> BytesN<32> { + // TODO: make a ghost map to track these. + nondet_bytes_n() +} + // ################## CHANGE STATE ################## /// Creates a new context rule with the specified configuration. Returns the @@ -628,7 +741,16 @@ pub fn add_context_rule( } } + #[cfg(not(feature = "certora"))] let policies_vec = Vec::from_iter(e, policies.keys()); + #[cfg(feature = "certora")] + let policies_vec = { + let mut tmp = Vec::new(e); + for key in policies.keys() { + tmp.push_back(key); + } + tmp + }; validate_signers_and_policies(e, &unique_signers, &policies_vec); validate_and_set_fingerprint(e, context_type, &unique_signers, &policies_vec); @@ -658,10 +780,19 @@ pub fn add_context_rule( // Install the policies for (policy, param) in policies.iter() { + #[cfg(not(feature = "certora"))] PolicyClient::new(e, &policy).install(¶m, &context_rule, &e.current_contract_address()); + #[cfg(feature = "certora")] + SimpleThresholdPolicyContract::install( + &e, + SimpleThresholdAccountParams::from_val(e, ¶m), + context_rule.clone(), + e.current_contract_address(), + ); } // Emit event + #[cfg(not(feature = "certora"))] emit_context_rule_added(e, &context_rule); // Increment next id @@ -717,6 +848,7 @@ pub fn update_context_rule_name(e: &Env, id: u32, name: &String) -> ContextRule }; // Emit event + #[cfg(not(feature = "certora"))] emit_context_rule_updated(e, id, &meta); context_rule @@ -774,6 +906,7 @@ pub fn update_context_rule_valid_until(e: &Env, id: u32, valid_until: Option>( +pub(crate) fn get_persistent_entry>( e: &Env, key: &SmartAccountStorageKey, ) -> Option { diff --git a/packages/accounts/src/smart_account/test/context_rules.rs b/packages/accounts/src/smart_account/test/context_rules.rs index bcc9a59d..1cdd6861 100644 --- a/packages/accounts/src/smart_account/test/context_rules.rs +++ b/packages/accounts/src/smart_account/test/context_rules.rs @@ -201,7 +201,7 @@ fn do_check_auth_multiple_contexts_success() { } #[test] -#[should_panic(expected = "Error(Contract, #2003)")] +#[should_panic(expected = "Error(Contract, #3003)")] fn do_check_auth_authentication_fails() { let e = Env::default(); let address = e.register(MockContract, ()); @@ -242,7 +242,7 @@ fn do_check_auth_authentication_fails() { } #[test] -#[should_panic(expected = "Error(Contract, #2002)")] +#[should_panic(expected = "Error(Contract, #3002)")] fn do_check_auth_context_validation_fails() { let e = Env::default(); let address = e.register(MockContract, ()); @@ -373,7 +373,7 @@ fn add_context_rule_different_context_types() { } #[test] -#[should_panic(expected = "Error(Contract, #2005)")] +#[should_panic(expected = "Error(Contract, #3005)")] fn add_context_rule_past_valid_until_fails() { let e = Env::default(); let address = e.register(MockContract, ()); @@ -429,7 +429,7 @@ fn update_context_rule_success() { } #[test] -#[should_panic(expected = "Error(Contract, #2000)")] +#[should_panic(expected = "Error(Contract, #3000)")] fn update_context_rule_nonexistent_fails() { let e = Env::default(); let address = e.register(MockContract, ()); @@ -441,7 +441,7 @@ fn update_context_rule_nonexistent_fails() { } #[test] -#[should_panic(expected = "Error(Contract, #2005)")] +#[should_panic(expected = "Error(Contract, #3005)")] fn update_context_rule_past_valid_until_fails() { let e = Env::default(); let address = e.register(MockContract, ()); @@ -474,7 +474,7 @@ fn remove_context_rule_success() { } #[test] -#[should_panic(expected = "Error(Contract, #2000)")] +#[should_panic(expected = "Error(Contract, #3000)")] fn remove_context_rule_nonexistent_fails() { let e = Env::default(); let address = e.register(MockContract, ()); @@ -525,7 +525,7 @@ fn can_enforce_all_policies_one_fails() { } #[test] -#[should_panic(expected = "Error(Contract, #2003)")] +#[should_panic(expected = "Error(Contract, #3003)")] fn authenticate_external_signer_verification_fails() { let e = Env::default(); let address = e.register(MockContract, ()); @@ -962,7 +962,7 @@ fn get_validated_context_create_contract_success() { } #[test] -#[should_panic(expected = "Error(Contract, #2002)")] +#[should_panic(expected = "Error(Contract, #3002)")] fn get_validated_context_call_contract_with_policies_fails() { let e = Env::default(); let address = e.register(MockContract, ()); @@ -991,7 +991,7 @@ fn get_validated_context_call_contract_with_policies_fails() { } #[test] -#[should_panic(expected = "Error(Contract, #2002)")] +#[should_panic(expected = "Error(Contract, #3002)")] fn get_validated_context_insufficient_signers_fails() { let e = Env::default(); let address = e.register(MockContract, ()); @@ -1019,7 +1019,7 @@ fn get_validated_context_insufficient_signers_fails() { } #[test] -#[should_panic(expected = "Error(Contract, #2002)")] +#[should_panic(expected = "Error(Contract, #3002)")] fn get_validated_context_no_matching_rules_fails() { let e = Env::default(); let address = e.register(MockContract, ()); @@ -1114,7 +1114,7 @@ fn get_context_rules_empty_result() { } #[test] -#[should_panic(expected = "Error(Contract, #2007)")] +#[should_panic(expected = "Error(Contract, #3007)")] fn add_context_rule_duplicate_signer_fails() { let e = Env::default(); let address = e.register(MockContract, ()); @@ -1146,7 +1146,7 @@ fn add_context_rule_duplicate_signer_fails() { } #[test] -#[should_panic(expected = "Error(Contract, #2012)")] +#[should_panic(expected = "Error(Contract, #3012)")] fn add_context_rule_too_many_rules_fails() { let e = Env::default(); let address = e.register(MockContract, ()); diff --git a/packages/accounts/src/smart_account/test/fingerprints.rs b/packages/accounts/src/smart_account/test/fingerprints.rs index 846558c8..423aba92 100644 --- a/packages/accounts/src/smart_account/test/fingerprints.rs +++ b/packages/accounts/src/smart_account/test/fingerprints.rs @@ -190,7 +190,7 @@ fn compute_fingerprint_mixed_signer_same_fingerprint() { } #[test] -#[should_panic(expected = "Error(Contract, #2007)")] +#[should_panic(expected = "Error(Contract, #3007)")] fn compute_fingerprint_duplicate_signers_fails() { let e = Env::default(); let address = e.register(MockContract, ()); @@ -207,7 +207,7 @@ fn compute_fingerprint_duplicate_signers_fails() { } #[test] -#[should_panic(expected = "Error(Contract, #2009)")] +#[should_panic(expected = "Error(Contract, #3009)")] fn compute_fingerprint_duplicate_policies_fails() { let e = Env::default(); let address = e.register(MockContract, ()); @@ -239,7 +239,7 @@ fn validate_and_set_fingerprint_success() { } #[test] -#[should_panic(expected = "Error(Contract, #2001)")] +#[should_panic(expected = "Error(Contract, #3001)")] fn validate_and_set_fingerprint_duplicate_fails() { let e = Env::default(); let address = e.register(MockContract, ()); @@ -258,7 +258,7 @@ fn validate_and_set_fingerprint_duplicate_fails() { } #[test] -#[should_panic(expected = "Error(Contract, #2001)")] +#[should_panic(expected = "Error(Contract, #3001)")] fn validate_and_set_fingerprint_same_valid_until_fails() { let e = Env::default(); let address = e.register(MockContract, ()); @@ -275,7 +275,7 @@ fn validate_and_set_fingerprint_same_valid_until_fails() { } #[test] -#[should_panic(expected = "Error(Contract, #2001)")] +#[should_panic(expected = "Error(Contract, #3001)")] fn add_context_rule_duplicate_fingerprint_fails() { let e = Env::default(); let address = e.register(MockContract, ()); @@ -501,7 +501,7 @@ fn remove_policy_updates_fingerprint() { } #[test] -#[should_panic(expected = "Error(Contract, #2001)")] +#[should_panic(expected = "Error(Contract, #3001)")] fn fingerprint_prevents_duplicate_rules_across_modifications() { let e = Env::default(); let address = e.register(MockContract, ()); diff --git a/packages/accounts/src/smart_account/test/signers_and_policies.rs b/packages/accounts/src/smart_account/test/signers_and_policies.rs index 5462ea22..58fa41a1 100644 --- a/packages/accounts/src/smart_account/test/signers_and_policies.rs +++ b/packages/accounts/src/smart_account/test/signers_and_policies.rs @@ -105,7 +105,7 @@ fn add_signer_success() { } #[test] -#[should_panic(expected = "Error(Contract, #2000)")] +#[should_panic(expected = "Error(Contract, #3000)")] fn add_signer_nonexistent_rule_fails() { let e = Env::default(); let address = e.register(MockContract, ()); @@ -117,7 +117,7 @@ fn add_signer_nonexistent_rule_fails() { } #[test] -#[should_panic(expected = "Error(Contract, #2007)")] +#[should_panic(expected = "Error(Contract, #3007)")] fn add_signer_duplicate_fails() { let e = Env::default(); let address = e.register(MockContract, ()); @@ -150,7 +150,7 @@ fn remove_signer_success() { } #[test] -#[should_panic(expected = "Error(Contract, #2000)")] +#[should_panic(expected = "Error(Contract, #3000)")] fn remove_signer_nonexistent_rule_fails() { let e = Env::default(); let address = e.register(MockContract, ()); @@ -162,7 +162,7 @@ fn remove_signer_nonexistent_rule_fails() { } #[test] -#[should_panic(expected = "Error(Contract, #2006)")] +#[should_panic(expected = "Error(Contract, #3006)")] fn remove_signer_not_found_fails() { let e = Env::default(); let address = e.register(MockContract, ()); @@ -176,7 +176,7 @@ fn remove_signer_not_found_fails() { } #[test] -#[should_panic(expected = "Error(Contract, #2004)")] +#[should_panic(expected = "Error(Contract, #3004)")] fn remove_signer_last_one_fails() { let e = Env::default(); let address = e.register(MockContract, ()); @@ -243,7 +243,7 @@ fn add_policy_success() { } #[test] -#[should_panic(expected = "Error(Contract, #2000)")] +#[should_panic(expected = "Error(Contract, #3000)")] fn add_policy_nonexistent_rule_fails() { let e = Env::default(); let address = e.register(MockContract, ()); @@ -257,7 +257,7 @@ fn add_policy_nonexistent_rule_fails() { } #[test] -#[should_panic(expected = "Error(Contract, #2009)")] +#[should_panic(expected = "Error(Contract, #3009)")] fn add_policy_duplicate_fails() { let e = Env::default(); let address = e.register(MockContract, ()); @@ -301,7 +301,7 @@ fn remove_policy_success() { } #[test] -#[should_panic(expected = "Error(Contract, #2000)")] +#[should_panic(expected = "Error(Contract, #3000)")] fn remove_policy_nonexistent_rule_fails() { let e = Env::default(); let address = e.register(MockContract, ()); @@ -313,7 +313,7 @@ fn remove_policy_nonexistent_rule_fails() { } #[test] -#[should_panic(expected = "Error(Contract, #2008)")] +#[should_panic(expected = "Error(Contract, #3008)")] fn remove_policy_not_found_fails() { let e = Env::default(); let address = e.register(MockContract, ()); @@ -327,7 +327,7 @@ fn remove_policy_not_found_fails() { } #[test] -#[should_panic(expected = "Error(Contract, #2004)")] +#[should_panic(expected = "Error(Contract, #3004)")] fn remove_policy_last_one_fails() { let e = Env::default(); let address = e.register(MockContract, ()); @@ -392,7 +392,7 @@ fn validate_signers_and_policies_success() { } #[test] -#[should_panic(expected = "Error(Contract, #2004)")] +#[should_panic(expected = "Error(Contract, #3004)")] fn validate_signers_and_policies_no_signers_and_policies_fails() { let e = Env::default(); let address = e.register(MockContract, ()); @@ -406,7 +406,7 @@ fn validate_signers_and_policies_no_signers_and_policies_fails() { } #[test] -#[should_panic(expected = "Error(Contract, #2010)")] +#[should_panic(expected = "Error(Contract, #3010)")] fn validate_signers_and_policies_too_many_signers_fails() { let e = Env::default(); let address = e.register(MockContract, ()); @@ -424,7 +424,7 @@ fn validate_signers_and_policies_too_many_signers_fails() { } #[test] -#[should_panic(expected = "Error(Contract, #2011)")] +#[should_panic(expected = "Error(Contract, #3011)")] fn validate_signers_and_policies_too_many_policies_fails() { let e = Env::default(); let address = e.register(MockContract, ()); diff --git a/packages/accounts/src/verifiers/mod.rs b/packages/accounts/src/verifiers/mod.rs index 20080f73..ff5de390 100644 --- a/packages/accounts/src/verifiers/mod.rs +++ b/packages/accounts/src/verifiers/mod.rs @@ -12,6 +12,9 @@ pub mod utils; pub mod webauthn; use soroban_sdk::{contractclient, Bytes, Env, FromVal, Val}; +#[cfg(feature = "certora")] +pub mod specs; + /// Core trait for cryptographic signature verification in smart accounts. /// /// This trait defines the interface for verifying digital signatures against diff --git a/packages/accounts/src/verifiers/specs/mod.rs b/packages/accounts/src/verifiers/specs/mod.rs new file mode 100644 index 00000000..38b0d5ba --- /dev/null +++ b/packages/accounts/src/verifiers/specs/mod.rs @@ -0,0 +1 @@ +pub mod verifiers_sanity; diff --git a/packages/accounts/src/verifiers/specs/verifiers_sanity.rs b/packages/accounts/src/verifiers/specs/verifiers_sanity.rs new file mode 100644 index 00000000..e69de29b diff --git a/packages/accounts/src/verifiers/test/webauthn.rs b/packages/accounts/src/verifiers/test/webauthn.rs index aaa30059..5bc02827 100644 --- a/packages/accounts/src/verifiers/test/webauthn.rs +++ b/packages/accounts/src/verifiers/test/webauthn.rs @@ -82,7 +82,7 @@ fn validate_expected_type_valid() { } #[test] -#[should_panic(expected = "Error(Contract, #2113)")] +#[should_panic(expected = "Error(Contract, #3113)")] fn validate_expected_type_invalid() { let e = Env::default(); let address = e.register(MockContract, ()); @@ -119,7 +119,7 @@ fn validate_challenge_valid() { } #[test] -#[should_panic(expected = "Error(Contract, #2114)")] +#[should_panic(expected = "Error(Contract, #3114)")] fn validate_challenge_invalid() { let e = Env::default(); let address = e.register(MockContract, ()); @@ -136,7 +136,7 @@ fn validate_challenge_invalid() { } #[test] -#[should_panic(expected = "Error(Contract, #2110)")] +#[should_panic(expected = "Error(Contract, #3110)")] fn validate_challenge_invalid_payload_size() { let e = Env::default(); let address = e.register(MockContract, ()); @@ -166,7 +166,7 @@ fn validate_user_present_bit_set_valid() { } #[test] -#[should_panic(expected = "Error(Contract, #2116)")] +#[should_panic(expected = "Error(Contract, #3116)")] fn validate_user_present_bit_set_invalid() { let e = Env::default(); let address = e.register(MockContract, ()); @@ -193,7 +193,7 @@ fn validate_user_verified_bit_set_valid() { } #[test] -#[should_panic(expected = "Error(Contract, #2117)")] +#[should_panic(expected = "Error(Contract, #3117)")] fn validate_user_verified_bit_set_invalid() { let e = Env::default(); let address = e.register(MockContract, ()); @@ -246,7 +246,7 @@ fn validate_backup_eligibility_and_state_valid_be0_bs0() { } #[test] -#[should_panic(expected = "Error(Contract, #2218)")] +#[should_panic(expected = "Error(Contract, #3118)")] fn validate_backup_eligibility_and_state_invalid_be0_bs1() { let e = Env::default(); let address = e.register(MockContract, ()); @@ -325,7 +325,7 @@ fn webauthn_verify_fake_signature_fails() { } #[test] -#[should_panic(expected = "Error(Contract, #2111)")] +#[should_panic(expected = "Error(Contract, #3111)")] fn verify_client_data_too_long() { let e = Env::default(); let address = e.register(MockContract, ()); @@ -356,7 +356,7 @@ fn verify_client_data_too_long() { } #[test] -#[should_panic(expected = "Error(Contract, #2115)")] +#[should_panic(expected = "Error(Contract, #3115)")] fn verify_authenticator_data_too_short() { let e = Env::default(); let key_data = BytesN::<65>::from_array(&e, &[1u8; 65]); @@ -382,7 +382,7 @@ fn verify_authenticator_data_too_short() { } #[test] -#[should_panic(expected = "Error(Contract, #2112)")] +#[should_panic(expected = "Error(Contract, #3112)")] fn verify_json_parse_error() { let e = Env::default(); let address = e.register(MockContract, ()); diff --git a/packages/accounts/src/verifiers/webauthn.rs b/packages/accounts/src/verifiers/webauthn.rs index eb25b4c3..4ba230b4 100644 --- a/packages/accounts/src/verifiers/webauthn.rs +++ b/packages/accounts/src/verifiers/webauthn.rs @@ -50,23 +50,23 @@ pub const AUTHENTICATOR_DATA_MIN_LEN: usize = 37; #[repr(u32)] pub enum WebAuthnError { /// The signature payload is invalid or has incorrect format. - SignaturePayloadInvalid = 2110, + SignaturePayloadInvalid = 3110, /// The client data exceeds the maximum allowed length. - ClientDataTooLong = 2111, + ClientDataTooLong = 3111, /// Failed to parse JSON from client data. - JsonParseError = 2112, + JsonParseError = 3112, /// The type field in client data is not "webauthn.get". - TypeFieldInvalid = 2113, + TypeFieldInvalid = 3113, /// The challenge in client data does not match expected value. - ChallengeInvalid = 2114, + ChallengeInvalid = 3114, /// The authenticator data format is invalid or too short. - AuthDataFormatInvalid = 2115, + AuthDataFormatInvalid = 3115, /// The User Present (UP) bit is not set in authenticator flags. - PresentBitNotSet = 2116, + PresentBitNotSet = 3116, /// The User Verified (UV) bit is not set in authenticator flags. - VerifiedBitNotSet = 2117, + VerifiedBitNotSet = 3117, /// Invalid relationship between Backup Eligibility and State bits. - BackupEligibilityAndStateNotSet = 2218, + BackupEligibilityAndStateNotSet = 3118, } /// Parsed client data JSON structure for WebAuthn authentication. @@ -115,7 +115,7 @@ pub struct WebAuthnSigData { /// /// # Reference /// -/// Step 11 in https://www.w3.org/TR/webauthn-2/#sctn-verifying-assertion +/// Step 11 in pub fn validate_expected_type(e: &Env, client_data_json: &ClientDataJson) { let type_field = String::from_str(e, "webauthn.get"); if String::from_str(e, client_data_json.type_field) != type_field { @@ -145,7 +145,7 @@ pub fn validate_expected_type(e: &Env, client_data_json: &ClientDataJson) { /// /// # Reference /// -/// Step 12 in https://www.w3.org/TR/webauthn-2/#sctn-verifying-assertion +/// Step 12 in pub fn validate_challenge(e: &Env, client_data_json: &ClientDataJson, signature_payload: &Bytes) { let signature_payload: BytesN<32> = extract_from_bytes(e, signature_payload, 0..32) .unwrap_or_else(|| panic_with_error!(e, WebAuthnError::SignaturePayloadInvalid)); @@ -178,9 +178,9 @@ pub fn validate_challenge(e: &Env, client_data_json: &ClientDataJson, signature_ /// /// # Reference /// -/// Step 16 in https://www.w3.org/TR/webauthn-2/#sctn-verifying-assertion +/// Step 16 in pub fn validate_user_present_bit_set(e: &Env, flags: u8) { - // Validates that the https://www.w3.org/TR/webauthn-2/#up bit is set. + // Validates that the bit is set. if (flags & AUTH_DATA_FLAGS_UP) == 0 { panic_with_error!(e, WebAuthnError::PresentBitNotSet) } @@ -211,7 +211,7 @@ pub fn validate_user_present_bit_set(e: &Env, flags: u8) { /// /// # Reference /// -/// Step 17 in https://www.w3.org/TR/webauthn-2/#sctn-verifying-assertion +/// Step 17 in pub fn validate_user_verified_bit_set(e: &Env, flags: u8) { if (flags & AUTH_DATA_FLAGS_UV) == 0 { panic_with_error!(e, WebAuthnError::VerifiedBitNotSet) @@ -296,7 +296,7 @@ pub fn validate_backup_eligibility_and_state(e: &Env, flags: u8) { /// /// # Reference /// -/// https://www.w3.org/TR/webauthn-2/#sctn-verifying-assertion +/// pub fn verify( e: &Env, signature_payload: &Bytes, diff --git a/packages/contract-utils/Cargo.toml b/packages/contract-utils/Cargo.toml index d87754c1..5c28084f 100644 --- a/packages/contract-utils/Cargo.toml +++ b/packages/contract-utils/Cargo.toml @@ -13,11 +13,12 @@ crate-type = ["lib", "cdylib"] doctest = false [dependencies] -soroban-sdk = { workspace = true } +soroban-sdk = { workspace = true} cvlr = { workspace = true, default-features = false } cvlr-soroban = { workspace = true } cvlr-soroban-macros = { workspace = true } cvlr-soroban-derive = { workspace = true } +stellar-macros = { workspace = true } [dev-dependencies] soroban-sdk = { workspace = true, features = ["testutils"] } @@ -26,4 +27,4 @@ hex-literal = { workspace = true } stellar-event-assertion = { workspace = true } [features] -certora = [] \ No newline at end of file +certora = [] \ No newline at end of file diff --git a/packages/contract-utils/README.md b/packages/contract-utils/README.md index d06da41c..851c8773 100644 --- a/packages/contract-utils/README.md +++ b/packages/contract-utils/README.md @@ -133,9 +133,9 @@ Add this to your `Cargo.toml`: ```toml [dependencies] # We recommend pinning to a specific version, because rapid iterations are expected as the library is in an active development phase. -stellar-contract-utils = "=0.4.0" +stellar-contract-utils = "=0.5.1" # Add this if you want to use macros -stellar-macros = "=0.4.0" +stellar-macros = "=0.5.1" ``` ## Examples diff --git a/packages/contract-utils/certora_build.py b/packages/contract-utils/certora_build.py index 1b6c9cde..ee1a21c5 100755 --- a/packages/contract-utils/certora_build.py +++ b/packages/contract-utils/certora_build.py @@ -14,7 +14,7 @@ # JSON FIELDS PROJECT_DIR = (SCRIPT_DIR / "../").resolve() SOURCES = ["contract_utils/src/**/*.rs"] -EXECUTABLES = "../target/wasm32v1-none/release/stellar_contract_utils.wasm" +EXECUTABLES = "../target/wasm32-unknown-unknown/release/stellar_contract_utils.wasm" VERBOSE = False diff --git a/packages/contract-utils/confs/math_integrity.conf b/packages/contract-utils/confs/math_integrity.conf new file mode 100644 index 00000000..7064c510 --- /dev/null +++ b/packages/contract-utils/confs/math_integrity.conf @@ -0,0 +1,11 @@ + +{ + "build_script": "../certora_build.py", + "msg": "Integrity Rules Math", + "rule": [ + "fixed_mul_floor_integrity", + "fixed_mul_ceil_integrity" + ], + "server": "production", + "prover_version": "stellar-oz-changes" +} \ No newline at end of file diff --git a/packages/contract-utils/confs/math_non_panics.conf b/packages/contract-utils/confs/math_non_panics.conf new file mode 100644 index 00000000..96b2807d --- /dev/null +++ b/packages/contract-utils/confs/math_non_panics.conf @@ -0,0 +1,9 @@ +{ + "build_script": "../certora_build.py", + "msg": "Panic Rules Math", + "rule": [ + "fixed_mul_floor_non_panic", + ], + "server": "production", + "prover_version": "stellar-oz-changes" +} \ No newline at end of file diff --git a/packages/contract-utils/confs/math_panics.conf b/packages/contract-utils/confs/math_panics.conf new file mode 100644 index 00000000..022bb049 --- /dev/null +++ b/packages/contract-utils/confs/math_panics.conf @@ -0,0 +1,12 @@ +{ + "build_script": "../certora_build.py", + "msg": "Panic Rules Math", + "rule": [ + "fixed_mul_floor_panics_if_zero_denominator", + "fixed_mul_ceil_panics_if_zero_denominator", + "fixed_mul_floor_panics_if_result_overflows", + "fixed_mul_ceil_panics_if_result_overflows" + ], + "server": "production", + "prover_version": "stellar-oz-changes" +} \ No newline at end of file diff --git a/packages/contract-utils/confs/math_sanity.conf b/packages/contract-utils/confs/math_sanity.conf index 4f7b47ce..19439a2c 100644 --- a/packages/contract-utils/confs/math_sanity.conf +++ b/packages/contract-utils/confs/math_sanity.conf @@ -1,13 +1,14 @@ { "build_script": "../certora_build.py", - "optimistic_loop": true, - "msg": "Sanity Rules for OZ contract_utils crate, math", + "msg": "Sanity Rules Math", "rule": [ "div_floor_sanity", "div_ceil_sanity", "scaled_mul_div_floor_sanity", - "scaled_mul_div_ceil_sanity" + "scaled_mul_div_ceil_sanity", + "fixed_mul_floor_sanity", + "fixed_mul_ceil_sanity" ], - "server": "prover", - "prover_version": "abakst/soroban-new-summaries" + "server": "production", + "prover_version": "stellar-oz-changes" } \ No newline at end of file diff --git a/packages/contract-utils/confs/merkle_distributor_contract_sanity.conf b/packages/contract-utils/confs/merkle_distributor_contract_sanity.conf new file mode 100644 index 00000000..8c689bc9 --- /dev/null +++ b/packages/contract-utils/confs/merkle_distributor_contract_sanity.conf @@ -0,0 +1,13 @@ +{ + "build_script": "../certora_build.py", + "msg": "Sanity Rules OZ contract_utils crate, merkle_distributor", + "rule": [ + "merkle_distributor_constructor_sanity", + "get_root_sanity", + "is_claimed_sanity", + "set_claimed_sanity", + "claim_sanity" + ], + "server": "production", + "prover_version": "stellar-oz-changes" +} \ No newline at end of file diff --git a/packages/contract-utils/confs/merkle_distributor_integrity.conf b/packages/contract-utils/confs/merkle_distributor_integrity.conf new file mode 100644 index 00000000..0b797535 --- /dev/null +++ b/packages/contract-utils/confs/merkle_distributor_integrity.conf @@ -0,0 +1,11 @@ +{ + "build_script": "../certora_build.py", + "msg": "Integrity Rules Merkle Distributor Contract", + "rule": [ + "merkle_distributor_constructor_integrity", + "set_claimed_integrity", + "claim_integrity" + ], + "server": "production", + "prover_version": "stellar-oz-changes" +} \ No newline at end of file diff --git a/packages/contract-utils/confs/merkle_distributor_sanity.conf b/packages/contract-utils/confs/merkle_distributor_sanity.conf new file mode 100644 index 00000000..b0ccc0ef --- /dev/null +++ b/packages/contract-utils/confs/merkle_distributor_sanity.conf @@ -0,0 +1,14 @@ +{ + "build_script": "../certora_build.py", + "msg": "Sanity Rules OZ contract_utils crate, merkle_distributor", + "rule": [ + "get_root_sanity", + "is_claimed_sanity", + "set_root_sanity", + "set_claimed_sanity", + "verify_and_set_claimed_sanity", + "verify_with_index_and_set_claimed_sanity" + ], + "server": "production", + "prover_version": "stellar-oz-changes" +} \ No newline at end of file diff --git a/packages/contract-utils/confs/pausable_integrity.conf b/packages/contract-utils/confs/pausable_integrity.conf new file mode 100644 index 00000000..942cdeb4 --- /dev/null +++ b/packages/contract-utils/confs/pausable_integrity.conf @@ -0,0 +1,11 @@ +{ + "build_script": "../certora_build.py", + "msg": "Integrity Rules Pausable", + "rule": [ + "pause_integrity", + "unpause_integrity" + ], + "server": "production", + "precise_bitwise_ops": true, + "prover_version": "stellar-oz-changes" +} \ No newline at end of file diff --git a/packages/contract-utils/confs/pausable_non_panics.conf b/packages/contract-utils/confs/pausable_non_panics.conf new file mode 100644 index 00000000..bb8ddb87 --- /dev/null +++ b/packages/contract-utils/confs/pausable_non_panics.conf @@ -0,0 +1,17 @@ +{ + "build_script": "../certora_build.py", + "msg": "Non-Panic Rules Pausable", + "rule": [ + "pause_non_panic", + "unpause_non_panic", + "when_not_paused_non_panic", + "when_paused_non_panic", + "pause_non_panic_sanity", + "unpause_non_panic_sanity", + "when_not_paused_non_panic_sanity", + "when_paused_non_panic_sanity" + ], + "prover_args": ["-trapAsAssert true"], + "server": "production", + "prover_version": "stellar-oz-changes" +} \ No newline at end of file diff --git a/packages/contract-utils/confs/pausable_panics.conf b/packages/contract-utils/confs/pausable_panics.conf new file mode 100644 index 00000000..f353235e --- /dev/null +++ b/packages/contract-utils/confs/pausable_panics.conf @@ -0,0 +1,13 @@ +{ + "build_script": "../certora_build.py", + "msg": "Panic Rules Pausable", + "rule": [ + "pause_panics_if_paused", + "unpause_panics_if_not_paused", + "when_not_paused_panics_if_paused", + "when_paused_panics_if_not_paused" + ], + "precise_bitwise_ops": true, + "server": "production", + "prover_version": "stellar-oz-changes" +} \ No newline at end of file diff --git a/packages/contract-utils/confs/pausable_sanity.conf b/packages/contract-utils/confs/pausable_sanity.conf index 0fa687e2..69a0d12e 100644 --- a/packages/contract-utils/confs/pausable_sanity.conf +++ b/packages/contract-utils/confs/pausable_sanity.conf @@ -1,7 +1,6 @@ { "build_script": "../certora_build.py", - "optimistic_loop": true, - "msg": "Sanity Rules for OZ contract_utils crate, pausable", + "msg": "Sanity Rules OZ contract_utils crate, pausable", "rule": [ "paused_sanity", "pause_sanity", @@ -9,6 +8,6 @@ "when_not_paused_sanity", "when_paused_sanity" ], - "server": "prover", - "prover_version": "abakst/soroban-new-summaries" + "server": "production", + "prover_version": "stellar-oz-changes" } \ No newline at end of file diff --git a/packages/contract-utils/confs/upgradeable_integrity.conf b/packages/contract-utils/confs/upgradeable_integrity.conf new file mode 100644 index 00000000..a392e6aa --- /dev/null +++ b/packages/contract-utils/confs/upgradeable_integrity.conf @@ -0,0 +1,8 @@ +{ + "build_script": "../certora_build.py", + "msg": "Sanity Rules OZ contract_utils crate, upgradeable", + "rule": [ + ], + "server": "production", + "prover_version": "stellar-oz-changes" +} \ No newline at end of file diff --git a/packages/contract-utils/confs/upgradeable_non_panics.conf b/packages/contract-utils/confs/upgradeable_non_panics.conf new file mode 100644 index 00000000..a392e6aa --- /dev/null +++ b/packages/contract-utils/confs/upgradeable_non_panics.conf @@ -0,0 +1,8 @@ +{ + "build_script": "../certora_build.py", + "msg": "Sanity Rules OZ contract_utils crate, upgradeable", + "rule": [ + ], + "server": "production", + "prover_version": "stellar-oz-changes" +} \ No newline at end of file diff --git a/packages/contract-utils/confs/upgradeable_panics.conf b/packages/contract-utils/confs/upgradeable_panics.conf new file mode 100644 index 00000000..a392e6aa --- /dev/null +++ b/packages/contract-utils/confs/upgradeable_panics.conf @@ -0,0 +1,8 @@ +{ + "build_script": "../certora_build.py", + "msg": "Sanity Rules OZ contract_utils crate, upgradeable", + "rule": [ + ], + "server": "production", + "prover_version": "stellar-oz-changes" +} \ No newline at end of file diff --git a/packages/contract-utils/confs/upgradeable_sanity.conf b/packages/contract-utils/confs/upgradeable_sanity.conf index 7b30b632..327eef4c 100644 --- a/packages/contract-utils/confs/upgradeable_sanity.conf +++ b/packages/contract-utils/confs/upgradeable_sanity.conf @@ -1,13 +1,14 @@ { "build_script": "../certora_build.py", - "optimistic_loop": true, - "msg": "Sanity Rules for OZ contract_utils crate, upgradeable", + "msg": "Sanity Rules OZ contract_utils crate, upgradeable", "rule": [ "enable_migration_sanity", "can_complete_migration_sanity", "complete_migration_sanity", - "ensure_can_complete_migration_sanity" + "ensure_can_complete_migration_sanity", + // "upgradeable_migratable_contract_upgrade_sanity", + // "upgradeable_migratable_contract_migrate_sanity" ], - "server": "prover", - "prover_version": "abakst/soroban-new-summaries" + "server": "production", + "prover_version": "stellar-oz-changes" } \ No newline at end of file diff --git a/packages/contract-utils/justfile b/packages/contract-utils/justfile index 36fa02a9..4d26eddf 100644 --- a/packages/contract-utils/justfile +++ b/packages/contract-utils/justfile @@ -3,7 +3,7 @@ export RUSTFLAGS := "-C strip=none" target := "../../target" build: - cargo build --target=wasm32v1-none --release --features certora + cargo +nightly-2024-11-22 build --target=wasm32-unknown-unknown --release --features certora clean: rm -rf {{target}} \ No newline at end of file diff --git a/packages/contract-utils/src/lib.rs b/packages/contract-utils/src/lib.rs index b4d5a49f..6f8756e6 100644 --- a/packages/contract-utils/src/lib.rs +++ b/packages/contract-utils/src/lib.rs @@ -1,5 +1,6 @@ #![no_std] #![cfg_attr(feature = "certora", allow(unused_variables, unused_imports, dead_code))] +#![feature(unsigned_is_multiple_of)] pub mod crypto; pub mod math; diff --git a/packages/contract-utils/src/math/i128_fixed_point.rs b/packages/contract-utils/src/math/i128_fixed_point.rs index 19021e0c..07d57685 100644 --- a/packages/contract-utils/src/math/i128_fixed_point.rs +++ b/packages/contract-utils/src/math/i128_fixed_point.rs @@ -24,9 +24,9 @@ SOFTWARE. // Based on the Soroban fixed-point mathematics library // Original implementation: https://github.com/script3/soroban-fixed-point-math -use soroban_sdk::{unwrap::UnwrapOptimized, Env, I256}; +use soroban_sdk::{panic_with_error, Env, I256}; -use crate::math::soroban_fixed_point::SorobanFixedPoint; +use crate::math::soroban_fixed_point::{SorobanFixedPoint, SorobanFixedPointError}; /// Performs floor(r / z) pub fn div_floor(r: i128, z: i128) -> Option { @@ -65,7 +65,8 @@ impl SorobanFixedPoint for i128 { /// Performs floor(x * y / z) pub fn scaled_mul_div_floor(x: &i128, env: &Env, y: &i128, z: &i128) -> i128 { match x.checked_mul(*y) { - Some(r) => div_floor(r, *z).unwrap_optimized(), + Some(r) => div_floor(r, *z) + .unwrap_or_else(|| panic_with_error!(env, SorobanFixedPointError::ZeroDenominator)), None => { // scale to i256 and retry let res = crate::math::i256_fixed_point::mul_div_floor( @@ -74,8 +75,8 @@ pub fn scaled_mul_div_floor(x: &i128, env: &Env, y: &i128, z: &i128) -> i128 { &I256::from_i128(env, *y), &I256::from_i128(env, *z), ); - // will panic if result is not representable in i128 - res.to_i128().unwrap_optimized() + res.to_i128() + .unwrap_or_else(|| panic_with_error!(env, SorobanFixedPointError::ResultOverflow)) } } } @@ -83,7 +84,8 @@ pub fn scaled_mul_div_floor(x: &i128, env: &Env, y: &i128, z: &i128) -> i128 { /// Performs floor(x * y / z) pub fn scaled_mul_div_ceil(x: &i128, env: &Env, y: &i128, z: &i128) -> i128 { match x.checked_mul(*y) { - Some(r) => div_ceil(r, *z).unwrap_optimized(), + Some(r) => div_ceil(r, *z) + .unwrap_or_else(|| panic_with_error!(env, SorobanFixedPointError::ZeroDenominator)), None => { // scale to i256 and retry let res = crate::math::i256_fixed_point::mul_div_ceil( @@ -92,8 +94,8 @@ pub fn scaled_mul_div_ceil(x: &i128, env: &Env, y: &i128, z: &i128) -> i128 { &I256::from_i128(env, *y), &I256::from_i128(env, *z), ); - // will panic if result is not representable in i128 - res.to_i128().unwrap_optimized() + res.to_i128() + .unwrap_or_else(|| panic_with_error!(env, SorobanFixedPointError::ResultOverflow)) } } } diff --git a/packages/contract-utils/src/math/i256_fixed_point.rs b/packages/contract-utils/src/math/i256_fixed_point.rs index 5663ecaf..842a5dbf 100644 --- a/packages/contract-utils/src/math/i256_fixed_point.rs +++ b/packages/contract-utils/src/math/i256_fixed_point.rs @@ -24,9 +24,9 @@ SOFTWARE. // Based on the Soroban fixed-point mathematics library // Original implementation: https://github.com/script3/soroban-fixed-point-math -use soroban_sdk::{Env, I256}; +use soroban_sdk::{panic_with_error, Env, I256}; -use crate::math::soroban_fixed_point::SorobanFixedPoint; +use crate::math::soroban_fixed_point::{SorobanFixedPoint, SorobanFixedPointError}; impl SorobanFixedPoint for I256 { fn fixed_mul_floor(&self, env: &Env, y: &I256, denominator: &I256) -> I256 { @@ -42,13 +42,18 @@ impl SorobanFixedPoint for I256 { pub(crate) fn mul_div_floor(env: &Env, x: &I256, y: &I256, z: &I256) -> I256 { let zero = I256::from_i32(env, 0); let r = x.mul(y); + + if z.clone() == zero { + panic_with_error!(env, SorobanFixedPointError::ZeroDenominator); + } + if r < zero || (r > zero && z.clone() < zero) { - // ceiling is taken by default for a negative result + // ceil is taken by default for a negative result let remainder = r.rem_euclid(z); let one = I256::from_i32(env, 1); r.div(z).sub(if remainder > zero { &one } else { &zero }) } else { - // floor taken by default for a positive or zero result + // floor is taken by default for a positive or zero result r.div(z) } } @@ -58,11 +63,15 @@ pub(crate) fn mul_div_ceil(env: &Env, x: &I256, y: &I256, z: &I256) -> I256 { let zero = I256::from_i32(env, 0); let r = x.mul(y); + if z.clone() == zero { + panic_with_error!(env, SorobanFixedPointError::ZeroDenominator); + } + if z.clone() < zero || r <= zero { - // ceiling is taken by default for a negative or zero result + // ceil is taken by default for a negative or zero result r.div(z) } else { - // floor taken by default for a positive result + // floor is taken by default for a positive result let remainder = r.rem_euclid(z); let one = I256::from_i32(env, 1); r.div(z).add(if remainder > zero { &one } else { &zero }) diff --git a/packages/contract-utils/src/math/mod.rs b/packages/contract-utils/src/math/mod.rs index 6aec68a2..3796f514 100644 --- a/packages/contract-utils/src/math/mod.rs +++ b/packages/contract-utils/src/math/mod.rs @@ -1,9 +1,9 @@ pub mod fixed_point; -mod i128_fixed_point; +pub mod i128_fixed_point; mod i256_fixed_point; mod soroban_fixed_point; mod test; #[cfg(feature = "certora")] -pub mod spec; +pub mod specs; \ No newline at end of file diff --git a/packages/contract-utils/src/math/soroban_fixed_point.rs b/packages/contract-utils/src/math/soroban_fixed_point.rs index 700acd97..e8fc879f 100644 --- a/packages/contract-utils/src/math/soroban_fixed_point.rs +++ b/packages/contract-utils/src/math/soroban_fixed_point.rs @@ -24,7 +24,7 @@ SOFTWARE. // Based on the Soroban fixed-point mathematics library // Original implementation: https://github.com/script3/soroban-fixed-point-math -use soroban_sdk::Env; +use soroban_sdk::{contracterror, Env}; // @dev - more detail about the forced panic can be found here: https://github.com/stellar/rs-soroban-env/pull/1091 // @@ -50,3 +50,17 @@ pub trait SorobanFixedPoint: Sized { /// occurs, or the result does not fit in Self. fn fixed_mul_ceil(&self, env: &Env, y: &Self, denominator: &Self) -> Self; } + +// ################## ERRORS ################## + +#[contracterror] +#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] +#[repr(u32)] +pub enum SorobanFixedPointError { + /// The operation failed because the denominator is 0. + ZeroDenominator = 1500, + /// The operation failed because a phantom overflow occurred. + PhantomOverflow = 1501, + /// The operation failed because the result does not fit in Self. + ResultOverflow = 1502, +} diff --git a/packages/contract-utils/src/math/spec/mod.rs b/packages/contract-utils/src/math/spec/mod.rs deleted file mode 100644 index 7acea00e..00000000 --- a/packages/contract-utils/src/math/spec/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod math_sanity_rules; \ No newline at end of file diff --git a/packages/contract-utils/src/math/specs/math_integrity.rs b/packages/contract-utils/src/math/specs/math_integrity.rs new file mode 100644 index 00000000..eeda7e24 --- /dev/null +++ b/packages/contract-utils/src/math/specs/math_integrity.rs @@ -0,0 +1,54 @@ +use cvlr::{cvlr_assert,cvlr_assume,cvlr_satisfy};use cvlr_soroban_derive::rule; +use cvlr::nondet::Nondet; +use cvlr::clog; +use soroban_sdk::{Env}; +use crate::math::i128_fixed_point::*; +use crate::math::fixed_point::Rounding; +use crate::math::soroban_fixed_point::SorobanFixedPoint; + +// todo: handle the muldiv function directly, need support for nondet_rounding. + +#[rule] +// result is at most expected +// status: first assert verified, second violated spurious +// +pub fn fixed_mul_floor_integrity(e: &Env) { + let x = i128::nondet(); + clog!(x); + let y = i128::nondet(); + clog!(y); + let z = i128::nondet(); + clog!(z); + let result = x.fixed_mul_floor(e, &y, &z); + clog!(result); + let expected_result = x * y / z; + clog!(expected_result); + let max_rounding_error: i128 = 1; + clog!(max_rounding_error); + let lower_bound = expected_result.checked_sub(max_rounding_error).unwrap(); + clog!(lower_bound); + cvlr_assert!(result <= expected_result); + cvlr_assert!(result >= lower_bound); +} + +#[rule] +// result is at least expected +// status: first assert verified, second violated spurious +pub fn fixed_mul_ceil_integrity(e: &Env) { + let x = i128::nondet(); + clog!(x); + let y = i128::nondet(); + clog!(y); + let z = i128::nondet(); + clog!(z); + let result = x.fixed_mul_ceil(e, &y, &z); + clog!(result); + let expected_result = x * y / z; + clog!(expected_result); + let max_rounding_error = 1; + clog!(max_rounding_error); + let upper_bound = expected_result.checked_add(max_rounding_error).unwrap(); + clog!(upper_bound); + cvlr_assert!(result >= expected_result); + cvlr_assert!(result <= upper_bound); +} \ No newline at end of file diff --git a/packages/contract-utils/src/math/specs/math_non_panics.rs b/packages/contract-utils/src/math/specs/math_non_panics.rs new file mode 100644 index 00000000..2a5422a4 --- /dev/null +++ b/packages/contract-utils/src/math/specs/math_non_panics.rs @@ -0,0 +1,36 @@ +// todo + +use cvlr::{cvlr_assert,cvlr_assume,cvlr_satisfy};use cvlr_soroban_derive::rule; +use cvlr::nondet::Nondet; +use soroban_sdk::{Env}; +use crate::math::i128_fixed_point::*; +use crate::math::fixed_point::Rounding; +use crate::math::soroban_fixed_point::SorobanFixedPoint; +use soroban_sdk::I256; +use cvlr::clog; + +// todo: handle the muldiv function directly, need support for nondet_rounding. +// todo: overflow panics (not sure how) +// These rules require the prover arg "prover_args": ["-trapAsAssert true"] to consider also panicking paths. + + +#[rule] +// requires: z != 0 +// status: timeout -- do panics first. +pub fn fixed_mul_floor_non_panic(e: &Env) { + let x = i128::nondet(); + clog!(x); + let y = i128::nondet(); + clog!(y); + let z = i128::nondet(); + clog!(z); + cvlr_assume!(z != 0); + let x_256 = I256::from_i128(e, x); + let y_256 = I256::from_i128(e, y); + let z_256 = I256::from_i128(e, z); + let result = x_256.mul(&y_256).div(&z_256); + cvlr_assume!(result <= I256::from_i128(e, i128::MAX)); + let result = x.fixed_mul_floor(e, &y, &z); + clog!(result); + cvlr_assert!(true); +} \ No newline at end of file diff --git a/packages/contract-utils/src/math/specs/math_panics.rs b/packages/contract-utils/src/math/specs/math_panics.rs new file mode 100644 index 00000000..86de57d1 --- /dev/null +++ b/packages/contract-utils/src/math/specs/math_panics.rs @@ -0,0 +1,77 @@ +use cvlr::{cvlr_assert,cvlr_assume,cvlr_satisfy};use cvlr_soroban_derive::rule; +use cvlr::nondet::Nondet; + +use soroban_sdk::{Env}; +use crate::math::i128_fixed_point::*; +use crate::math::fixed_point::Rounding; +use crate::math::soroban_fixed_point::SorobanFixedPoint; +use soroban_sdk::I256; +use cvlr::clog; + +// todo: handle the muldiv function directly, need support for nondet_rounding. +// todo: overflow panics (not sure how) + +#[rule] +// fixed_mul_floor panics if the denominator is 0 +// status: verified +pub fn fixed_mul_floor_panics_if_zero_denominator(e: &Env) { + let x = i128::nondet(); + let y = i128::nondet(); + let z = i128::nondet(); + cvlr_assume!(z == 0); + let _ = x.fixed_mul_floor(e, &y, &z); + cvlr_assert!(false); +} + +#[rule] +// fixed_mul_ceil panics if the denominator is 0 +// status: verified +pub fn fixed_mul_ceil_panics_if_zero_denominator(e: &Env) { + let x = i128::nondet(); + let y = i128::nondet(); + let z = i128::nondet(); + cvlr_assume!(z == 0); + let _ = x.fixed_mul_ceil(e, &y, &z); + cvlr_assert!(false); +} + + #[rule] + // fixed_mul_floor panics if the result overflows + // status: violated - spurious? + pub fn fixed_mul_floor_panics_if_result_overflows(e: &Env) { + let x = i128::nondet(); + clog!(x); + let y = i128::nondet(); + clog!(y); + let z = i128::nondet(); + clog!(z); + let x_256 = I256::from_i128(e, x); + let y_256 = I256::from_i128(e, y); + let z_256 = I256::from_i128(e, z); + let result = x_256.mul(&y_256).div(&z_256); + cvlr_assume!(result > I256::from_i128(e, i128::MAX)); + let result = x.fixed_mul_floor(e, &y, &z); + clog!(result); + cvlr_assert!(false); + } + + + #[rule] + // fixed_mul_ceil panics if the result overflows + // status: violated - spurious? + pub fn fixed_mul_ceil_panics_if_result_overflows(e: &Env) { + let x = i128::nondet(); + clog!(x); + let y = i128::nondet(); + clog!(y); + let z = i128::nondet(); + clog!(z); + let x_256 = I256::from_i128(e, x); + let y_256 = I256::from_i128(e, y); + let z_256 = I256::from_i128(e, z); + let result = x_256.mul(&y_256).div(&z_256); + cvlr_assume!(result > I256::from_i128(e, i128::MAX)); + let result = x.fixed_mul_ceil(e, &y, &z); + clog!(result); + cvlr_assert!(false); + } \ No newline at end of file diff --git a/packages/contract-utils/src/math/spec/math_sanity_rules.rs b/packages/contract-utils/src/math/specs/math_sanity_rules.rs similarity index 52% rename from packages/contract-utils/src/math/spec/math_sanity_rules.rs rename to packages/contract-utils/src/math/specs/math_sanity_rules.rs index 263676cd..cc066323 100644 --- a/packages/contract-utils/src/math/spec/math_sanity_rules.rs +++ b/packages/contract-utils/src/math/specs/math_sanity_rules.rs @@ -1,10 +1,10 @@ -use cvlr::{cvlr_assert}; -use cvlr_soroban_derive::rule; +use cvlr::{cvlr_assert,cvlr_satisfy};use cvlr_soroban_derive::rule; use cvlr::nondet::Nondet; use soroban_sdk::{Env}; use crate::math::i128_fixed_point::*; - +use crate::math::fixed_point::Rounding; +use crate::math::soroban_fixed_point::SorobanFixedPoint; // TODO: need 256 support #[rule] @@ -12,7 +12,7 @@ pub fn div_floor_sanity() { let r = i128::nondet(); let z = i128::nondet(); let _ = div_floor(r, z); - cvlr_assert!(false); + cvlr_satisfy!(true); } #[rule] @@ -20,7 +20,7 @@ pub fn div_ceil_sanity() { let r = i128::nondet(); let z = i128::nondet(); let _ = div_ceil(r, z); - cvlr_assert!(false); + cvlr_satisfy!(true); } #[rule] @@ -29,7 +29,7 @@ pub fn scaled_mul_div_floor_sanity(e: &Env) { let y = i128::nondet(); let z = i128::nondet(); let _ = scaled_mul_div_floor(&x, &e, &y, &z); - cvlr_assert!(false); + cvlr_satisfy!(true); } #[rule] @@ -38,5 +38,23 @@ pub fn scaled_mul_div_ceil_sanity(e: &Env) { let y = i128::nondet(); let z = i128::nondet(); let _ = scaled_mul_div_ceil(&x, &e, &y, &z); - cvlr_assert!(false); + cvlr_satisfy!(true); +} + +#[rule] +pub fn fixed_mul_floor_sanity(e: &Env) { + let x = i128::nondet(); + let y = i128::nondet(); + let z = i128::nondet(); + let _ = x.fixed_mul_floor(e, &y, &z); + cvlr_satisfy!(true); +} + +#[rule] +pub fn fixed_mul_ceil_sanity(e: &Env) { + let x = i128::nondet(); + let y = i128::nondet(); + let z = i128::nondet(); + let _ = x.fixed_mul_ceil(e, &y, &z); + cvlr_satisfy!(true); } \ No newline at end of file diff --git a/packages/contract-utils/src/math/specs/mod.rs b/packages/contract-utils/src/math/specs/mod.rs new file mode 100644 index 00000000..9a28756d --- /dev/null +++ b/packages/contract-utils/src/math/specs/mod.rs @@ -0,0 +1,4 @@ +pub mod math_sanity_rules; +pub mod math_panics; +pub mod math_integrity; +pub mod math_non_panics; \ No newline at end of file diff --git a/packages/contract-utils/src/math/test.rs b/packages/contract-utils/src/math/test.rs index a19c6c42..6a7e3f20 100644 --- a/packages/contract-utils/src/math/test.rs +++ b/packages/contract-utils/src/math/test.rs @@ -124,7 +124,296 @@ mod test_soroban_fixed_point { } #[cfg(test)] -mod tests { +mod test_muldiv { + use soroban_sdk::Env; + + use crate::math::fixed_point::{muldiv, Rounding}; + + #[test] + fn test_muldiv_floor_rounds_down() { + let env = Env::default(); + let x: i128 = 1_5391283; + let y: i128 = 314_1592653; + let denominator: i128 = 1_0000001; + + let result = muldiv(&env, x, y, denominator, Rounding::Floor); + + assert_eq!(result, 483_5313675); + } + + #[test] + fn test_muldiv_ceil_rounds_up() { + let env = Env::default(); + let x: i128 = 1_5391283; + let y: i128 = 314_1592653; + let denominator: i128 = 1_0000001; + + let result = muldiv(&env, x, y, denominator, Rounding::Ceil); + + assert_eq!(result, 483_5313676); + } + + #[test] + fn test_muldiv_floor_negative() { + let env = Env::default(); + let x: i128 = -1_5391283; + let y: i128 = 314_1592653; + let denominator: i128 = 1_0000001; + + let result = muldiv(&env, x, y, denominator, Rounding::Floor); + + assert_eq!(result, -483_5313676); + } + + #[test] + fn test_muldiv_ceil_negative() { + let env = Env::default(); + let x: i128 = -1_5391283; + let y: i128 = 314_1592653; + let denominator: i128 = 1_0000001; + + let result = muldiv(&env, x, y, denominator, Rounding::Ceil); + + assert_eq!(result, -483_5313675); + } + + #[test] + fn test_muldiv_exact_division() { + let env = Env::default(); + let x: i128 = 100; + let y: i128 = 50; + let denominator: i128 = 10; + + let result_floor = muldiv(&env, x, y, denominator, Rounding::Floor); + let result_ceil = muldiv(&env, x, y, denominator, Rounding::Ceil); + + assert_eq!(result_floor, 500); + assert_eq!(result_ceil, 500); + } + + #[test] + fn test_muldiv_with_zero_x() { + let env = Env::default(); + let x: i128 = 0; + let y: i128 = 314_1592653; + let denominator: i128 = 1_0000001; + + let result = muldiv(&env, x, y, denominator, Rounding::Floor); + + assert_eq!(result, 0); + } + + #[test] + fn test_muldiv_with_zero_y() { + let env = Env::default(); + let x: i128 = 1_5391283; + let y: i128 = 0; + let denominator: i128 = 1_0000001; + + let result = muldiv(&env, x, y, denominator, Rounding::Ceil); + + assert_eq!(result, 0); + } + + #[test] + #[should_panic(expected = "Error(Contract, #1500)")] + fn test_muldiv_zero_denominator() { + let env = Env::default(); + let x: i128 = 100; + let y: i128 = 50; + let denominator: i128 = 0; + + muldiv(&env, x, y, denominator, Rounding::Floor); + } + + #[test] + fn test_muldiv_phantom_overflow_scales() { + let env = Env::default(); + let x: i128 = 170_141_183_460_469_231_731; + let y: i128 = 10i128.pow(27); + let denominator: i128 = 10i128.pow(18); + + let result = muldiv(&env, x, y, denominator, Rounding::Floor); + + assert_eq!(result, 170_141_183_460_469_231_731 * 10i128.pow(9)); + } +} + +#[cfg(test)] +mod test_i128_errors { + use soroban_sdk::Env; + + use crate::math::soroban_fixed_point::SorobanFixedPoint; + + #[test] + #[should_panic(expected = "Error(Contract, #1500)")] + fn test_fixed_mul_floor_zero_denominator() { + let env = Env::default(); + let x: i128 = 100; + let y: i128 = 50; + let denominator: i128 = 0; + + x.fixed_mul_floor(&env, &y, &denominator); + } + + #[test] + #[should_panic(expected = "Error(Contract, #1500)")] + fn test_fixed_mul_ceil_zero_denominator() { + let env = Env::default(); + let x: i128 = 100; + let y: i128 = 50; + let denominator: i128 = 0; + + x.fixed_mul_ceil(&env, &y, &denominator); + } + + #[test] + #[should_panic(expected = "Error(Contract, #1502)")] + fn test_fixed_mul_floor_result_overflow() { + let env = Env::default(); + // This will overflow i128 even after scaling to I256 + let x: i128 = i128::MAX; + let y: i128 = i128::MAX; + let denominator: i128 = 1; + + x.fixed_mul_floor(&env, &y, &denominator); + } + + #[test] + #[should_panic(expected = "Error(Contract, #1502)")] + fn test_fixed_mul_ceil_result_overflow() { + let env = Env::default(); + // This will overflow i128 even after scaling to I256 + let x: i128 = i128::MAX; + let y: i128 = i128::MAX; + let denominator: i128 = 1; + + x.fixed_mul_ceil(&env, &y, &denominator); + } + + #[test] + fn test_fixed_mul_floor_with_zero_x() { + let env = Env::default(); + let x: i128 = 0; + let y: i128 = 314_1592653; + let denominator: i128 = 1_0000001; + + let result = x.fixed_mul_floor(&env, &y, &denominator); + + assert_eq!(result, 0); + } + + #[test] + fn test_fixed_mul_ceil_with_zero_y() { + let env = Env::default(); + let x: i128 = 1_5391283; + let y: i128 = 0; + let denominator: i128 = 1_0000001; + + let result = x.fixed_mul_ceil(&env, &y, &denominator); + + assert_eq!(result, 0); + } + + #[test] + fn test_fixed_mul_floor_exact_division() { + let env = Env::default(); + let x: i128 = 100; + let y: i128 = 50; + let denominator: i128 = 10; + + let result = x.fixed_mul_floor(&env, &y, &denominator); + + assert_eq!(result, 500); + } + + #[test] + fn test_fixed_mul_ceil_exact_division() { + let env = Env::default(); + let x: i128 = 100; + let y: i128 = 50; + let denominator: i128 = 10; + + let result = x.fixed_mul_ceil(&env, &y, &denominator); + + assert_eq!(result, 500); + } + + #[test] + fn test_fixed_mul_floor_one_denominator() { + let env = Env::default(); + let x: i128 = 123_456_789; + let y: i128 = 987_654_321; + let denominator: i128 = 1; + + let result = x.fixed_mul_floor(&env, &y, &denominator); + + assert_eq!(result, x * y); + } + + #[test] + fn test_fixed_mul_ceil_one_denominator() { + let env = Env::default(); + let x: i128 = 123_456_789; + let y: i128 = 987_654_321; + let denominator: i128 = 1; + + let result = x.fixed_mul_ceil(&env, &y, &denominator); + + assert_eq!(result, x * y); + } + + #[test] + fn test_fixed_mul_floor_negative_denominator() { + let env = Env::default(); + let x: i128 = 100; + let y: i128 = 50; + let denominator: i128 = -10; + + let result = x.fixed_mul_floor(&env, &y, &denominator); + + assert_eq!(result, -500); + } + + #[test] + fn test_fixed_mul_ceil_negative_denominator() { + let env = Env::default(); + let x: i128 = 100; + let y: i128 = 50; + let denominator: i128 = -10; + + let result = x.fixed_mul_ceil(&env, &y, &denominator); + + assert_eq!(result, -500); + } + + #[test] + fn test_fixed_mul_floor_all_negative() { + let env = Env::default(); + let x: i128 = -100; + let y: i128 = -50; + let denominator: i128 = -10; + + let result = x.fixed_mul_floor(&env, &y, &denominator); + + assert_eq!(result, -500); + } + + #[test] + fn test_fixed_mul_ceil_all_negative() { + let env = Env::default(); + let x: i128 = -100; + let y: i128 = -50; + let denominator: i128 = -10; + + let result = x.fixed_mul_ceil(&env, &y, &denominator); + + assert_eq!(result, -500); + } +} + +#[cfg(test)] +mod test_i256 { /********** fixed_mul_floor ********* */ @@ -234,3 +523,152 @@ mod tests { x.fixed_mul_ceil(&env, &y, &denominator); } } + +#[cfg(test)] +mod test_i256_errors { + use soroban_sdk::{Env, I256}; + + use crate::math::soroban_fixed_point::SorobanFixedPoint; + + #[test] + #[should_panic(expected = "Error(Contract, #1500)")] + fn test_fixed_mul_floor_zero_denominator() { + let env = Env::default(); + let x: I256 = I256::from_i128(&env, 100); + let y: I256 = I256::from_i128(&env, 50); + let denominator: I256 = I256::from_i128(&env, 0); + + x.fixed_mul_floor(&env, &y, &denominator); + } + + #[test] + #[should_panic(expected = "Error(Contract, #1500)")] + fn test_fixed_mul_ceil_zero_denominator() { + let env = Env::default(); + let x: I256 = I256::from_i128(&env, 100); + let y: I256 = I256::from_i128(&env, 50); + let denominator: I256 = I256::from_i128(&env, 0); + + x.fixed_mul_ceil(&env, &y, &denominator); + } + + #[test] + fn test_fixed_mul_floor_with_zero_x() { + let env = Env::default(); + let x: I256 = I256::from_i128(&env, 0); + let y: I256 = I256::from_i128(&env, 314_1592653); + let denominator: I256 = I256::from_i128(&env, 1_0000001); + + let result = x.fixed_mul_floor(&env, &y, &denominator); + + assert_eq!(result, I256::from_i128(&env, 0)); + } + + #[test] + fn test_fixed_mul_ceil_with_zero_y() { + let env = Env::default(); + let x: I256 = I256::from_i128(&env, 1_5391283); + let y: I256 = I256::from_i128(&env, 0); + let denominator: I256 = I256::from_i128(&env, 1_0000001); + + let result = x.fixed_mul_ceil(&env, &y, &denominator); + + assert_eq!(result, I256::from_i128(&env, 0)); + } + + #[test] + fn test_fixed_mul_floor_exact_division() { + let env = Env::default(); + let x: I256 = I256::from_i128(&env, 100); + let y: I256 = I256::from_i128(&env, 50); + let denominator: I256 = I256::from_i128(&env, 10); + + let result = x.fixed_mul_floor(&env, &y, &denominator); + + assert_eq!(result, I256::from_i128(&env, 500)); + } + + #[test] + fn test_fixed_mul_ceil_exact_division() { + let env = Env::default(); + let x: I256 = I256::from_i128(&env, 100); + let y: I256 = I256::from_i128(&env, 50); + let denominator: I256 = I256::from_i128(&env, 10); + + let result = x.fixed_mul_ceil(&env, &y, &denominator); + + assert_eq!(result, I256::from_i128(&env, 500)); + } + + #[test] + fn test_fixed_mul_floor_one_denominator() { + let env = Env::default(); + let x: I256 = I256::from_i128(&env, 123_456_789); + let y: I256 = I256::from_i128(&env, 987_654_321); + let denominator: I256 = I256::from_i128(&env, 1); + + let result = x.clone().fixed_mul_floor(&env, &y, &denominator); + + assert_eq!(result, x.mul(&y)); + } + + #[test] + fn test_fixed_mul_ceil_one_denominator() { + let env = Env::default(); + let x: I256 = I256::from_i128(&env, 123_456_789); + let y: I256 = I256::from_i128(&env, 987_654_321); + let denominator: I256 = I256::from_i128(&env, 1); + + let result = x.clone().fixed_mul_ceil(&env, &y, &denominator); + + assert_eq!(result, x.mul(&y)); + } + + #[test] + fn test_fixed_mul_floor_negative_denominator() { + let env = Env::default(); + let x: I256 = I256::from_i128(&env, 100); + let y: I256 = I256::from_i128(&env, 50); + let denominator: I256 = I256::from_i128(&env, -10); + + let result = x.fixed_mul_floor(&env, &y, &denominator); + + assert_eq!(result, I256::from_i128(&env, -500)); + } + + #[test] + fn test_fixed_mul_ceil_negative_denominator() { + let env = Env::default(); + let x: I256 = I256::from_i128(&env, 100); + let y: I256 = I256::from_i128(&env, 50); + let denominator: I256 = I256::from_i128(&env, -10); + + let result = x.fixed_mul_ceil(&env, &y, &denominator); + + assert_eq!(result, I256::from_i128(&env, -500)); + } + + #[test] + fn test_fixed_mul_floor_all_negative() { + let env = Env::default(); + let x: I256 = I256::from_i128(&env, -100); + let y: I256 = I256::from_i128(&env, -50); + let denominator: I256 = I256::from_i128(&env, -10); + + let result = x.fixed_mul_floor(&env, &y, &denominator); + + assert_eq!(result, I256::from_i128(&env, -500)); + } + + #[test] + fn test_fixed_mul_ceil_all_negative() { + let env = Env::default(); + let x: I256 = I256::from_i128(&env, -100); + let y: I256 = I256::from_i128(&env, -50); + let denominator: I256 = I256::from_i128(&env, -10); + + let result = x.fixed_mul_ceil(&env, &y, &denominator); + + assert_eq!(result, I256::from_i128(&env, -500)); + } +} diff --git a/packages/contract-utils/src/merkle_distributor/mod.rs b/packages/contract-utils/src/merkle_distributor/mod.rs index 876e2e57..c0cde192 100644 --- a/packages/contract-utils/src/merkle_distributor/mod.rs +++ b/packages/contract-utils/src/merkle_distributor/mod.rs @@ -51,9 +51,18 @@ mod storage; #[cfg(test)] mod test; +#[cfg(feature = "certora")] +pub mod specs; + use core::marker::PhantomData; -use soroban_sdk::{contracterror, contractevent, Bytes, Env, Val}; +#[cfg(not(feature = "certora"))] +use soroban_sdk::{contractevent}; + +#[cfg(feature = "certora")] +use cvlr_soroban_derive::contractevent; + +use soroban_sdk::{contracterror, Bytes, Env, Val}; use crate::crypto::hasher::Hasher; pub use crate::merkle_distributor::storage::MerkleDistributorStorageKey; @@ -106,6 +115,7 @@ pub struct SetClaimed { /// /// * `e` - The Soroban environment. /// * `root` - The merkle root. +#[cfg(not(feature = "certora"))] pub fn emit_set_root(e: &Env, root: Bytes) { SetRoot { root }.publish(e); } @@ -116,6 +126,7 @@ pub fn emit_set_root(e: &Env, root: Bytes) { /// /// * `e` - The Soroban environment. /// * `index` - The index that was claimed. +#[cfg(not(feature = "certora"))] pub fn emit_set_claimed(e: &Env, index: Val) { SetClaimed { index }.publish(e); } diff --git a/packages/contract-utils/src/merkle_distributor/specs/merkle_distributor_contract.rs b/packages/contract-utils/src/merkle_distributor/specs/merkle_distributor_contract.rs new file mode 100644 index 00000000..fb0d1831 --- /dev/null +++ b/packages/contract-utils/src/merkle_distributor/specs/merkle_distributor_contract.rs @@ -0,0 +1,64 @@ +use cvlr::nondet::Nondet; +use soroban_sdk::contracttype; +use soroban_sdk::symbol_short; +use soroban_sdk::Symbol; + +use crate::crypto::sha256::Sha256; +use crate::merkle_distributor::IndexableLeaf; +use crate::merkle_distributor::MerkleDistributor; +use soroban_sdk::{contract, contractimpl, Address, BytesN, Env, Vec}; + +#[contracttype] +#[derive(Clone)] +pub struct Leaf { + pub index: u32 +} + +impl Nondet for Leaf { + fn nondet() -> Self { + Leaf { + index: u32::nondet(), + } + } +} + +impl IndexableLeaf for Leaf { + fn index(&self) -> u32 { + self.index + } +} + +type MerkleDistributorSha256 = MerkleDistributor; + + +pub const OWNER: Symbol = symbol_short!("OWNER"); + +#[contract] +pub struct MerkleDistributorContract; + +#[contractimpl] +impl MerkleDistributorContract { + // sorted merkle tree + pub fn merkle_distributor_constructor(e: &Env, root_hash: BytesN<32>, owner: Address) { + MerkleDistributorSha256::set_root(&e, root_hash); + e.storage().instance().set(&OWNER, &owner); + } + + pub fn get_root(e: &Env) -> BytesN<32> { + MerkleDistributorSha256::get_root(e) + } + + pub fn is_claimed(e: &Env, index: u32) -> bool { + MerkleDistributorSha256::is_claimed(e, index) + } + + pub fn set_claimed(e: &Env, index: u32) { + let owner: Address = e.storage().instance().get(&OWNER).expect("owner should be set"); + owner.require_auth(); + MerkleDistributorSha256::set_claimed(e, index); + } + + pub fn claim(e: &Env, leaf: Leaf, proof: Vec>) { + MerkleDistributorSha256::verify_and_set_claimed(e, leaf, proof); + } +} diff --git a/packages/contract-utils/src/merkle_distributor/specs/merkle_distributor_contract_sanity.rs b/packages/contract-utils/src/merkle_distributor/specs/merkle_distributor_contract_sanity.rs new file mode 100644 index 00000000..0cf373b9 --- /dev/null +++ b/packages/contract-utils/src/merkle_distributor/specs/merkle_distributor_contract_sanity.rs @@ -0,0 +1,44 @@ +use cvlr::{cvlr_assert, nondet::*, cvlr_satisfy}; +use cvlr_soroban::{nondet_address, nondet_bytes, nondet_bytes_n, nondet_vec}; +use cvlr_soroban_derive::rule; + +use soroban_sdk::{Env, Vec, contracttype}; + +use crate::{crypto::sha256::Sha256, merkle_distributor::{IndexableLeaf, MerkleDistributor}}; +use crate::merkle_distributor::specs::merkle_distributor_contract::{MerkleDistributorContract, Leaf}; + +#[rule] +pub fn merkle_distributor_constructor_sanity(e: Env) { + let root_hash = nondet_bytes_n(); + let owner = nondet_address(); + MerkleDistributorContract::merkle_distributor_constructor(&e, root_hash, owner); + cvlr_satisfy!(true); +} + +#[rule] +pub fn get_root_sanity(e: Env) { + let root = MerkleDistributorContract::get_root(&e); + cvlr_satisfy!(true); +} + +#[rule] +pub fn is_claimed_sanity(e: Env) { + let index: u32 = nondet(); + let claimed = MerkleDistributorContract::is_claimed(&e, index); + cvlr_satisfy!(claimed); +} + +#[rule] +pub fn set_claimed_sanity(e: Env) { + let index: u32 = nondet(); + MerkleDistributorContract::set_claimed(&e, index); + cvlr_satisfy!(true); +} + +#[rule] +pub fn claim_sanity(e: Env) { + let leaf = Leaf::nondet(); + let proof = nondet_vec(); + MerkleDistributorContract::claim(&e, leaf, proof); + cvlr_satisfy!(true); +} \ No newline at end of file diff --git a/packages/contract-utils/src/merkle_distributor/specs/merkle_distributor_integrity.rs b/packages/contract-utils/src/merkle_distributor/specs/merkle_distributor_integrity.rs new file mode 100644 index 00000000..d5c6d130 --- /dev/null +++ b/packages/contract-utils/src/merkle_distributor/specs/merkle_distributor_integrity.rs @@ -0,0 +1,42 @@ +use cvlr::{cvlr_assert, nondet::*, cvlr_satisfy}; +use cvlr_soroban::{nondet_address, nondet_bytes, nondet_bytes_n, nondet_vec}; +use cvlr_soroban_derive::rule; +use cvlr::clog; + +use soroban_sdk::{Env, Vec, contracttype}; + +use crate::{crypto::sha256::Sha256, merkle_distributor::{IndexableLeaf, MerkleDistributor}}; +use crate::merkle_distributor::specs::merkle_distributor_contract::{MerkleDistributorContract, Leaf}; + +#[rule] +// status: violated - spurious +pub fn merkle_distributor_constructor_integrity(e: Env) { + let root_hash = nondet_bytes_n(); + let owner = nondet_address(); + clog!(cvlr_soroban::Addr(&owner)); + MerkleDistributorContract::merkle_distributor_constructor(&e, root_hash.clone(), owner); + let root = MerkleDistributorContract::get_root(&e); + cvlr_assert!(root == root_hash); +} + +#[rule] +// status: violated - spurious +pub fn set_claimed_integrity(e: Env) { + let index: u32 = nondet(); + clog!(index); + MerkleDistributorContract::set_claimed(&e, index); + let claimed = MerkleDistributorContract::is_claimed(&e, index); + clog!(claimed); + cvlr_assert!(claimed); +} + +#[rule] +// status: violated - spurious +pub fn claim_integrity(e: Env) { + let leaf = Leaf::nondet(); + let proof = nondet_vec(); + MerkleDistributorContract::claim(&e, leaf.clone(), proof); + let claimed = MerkleDistributorContract::is_claimed(&e, leaf.index); + clog!(claimed); + cvlr_assert!(claimed); +} \ No newline at end of file diff --git a/packages/contract-utils/src/merkle_distributor/specs/merkle_distributor_sanity.rs b/packages/contract-utils/src/merkle_distributor/specs/merkle_distributor_sanity.rs new file mode 100644 index 00000000..f431eaa5 --- /dev/null +++ b/packages/contract-utils/src/merkle_distributor/specs/merkle_distributor_sanity.rs @@ -0,0 +1,60 @@ +use cvlr::{cvlr_assert, nondet::*, cvlr_satisfy}; +use cvlr_soroban::{nondet_address, nondet_bytes, nondet_bytes_n, nondet_vec}; +use cvlr_soroban_derive::rule; + +use soroban_sdk::{Env, Vec, contracttype}; + +use crate::merkle_distributor::specs::merkle_distributor_contract::Leaf; +use crate::{crypto::sha256::Sha256, merkle_distributor::{IndexableLeaf, MerkleDistributor}}; + +type Distributor = MerkleDistributor; + +#[rule] +pub fn get_root_sanity(e: Env) { + let root = nondet_bytes_n(); + Distributor::set_root(&e, root); + let _ = Distributor::get_root(&e); + cvlr_satisfy!(true); +} + +#[rule] +pub fn is_claimed_sanity(e: Env) { + let idx: u32 = nondet(); + let _ = Distributor::is_claimed(&e, idx); + cvlr_satisfy!(true); +} + +#[rule] +pub fn set_root_sanity(e: Env) { + let root = nondet_bytes_n(); + Distributor::set_root(&e, root); + cvlr_satisfy!(true); +} + +#[rule] +pub fn set_claimed_sanity(e: Env) { + let idx: u32 = nondet(); + Distributor::set_claimed(&e, idx); + cvlr_satisfy!(true); +} + + +#[rule] +pub fn verify_and_set_claimed_sanity(e: Env) { + let root = nondet_bytes_n(); + Distributor::set_root(&e, root); + let leaf: Leaf = nondet(); + let proof = nondet_vec(); + Distributor::verify_and_set_claimed(&e, leaf, proof); + cvlr_satisfy!(true); +} + +#[rule] +pub fn verify_with_index_and_set_claimed_sanity(e: Env) { + let root = nondet_bytes_n(); + Distributor::set_root(&e, root); + let leaf: Leaf = nondet(); + let proof = nondet_vec(); + Distributor::verify_with_index_and_set_claimed(&e, leaf, proof); + cvlr_satisfy!(true); +} diff --git a/packages/contract-utils/src/merkle_distributor/specs/mod.rs b/packages/contract-utils/src/merkle_distributor/specs/mod.rs new file mode 100644 index 00000000..78b799c7 --- /dev/null +++ b/packages/contract-utils/src/merkle_distributor/specs/mod.rs @@ -0,0 +1,4 @@ +// pub mod merkle_distributor_sanity; +pub mod merkle_distributor_contract; +pub mod merkle_distributor_contract_sanity; +pub mod merkle_distributor_integrity; \ No newline at end of file diff --git a/packages/contract-utils/src/merkle_distributor/storage.rs b/packages/contract-utils/src/merkle_distributor/storage.rs index fa61ef17..0d04b3f5 100644 --- a/packages/contract-utils/src/merkle_distributor/storage.rs +++ b/packages/contract-utils/src/merkle_distributor/storage.rs @@ -1,9 +1,12 @@ use soroban_sdk::{contracttype, panic_with_error, xdr::ToXdr, BytesN, Env, Vec}; +#[cfg(not(feature = "certora"))] +use crate::{merkle_distributor::{emit_set_claimed, emit_set_root}}; + use crate::{ crypto::{hasher::Hasher, merkle::Verifier}, merkle_distributor::{ - emit_set_claimed, emit_set_root, IndexableLeaf, MerkleDistributor, MerkleDistributorError, + IndexableLeaf, MerkleDistributor, MerkleDistributorError, MERKLE_CLAIMED_EXTEND_AMOUNT, MERKLE_CLAIMED_TTL_THRESHOLD, }, }; @@ -79,6 +82,7 @@ where pub fn set_root(e: &Env, root: H::Output) { let key = MerkleDistributorStorageKey::Root; e.storage().instance().set(&key, &root); + #[cfg(not(feature = "certora"))] emit_set_root(e, root.into()); } @@ -101,6 +105,7 @@ where pub fn set_claimed(e: &Env, index: u32) { let key = MerkleDistributorStorageKey::Claimed(index); e.storage().persistent().set(&key, &true); + #[cfg(not(feature = "certora"))] emit_set_claimed(e, index.into()); } diff --git a/packages/contract-utils/src/pausable/mod.rs b/packages/contract-utils/src/pausable/mod.rs index 007461c2..50041d51 100644 --- a/packages/contract-utils/src/pausable/mod.rs +++ b/packages/contract-utils/src/pausable/mod.rs @@ -47,15 +47,21 @@ //! to see it all in action, check out the `examples/pausable/src/contract.rs` //! file. -#[cfg(feature = "certora")] -pub mod spec; - mod storage; #[cfg(test)] mod test; -use soroban_sdk::{contracterror, contractevent, Address, Env}; +#[cfg(feature = "certora")] +pub mod specs; + +use soroban_sdk::{contracterror, Address, Env}; + +#[cfg(not(feature = "certora"))] +use soroban_sdk::{contractevent}; + +#[cfg(feature = "certora")] +use cvlr_soroban_derive::contractevent; pub use crate::pausable::storage::{pause, paused, unpause, when_not_paused, when_paused}; @@ -152,6 +158,7 @@ pub struct Paused {} /// # Arguments /// /// * `e` - The Soroban environment. +#[cfg(not(feature = "certora"))] pub fn emit_paused(e: &Env) { Paused {}.publish(e); } @@ -166,6 +173,7 @@ pub struct Unpaused {} /// # Arguments /// /// * `e` - The Soroban environment. +#[cfg(not(feature = "certora"))] pub fn emit_unpaused(e: &Env) { Unpaused {}.publish(e); } diff --git a/packages/contract-utils/src/pausable/spec/mod.rs b/packages/contract-utils/src/pausable/spec/mod.rs deleted file mode 100644 index e9463bd5..00000000 --- a/packages/contract-utils/src/pausable/spec/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod pausable_sanity_rules; \ No newline at end of file diff --git a/packages/contract-utils/src/pausable/spec/pausable_sanity_rules.rs b/packages/contract-utils/src/pausable/spec/pausable_sanity_rules.rs deleted file mode 100644 index 45862daa..00000000 --- a/packages/contract-utils/src/pausable/spec/pausable_sanity_rules.rs +++ /dev/null @@ -1,36 +0,0 @@ -use cvlr::{cvlr_assert}; -use cvlr_soroban_derive::rule; - -use soroban_sdk::{Env, Symbol}; - -use crate::pausable::*; - -#[rule] -pub fn paused_sanity(e: Env) { - paused(&e); - cvlr_assert!(false); -} - -#[rule] -pub fn pause_sanity(e: Env) { - pause(&e); - cvlr_assert!(false); -} - -#[rule] -pub fn unpause_sanity(e: Env) { - unpause(&e); - cvlr_assert!(false); -} - -#[rule] -pub fn when_not_paused_sanity(e: Env) { - when_not_paused(&e); - cvlr_assert!(false); -} - -#[rule] -pub fn when_paused_sanity(e: Env) { - when_paused(&e); - cvlr_assert!(false); -} \ No newline at end of file diff --git a/packages/contract-utils/src/pausable/specs/mod.rs b/packages/contract-utils/src/pausable/specs/mod.rs new file mode 100644 index 00000000..24154852 --- /dev/null +++ b/packages/contract-utils/src/pausable/specs/mod.rs @@ -0,0 +1,5 @@ +pub mod pausable_sanity; +pub mod pausable_contract; +pub mod pausable_integrity; +pub mod pausable_panics; +pub mod pausable_non_panics; \ No newline at end of file diff --git a/packages/contract-utils/src/pausable/specs/pausable_contract.rs b/packages/contract-utils/src/pausable/specs/pausable_contract.rs new file mode 100644 index 00000000..04dab53f --- /dev/null +++ b/packages/contract-utils/src/pausable/specs/pausable_contract.rs @@ -0,0 +1,33 @@ +use soroban_sdk::{contract, contractimpl, Address, Env}; +use crate::pausable::{self, Pausable}; +use stellar_macros::{when_not_paused, when_paused}; + +use crate as stellar_contract_utils; + +#[contract] +pub struct PausableContract; + +#[contractimpl] +impl PausableContract { + pub fn __constructor(_e: &Env) { + } + + #[when_not_paused] + pub fn when_not_paused_func(e: &Env) { + } + + #[when_paused] + pub fn when_paused_func(e: &Env) { + } +} + +#[contractimpl] +impl Pausable for PausableContract { + fn pause(e: &Env, _caller: Address) { + pausable::pause(e); + } + + fn unpause(e: &Env, _caller: Address) { + pausable::unpause(e); + } +} diff --git a/packages/contract-utils/src/pausable/specs/pausable_integrity.rs b/packages/contract-utils/src/pausable/specs/pausable_integrity.rs new file mode 100644 index 00000000..cbda8cb2 --- /dev/null +++ b/packages/contract-utils/src/pausable/specs/pausable_integrity.rs @@ -0,0 +1,28 @@ +use cvlr::{cvlr_assert,cvlr_satisfy}; +use cvlr_soroban::nondet_address; +use cvlr_soroban_derive::rule; +use cvlr::clog; +use soroban_sdk::Env; +use crate::pausable::specs::pausable_contract::PausableContract; +use crate::pausable::Pausable; +use crate::pausable::{pause, paused}; + +#[rule] +// after call to pause the contract is paused +// status: verified +pub fn pause_integrity(e: Env) { + let caller = nondet_address(); + PausableContract::pause(&e, caller); + let paused_post = PausableContract::paused(&e); + cvlr_assert!(paused_post); +} + +#[rule] +// after call to unpause the contract is not paused +// status: verified +pub fn unpause_integrity(e: Env) { + let caller = nondet_address(); + PausableContract::unpause(&e, caller); + let paused_post = PausableContract::paused(&e); + cvlr_assert!(!paused_post); +} \ No newline at end of file diff --git a/packages/contract-utils/src/pausable/specs/pausable_non_panics.rs b/packages/contract-utils/src/pausable/specs/pausable_non_panics.rs new file mode 100644 index 00000000..d3379b12 --- /dev/null +++ b/packages/contract-utils/src/pausable/specs/pausable_non_panics.rs @@ -0,0 +1,122 @@ +use cvlr::{cvlr_assert,cvlr_assume,cvlr_satisfy}; +use cvlr_soroban::nondet_address; +use cvlr::nondet::Nondet; +use cvlr_soroban_derive::rule; +use soroban_sdk::Env; +use crate::pausable::specs::pausable_contract::PausableContract; +use crate::pausable::Pausable; +use crate::pausable::{pause, paused}; +use crate::pausable::storage::PausableStorageKey; + +// These rules require the prover arg "prover_args": ["-trapAsAssert true"] to consider also panicking paths. + +#[rule] +// requires +// unpaused +// status: verified +pub fn pause_non_panic(e: Env) { + // storage set up + let bool = bool::nondet(); + e.storage().instance().set(&PausableStorageKey::Paused, &bool); + let paused_pre = PausableContract::paused(&e); + cvlr_assume!(!paused_pre); + let caller = nondet_address(); + PausableContract::pause(&e, caller); + cvlr_assert!(true); +} + +#[rule] +// sanity +// status: verified +pub fn pause_non_panic_sanity(e: Env) { + // storage set up + let bool = bool::nondet(); + e.storage().instance().set(&PausableStorageKey::Paused, &bool); + let paused_pre = PausableContract::paused(&e); + cvlr_assume!(!paused_pre); + let caller = nondet_address(); + PausableContract::pause(&e, caller); + cvlr_satisfy!(true); +} + +#[rule] +// requires +// paused +// status: verified +pub fn unpause_non_panic(e: Env) { + // storage set up + let bool = bool::nondet(); + e.storage().instance().set(&PausableStorageKey::Paused, &bool); + let paused_pre = PausableContract::paused(&e); + cvlr_assume!(paused_pre); + let caller = nondet_address(); + PausableContract::unpause(&e, caller); + cvlr_assert!(true); +} + +#[rule] +// sanity +// status: verified +pub fn unpause_non_panic_sanity(e: Env) { + // storage set up + let bool = bool::nondet(); + e.storage().instance().set(&PausableStorageKey::Paused, &bool); + let paused_pre = PausableContract::paused(&e); + cvlr_assume!(paused_pre); + let caller = nondet_address(); + PausableContract::unpause(&e, caller); + cvlr_satisfy!(true); +} + +#[rule] +// requires +// unpaused +// status: verified +pub fn when_not_paused_non_panic(e: Env) { + // storage set up + let bool = bool::nondet(); + e.storage().instance().set(&PausableStorageKey::Paused, &bool); + let paused_pre = PausableContract::paused(&e); + cvlr_assume!(!paused_pre); + PausableContract::when_not_paused_func(&e); + cvlr_assert!(true); +} + +#[rule] +// sanity +// status: verified +pub fn when_not_paused_non_panic_sanity(e: Env) { + // storage set up + let bool = bool::nondet(); + e.storage().instance().set(&PausableStorageKey::Paused, &bool); + let paused_pre = PausableContract::paused(&e); + cvlr_assume!(!paused_pre); + PausableContract::when_not_paused_func(&e); + cvlr_satisfy!(true); +} +#[rule] +// requires +// paused +// status: verified +pub fn when_paused_non_panic(e: Env) { + // storage set up + let bool = bool::nondet(); + e.storage().instance().set(&PausableStorageKey::Paused, &bool); + let paused_pre = PausableContract::paused(&e); + cvlr_assume!(paused_pre); + PausableContract::when_paused_func(&e); + cvlr_assert!(true); +} + +#[rule] +// sanity +// status: verified +pub fn when_paused_non_panic_sanity(e: Env) { + // storage set up + let bool = bool::nondet(); + e.storage().instance().set(&PausableStorageKey::Paused, &bool); + let paused_pre = PausableContract::paused(&e); + cvlr_assume!(paused_pre); + PausableContract::when_paused_func(&e); + cvlr_satisfy!(true); +} \ No newline at end of file diff --git a/packages/contract-utils/src/pausable/specs/pausable_panics.rs b/packages/contract-utils/src/pausable/specs/pausable_panics.rs new file mode 100644 index 00000000..92dea054 --- /dev/null +++ b/packages/contract-utils/src/pausable/specs/pausable_panics.rs @@ -0,0 +1,50 @@ +use cvlr::{cvlr_assert,cvlr_assume,cvlr_satisfy}; +use cvlr_soroban::nondet_address; +use cvlr_soroban_derive::rule; +use soroban_sdk::Env; +use crate::pausable::specs::pausable_contract::PausableContract; +use crate::pausable::Pausable; +use crate::pausable::{pause, paused}; + + +#[rule] +// pause panics if the contract is already paused +// status: verified +pub fn pause_panics_if_paused(e: Env) { + let paused_pre = PausableContract::paused(&e); + cvlr_assume!(paused_pre); + let caller = nondet_address(); + PausableContract::pause(&e, caller); + cvlr_assert!(false); +} + +#[rule] +// unpause panics if the contract is not paused +// status: verified with precise_bitwise_ops +pub fn unpause_panics_if_not_paused(e: Env) { + let paused_pre = PausableContract::paused(&e); + cvlr_assume!(!paused_pre); + let caller = nondet_address(); + PausableContract::unpause(&e, caller); + cvlr_assert!(false); +} + +#[rule] +// when_not_paused_func panics if paused +// status: verified +pub fn when_not_paused_panics_if_paused(e: Env) { + let paused_pre = PausableContract::paused(&e); + cvlr_assume!(paused_pre); + PausableContract::when_not_paused_func(&e); + cvlr_assert!(false); +} + +#[rule] +// when_paused_func panics if not paused +// status: verified with precise_bitwise_ops +pub fn when_paused_panics_if_not_paused(e: Env) { + let paused_pre = PausableContract::paused(&e); + cvlr_assume!(!paused_pre); + PausableContract::when_paused_func(&e); + cvlr_assert!(false); +} \ No newline at end of file diff --git a/packages/contract-utils/src/pausable/specs/pausable_sanity.rs b/packages/contract-utils/src/pausable/specs/pausable_sanity.rs new file mode 100644 index 00000000..2a875b12 --- /dev/null +++ b/packages/contract-utils/src/pausable/specs/pausable_sanity.rs @@ -0,0 +1,38 @@ +use cvlr::{cvlr_assert,cvlr_satisfy};use cvlr_soroban::nondet_address; +use cvlr_soroban_derive::rule; + +use soroban_sdk::{Env}; + +use crate::pausable::{specs::pausable_contract::PausableContract, *}; + +#[rule] +pub fn paused_sanity(e: Env) { + PausableContract::paused(&e); + cvlr_satisfy!(true); +} + +#[rule] +pub fn pause_sanity(e: Env) { + let caller = nondet_address(); + PausableContract::pause(&e, caller); + cvlr_satisfy!(true); +} + +#[rule] +pub fn unpause_sanity(e: Env) { + let caller = nondet_address(); + PausableContract::unpause(&e, caller); + cvlr_satisfy!(true); +} + +#[rule] +pub fn when_not_paused_sanity(e: Env) { + when_not_paused(&e); + cvlr_satisfy!(true); +} + +#[rule] +pub fn when_paused_sanity(e: Env) { + when_paused(&e); + cvlr_satisfy!(true); +} \ No newline at end of file diff --git a/packages/contract-utils/src/pausable/storage.rs b/packages/contract-utils/src/pausable/storage.rs index 45a30bf3..8768b1b1 100644 --- a/packages/contract-utils/src/pausable/storage.rs +++ b/packages/contract-utils/src/pausable/storage.rs @@ -1,6 +1,9 @@ use soroban_sdk::{contracttype, panic_with_error, Env}; -use crate::pausable::{emit_paused, emit_unpaused, PausableError}; +use crate::pausable::{PausableError}; + +#[cfg(not(feature = "certora"))] +use crate::pausable::{emit_paused, emit_unpaused,}; /// Storage key for the pausable state #[contracttype] @@ -55,7 +58,12 @@ pub fn paused(e: &Env) -> bool { /// ``` pub fn pause(e: &Env) { when_not_paused(e); + #[cfg(not(feature = "certora"))] e.storage().instance().set(&PausableStorageKey::Paused, &true); + #[cfg(feature = "certora")] + let val = true; + #[cfg(feature = "certora")] + e.storage().instance().set(&PausableStorageKey::Paused, &val); #[cfg(not(feature = "certora"))] emit_paused(e); } @@ -92,7 +100,12 @@ pub fn pause(e: &Env) { /// ``` pub fn unpause(e: &Env) { when_paused(e); + #[cfg(not(feature = "certora"))] e.storage().instance().set(&PausableStorageKey::Paused, &false); + #[cfg(feature = "certora")] + let val = false; + #[cfg(feature = "certora")] + e.storage().instance().set(&PausableStorageKey::Paused, &val); #[cfg(not(feature = "certora"))] emit_unpaused(e); } diff --git a/packages/contract-utils/src/upgradeable/mod.rs b/packages/contract-utils/src/upgradeable/mod.rs index b80b72b6..4322fc87 100644 --- a/packages/contract-utils/src/upgradeable/mod.rs +++ b/packages/contract-utils/src/upgradeable/mod.rs @@ -72,14 +72,15 @@ //! you can also find a helper `Upgrader` contract that performs upgrade+migrate //! in a single transaction. -#[cfg(feature = "certora")] -pub mod spec; mod storage; #[cfg(test)] mod test; +#[cfg(feature = "certora")] +pub mod specs; + use soroban_sdk::{contractclient, contracterror, Address, BytesN, Env, FromVal, Val}; pub use crate::upgradeable::storage::{ diff --git a/packages/contract-utils/src/upgradeable/spec/mod.rs b/packages/contract-utils/src/upgradeable/spec/mod.rs deleted file mode 100644 index e6f5c998..00000000 --- a/packages/contract-utils/src/upgradeable/spec/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod upgradeable_sanity_rules; \ No newline at end of file diff --git a/packages/contract-utils/src/upgradeable/spec/upgradeable_sanity_rules.rs b/packages/contract-utils/src/upgradeable/spec/upgradeable_sanity_rules.rs deleted file mode 100644 index 814b267d..00000000 --- a/packages/contract-utils/src/upgradeable/spec/upgradeable_sanity_rules.rs +++ /dev/null @@ -1,31 +0,0 @@ -use cvlr::{cvlr_assert}; -use cvlr_soroban_derive::rule; - -use soroban_sdk::{Env}; - -use crate::upgradeable::*; - -#[rule] -pub fn enable_migration_sanity(e: Env) { - enable_migration(&e); - cvlr_assert!(false); -} - -#[rule] -pub fn can_complete_migration_sanity(e: Env) { - can_complete_migration(&e); - cvlr_assert!(false); -} - -#[rule] -pub fn complete_migration_sanity(e: Env) { - complete_migration(&e); - cvlr_assert!(false); -} - - -#[rule] -pub fn ensure_can_complete_migration_sanity(e: Env) { - ensure_can_complete_migration(&e); - cvlr_assert!(false); -} \ No newline at end of file diff --git a/packages/contract-utils/src/upgradeable/specs/mod.rs b/packages/contract-utils/src/upgradeable/specs/mod.rs new file mode 100644 index 00000000..3718050e --- /dev/null +++ b/packages/contract-utils/src/upgradeable/specs/mod.rs @@ -0,0 +1,2 @@ +pub mod upgradeable_sanity_rules; +pub mod upgradeable_migratable_contract; \ No newline at end of file diff --git a/packages/contract-utils/src/upgradeable/specs/upgradeable_integrity.rs b/packages/contract-utils/src/upgradeable/specs/upgradeable_integrity.rs new file mode 100644 index 00000000..0409df75 --- /dev/null +++ b/packages/contract-utils/src/upgradeable/specs/upgradeable_integrity.rs @@ -0,0 +1 @@ +// integrity rules for all functions \ No newline at end of file diff --git a/packages/contract-utils/src/upgradeable/specs/upgradeable_migratable_contract.rs b/packages/contract-utils/src/upgradeable/specs/upgradeable_migratable_contract.rs new file mode 100644 index 00000000..d87edbde --- /dev/null +++ b/packages/contract-utils/src/upgradeable/specs/upgradeable_migratable_contract.rs @@ -0,0 +1,28 @@ +use soroban_sdk::contract; +use stellar_macros::{Upgradeable, UpgradeableMigratable}; + +use crate::{ + self as stellar_contract_utils, + upgradeable::{UpgradeableMigratable, UpgradeableMigratableInternal}, +}; + +// Not sure what the desired behavior should be. Fill as needed. +// NOTE: Unclear what `MigrationData` should be so change as needed. +// NOTE: fill out `_migrate` and `_require_auth` before using! + +#[derive(UpgradeableMigratable)] +#[contract] +pub struct UpgradeableMigratableContract; + +impl UpgradeableMigratableInternal for UpgradeableMigratableContract { + // Made a mock Migration data. + type MigrationData = u32; + + fn _migrate(e: &soroban_sdk::Env, migration_data: &Self::MigrationData) { + todo!() + } + + fn _require_auth(e: &soroban_sdk::Env, operator: &soroban_sdk::Address) { + todo!() + } +} diff --git a/packages/contract-utils/src/upgradeable/specs/upgradeable_non_panics.rs b/packages/contract-utils/src/upgradeable/specs/upgradeable_non_panics.rs new file mode 100644 index 00000000..968c61f3 --- /dev/null +++ b/packages/contract-utils/src/upgradeable/specs/upgradeable_non_panics.rs @@ -0,0 +1 @@ +// non-panics rules for all functions \ No newline at end of file diff --git a/packages/contract-utils/src/upgradeable/specs/upgradeable_panics.rs b/packages/contract-utils/src/upgradeable/specs/upgradeable_panics.rs new file mode 100644 index 00000000..65e40130 --- /dev/null +++ b/packages/contract-utils/src/upgradeable/specs/upgradeable_panics.rs @@ -0,0 +1 @@ +// panics rules for all functions \ No newline at end of file diff --git a/packages/contract-utils/src/upgradeable/specs/upgradeable_sanity_rules.rs b/packages/contract-utils/src/upgradeable/specs/upgradeable_sanity_rules.rs new file mode 100644 index 00000000..80cc7979 --- /dev/null +++ b/packages/contract-utils/src/upgradeable/specs/upgradeable_sanity_rules.rs @@ -0,0 +1,55 @@ +use cvlr::{cvlr_assert,cvlr_satisfy, nondet}; +use cvlr_soroban::{nondet_address, nondet_bytes, nondet_bytes_n}; +use cvlr_soroban_derive::rule; + +use soroban_sdk::{Env}; + +use crate::upgradeable::{specs::{upgradeable_migratable_contract::UpgradeableMigratableContract}, *}; + + +// contract does not make sense because there are no default implementations for the upgradeable trait functions + + +#[rule] +pub fn enable_migration_sanity(e: Env) { + enable_migration(&e); + cvlr_satisfy!(true); +} + +#[rule] +pub fn can_complete_migration_sanity(e: Env) { + can_complete_migration(&e); + cvlr_satisfy!(true); +} + +#[rule] +pub fn complete_migration_sanity(e: Env) { + complete_migration(&e); + cvlr_satisfy!(true); +} + + +#[rule] +pub fn ensure_can_complete_migration_sanity(e: Env) { + ensure_can_complete_migration(&e); + cvlr_satisfy!(true); +} + + +// NOTE: should only run once relevant methods are implemented in the contract +#[rule] +pub fn upgradeable_migratable_contract_upgrade_sanity(e: Env) { + let wasm_hash: soroban_sdk::BytesN<32> = nondet_bytes_n(); + let operator = nondet_address(); + UpgradeableMigratableContract::upgrade(&e, wasm_hash, operator); + cvlr_satisfy!(true); +} + +// NOTE: should only run once relevant methods are implemented in the contract +#[rule] +pub fn upgradeable_migratable_contract_migrate_sanity(e: Env) { + let migrate_data: u32 = nondet(); + let operator = nondet_address(); + UpgradeableMigratableContract::migrate(&e, migrate_data, operator); + cvlr_satisfy!(true); +} \ No newline at end of file diff --git a/packages/macros/Cargo.toml b/packages/macros/Cargo.toml index 4660007f..77b67f15 100644 --- a/packages/macros/Cargo.toml +++ b/packages/macros/Cargo.toml @@ -15,4 +15,4 @@ doctest = false [dependencies] proc-macro2 = { workspace = true } quote = { workspace = true } -syn = { workspace = true } +syn = { workspace = true } \ No newline at end of file diff --git a/packages/macros/README.md b/packages/macros/README.md index 02feb356..e1ec5795 100644 --- a/packages/macros/README.md +++ b/packages/macros/README.md @@ -22,7 +22,7 @@ pub struct MyContract; #[contractimpl] impl FungibleToken for MyContract { type ContractType = Base; - + // Only provide overrides here, default implementations are auto-generated } ``` @@ -167,7 +167,7 @@ Add this to your `Cargo.toml`: ```toml [dependencies] # We recommend pinning to a specific version, because rapid iterations are expected as the library is in an active development phase. -stellar-macros = "=0.4.0" +stellar-macros = "=0.5.1" ``` ## Examples diff --git a/packages/tokens/Cargo.toml b/packages/tokens/Cargo.toml index e6520d9d..a85961e9 100644 --- a/packages/tokens/Cargo.toml +++ b/packages/tokens/Cargo.toml @@ -13,6 +13,10 @@ doctest = false [dependencies] soroban-sdk = { workspace = true } +cvlr = { workspace = true, default-features = false } +cvlr-soroban = { workspace = true } +cvlr-soroban-macros = { workspace = true } +cvlr-soroban-derive = { workspace = true } stellar-contract-utils = { workspace = true } [dev-dependencies] @@ -22,3 +26,6 @@ soroban-test-helpers = { workspace = true } stellar-event-assertion = { workspace = true } k256 = { workspace = true, features = ["ecdsa"] } p256 = { workspace = true, features = ["ecdsa"] } + +[features] +certora = ["stellar-contract-utils/certora"] \ No newline at end of file diff --git a/packages/tokens/README.md b/packages/tokens/README.md index 04830626..8158fb53 100644 --- a/packages/tokens/README.md +++ b/packages/tokens/README.md @@ -135,9 +135,9 @@ Add this to your `Cargo.toml`: ```toml [dependencies] # We recommend pinning to a specific version, because rapid iterations are expected as the library is in an active development phase. -stellar-tokens = "=0.4.0" +stellar-tokens = "=0.5.1" # Add this if you want to use macros -stellar-macros = "=0.4.0" +stellar-macros = "=0.5.1" ``` ## Examples diff --git a/packages/tokens/certora_build.py b/packages/tokens/certora_build.py new file mode 100755 index 00000000..a845f140 --- /dev/null +++ b/packages/tokens/certora_build.py @@ -0,0 +1,101 @@ +#!/usr/bin/env python3 +import argparse +import json +import subprocess +import tempfile +import sys +from pathlib import Path + +SCRIPT_DIR = Path(__file__).resolve().parent + +# Command to run for compiling the rust project. +COMMAND = "just build" + +# JSON FIELDS +PROJECT_DIR = (SCRIPT_DIR / "../").resolve() +SOURCES = ["tokens/src/**/*.rs"] +EXECUTABLES = "../target/wasm32-unknown-unknown/release/stellar_tokens.wasm" + +VERBOSE = False + +def log(msg): + if VERBOSE: + print(msg, file=sys.stderr) + +def run_command(command, to_stdout=False): + """Runs the build command and dumps output to temporary files.""" + log(f"Running '{command}'") + try: + if to_stdout: + result = subprocess.run( + command, + shell=True, + text=True + ) + return None, None, result.returncode + else: + with tempfile.NamedTemporaryFile(delete=False, mode='w', prefix="certora_build_", suffix='.stdout') as stdout_file, \ + tempfile.NamedTemporaryFile(delete=False, mode='w', prefix="certora_build_", suffix='.stderr') as stderr_file: + # Compile rust project and redirect stdout and stderr to a temp file + result = subprocess.run( + command, + shell=True, + stdout=stdout_file, + stderr=stderr_file, + text=True + ) + return stdout_file.name, stderr_file.name, result.returncode + except Exception as e: + log(f"Error running command '{command}': {e}") + return None, None -1 + +def write_output(output_data, output_file=None): + """Writes the JSON output either to a file or dumps it to the console.""" + if output_file: + with open(output_file, 'w') as f: + json.dump(output_data, f, indent=4) + log(f"Output written to {output_file}") + else: + print(json.dumps(output_data, indent=4), file=sys.stdout) + +def main(): + parser = argparse.ArgumentParser(description="Compile rust projects and generate JSON output to be used by Certora Prover.") + parser.add_argument("-o", "--output", metavar="FILE", help="Path to output JSON to a file.") + parser.add_argument("--json", action="store_true", help="Dump JSON output to the console.") + parser.add_argument("-l", "--log", action="store_true", help="Show log outputs from cargo build on standard out.") + parser.add_argument("-v", "--verbose", action="store_true", help="Be verbose.") + + args = parser.parse_args() + global VERBOSE + VERBOSE = args.verbose + + to_stdout = args.log + + # Compile rust project and dump the logs to tmp files + stdout_log, stderr_log, return_code = run_command(COMMAND, to_stdout) + + if stdout_log is not None: + log(f"Temporary log file located at:\n\t{stdout_log}\nand\n\t{stderr_log}") + + # JSON template + output_data = { + "project_directory": str(PROJECT_DIR), + "sources": SOURCES, + "executables": EXECUTABLES, + "success": True if return_code == 0 else False, + "return_code": return_code, + "log" : {"stdout": stdout_log, "stderr": stderr_log} + } + + # Handle output based on the provided argument + if args.output: + write_output(output_data, args.output) + + if args.json: + write_output(output_data) + + # Needed for mutations: if you run _this_ script inside another script, you can check this returncode and decide what to do + sys.exit(0 if return_code == 0 else 1) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/packages/tokens/confs/allowlist.conf b/packages/tokens/confs/allowlist.conf new file mode 100644 index 00000000..fc3234bc --- /dev/null +++ b/packages/tokens/confs/allowlist.conf @@ -0,0 +1,18 @@ +{ + "build_script": "../certora_build.py", + "msg": "Rules FungibleAllowList", + "rule": [ + "allow_user_integrity", + "disallow_user_integrity", + "transfer_panics_if_from_not_allowed", + "transfer_panics_if_to_not_allowed", + "transfer_from_panics_if_from_not_allowed", + "transfer_from_panics_if_to_not_allowed", + "approve_panics_if_owner_not_allowed", + "burn_panics_if_from_not_allowed", + "burn_from_panics_if_from_not_allowed" + ], + "server": "production", + "prover_version": "stellar-oz-changes" +} + diff --git a/packages/tokens/confs/blocklist.conf b/packages/tokens/confs/blocklist.conf new file mode 100644 index 00000000..30b08cc5 --- /dev/null +++ b/packages/tokens/confs/blocklist.conf @@ -0,0 +1,18 @@ +{ + "build_script": "../certora_build.py", + "msg": "Rules FungibleBlockList", + "rule": [ + "block_user_integrity", + "unblock_user_integrity", + "transfer_panics_if_from_blocked", + "transfer_panics_if_to_blocked", + "transfer_from_panics_if_from_blocked", + "transfer_from_panics_if_to_blocked", + "approve_panics_if_owner_blocked", + "burn_panics_if_from_blocked", + "burn_from_panics_if_from_blocked" + ], + "server": "production", + "prover_version": "stellar-oz-changes" +} + diff --git a/packages/tokens/confs/burnable.conf b/packages/tokens/confs/burnable.conf new file mode 100644 index 00000000..2b2f4d3d --- /dev/null +++ b/packages/tokens/confs/burnable.conf @@ -0,0 +1,20 @@ +{ + "build_script": "../certora_build.py", + "msg": "Rules FungibleBurnable", + "rule": [ + "burn_integrity", + "burn_from_integrity", + "burn_panics_if_unauthorized", + "burn_panics_if_not_enough_balance", + "burn_panics_if_amount_less_than_zero", + "burn_from_panics_if_spender_unauthorized", + "burn_from_panics_if_not_enough_balance", + "burn_from_panics_if_not_enough_allowance", + "burn_from_panics_if_amount_less_than_zero", + "after_burn_total_supply_geq_balance", + "after_burn_from_total_supply_geq_balance" + ], + "server": "production", + "prover_version": "stellar-oz-changes" +} + diff --git a/packages/tokens/confs/burnable_nft_sanity.conf b/packages/tokens/confs/burnable_nft_sanity.conf new file mode 100644 index 00000000..300ccb0d --- /dev/null +++ b/packages/tokens/confs/burnable_nft_sanity.conf @@ -0,0 +1,21 @@ +{ + "build_script": "../certora_build.py", + "msg": "Sanity Rules Burnable NFT", + "rule": [ + "burnable_nft_balance_sanity", + "burnable_nft_owner_of_sanity", + "burnable_nft_transfer_sanity", + "burnable_nft_transfer_from_sanity", + "burnable_nft_approve_sanity", + "burnable_nft_approve_for_all_sanity", + "burnable_nft_get_approved_sanity", + "burnable_nft_is_approved_for_all_sanity", + "burnable_nft_name_sanity", + "burnable_nft_symbol_sanity", + // "burnable_nft_token_uri_sanity", + "burnable_nft_burn_sanity", + "burnable_nft_burn_from_sanity" + ], + "server": "production", + "prover_version": "stellar-oz-changes" +} diff --git a/packages/tokens/confs/capped.conf b/packages/tokens/confs/capped.conf new file mode 100644 index 00000000..5770937d --- /dev/null +++ b/packages/tokens/confs/capped.conf @@ -0,0 +1,12 @@ +{ + "build_script": "../certora_build.py", + "msg": "Rules FungibleCapped", + "rule": [ + "mint_integrity", + "constructor_integrity", + "mint_preserves_cap" + ], + "server": "production", + "prover_version": "stellar-oz-changes" +} + diff --git a/packages/tokens/confs/consecutive_nft_sanity.conf b/packages/tokens/confs/consecutive_nft_sanity.conf new file mode 100644 index 00000000..ad952fac --- /dev/null +++ b/packages/tokens/confs/consecutive_nft_sanity.conf @@ -0,0 +1,22 @@ +{ + "build_script": "../certora_build.py", + "msg": "Sanity Rules Consecutive NFT", + "rule": [ + "consecutive_nft_balance_sanity", + "consecutive_nft_owner_of_sanity", + "consecutive_nft_transfer_sanity", + "consecutive_nft_transfer_from_sanity", + "consecutive_nft_approve_sanity", + "consecutive_nft_approve_for_all_sanity", + "consecutive_nft_get_approved_sanity", + "consecutive_nft_is_approved_for_all_sanity", + "consecutive_nft_name_sanity", + "consecutive_nft_symbol_sanity", + // "consecutive_nft_token_uri_sanity", + "consecutive_nft_burn_sanity", + "consecutive_nft_burn_from_sanity", + "consecutive_nft_batch_mint_sanity" + ], + "server": "production", + "prover_version": "stellar-oz-changes" +} diff --git a/packages/tokens/confs/enumerable_nft_sanity.conf b/packages/tokens/confs/enumerable_nft_sanity.conf new file mode 100644 index 00000000..72e33c9d --- /dev/null +++ b/packages/tokens/confs/enumerable_nft_sanity.conf @@ -0,0 +1,26 @@ +{ + "build_script": "../certora_build.py", + "msg": "Sanity Rules Enumerable NFT", + "rule": [ + "enumerable_nft_balance_sanity", + "enumerable_nft_owner_of_sanity", + "enumerable_nft_transfer_sanity", + "enumerable_nft_transfer_from_sanity", + "enumerable_nft_approve_sanity", + "enumerable_nft_approve_for_all_sanity", + "enumerable_nft_get_approved_sanity", + "enumerable_nft_is_approved_for_all_sanity", + "enumerable_nft_name_sanity", + "enumerable_nft_symbol_sanity", + // "enumerable_nft_token_uri_sanity", + "enumerable_nft_burn_sanity", + "enumerable_nft_burn_from_sanity", + "enumerable_nft_total_supply_sanity", + "enumerable_nft_get_owner_token_id_sanity", + "enumerable_nft_get_token_id_sanity", + "enumerable_nft_seq_mint_sanity", + "enumerable_nft_nonseq_mint_sanity" + ], + "server": "production", + "prover_version": "stellar-oz-changes" +} diff --git a/packages/tokens/confs/fungible_integrity.conf b/packages/tokens/confs/fungible_integrity.conf new file mode 100644 index 00000000..5018b420 --- /dev/null +++ b/packages/tokens/confs/fungible_integrity.conf @@ -0,0 +1,11 @@ +{ + "build_script": "../certora_build.py", + "msg": "Integrity Rules Fungible", + "rule": [ + "transfer_integrity", + "transfer_from_integrity", + "approve_integrity", + ], + "server": "production", + "prover_version": "stellar-oz-changes" +} \ No newline at end of file diff --git a/packages/tokens/confs/fungible_invariants.conf b/packages/tokens/confs/fungible_invariants.conf new file mode 100644 index 00000000..9336069e --- /dev/null +++ b/packages/tokens/confs/fungible_invariants.conf @@ -0,0 +1,11 @@ +{ + "build_script": "../certora_build.py", + "msg": "Invariants Rules Fungible", + "rule": [ + "after_transfer_total_supply_geq_balance", + "after_transfer_from_total_supply_geq_balance", + "after_approve_total_supply_geq_balance", + ], + "server": "production", + "prover_version": "stellar-oz-changes" +} \ No newline at end of file diff --git a/packages/tokens/confs/fungible_non_panics.conf b/packages/tokens/confs/fungible_non_panics.conf new file mode 100644 index 00000000..85fc7ce3 --- /dev/null +++ b/packages/tokens/confs/fungible_non_panics.conf @@ -0,0 +1,15 @@ +{ + "build_script": "../certora_build.py", + "msg": "Non-Panic Rules Fungible", + "rule": [ + "transfer_non_panic", + "transfer_non_panic_sanity", + "transfer_from_non_panic", + "transfer_from_non_panic_sanity", + "approve_non_panic", + "approve_non_panic_sanity", + ], + "server": "production", + "prover_args": ["-trapAsAssert true"], + "prover_version": "stellar-oz-changes" +} \ No newline at end of file diff --git a/packages/tokens/confs/fungible_panics.conf b/packages/tokens/confs/fungible_panics.conf new file mode 100644 index 00000000..b225f8f0 --- /dev/null +++ b/packages/tokens/confs/fungible_panics.conf @@ -0,0 +1,19 @@ +{ + "build_script": "../certora_build.py", + "msg": "Panic Rules Fungible", + "rule": [ + "transfer_panics_if_unauthorized", + "transfer_panics_if_not_enough_balance", + "transfer_panics_if_amount_less_than_zero", + "transfer_from_panics_if_spender_unauthorized", + "transfer_from_panics_if_not_enough_balance", + "transfer_from_panics_if_not_enough_allowance", + "transfer_from_panics_if_amount_less_than_zero", + "approve_panics_if_unauthorized", + "approve_panics_if_amount_less_than_zero", + "approve_panics_if_live_until_ledger_greater_than_max_ledger", + "approve_panics_if_live_until_ledger_less_than_current_ledger", + ], + "server": "production", + "prover_version": "stellar-oz-changes" +} \ No newline at end of file diff --git a/packages/tokens/confs/fungible_sanity.conf b/packages/tokens/confs/fungible_sanity.conf new file mode 100644 index 00000000..e158723f --- /dev/null +++ b/packages/tokens/confs/fungible_sanity.conf @@ -0,0 +1,17 @@ +{ + "build_script": "../certora_build.py", + "msg": "Sanity Rules Fungible Contract", + "rule": [ + "total_supply_sanity", + "balance_sanity", + "allowance_sanity", + "transfer_sanity", + "transfer_from_sanity", + "approve_sanity", + "decimals_sanity", + "name_sanity", + "symbol_sanity" + ], + "server": "production", + "prover_version": "stellar-oz-changes" +} \ No newline at end of file diff --git a/packages/tokens/confs/non_fungible_integrity.conf b/packages/tokens/confs/non_fungible_integrity.conf new file mode 100644 index 00000000..810233c9 --- /dev/null +++ b/packages/tokens/confs/non_fungible_integrity.conf @@ -0,0 +1,15 @@ +{ + "build_script": "../certora_build.py", + "msg": "Integrity Rules Non-Fungible", + "rule": [ + "nft_transfer_integrity", + "nft_transfer_from_integrity", + "nft_approve_integrity", + "nft_approve_for_all_integrity", + "nft_mint_integrity", + "nft_token_uri_injective", + ], + "server": "production", + "prover_version": "stellar-oz-changes" +} + diff --git a/packages/tokens/confs/non_fungible_invariants.conf b/packages/tokens/confs/non_fungible_invariants.conf new file mode 100644 index 00000000..3495bd74 --- /dev/null +++ b/packages/tokens/confs/non_fungible_invariants.conf @@ -0,0 +1,9 @@ +{ + "build_script": "../certora_build.py", + "msg": "Integrity Rules Non-Fungible", + "rule": [ + ], + "server": "production", + "prover_version": "stellar-oz-changes" +} + diff --git a/packages/tokens/confs/non_fungible_non_panics.conf b/packages/tokens/confs/non_fungible_non_panics.conf new file mode 100644 index 00000000..2ffdbbb5 --- /dev/null +++ b/packages/tokens/confs/non_fungible_non_panics.conf @@ -0,0 +1,14 @@ +{ + "build_script": "../certora_build.py", + "msg": "Non-Panic Rules Non-Fungible", + "rule": [ + "nft_transfer_non_panic", + "nft_transfer_from_non_panic", + "nft_approve_non_panic", + "nft_approve_for_all_non_panic", + ], + "server": "production", + "prover_args": ["-trapAsAssert true"], + "prover_version": "stellar-oz-changes" +} + diff --git a/packages/tokens/confs/non_fungible_panics.conf b/packages/tokens/confs/non_fungible_panics.conf new file mode 100644 index 00000000..92624c85 --- /dev/null +++ b/packages/tokens/confs/non_fungible_panics.conf @@ -0,0 +1,17 @@ +{ + "build_script": "../certora_build.py", + "msg": "Panic Rules Non-Fungible", + "rule": [ + "nft_transfer_panics_if_unauthorized", + "nft_transfer_panics_if_not_owner", + "nft_transfer_from_panics_if_unauthorized", + "nft_transfer_from_panics_if_not_owner", + "nft_transfer_from_panics_if_not_approved", + "nft_approve_panics_if_unauthorized", + "nft_approve_panics_if_live_until_ledger_greater_than_max_ledger", + "nft_approve_panics_if_live_until_ledger_less_than_current_ledger", + ], + "server": "production", + "prover_version": "stellar-oz-changes" +} + diff --git a/packages/tokens/confs/royalties_nft_sanity.conf b/packages/tokens/confs/royalties_nft_sanity.conf new file mode 100644 index 00000000..e45ca9e4 --- /dev/null +++ b/packages/tokens/confs/royalties_nft_sanity.conf @@ -0,0 +1,23 @@ +{ + "build_script": "../certora_build.py", + "msg": "Sanity Rules Royalties NFT", + "rule": [ + "royalties_nft_balance_sanity", + "royalties_nft_owner_of_sanity", + "royalties_nft_transfer_sanity", + "royalties_nft_transfer_from_sanity", + "royalties_nft_approve_sanity", + "royalties_nft_approve_for_all_sanity", + "royalties_nft_get_approved_sanity", + "royalties_nft_is_approved_for_all_sanity", + "royalties_nft_name_sanity", + "royalties_nft_symbol_sanity", + // "royalties_nft_token_uri_sanity", + "royalties_nft_set_default_royalty_sanity", + "royalties_nft_set_token_royalty_sanity", + "royalties_nft_remove_token_royalty_sanity", + "royalties_nft_royalty_info_sanity" + ], + "server": "production", + "prover_version": "stellar-oz-changes" +} diff --git a/packages/tokens/confs/vault_sanity.conf b/packages/tokens/confs/vault_sanity.conf new file mode 100644 index 00000000..8b0de3c0 --- /dev/null +++ b/packages/tokens/confs/vault_sanity.conf @@ -0,0 +1,31 @@ +{ + "build_script": "../certora_build.py", + "msg": "Sanity Rules Vault", + "rule": [ + "vault_query_asset_sanity", + "vault_total_assets_sanity", + "vault_convert_to_shares_sanity", + "vault_convert_to_assets_sanity", + "vault_max_deposit_sanity", + "vault_preview_deposit_sanity", + "vault_max_mint_sanity", + "vault_preview_mint_sanity", + "vault_max_withdraw_sanity", + "vault_preview_withdraw_sanity", + "vault_max_redeem_sanity", + "vault_preview_redeem_sanity", + "vault_deposit_sanity", + "vault_mint_sanity", + "vault_withdraw_sanity", + "vault_redeem_sanity", + "vault_decimals_sanity", + "vault_set_assset_sanity", + "vault_set_decimals_offset_sanity", + "vault_deposit_internal_sanity", + "vault_withdraw_internal_sanity", + "vault_get_decimals_offset_sanity", + "vault_get_underlying_asset_decimals_sanity" + ], + "server": "production", + "prover_version": "stellar-oz-changes" +} \ No newline at end of file diff --git a/packages/tokens/justfile b/packages/tokens/justfile new file mode 100644 index 00000000..4d26eddf --- /dev/null +++ b/packages/tokens/justfile @@ -0,0 +1,9 @@ +export RUSTFLAGS := "-C strip=none" + +target := "../../target" + +build: + cargo +nightly-2024-11-22 build --target=wasm32-unknown-unknown --release --features certora + +clean: + rm -rf {{target}} \ No newline at end of file diff --git a/packages/tokens/src/fungible/extensions/allowlist/mod.rs b/packages/tokens/src/fungible/extensions/allowlist/mod.rs index 77ff3435..7e2fc195 100644 --- a/packages/tokens/src/fungible/extensions/allowlist/mod.rs +++ b/packages/tokens/src/fungible/extensions/allowlist/mod.rs @@ -3,7 +3,13 @@ pub mod storage; #[cfg(test)] mod test; -use soroban_sdk::{contractevent, Address, Env}; +#[cfg(not(feature = "certora"))] +use soroban_sdk::{contractevent}; + +#[cfg(feature = "certora")] +use cvlr_soroban_derive::contractevent; + +use soroban_sdk::{Address, Env}; pub use storage::AllowList; use crate::fungible::FungibleToken; @@ -91,6 +97,7 @@ pub struct UserDisallowed { /// /// * `e` - Access to Soroban environment. /// * `user` - The address that is allowed to transfer tokens. +#[cfg(not(feature = "certora"))] pub fn emit_user_allowed(e: &Env, user: &Address) { UserAllowed { user: user.clone() }.publish(e); } @@ -101,6 +108,7 @@ pub fn emit_user_allowed(e: &Env, user: &Address) { /// /// * `e` - Access to Soroban environment. /// * `user` - The address that is disallowed from transferring tokens. +#[cfg(not(feature = "certora"))] pub fn emit_user_disallowed(e: &Env, user: &Address) { UserDisallowed { user: user.clone() }.publish(e); } diff --git a/packages/tokens/src/fungible/extensions/allowlist/storage.rs b/packages/tokens/src/fungible/extensions/allowlist/storage.rs index 4182712d..2338fba6 100644 --- a/packages/tokens/src/fungible/extensions/allowlist/storage.rs +++ b/packages/tokens/src/fungible/extensions/allowlist/storage.rs @@ -1,7 +1,11 @@ use soroban_sdk::{contracttype, panic_with_error, Address, Env}; +#[cfg(not(feature = "certora"))] use crate::fungible::{ extensions::allowlist::{emit_user_allowed, emit_user_disallowed}, +}; + +use crate::fungible::{ overrides::{Base, ContractOverrides}, FungibleTokenError, ALLOW_BLOCK_EXTEND_AMOUNT, ALLOW_BLOCK_TTL_THRESHOLD, }; @@ -82,7 +86,7 @@ impl AllowList { // if the user is not allowed, allow them if !e.storage().persistent().has(&key) { e.storage().persistent().set(&key, &()); - + #[cfg(not(feature = "certora"))] emit_user_allowed(e, user); } } @@ -115,7 +119,7 @@ impl AllowList { // if the user is currently allowed, disallow them if e.storage().persistent().has(&key) { e.storage().persistent().remove(&key); - + #[cfg(not(feature = "certora"))] emit_user_disallowed(e, user); } } diff --git a/packages/tokens/src/fungible/extensions/blocklist/mod.rs b/packages/tokens/src/fungible/extensions/blocklist/mod.rs index 543a5e34..acbe368d 100644 --- a/packages/tokens/src/fungible/extensions/blocklist/mod.rs +++ b/packages/tokens/src/fungible/extensions/blocklist/mod.rs @@ -3,7 +3,13 @@ pub mod storage; #[cfg(test)] mod test; -use soroban_sdk::{contractevent, Address, Env}; +#[cfg(not(feature = "certora"))] +use soroban_sdk::{contractevent}; + +#[cfg(feature = "certora")] +use cvlr_soroban_derive::contractevent; + +use soroban_sdk::{Address, Env}; pub use storage::BlockList; use crate::fungible::FungibleToken; @@ -91,6 +97,7 @@ pub struct UserUnblocked { /// /// * `e` - Access to Soroban environment. /// * `user` - The address that is blocked from transferring tokens. +#[cfg(not(feature = "certora"))] pub fn emit_user_blocked(e: &Env, user: &Address) { UserBlocked { user: user.clone() }.publish(e); } @@ -102,6 +109,7 @@ pub fn emit_user_blocked(e: &Env, user: &Address) { /// /// * `e` - Access to Soroban environment. /// * `user` - The address that is unblocked. +#[cfg(not(feature = "certora"))] pub fn emit_user_unblocked(e: &Env, user: &Address) { UserUnblocked { user: user.clone() }.publish(e); } diff --git a/packages/tokens/src/fungible/extensions/blocklist/storage.rs b/packages/tokens/src/fungible/extensions/blocklist/storage.rs index 89dfead5..afc5b5ef 100644 --- a/packages/tokens/src/fungible/extensions/blocklist/storage.rs +++ b/packages/tokens/src/fungible/extensions/blocklist/storage.rs @@ -1,7 +1,11 @@ use soroban_sdk::{contracttype, panic_with_error, Address, Env}; +#[cfg(not(feature = "certora"))] use crate::fungible::{ extensions::blocklist::{emit_user_blocked, emit_user_unblocked}, +}; + +use crate::fungible::{ overrides::{Base, ContractOverrides}, FungibleTokenError, ALLOW_BLOCK_EXTEND_AMOUNT, ALLOW_BLOCK_TTL_THRESHOLD, }; @@ -82,7 +86,7 @@ impl BlockList { // if the user is not blocked, block them if !e.storage().persistent().has(&key) { e.storage().persistent().set(&key, &()); - + #[cfg(not(feature = "certora"))] emit_user_blocked(e, user); } } @@ -115,7 +119,7 @@ impl BlockList { // if the user is currently blocked, unblock them if e.storage().persistent().has(&key) { e.storage().persistent().remove(&key); - + #[cfg(not(feature = "certora"))] emit_user_unblocked(e, user); } } diff --git a/packages/tokens/src/fungible/extensions/burnable/mod.rs b/packages/tokens/src/fungible/extensions/burnable/mod.rs index 4b2ae474..8578de61 100644 --- a/packages/tokens/src/fungible/extensions/burnable/mod.rs +++ b/packages/tokens/src/fungible/extensions/burnable/mod.rs @@ -3,7 +3,13 @@ mod storage; #[cfg(test)] mod test; -use soroban_sdk::{contractevent, Address, Env}; +#[cfg(not(feature = "certora"))] +use soroban_sdk::{contractevent}; + +#[cfg(feature = "certora")] +use cvlr_soroban_derive::contractevent; + +use soroban_sdk::{Address, Env}; use crate::fungible::FungibleToken; @@ -87,6 +93,7 @@ pub struct Burn { /// * `e` - Access to Soroban environment. /// * `from` - The address holding the tokens. /// * `amount` - The amount of tokens to be burned. +#[cfg(not(feature = "certora"))] pub fn emit_burn(e: &Env, from: &Address, amount: i128) { Burn { from: from.clone(), amount }.publish(e); } diff --git a/packages/tokens/src/fungible/extensions/burnable/storage.rs b/packages/tokens/src/fungible/extensions/burnable/storage.rs index 88cf8573..3b204c8d 100644 --- a/packages/tokens/src/fungible/extensions/burnable/storage.rs +++ b/packages/tokens/src/fungible/extensions/burnable/storage.rs @@ -1,7 +1,10 @@ use soroban_sdk::{Address, Env}; +#[cfg(not(feature = "certora"))] use crate::fungible::{extensions::burnable::emit_burn, Base}; +use crate::fungible::{Base}; + impl Base { /// Destroys `amount` of tokens from `from`. Updates the total /// supply accordingly. @@ -27,6 +30,7 @@ impl Base { pub fn burn(e: &Env, from: &Address, amount: i128) { from.require_auth(); Base::update(e, Some(from), None, amount); + #[cfg(not(feature = "certora"))] emit_burn(e, from, amount); } @@ -59,6 +63,7 @@ impl Base { spender.require_auth(); Base::spend_allowance(e, from, spender, amount); Base::update(e, Some(from), None, amount); + #[cfg(not(feature = "certora"))] emit_burn(e, from, amount); } } diff --git a/packages/tokens/src/fungible/extensions/mod.rs b/packages/tokens/src/fungible/extensions/mod.rs index 41761042..dc364e5a 100644 --- a/packages/tokens/src/fungible/extensions/mod.rs +++ b/packages/tokens/src/fungible/extensions/mod.rs @@ -2,4 +2,3 @@ pub mod allowlist; pub mod blocklist; pub mod burnable; pub mod capped; -pub mod vault; diff --git a/packages/tokens/src/fungible/mod.rs b/packages/tokens/src/fungible/mod.rs index c1762429..c27f24b8 100644 --- a/packages/tokens/src/fungible/mod.rs +++ b/packages/tokens/src/fungible/mod.rs @@ -75,12 +75,21 @@ mod utils; #[cfg(test)] mod test; -pub use extensions::{allowlist, blocklist, burnable, capped, vault}; +pub use extensions::{allowlist, blocklist, burnable, capped}; pub use overrides::{Base, ContractOverrides}; -use soroban_sdk::{contracterror, contractevent, Address, Env, String}; +use soroban_sdk::{contracterror, Address, Env, String}; pub use storage::{AllowanceData, AllowanceKey, StorageKey}; pub use utils::{sac_admin_generic, sac_admin_wrapper}; +#[cfg(feature = "certora")] +pub mod specs; + +#[cfg(not(feature = "certora"))] +use soroban_sdk::{contractevent}; + +#[cfg(feature = "certora")] +use cvlr_soroban_derive::contractevent; + /// Vanilla Fungible Token Trait /// /// The `FungibleToken` trait defines the core functionality for fungible @@ -314,26 +323,6 @@ pub enum FungibleTokenError { UserNotAllowed = 113, /// The user is blocked and cannot perform this operation UserBlocked = 114, - /// Indicates access to uninitialized vault asset address. - VaultAssetAddressNotSet = 115, - /// Indicates that vault asset address is already set. - VaultAssetAddressAlreadySet = 116, - /// Indicates that vault virtual decimals offset is already set. - VaultVirtualDecimalsOffsetAlreadySet = 117, - /// Indicates the amount is not a valid vault assets value. - VaultInvalidAssetsAmount = 118, - /// Indicates the amount is not a valid vault shares value. - VaultInvalidSharesAmount = 119, - /// Attempted to deposit more assets than the max amount for address. - VaultExceededMaxDeposit = 120, - /// Attempted to mint more shares than the max amount for address. - VaultExceededMaxMint = 121, - /// Attempted to withdraw more assets than the max amount for address. - VaultExceededMaxWithdraw = 122, - /// Attempted to redeem more shares than the max amount for address. - VaultExceededMaxRedeem = 123, - /// Maximum number of decimals offset exceeded - VaultMaxDecimalsOffsetExceeded = 124, } // ################## CONSTANTS ################## @@ -346,9 +335,6 @@ pub const ALLOW_BLOCK_TTL_THRESHOLD: u32 = ALLOW_BLOCK_EXTEND_AMOUNT - DAY_IN_LE pub const INSTANCE_EXTEND_AMOUNT: u32 = 7 * DAY_IN_LEDGERS; pub const INSTANCE_TTL_THRESHOLD: u32 = INSTANCE_EXTEND_AMOUNT - DAY_IN_LEDGERS; -// Suggested upper-bound for decimals to maximize both security and UX -pub const MAX_DECIMALS_OFFSET: u32 = 10; - // ################## EVENTS ################## /// Event emitted when tokens are transferred between addresses. @@ -370,6 +356,7 @@ pub struct Transfer { /// * `from` - The address holding the tokens. /// * `to` - The address receiving the transferred tokens. /// * `amount` - The amount of tokens to be transferred. +#[cfg(not(feature = "certora"))] pub fn emit_transfer(e: &Env, from: &Address, to: &Address, amount: i128) { Transfer { from: from.clone(), to: to.clone(), amount }.publish(e); } @@ -395,6 +382,7 @@ pub struct Approve { /// * `spender` - The address authorized to spend the tokens. /// * `amount` - The amount of tokens made available to `spender`. /// * `live_until_ledger` - The ledger number at which the allowance expires. +#[cfg(not(feature = "certora"))] pub fn emit_approve( e: &Env, owner: &Address, @@ -422,6 +410,7 @@ pub struct Mint { /// * `e` - Access to Soroban environment. /// * `to` - The address receiving the new tokens. /// * `amount` - The amount of tokens to mint. +#[cfg(not(feature = "certora"))] pub fn emit_mint(e: &Env, to: &Address, amount: i128) { Mint { to: to.clone(), amount }.publish(e); } diff --git a/packages/tokens/src/fungible/specs/allowlist.rs b/packages/tokens/src/fungible/specs/allowlist.rs new file mode 100644 index 00000000..d6ac7f6e --- /dev/null +++ b/packages/tokens/src/fungible/specs/allowlist.rs @@ -0,0 +1,144 @@ +use cvlr::{cvlr_assert, cvlr_satisfy, cvlr_assume, nondet::*}; +use cvlr_soroban::nondet_address; +use cvlr::clog; +use cvlr_soroban_derive::rule; +use soroban_sdk::{Address, Env}; +use crate::fungible::FungibleToken; +use crate::fungible::Base; +use crate::fungible::allowlist::AllowList; + +// ################## INTEGRITY RULES ################## + +#[rule] +// allow_user sets allowed to true +// status: verified +pub fn allow_user_integrity(e: Env) { + let account = nondet_address(); + AllowList::allow_user(&e, &account); + let allowed_post = AllowList::allowed(&e, &account); + cvlr_assert!(allowed_post == true); +} + +#[rule] +// disallow_user sets allowed to false +// status: verified +pub fn disallow_user_integrity(e: Env) { + let account = nondet_address(); + AllowList::disallow_user(&e, &account); + let allowed_post = AllowList::allowed(&e, &account); + cvlr_assert!(allowed_post == false); +} + +// ################## PANIC RULES ################## + +#[rule] +// transfer panics if from is not allowed +// status: verified +pub fn transfer_panics_if_from_not_allowed(e: Env) { + let to = nondet_address(); + clog!(cvlr_soroban::Addr(&to)); + let from = nondet_address(); + clog!(cvlr_soroban::Addr(&from)); + let amount:i128 = nondet(); + clog!(amount); + cvlr_assume!(!AllowList::allowed(&e, &from)); + AllowList::transfer(&e, &from, &to, amount); + cvlr_assert!(false); +} + +#[rule] +// transfer panics if to is not allowed +// status: verified +pub fn transfer_panics_if_to_not_allowed(e: Env) { + let to = nondet_address(); + clog!(cvlr_soroban::Addr(&to)); + let from = nondet_address(); + clog!(cvlr_soroban::Addr(&from)); + let amount:i128 = nondet(); + clog!(amount); + cvlr_assume!(!AllowList::allowed(&e, &to)); + AllowList::transfer(&e, &from, &to, amount); + cvlr_assert!(false); +} + +#[rule] +// transfer_from panics if from is not allowed +// status: verified +pub fn transfer_from_panics_if_from_not_allowed(e: Env) { + let to = nondet_address(); + clog!(cvlr_soroban::Addr(&to)); + let spender = nondet_address(); + clog!(cvlr_soroban::Addr(&spender)); + let from = nondet_address(); + clog!(cvlr_soroban::Addr(&from)); + let amount:i128 = nondet(); + clog!(amount); + cvlr_assume!(!AllowList::allowed(&e, &from)); + AllowList::transfer_from(&e, &spender, &from, &to, amount); + cvlr_assert!(false); +} + +#[rule] +// transfer_from panics if to is not allowed +// status: verified +pub fn transfer_from_panics_if_to_not_allowed(e: Env) { + let to = nondet_address(); + clog!(cvlr_soroban::Addr(&to)); + let spender = nondet_address(); + clog!(cvlr_soroban::Addr(&spender)); + let from = nondet_address(); + clog!(cvlr_soroban::Addr(&from)); + let amount:i128 = nondet(); + clog!(amount); + cvlr_assume!(!AllowList::allowed(&e, &to)); + AllowList::transfer_from(&e, &spender, &from, &to, amount); + cvlr_assert!(false); +} + +// spender does not have to be allowed - see mod + +#[rule] +// approve panics if owner is not allowed +// status: verified +pub fn approve_panics_if_owner_not_allowed(e: Env) { + let owner = nondet_address(); + clog!(cvlr_soroban::Addr(&owner)); + let spender = nondet_address(); + clog!(cvlr_soroban::Addr(&spender)); + let amount:i128 = nondet(); + clog!(amount); + let live_until_ledger = u32::nondet(); + clog!(live_until_ledger); + cvlr_assume!(!AllowList::allowed(&e, &owner)); + AllowList::approve(&e, &owner, &spender, amount, live_until_ledger); + cvlr_assert!(false); +} + +#[rule] +// burn panics if from is not allowed +// status: verified +pub fn burn_panics_if_from_not_allowed(e: Env) { + let from = nondet_address(); + clog!(cvlr_soroban::Addr(&from)); + let amount:i128 = nondet(); + clog!(amount); + cvlr_assume!(!AllowList::allowed(&e, &from)); + AllowList::burn(&e, &from, amount); + cvlr_assert!(false); +} + +#[rule] +// burn_from panics if from is not allowed +// status: verified +pub fn burn_from_panics_if_from_not_allowed(e: Env) { + let spender = nondet_address(); + clog!(cvlr_soroban::Addr(&spender)); + let from = nondet_address(); + clog!(cvlr_soroban::Addr(&from)); + let amount:i128 = nondet(); + clog!(amount); + cvlr_assume!(!AllowList::allowed(&e, &from)); + AllowList::burn_from(&e, &spender, &from, amount); + cvlr_assert!(false); +} + diff --git a/packages/tokens/src/fungible/specs/blocklist.rs b/packages/tokens/src/fungible/specs/blocklist.rs new file mode 100644 index 00000000..527ad3ed --- /dev/null +++ b/packages/tokens/src/fungible/specs/blocklist.rs @@ -0,0 +1,144 @@ +use cvlr::{cvlr_assert, cvlr_satisfy, cvlr_assume, nondet::*}; +use cvlr_soroban::nondet_address; +use cvlr::clog; +use cvlr_soroban_derive::rule; +use soroban_sdk::{Address, Env}; +use crate::fungible::FungibleToken; +use crate::fungible::Base; +use crate::fungible::blocklist::BlockList; + +// ################## INTEGRITY RULES ################## + +#[rule] +// block_user sets blocked to true +// status: verified +pub fn block_user_integrity(e: Env) { + let account = nondet_address(); + BlockList::block_user(&e, &account); + let blocked_post = BlockList::blocked(&e, &account); + cvlr_assert!(blocked_post == true); +} + +#[rule] +// unblock_user sets blocked to false +// status: verified +pub fn unblock_user_integrity(e: Env) { + let account = nondet_address(); + BlockList::unblock_user(&e, &account); + let blocked_post = BlockList::blocked(&e, &account); + cvlr_assert!(blocked_post == false); +} + +// ################## PANIC RULES ################## + +#[rule] +// transfer panics if from is blocked +// status: verified +pub fn transfer_panics_if_from_blocked(e: Env) { + let to = nondet_address(); + clog!(cvlr_soroban::Addr(&to)); + let from = nondet_address(); + clog!(cvlr_soroban::Addr(&from)); + let amount:i128 = nondet(); + clog!(amount); + cvlr_assume!(BlockList::blocked(&e, &from)); + BlockList::transfer(&e, &from, &to, amount); + cvlr_assert!(false); +} + +#[rule] +// transfer panics if to is blocked +// status: verified +pub fn transfer_panics_if_to_blocked(e: Env) { + let to = nondet_address(); + clog!(cvlr_soroban::Addr(&to)); + let from = nondet_address(); + clog!(cvlr_soroban::Addr(&from)); + let amount:i128 = nondet(); + clog!(amount); + cvlr_assume!(BlockList::blocked(&e, &to)); + BlockList::transfer(&e, &from, &to, amount); + cvlr_assert!(false); +} + +#[rule] +// transfer_from panics if from is blocked +// status: verified +pub fn transfer_from_panics_if_from_blocked(e: Env) { + let to = nondet_address(); + clog!(cvlr_soroban::Addr(&to)); + let spender = nondet_address(); + clog!(cvlr_soroban::Addr(&spender)); + let from = nondet_address(); + clog!(cvlr_soroban::Addr(&from)); + let amount:i128 = nondet(); + clog!(amount); + cvlr_assume!(BlockList::blocked(&e, &from)); + BlockList::transfer_from(&e, &spender, &from, &to, amount); + cvlr_assert!(false); +} + +#[rule] +// transfer_from panics if to is blocked +// status: verified +pub fn transfer_from_panics_if_to_blocked(e: Env) { + let to = nondet_address(); + clog!(cvlr_soroban::Addr(&to)); + let spender = nondet_address(); + clog!(cvlr_soroban::Addr(&spender)); + let from = nondet_address(); + clog!(cvlr_soroban::Addr(&from)); + let amount:i128 = nondet(); + clog!(amount); + cvlr_assume!(BlockList::blocked(&e, &to)); + BlockList::transfer_from(&e, &spender, &from, &to, amount); + cvlr_assert!(false); +} + +// spender may be blocked - see mod + +#[rule] +// approve panics if owner is blocked +// status: verified +pub fn approve_panics_if_owner_blocked(e: Env) { + let owner = nondet_address(); + clog!(cvlr_soroban::Addr(&owner)); + let spender = nondet_address(); + clog!(cvlr_soroban::Addr(&spender)); + let amount:i128 = nondet(); + clog!(amount); + let live_until_ledger = u32::nondet(); + clog!(live_until_ledger); + cvlr_assume!(BlockList::blocked(&e, &owner)); + BlockList::approve(&e, &owner, &spender, amount, live_until_ledger); + cvlr_assert!(false); +} + +#[rule] +// burn panics if from is blocked +// status: verified +pub fn burn_panics_if_from_blocked(e: Env) { + let from = nondet_address(); + clog!(cvlr_soroban::Addr(&from)); + let amount:i128 = nondet(); + clog!(amount); + cvlr_assume!(BlockList::blocked(&e, &from)); + BlockList::burn(&e, &from, amount); + cvlr_assert!(false); +} + +#[rule] +// burn_from panics if from is blocked +// status: verified +pub fn burn_from_panics_if_from_blocked(e: Env) { + let spender = nondet_address(); + clog!(cvlr_soroban::Addr(&spender)); + let from = nondet_address(); + clog!(cvlr_soroban::Addr(&from)); + let amount:i128 = nondet(); + clog!(amount); + cvlr_assume!(BlockList::blocked(&e, &from)); + BlockList::burn_from(&e, &spender, &from, amount); + cvlr_assert!(false); +} + diff --git a/packages/tokens/src/fungible/specs/burnable.rs b/packages/tokens/src/fungible/specs/burnable.rs new file mode 100644 index 00000000..908a2ce1 --- /dev/null +++ b/packages/tokens/src/fungible/specs/burnable.rs @@ -0,0 +1,293 @@ +use cvlr::{cvlr_assert, cvlr_satisfy, cvlr_assume, nondet::*}; +use cvlr_soroban::{nondet_address, is_auth}; +use cvlr::clog; +use cvlr_soroban_derive::rule; +use soroban_sdk::{Address, Env}; +use crate::fungible::Base; +use crate::fungible::specs::fungible_invariants::{assume_pre_total_supply_geq_balance, assert_post_total_supply_geq_balance}; +use crate::fungible::specs::fungible_non_panics::{storage_setup_balance, storage_setup_allowance}; + +// ################## INTEGRITY RULES ################## + +#[rule] +// after burn the account's balance and total supply decrease by amount +// status: verified +// note: 20 minutes +pub fn burn_integrity(e: Env) { + let account = nondet_address(); + let amount = nondet(); + let balance_pre = Base::balance(&e, &account); + let total_supply_pre = Base::total_supply(&e); + Base::burn(&e, &account, amount); + let balance_post = Base::balance(&e, &account); + let total_supply_post = Base::total_supply(&e); + cvlr_assert!(balance_post == balance_pre - amount); + cvlr_assert!(total_supply_post == total_supply_pre - amount); +} + +#[rule] +// after burn_from the account's balance and total supply decrease by amount +// status: timeout (58min, 87%) +pub fn burn_from_integrity(e: Env) { + let account = nondet_address(); + let amount = nondet(); + let balance_pre = Base::balance(&e, &account); + let total_supply_pre = Base::total_supply(&e); + Base::burn_from(&e, &account, &account, amount); + let balance_post = Base::balance(&e, &account); + let total_supply_post = Base::total_supply(&e); + cvlr_assert!(balance_post == balance_pre - amount); + cvlr_assert!(total_supply_post == total_supply_pre - amount); +} + +// ################## PANIC RULES ################## + +#[rule] +// burn panics if not auth by from +// status: verified +pub fn burn_panics_if_unauthorized(e: Env) { + let from = nondet_address(); + clog!(cvlr_soroban::Addr(&from)); + let amount: i128 = nondet(); + clog!(amount); + cvlr_assume!(!is_auth(from.clone())); + Base::burn(&e, &from, amount); + cvlr_assert!(false); +} + +#[rule] +// burn panics if not enough balance +// status: verified +pub fn burn_panics_if_not_enough_balance(e: Env) { + let from = nondet_address(); + clog!(cvlr_soroban::Addr(&from)); + let amount: i128 = nondet(); + clog!(amount); + let balance = Base::balance(&e, &from); + clog!(balance); + cvlr_assume!(balance < amount); + Base::burn(&e, &from, amount); + cvlr_assert!(false); +} + +#[rule] +// burn panics if amount < 0 +// status: verified +pub fn burn_panics_if_amount_less_than_zero(e: Env) { + let from = nondet_address(); + clog!(cvlr_soroban::Addr(&from)); + let amount: i128 = nondet(); + clog!(amount); + cvlr_assume!(amount < 0); + Base::burn(&e, &from, amount); + cvlr_assert!(false); +} + +#[rule] +// burn_from panics if not auth by spender +// status: verified +pub fn burn_from_panics_if_spender_unauthorized(e: Env) { + let spender = nondet_address(); + clog!(cvlr_soroban::Addr(&spender)); + let from = nondet_address(); + clog!(cvlr_soroban::Addr(&from)); + let amount: i128 = nondet(); + clog!(amount); + cvlr_assume!(!is_auth(spender.clone())); + Base::burn_from(&e, &spender, &from, amount); + cvlr_assert!(false); +} + +#[rule] +// burn_from panics if not enough balance +// status: verified +pub fn burn_from_panics_if_not_enough_balance(e: Env) { + let spender = nondet_address(); + clog!(cvlr_soroban::Addr(&spender)); + let from = nondet_address(); + clog!(cvlr_soroban::Addr(&from)); + let amount: i128 = nondet(); + clog!(amount); + let balance = Base::balance(&e, &from); + clog!(balance); + cvlr_assume!(balance < amount); + Base::burn_from(&e, &spender, &from, amount); + cvlr_assert!(false); +} + +#[rule] +// burn_from panics if not enough allowance +// status: bug +// same bug as in transfer_from +pub fn burn_from_panics_if_not_enough_allowance(e: Env) { + let spender = nondet_address(); + clog!(cvlr_soroban::Addr(&spender)); + let from = nondet_address(); + clog!(cvlr_soroban::Addr(&from)); + let amount: i128 = nondet(); + clog!(amount); + let allowance = Base::allowance(&e, &from, &spender); + clog!(allowance); + cvlr_assume!(allowance < amount); + Base::burn_from(&e, &spender, &from, amount); + cvlr_assert!(false); +} + +#[rule] +// burn_from panics if amount < 0 +// status: verified +pub fn burn_from_panics_if_amount_less_than_zero(e: Env) { + let spender = nondet_address(); + clog!(cvlr_soroban::Addr(&spender)); + let from = nondet_address(); + clog!(cvlr_soroban::Addr(&from)); + let amount: i128 = nondet(); + clog!(amount); + cvlr_assume!(amount < 0); + Base::burn_from(&e, &spender, &from, amount); + cvlr_assert!(false); +} + +// ################## NON-PANIC RULES ################## + +#[rule] +// requires +// from auth +// from has enough balance +// amount >= 0 +// status: wip - waiting +pub fn burn_non_panic(e: Env) { + let from = nondet_address(); + clog!(cvlr_soroban::Addr(&from)); + let amount: i128 = nondet(); + clog!(amount); + cvlr_assume!(is_auth(from.clone())); + storage_setup_balance(e.clone(), from.clone()); + let from_balance = Base::balance(&e, &from); + clog!(from_balance); + cvlr_assume!(from_balance >= amount); + cvlr_assume!(amount >= 0); + Base::burn(&e, &from, amount); + cvlr_assert!(true); +} + +#[rule] +// sanity +// status: +pub fn burn_non_panic_sanity(e: Env) { + let from = nondet_address(); + clog!(cvlr_soroban::Addr(&from)); + let amount: i128 = nondet(); + clog!(amount); + cvlr_assume!(is_auth(from.clone())); + storage_setup_balance(e.clone(), from.clone()); + let from_balance = Base::balance(&e, &from); + clog!(from_balance); + cvlr_assume!(from_balance >= amount); + cvlr_assume!(amount >= 0); + Base::burn(&e, &from, amount); + cvlr_satisfy!(true); +} + +#[rule] +// requires +// spender auth +// from has enough balance +// from has enough allowance +// amount >= 0 +// status: wip +pub fn burn_from_non_panic(e: Env) { + let spender = nondet_address(); + clog!(cvlr_soroban::Addr(&spender)); + let from = nondet_address(); + clog!(cvlr_soroban::Addr(&from)); + let amount: i128 = nondet(); + clog!(amount); + cvlr_assume!(is_auth(spender.clone())); + storage_setup_balance(e.clone(), from.clone()); + let balance_from = Base::balance(&e, &from); + clog!(balance_from); + cvlr_assume!(balance_from >= amount); + storage_setup_allowance(e.clone(), from.clone(), spender.clone()); + let allowance_spender = Base::allowance(&e, &from, &spender); + clog!(allowance_spender); + cvlr_assume!(allowance_spender >= amount); + cvlr_assume!(amount >= 0); + Base::burn_from(&e, &spender, &from, amount); + cvlr_assert!(true); +} + +#[rule] +// sanity +// status: +pub fn burn_from_non_panic_sanity(e: Env) { + let spender = nondet_address(); + clog!(cvlr_soroban::Addr(&spender)); + let from = nondet_address(); + clog!(cvlr_soroban::Addr(&from)); + let amount: i128 = nondet(); + clog!(amount); + cvlr_assume!(is_auth(spender.clone())); + storage_setup_balance(e.clone(), from.clone()); + let balance_from = Base::balance(&e, &from); + clog!(balance_from); + cvlr_assume!(balance_from >= amount); + storage_setup_allowance(e.clone(), from.clone(), spender.clone()); + let allowance_spender = Base::allowance(&e, &from, &spender); + clog!(allowance_spender); + cvlr_assume!(allowance_spender >= amount); + cvlr_assume!(amount >= 0); + Base::burn_from(&e, &spender, &from, amount); + cvlr_satisfy!(true); +} + +// ################## INVARIANT RULES ################## + +// we can't prove this without ghosts and hooks. +pub fn assume_pre_total_supply_geq_two_balances(e: Env, account1: &Address, account2: &Address) { + clog!(cvlr_soroban::Addr(account1)); + clog!(cvlr_soroban::Addr(account2)); + let total_supply = Base::total_supply(&e); + clog!(total_supply); + let balance1 = Base::balance(&e, account1); + clog!(balance1); + let balance2 = Base::balance(&e, account2); + clog!(balance2); + cvlr_assume!(total_supply >= balance1 + balance2); +} + +#[rule] +// after burn total_supply >= balance for any account +// status: +pub fn after_burn_total_supply_geq_balance(e: Env) { + let from = nondet_address(); + clog!(cvlr_soroban::Addr(&from)); + let amount = nondet(); + clog!(amount); + let account = nondet_address(); + clog!(cvlr_soroban::Addr(&account)); + assume_pre_total_supply_geq_balance(e.clone(), &account); + assume_pre_total_supply_geq_balance(e.clone(), &from); + assume_pre_total_supply_geq_two_balances(e.clone(), &account, &from); + Base::burn(&e, &from, amount); + assert_post_total_supply_geq_balance(e, &account); +} + +#[rule] +// after burn_from total_supply >= balance for any account +// status: +pub fn after_burn_from_total_supply_geq_balance(e: Env) { + let spender = nondet_address(); + clog!(cvlr_soroban::Addr(&spender)); + let from = nondet_address(); + clog!(cvlr_soroban::Addr(&from)); + let amount = nondet(); + clog!(amount); + let account = nondet_address(); + clog!(cvlr_soroban::Addr(&account)); + assume_pre_total_supply_geq_balance(e.clone(), &account); + assume_pre_total_supply_geq_balance(e.clone(), &from); + assume_pre_total_supply_geq_two_balances(e.clone(), &account, &from); + Base::burn_from(&e, &spender, &from, amount); + assert_post_total_supply_geq_balance(e, &account); +} \ No newline at end of file diff --git a/packages/tokens/src/fungible/specs/capped.rs b/packages/tokens/src/fungible/specs/capped.rs new file mode 100644 index 00000000..8a2beffd --- /dev/null +++ b/packages/tokens/src/fungible/specs/capped.rs @@ -0,0 +1,55 @@ +// invariant total supply less than cap +// need to implement a mint function for this. +// and constructor. + +use crate::fungible::specs::capped_contract::CappedTokenContract; +use crate::fungible::FungibleToken; + +use cvlr::{cvlr_assert, cvlr_satisfy, nondet::*}; +use cvlr_soroban::nondet_address; +use cvlr_soroban_derive::rule; +use soroban_sdk::Env; + +#[rule] +// after mint the account's balance increases by amount +// total supply increases by amount +// status: verified +// note: 26 minutes +pub fn mint_integrity(e: Env) { + let account = nondet_address(); + let amount = nondet(); + let balance_pre = CappedTokenContract::balance(&e, account.clone()); + let total_supply_pre = CappedTokenContract::total_supply(&e); + CappedTokenContract::mint(&e, account.clone(), amount); + let balance_post = CappedTokenContract::balance(&e, account); + let total_supply_post = CappedTokenContract::total_supply(&e); + cvlr_assert!(balance_post == balance_pre + amount); + cvlr_assert!(total_supply_post == total_supply_pre + amount); +} + +#[rule] +// after a mint the total supply doesn't surpass the cap +// status: verified +// note: 18 minutes +pub fn mint_preserves_cap(e: Env) { + let amount = nondet(); + let account = nondet_address(); + CappedTokenContract::mint(&e, account.clone(), amount); + let total_supply = CappedTokenContract::total_supply(&e); + let cap = CappedTokenContract::get_cap(&e); + cvlr_assert!(total_supply <= cap); +} + +#[rule] +// after constructor the cap is set +// status: +pub fn constructor_integrity(e: Env) { + let cap = nondet(); + CappedTokenContract::__constructor(&e, cap); + let cap_post = CappedTokenContract::get_cap(&e); + cvlr_assert!(cap_post == cap); + cvlr_assert!(cap >= 0); +} + +// TODO: invariants +// panics and non-panics are not interesting. \ No newline at end of file diff --git a/packages/tokens/src/fungible/specs/capped_contract.rs b/packages/tokens/src/fungible/specs/capped_contract.rs new file mode 100644 index 00000000..ec776500 --- /dev/null +++ b/packages/tokens/src/fungible/specs/capped_contract.rs @@ -0,0 +1,63 @@ +use soroban_sdk::{contract, contractimpl, Address, Env, String}; + +use crate::fungible::{extensions::capped::{check_cap, query_cap, set_cap}, Base, FungibleToken}; + +#[contract] +pub struct CappedTokenContract; + +#[contractimpl] +impl CappedTokenContract { + pub fn __constructor(e: &Env, cap: i128) { + set_cap(e, cap); + } + + pub fn mint(e: &Env, account: Address, amount: i128) { + check_cap(e, amount); + Base::mint(e, &account, amount); + } + + pub fn get_cap(e: &Env) -> i128 { + query_cap(e) + } +} + +#[contractimpl] +impl FungibleToken for CappedTokenContract { + type ContractType = Base; + + fn total_supply(e: &Env) -> i128 { + Self::ContractType::total_supply(e) + } + + fn balance(e: &Env, account: Address) -> i128 { + Self::ContractType::balance(e, &account) + } + + fn allowance(e: &Env, owner: Address, spender: Address) -> i128 { + Self::ContractType::allowance(e, &owner, &spender) + } + + fn transfer(e: &Env, from: Address, to: Address, amount: i128) { + Self::ContractType::transfer(e, &from, &to, amount); + } + + fn transfer_from(e: &Env, spender: Address, from: Address, to: Address, amount: i128) { + Self::ContractType::transfer_from(e, &spender, &from, &to, amount); + } + + fn approve(e: &Env, owner: Address, spender: Address, amount: i128, live_until_ledger: u32) { + Self::ContractType::approve(e, &owner, &spender, amount, live_until_ledger); + } + + fn decimals(e: &Env) -> u32 { + Self::ContractType::decimals(e) + } + + fn name(e: &Env) -> String { + Self::ContractType::name(e) + } + + fn symbol(e: &Env) -> String { + Self::ContractType::symbol(e) + } +} diff --git a/packages/tokens/src/fungible/specs/fungible_integrity.rs b/packages/tokens/src/fungible/specs/fungible_integrity.rs new file mode 100644 index 00000000..db759390 --- /dev/null +++ b/packages/tokens/src/fungible/specs/fungible_integrity.rs @@ -0,0 +1,104 @@ +use cvlr::{cvlr_assert, cvlr_satisfy, nondet::*}; +use cvlr_soroban::nondet_address; +use cvlr::clog; +use cvlr_soroban_derive::rule; +use soroban_sdk::{Address, Env}; +use crate::fungible::FungibleToken; +use crate::fungible::Base; + +#[rule] +// transfer changes balances accordingly +// status: verified +pub fn transfer_integrity(e: Env) { + let to = nondet_address(); + clog!(cvlr_soroban::Addr(&to)); + let from = nondet_address(); + clog!(cvlr_soroban::Addr(&from)); + let amount:i128 = nondet(); + clog!(amount); + let balance_from_pre = Base::balance(&e, &from); + clog!(balance_from_pre); + let balance_to_pre = Base::balance(&e, &to); + clog!(balance_to_pre); + let total_supply_pre = Base::total_supply(&e); + clog!(total_supply_pre); + Base::transfer(&e, &from, &to, amount); + let balance_from_post = Base::balance(&e, &from); + clog!(balance_from_post); + let balance_to_post = Base::balance(&e, &to); + clog!(balance_to_post); + let total_supply_post = Base::total_supply(&e); + clog!(total_supply_post); + cvlr_assert!(total_supply_post == total_supply_pre); + if to != from { + cvlr_assert!(balance_from_post == balance_from_pre - amount); + cvlr_assert!(balance_to_post == balance_to_pre + amount); + } else { + cvlr_assert!(balance_to_post == balance_to_pre); + } +} + +#[rule] +// transfer_from changes balances and allowance accordingly +// status: timeout +pub fn transfer_from_integrity(e: Env) { + let spender = nondet_address(); + clog!(cvlr_soroban::Addr(&spender)); + let from = nondet_address(); + clog!(cvlr_soroban::Addr(&from)); + let to = nondet_address(); + clog!(cvlr_soroban::Addr(&to)); + let amount:i128 = nondet(); + clog!(amount); + let balance_from_pre = Base::balance(&e, &from); + clog!(balance_from_pre); + let balance_to_pre = Base::balance(&e, &to); + clog!(balance_to_pre); + let allowance_pre = Base::allowance(&e, &from, &spender); + clog!(allowance_pre); + let total_supply_pre = Base::total_supply(&e); + clog!(total_supply_pre); + Base::transfer_from(&e, &spender, &from, &to, amount); + let balance_from_post = Base::balance(&e, &from); + clog!(balance_from_post); + let balance_to_post = Base::balance(&e, &to); + clog!(balance_to_post); + let allowance_post = Base::allowance(&e, &from, &spender); + clog!(allowance_post); + let total_supply_post = Base::total_supply(&e); + clog!(total_supply_post); + cvlr_assert!(total_supply_post == total_supply_pre); + if to != from { + cvlr_assert!(balance_from_post == balance_from_pre - amount); + cvlr_assert!(balance_to_post == balance_to_pre + amount); + } else { + cvlr_assert!(balance_from_post == balance_from_pre); + cvlr_assert!(balance_to_post == balance_to_pre); + } + if spender != from { + cvlr_assert!(allowance_post == allowance_pre - amount); + } else { + cvlr_assert!(allowance_post == allowance_pre); + } +} + +#[rule] +// approve changes allowance accordingly +// status: verified +pub fn approve_integrity(e: Env) { + // note - the allowance and approve are all in the same env. + let owner = nondet_address(); + clog!(cvlr_soroban::Addr(&owner)); + let spender = nondet_address(); + clog!(cvlr_soroban::Addr(&spender)); + let amount:i128 = nondet(); + clog!(amount); + let live_until_ledger:u32 = nondet(); + clog!(live_until_ledger); + let allowance_pre = Base::allowance(&e, &owner, &spender); + clog!(allowance_pre); + Base::approve(&e, &owner, &spender, amount, live_until_ledger); + let allowance_post = Base::allowance(&e, &owner, &spender); + clog!(allowance_post); + cvlr_assert!(allowance_post == amount); +} \ No newline at end of file diff --git a/packages/tokens/src/fungible/specs/fungible_invariants.rs b/packages/tokens/src/fungible/specs/fungible_invariants.rs new file mode 100644 index 00000000..2e1b35eb --- /dev/null +++ b/packages/tokens/src/fungible/specs/fungible_invariants.rs @@ -0,0 +1,85 @@ +use cvlr::{cvlr_assert, cvlr_assume, cvlr_satisfy, nondet::*}; +use cvlr_soroban::nondet_address; +use cvlr::clog; +use cvlr_soroban_derive::rule; +use soroban_sdk::{Address, Env}; +use crate::fungible::FungibleToken; +use crate::fungible::Base; + +// total_supply does not change other than mint. +// total_supply >= balance(a1)+balance(a2) + +// maybe its not right to talk about invariants just for fungible because its not really a contract setting (?) +// or maybe its fine + +// helpers +pub fn assume_pre_total_supply_geq_balance(e: Env, account: &Address) { + clog!(cvlr_soroban::Addr(account)); + let total_supply = Base::total_supply(&e); + clog!(total_supply); + let balance = Base::balance(&e, account); + clog!(balance); + cvlr_assume!(total_supply >= balance); +} + +pub fn assert_post_total_supply_geq_balance(e: Env, account: &Address) { + clog!(cvlr_soroban::Addr(account)); + let total_supply = Base::total_supply(&e); + clog!(total_supply); + let balance = Base::balance(&e, account); + clog!(balance); + cvlr_assert!(total_supply >= balance); +} + +#[rule] +// status: violated - spurious +// https://prover.certora.com/output/5771024/7ac81c9f026e44b1a29a116052a06333/ +pub fn after_transfer_total_supply_geq_balance(e: Env) { + let to = nondet_address(); + clog!(cvlr_soroban::Addr(&to)); + let from = nondet_address(); + clog!(cvlr_soroban::Addr(&from)); + let amount = nondet(); + clog!(amount); + let account = nondet_address(); + clog!(cvlr_soroban::Addr(&account)); + assume_pre_total_supply_geq_balance(e.clone(), &account); + Base::transfer(&e, &from, &to, amount); + assert_post_total_supply_geq_balance(e, &account); +} + +#[rule] +// status: violated - seems spurious +pub fn after_transfer_from_total_supply_geq_balance(e: Env) { + let spender = nondet_address(); + clog!(cvlr_soroban::Addr(&spender)); + let from = nondet_address(); + clog!(cvlr_soroban::Addr(&from)); + let to = nondet_address(); + clog!(cvlr_soroban::Addr(&to)); + let amount = nondet(); + clog!(amount); + let account = nondet_address(); + clog!(cvlr_soroban::Addr(&account)); + assume_pre_total_supply_geq_balance(e.clone(), &account); + Base::transfer_from(&e, &spender, &from, &to, amount); + assert_post_total_supply_geq_balance(e, &account); +} + +#[rule] +// status: verified +pub fn after_approve_total_supply_geq_balance(e: Env) { + let owner = nondet_address(); + clog!(cvlr_soroban::Addr(&owner)); + let spender = nondet_address(); + clog!(cvlr_soroban::Addr(&spender)); + let amount = nondet(); + clog!(amount); + let account = nondet_address(); + clog!(cvlr_soroban::Addr(&account)); + let live_until_ledger = nondet(); + clog!(live_until_ledger); + assume_pre_total_supply_geq_balance(e.clone(), &account); + Base::approve(&e, &owner, &spender, amount, live_until_ledger); + assert_post_total_supply_geq_balance(e, &account); +} \ No newline at end of file diff --git a/packages/tokens/src/fungible/specs/fungible_non_panics.rs b/packages/tokens/src/fungible/specs/fungible_non_panics.rs new file mode 100644 index 00000000..b33b53b4 --- /dev/null +++ b/packages/tokens/src/fungible/specs/fungible_non_panics.rs @@ -0,0 +1,174 @@ +use cvlr::{cvlr_assert, cvlr_satisfy, cvlr_assume, nondet::*}; +use cvlr_soroban::{nondet_address, is_auth}; +use cvlr::clog; +use cvlr_soroban_derive::rule; +use soroban_sdk::{Address, Env}; +use crate::fungible::FungibleToken; +use crate::fungible::Base; +use crate::fungible::storage::{StorageKey, AllowanceKey, AllowanceData}; +// These rules require the prover arg "prover_args": ["-trapAsAssert true"] to consider also panicking paths. + +pub fn storage_setup_balance(e: Env, account: Address) { + let balance:i128 = nondet(); + e.storage().persistent().set(&StorageKey::Balance(account), &balance); +} + +pub fn storage_setup_allowance(e: Env, owner: Address, spender: Address) { + let amount:i128 = nondet(); + let live_until_ledger:u32 = nondet(); + let allowance = AllowanceData { amount, live_until_ledger }; + e.storage().temporary().set(&StorageKey::Allowance(AllowanceKey { owner, spender }), &allowance); +} + +#[rule] +// requires +// from auth +// from has enough balance +// amount >= 0 +// status: violated - problem with storage and nondet? +pub fn transfer_non_panic(e: Env) { + let to: Address = nondet_address(); + clog!(cvlr_soroban::Addr(&to)); + let from = nondet_address(); + clog!(cvlr_soroban::Addr(&from)); + let amount:i128 = nondet(); + clog!(amount); + cvlr_assume!(is_auth(from.clone())); + storage_setup_balance(e.clone(), from.clone()); + let from_balance = Base::balance(&e, &from); + clog!(from_balance); + cvlr_assume!(from_balance >= amount); + cvlr_assume!(amount >= 0); + Base::transfer(&e, &from, &to, amount); + cvlr_assert!(true); +} + +#[rule] +// sanity +// status: verified +pub fn transfer_non_panic_sanity(e: Env) { + let to: Address = nondet_address(); + clog!(cvlr_soroban::Addr(&to)); + let from = nondet_address(); + clog!(cvlr_soroban::Addr(&from)); + let amount:i128 = nondet(); + clog!(amount); + cvlr_assume!(is_auth(from.clone())); + storage_setup_balance(e.clone(), from.clone()); + let from_balance = Base::balance(&e, &from); + clog!(from_balance); + cvlr_assume!(from_balance >= amount); + cvlr_assume!(amount >= 0); + Base::transfer(&e, &from, &to, amount); + cvlr_satisfy!(true); +} + +#[rule] +// requires +// spender auth +// from has enough allowance +// spender has enough allowance +// amount >= 0 +// status: violated - problem with storage and nondet? +pub fn transfer_from_non_panic(e: Env) { + let to: Address = nondet_address(); + clog!(cvlr_soroban::Addr(&to)); + let from = nondet_address(); + clog!(cvlr_soroban::Addr(&from)); + let spender = nondet_address(); + clog!(cvlr_soroban::Addr(&spender)); + let amount:i128 = nondet(); + clog!(amount); + cvlr_assume!(is_auth(spender.clone())); + storage_setup_balance(e.clone(), from.clone()); + let balance_from = Base::balance(&e, &from); + clog!(balance_from); + cvlr_assume!(balance_from >= amount); + storage_setup_allowance(e.clone(), from.clone(), spender.clone()); + let allowance_spender = Base::allowance(&e, &from, &spender); + clog!(allowance_spender); + cvlr_assume!(allowance_spender >= amount); + cvlr_assume!(amount >= 0); + Base::transfer_from(&e, &spender, &from, &to, amount); + cvlr_assert!(true); +} + +#[rule] +// sanity +// status: verified +pub fn transfer_from_non_panic_sanity(e: Env) { + let to: Address = nondet_address(); + clog!(cvlr_soroban::Addr(&to)); + let from = nondet_address(); + clog!(cvlr_soroban::Addr(&from)); + let spender = nondet_address(); + clog!(cvlr_soroban::Addr(&spender)); + let amount:i128 = nondet(); + clog!(amount); + cvlr_assume!(is_auth(spender.clone())); + storage_setup_balance(e.clone(), from.clone()); + let balance_from = Base::balance(&e, &from); + clog!(balance_from); + cvlr_assume!(balance_from >= amount); + storage_setup_allowance(e.clone(), from.clone(), spender.clone()); + let allowance_spender = Base::allowance(&e, &from, &spender); + clog!(allowance_spender); + cvlr_assume!(allowance_spender >= amount); + cvlr_assume!(amount >= 0); + Base::transfer_from(&e, &spender, &from, &to, amount); + cvlr_satisfy!(true); +} + +#[rule] +// requires +// owner auth +// amount >= 0 +// valid live until ledger +// status: verified +pub fn approve_non_panic(e: Env) { + let owner = nondet_address(); + clog!(cvlr_soroban::Addr(&owner)); + let spender = nondet_address(); + clog!(cvlr_soroban::Addr(&spender)); + let amount:i128 = nondet(); + clog!(amount); + let live_until_ledger = nondet(); + clog!(live_until_ledger); + cvlr_assume!(is_auth(owner.clone())); + cvlr_assume!(amount >= 0); + let current_ledger = e.ledger().sequence(); + let max_live_until_ledger = e.ledger().max_live_until_ledger(); + let non_zero_amount = amount > 0; + let ledger_more_than_max = live_until_ledger > max_live_until_ledger; + let ledger_less_than_current = live_until_ledger < current_ledger; + cvlr_assume!(!ledger_more_than_max); + cvlr_assume!(!(non_zero_amount && ledger_less_than_current)); + Base::approve(&e, &owner, &spender, amount, live_until_ledger); + cvlr_assert!(true); +} + + +#[rule] +// sanity +// status: verified +pub fn approve_non_panic_sanity(e: Env) { + let owner = nondet_address(); + clog!(cvlr_soroban::Addr(&owner)); + let spender = nondet_address(); + clog!(cvlr_soroban::Addr(&spender)); + let amount:i128 = nondet(); + clog!(amount); + let live_until_ledger = nondet(); + clog!(live_until_ledger); + cvlr_assume!(is_auth(owner.clone())); + cvlr_assume!(amount >= 0); + let current_ledger = e.ledger().sequence(); + let max_live_until_ledger = e.ledger().max_live_until_ledger(); + let non_zero_amount = amount > 0; + let ledger_more_than_max = live_until_ledger > max_live_until_ledger; + let ledger_less_than_current = live_until_ledger < current_ledger; + cvlr_assume!(!ledger_more_than_max); + cvlr_assume!(!(non_zero_amount && ledger_less_than_current)); + Base::approve(&e, &owner, &spender, amount, live_until_ledger); + cvlr_satisfy!(true); +} \ No newline at end of file diff --git a/packages/tokens/src/fungible/specs/fungible_panics.rs b/packages/tokens/src/fungible/specs/fungible_panics.rs new file mode 100644 index 00000000..74b82257 --- /dev/null +++ b/packages/tokens/src/fungible/specs/fungible_panics.rs @@ -0,0 +1,196 @@ +use cvlr::{cvlr_assert, cvlr_satisfy,cvlr_assume, nondet::*}; +use cvlr_soroban::{nondet_address, is_auth}; +use cvlr::clog; +use cvlr_soroban_derive::rule; +use soroban_sdk::{Address, Env}; +use crate::fungible::FungibleToken; +use crate::fungible::Base; + +#[rule] +// transfer panics if from does not auth +// status: verified +pub fn transfer_panics_if_unauthorized(e: Env) { + let to = nondet_address(); + clog!(cvlr_soroban::Addr(&to)); + let from = nondet_address(); + clog!(cvlr_soroban::Addr(&from)); + let amount:i128 = nondet(); + clog!(amount); + cvlr_assume!(!is_auth(from.clone())); + Base::transfer(&e, &from, &to, amount); + cvlr_assert!(false); +} + +#[rule] +// transfer panics if not enough balance +// status: verified +pub fn transfer_panics_if_not_enough_balance(e: Env) { + let to = nondet_address(); + clog!(cvlr_soroban::Addr(&to)); + let from = nondet_address(); + clog!(cvlr_soroban::Addr(&from)); + let amount:i128 = nondet(); + clog!(amount); + let balance = Base::balance(&e, &from); + clog!(balance); + cvlr_assume!(balance < amount); + Base::transfer(&e, &from, &to, amount); + cvlr_assert!(false); +} + +#[rule] +// transfer panics if amount < 0 +// status: verified +pub fn transfer_panics_if_amount_less_than_zero(e: Env) { + let to = nondet_address(); + clog!(cvlr_soroban::Addr(&to)); + let from = nondet_address(); + clog!(cvlr_soroban::Addr(&from)); + let amount:i128 = nondet(); + clog!(amount); + cvlr_assume!(amount < 0); + Base::transfer(&e, &from, &to, amount); + cvlr_assert!(false); +} + +#[rule] +// transfer_from panics if spender does not auth +// status: verified +pub fn transfer_from_panics_if_spender_unauthorized(e: Env) { + let to = nondet_address(); + clog!(cvlr_soroban::Addr(&to)); + let spender = nondet_address(); + clog!(cvlr_soroban::Addr(&spender)); + let from = nondet_address(); + clog!(cvlr_soroban::Addr(&from)); + let amount:i128 = nondet(); + clog!(amount); + cvlr_assume!(!is_auth(spender.clone())); + Base::transfer_from(&e, &spender, &from, &to, amount); + cvlr_assert!(false); +} + +#[rule] +// transfer_from panics if not enough balance +// status: verified +pub fn transfer_from_panics_if_not_enough_balance(e: Env) { + let to = nondet_address(); + clog!(cvlr_soroban::Addr(&to)); + let spender = nondet_address(); + clog!(cvlr_soroban::Addr(&spender)); + let from = nondet_address(); + clog!(cvlr_soroban::Addr(&from)); + let amount:i128 = nondet(); + clog!(amount); + let balance = Base::balance(&e, &from); + clog!(balance); + cvlr_assume!(balance < amount); + Base::transfer_from(&e, &spender, &from, &to, amount); + cvlr_assert!(false); +} + +#[rule] +// transfer_from panics if not enough allowance and spender != from +// status: bug +pub fn transfer_from_panics_if_not_enough_allowance(e: Env) { + let to = nondet_address(); + clog!(cvlr_soroban::Addr(&to)); + let spender = nondet_address(); + clog!(cvlr_soroban::Addr(&spender)); + let from = nondet_address(); + clog!(cvlr_soroban::Addr(&from)); + let amount:i128 = nondet(); + clog!(amount); + let allowance = Base::allowance(&e, &from, &spender); + clog!(allowance); + cvlr_assume!(allowance < amount); + cvlr_assume!(spender != from); + Base::transfer_from(&e, &spender, &from, &to, amount); + cvlr_assert!(false); +} + +#[rule] +// transfer_from panics if amount < 0 +// status: verified +pub fn transfer_from_panics_if_amount_less_than_zero(e: Env) { + let to = nondet_address(); + clog!(cvlr_soroban::Addr(&to)); + let spender = nondet_address(); + clog!(cvlr_soroban::Addr(&spender)); + let from = nondet_address(); + clog!(cvlr_soroban::Addr(&from)); + let amount:i128 = nondet(); + clog!(amount); + cvlr_assume!(amount < 0); + Base::transfer_from(&e, &spender, &from, &to, amount); + cvlr_assert!(false); +} + +#[rule] +// approve panics if owner does not auth +// status: verified +pub fn approve_panics_if_unauthorized(e: Env) { + let owner = nondet_address(); + clog!(cvlr_soroban::Addr(&owner)); + let spender = nondet_address(); + clog!(cvlr_soroban::Addr(&spender)); + let live_until_ledger = u32::nondet(); + clog!(live_until_ledger); + let amount:i128 = nondet(); + clog!(amount); + cvlr_assume!(!is_auth(owner.clone())); + Base::approve(&e, &owner, &spender, amount, live_until_ledger); + cvlr_assert!(false); +} + +#[rule] +// approve panics if amount < 0 +// status: verified +pub fn approve_panics_if_amount_less_than_zero(e: Env) { + let owner = nondet_address(); + clog!(cvlr_soroban::Addr(&owner)); + let spender = nondet_address(); + clog!(cvlr_soroban::Addr(&spender)); + let live_until_ledger = u32::nondet(); + clog!(live_until_ledger); + let amount:i128 = nondet(); + clog!(amount); + cvlr_assume!(amount < 0); + Base::approve(&e, &owner, &spender, amount, live_until_ledger); + cvlr_assert!(false); +} + +#[rule] +// approve panics if live_until_ledger > max_ledger +// status: verified +pub fn approve_panics_if_live_until_ledger_greater_than_max_ledger(e: Env) { + let owner = nondet_address(); + clog!(cvlr_soroban::Addr(&owner)); + let spender = nondet_address(); + clog!(cvlr_soroban::Addr(&spender)); + let amount:i128 = nondet(); + clog!(amount); + let live_until_ledger = u32::nondet(); + clog!(live_until_ledger); + cvlr_assume!(live_until_ledger > e.ledger().max_live_until_ledger()); + Base::approve(&e, &owner, &spender, amount, live_until_ledger); + cvlr_assert!(false); +} + +#[rule] +// approve panics if live_until_ledger < current_ledger & amount > 0 +// status: verified +pub fn approve_panics_if_live_until_ledger_less_than_current_ledger(e: Env) { + let owner = nondet_address(); + clog!(cvlr_soroban::Addr(&owner)); + let spender = nondet_address(); + clog!(cvlr_soroban::Addr(&spender)); + let amount:i128 = nondet(); + clog!(amount); + let live_until_ledger = u32::nondet(); + clog!(live_until_ledger); + cvlr_assume!(live_until_ledger < e.ledger().sequence()); + cvlr_assume!(amount > 0); + Base::approve(&e, &owner, &spender, amount, live_until_ledger); + cvlr_assert!(false); +} \ No newline at end of file diff --git a/packages/tokens/src/fungible/specs/fungible_sanity.rs b/packages/tokens/src/fungible/specs/fungible_sanity.rs new file mode 100644 index 00000000..27220f87 --- /dev/null +++ b/packages/tokens/src/fungible/specs/fungible_sanity.rs @@ -0,0 +1,75 @@ +use cvlr::{cvlr_assert, cvlr_satisfy, nondet::*}; +use cvlr_soroban::nondet_address; +use cvlr_soroban_derive::rule; +use soroban_sdk::{Address, Env}; +use crate::fungible::FungibleToken; +use crate::fungible::Base; + +#[rule] +pub fn total_supply_sanity(e: Env) { + let _ = Base::total_supply(&e); + cvlr_satisfy!(true); +} + +#[rule] +pub fn balance_sanity(e: Env) { + let account = nondet_address(); + let _ = Base::balance(&e, &account); + cvlr_satisfy!(true); +} + +#[rule] +pub fn allowance_sanity(e: Env) { + let owner = nondet_address(); + let spender = nondet_address(); + let _ = Base::allowance(&e, &owner, &spender); + cvlr_satisfy!(true); +} + +#[rule] +pub fn transfer_sanity(e: Env) { + let to = nondet_address(); + let from = nondet_address(); + let amount = nondet(); + Base::transfer(&e, &from, &to, amount); + cvlr_satisfy!(true); +} + +#[rule] +pub fn transfer_from_sanity(e: Env) { + let spender = nondet_address(); + let to = nondet_address(); + let from = nondet_address(); + let amount = nondet(); + Base::transfer_from(&e, &spender, &from, &to, amount); + cvlr_satisfy!(true); +} + +#[rule] +pub fn approve_sanity(e: Env) { + let owner = nondet_address(); + let spender = nondet_address(); + let amount = nondet(); + let until = nondet(); + Base::approve(&e, &owner, &spender, amount, until); + cvlr_satisfy!(true); +} + +#[rule] +pub fn decimals_sanity(e: Env) { + Base::decimals(&e); + cvlr_satisfy!(true); +} + +#[rule] +pub fn name_sanity(e: Env) { + Base::name(&e); + cvlr_satisfy!(true); +} + +#[rule] +pub fn symbol_sanity(e: Env) { + Base::symbol(&e); + cvlr_satisfy!(true); +} + diff --git a/packages/tokens/src/fungible/specs/mod.rs b/packages/tokens/src/fungible/specs/mod.rs new file mode 100644 index 00000000..269115a3 --- /dev/null +++ b/packages/tokens/src/fungible/specs/mod.rs @@ -0,0 +1,11 @@ +pub mod fungible_sanity; +pub mod fungible_integrity; +pub mod fungible_panics; +pub mod fungible_non_panics; +pub mod fungible_invariants; +pub mod allowlist; +pub mod blocklist; +pub mod capped; +pub mod capped_contract; +pub mod burnable; +// NOTE: may need to add rules for sac_admin related features. \ No newline at end of file diff --git a/packages/tokens/src/fungible/storage.rs b/packages/tokens/src/fungible/storage.rs index d890c685..f627c483 100644 --- a/packages/tokens/src/fungible/storage.rs +++ b/packages/tokens/src/fungible/storage.rs @@ -1,7 +1,13 @@ use soroban_sdk::{contracttype, panic_with_error, symbol_short, Address, Env, String, Symbol}; +#[cfg(not(feature = "certora"))] use crate::fungible::{ - emit_approve, emit_mint, emit_transfer, Base, FungibleTokenError, BALANCE_EXTEND_AMOUNT, + emit_approve, emit_mint, emit_transfer,}; + +use cvlr::clog; + +use crate::fungible::{ + Base, FungibleTokenError, BALANCE_EXTEND_AMOUNT, BALANCE_TTL_THRESHOLD, }; @@ -211,6 +217,7 @@ impl Base { ) { owner.require_auth(); Base::set_allowance(e, owner, spender, amount, live_until_ledger); + #[cfg(not(feature = "certora"))] emit_approve(e, owner, spender, amount, live_until_ledger); } @@ -308,6 +315,9 @@ impl Base { } let allowance = Base::allowance_data(e, owner, spender); + clog!(allowance.amount); + clog!(amount); + // maybe this is a bug? because it doesn't consider live_until_ledger, should be allowance() instead? if allowance.amount < amount { panic_with_error!(e, FungibleTokenError::InsufficientAllowance); @@ -348,6 +358,7 @@ impl Base { pub fn transfer(e: &Env, from: &Address, to: &Address, amount: i128) { from.require_auth(); Base::update(e, Some(from), Some(to), amount); + #[cfg(not(feature = "certora"))] emit_transfer(e, from, to, amount); } @@ -381,6 +392,7 @@ impl Base { spender.require_auth(); Base::spend_allowance(e, from, spender, amount); Base::update(e, Some(from), Some(to), amount); + #[cfg(not(feature = "certora"))] emit_transfer(e, from, to, amount); } @@ -476,6 +488,7 @@ impl Base { /// ``` pub fn mint(e: &Env, to: &Address, amount: i128) { Base::update(e, None, Some(to), amount); + #[cfg(not(feature = "certora"))] emit_mint(e, to, amount); } diff --git a/packages/tokens/src/lib.rs b/packages/tokens/src/lib.rs index 4eb3ebb7..e05b9a42 100644 --- a/packages/tokens/src/lib.rs +++ b/packages/tokens/src/lib.rs @@ -12,7 +12,8 @@ //! working with the respective token type. #![no_std] - +#![cfg_attr(feature = "certora", allow(unused_variables, unused_imports, dead_code))] pub mod fungible; pub mod non_fungible; pub mod rwa; +pub mod vault; diff --git a/packages/tokens/src/non_fungible/extensions/burnable/mod.rs b/packages/tokens/src/non_fungible/extensions/burnable/mod.rs index b126fcac..11a97384 100644 --- a/packages/tokens/src/non_fungible/extensions/burnable/mod.rs +++ b/packages/tokens/src/non_fungible/extensions/burnable/mod.rs @@ -4,7 +4,13 @@ use crate::non_fungible::NonFungibleToken; #[cfg(test)] mod test; -use soroban_sdk::{contractevent, Address, Env}; +#[cfg(not(feature = "certora"))] +use soroban_sdk::{contractevent}; + +#[cfg(feature = "certora")] +use cvlr_soroban_derive::contractevent; + +use soroban_sdk::{Address, Env}; /// Burnable Trait for Non-Fungible Token /// @@ -123,6 +129,7 @@ pub struct Burn { /// * `e` - The Soroban environment. /// * `from` - The sender address. /// * `token_id` - The token identifier. +#[cfg(not(feature = "certora"))] pub fn emit_burn(e: &Env, from: &Address, token_id: u32) { Burn { from: from.clone(), token_id }.publish(e); } diff --git a/packages/tokens/src/non_fungible/extensions/burnable/storage.rs b/packages/tokens/src/non_fungible/extensions/burnable/storage.rs index 4d2a82da..26c7cc1c 100644 --- a/packages/tokens/src/non_fungible/extensions/burnable/storage.rs +++ b/packages/tokens/src/non_fungible/extensions/burnable/storage.rs @@ -1,6 +1,9 @@ use soroban_sdk::{Address, Env}; -use crate::non_fungible::{burnable::emit_burn, Base}; +#[cfg(not(feature = "certora"))] +use crate::non_fungible::{burnable::emit_burn}; + +use crate::non_fungible::{Base}; impl Base { /// Destroys the token with `token_id` from `from`, ensuring ownership @@ -27,6 +30,7 @@ impl Base { pub fn burn(e: &Env, from: &Address, token_id: u32) { from.require_auth(); Base::update(e, Some(from), None, token_id); + #[cfg(not(feature = "certora"))] emit_burn(e, from, token_id); } @@ -58,6 +62,7 @@ impl Base { spender.require_auth(); Base::check_spender_approval(e, spender, from, token_id); Base::update(e, Some(from), None, token_id); + #[cfg(not(feature = "certora"))] emit_burn(e, from, token_id); } } diff --git a/packages/tokens/src/non_fungible/extensions/consecutive/mod.rs b/packages/tokens/src/non_fungible/extensions/consecutive/mod.rs index 89ef5d19..0fbc8aec 100644 --- a/packages/tokens/src/non_fungible/extensions/consecutive/mod.rs +++ b/packages/tokens/src/non_fungible/extensions/consecutive/mod.rs @@ -62,7 +62,14 @@ //! in this extension must be used. Using other minting functions will break //! the logic of tracking ownership. pub mod storage; -use soroban_sdk::{contractevent, Address, Env}; + +#[cfg(not(feature = "certora"))] +use soroban_sdk::{contractevent}; + +#[cfg(feature = "certora")] +use cvlr_soroban_derive::contractevent; + +use soroban_sdk::{Address}; pub use storage::Consecutive; use crate::non_fungible::NonFungibleToken; @@ -100,6 +107,7 @@ pub struct ConsecutiveMint { /// * `to` - The recipient address. /// * `from_token_id` - The starting token identifier. /// * `to_token_id` - The ending token identifier. +#[cfg(not(feature = "certora"))] pub fn emit_consecutive_mint(e: &Env, to: &Address, from_token_id: u32, to_token_id: u32) { ConsecutiveMint { to: to.clone(), from_token_id, to_token_id }.publish(e); } diff --git a/packages/tokens/src/non_fungible/extensions/consecutive/storage.rs b/packages/tokens/src/non_fungible/extensions/consecutive/storage.rs index a4d4d93f..68f8682d 100644 --- a/packages/tokens/src/non_fungible/extensions/consecutive/storage.rs +++ b/packages/tokens/src/non_fungible/extensions/consecutive/storage.rs @@ -2,10 +2,13 @@ use core::mem; use soroban_sdk::{contracttype, panic_with_error, Address, Env, String, TryFromVal, Val, Vec}; +#[cfg(not(feature = "certora"))] use crate::non_fungible::{ burnable::emit_burn, emit_transfer, - extensions::consecutive::emit_consecutive_mint, +}; + +use crate::non_fungible::{ sequential::{self as sequential}, Base, ContractOverrides, NonFungibleTokenError, OWNERSHIP_EXTEND_AMOUNT, OWNERSHIP_TTL_THRESHOLD, OWNER_EXTEND_AMOUNT, OWNER_TTL_THRESHOLD, TOKEN_EXTEND_AMOUNT, @@ -211,7 +214,7 @@ impl Consecutive { Self::set_ownership_in_bucket(e, last_id); e.storage().persistent().set(&NFTConsecutiveStorageKey::Owner(last_id), &to); - + #[cfg(not(feature = "certora"))] emit_consecutive_mint(e, to, first_id, last_id); // return the last minted id @@ -243,6 +246,7 @@ impl Consecutive { from.require_auth(); Consecutive::update(e, Some(from), None, token_id); + #[cfg(not(feature = "certora"))] emit_burn(e, from, token_id); } @@ -276,6 +280,7 @@ impl Consecutive { Base::check_spender_approval(e, spender, from, token_id); Consecutive::update(e, Some(from), None, token_id); + #[cfg(not(feature = "certora"))] emit_burn(e, from, token_id); } @@ -306,6 +311,7 @@ impl Consecutive { from.require_auth(); Consecutive::update(e, Some(from), Some(to), token_id); + #[cfg(not(feature = "certora"))] emit_transfer(e, from, to, token_id); } @@ -341,6 +347,7 @@ impl Consecutive { Base::check_spender_approval(e, spender, from, token_id); Consecutive::update(e, Some(from), Some(to), token_id); + #[cfg(not(feature = "certora"))] emit_transfer(e, from, to, token_id); } diff --git a/packages/tokens/src/non_fungible/extensions/enumerable/storage.rs b/packages/tokens/src/non_fungible/extensions/enumerable/storage.rs index cb9684e4..1cb70fb4 100644 --- a/packages/tokens/src/non_fungible/extensions/enumerable/storage.rs +++ b/packages/tokens/src/non_fungible/extensions/enumerable/storage.rs @@ -1,7 +1,12 @@ use soroban_sdk::{contracttype, panic_with_error, Address, Env}; +#[cfg(not(feature = "certora"))] use crate::non_fungible::{ - emit_mint, Base, ContractOverrides, NonFungibleTokenError, OWNER_EXTEND_AMOUNT, + emit_mint, +}; + +use crate::non_fungible::{ + Base, ContractOverrides, NonFungibleTokenError, OWNER_EXTEND_AMOUNT, OWNER_TTL_THRESHOLD, TOKEN_EXTEND_AMOUNT, TOKEN_TTL_THRESHOLD, }; @@ -199,6 +204,7 @@ impl Enumerable { /// implemented accordingly. pub fn non_sequential_mint(e: &Env, to: &Address, token_id: u32) { Base::update(e, None, Some(to), token_id); + #[cfg(not(feature = "certora"))] emit_mint(e, to, token_id); Enumerable::add_to_enumerations(e, to, token_id); diff --git a/packages/tokens/src/non_fungible/extensions/royalties/mod.rs b/packages/tokens/src/non_fungible/extensions/royalties/mod.rs index c032e83c..3f41992a 100644 --- a/packages/tokens/src/non_fungible/extensions/royalties/mod.rs +++ b/packages/tokens/src/non_fungible/extensions/royalties/mod.rs @@ -4,7 +4,13 @@ use crate::non_fungible::NonFungibleToken; #[cfg(test)] mod test; -use soroban_sdk::{contractevent, Address, Env}; +#[cfg(not(feature = "certora"))] +use soroban_sdk::{contractevent}; + +#[cfg(feature = "certora")] +use cvlr_soroban_derive::contractevent; + +use soroban_sdk::{Address, Env}; /// Royalties Trait for Non-Fungible Token (ERC2981) /// @@ -155,6 +161,7 @@ pub struct SetDefaultRoyalty { /// * `e` - The Soroban environment. /// * `receiver` - The royalty receiver address. /// * `basis_points` - The royalty basis points. +#[cfg(not(feature = "certora"))] pub fn emit_set_default_royalty(e: &Env, receiver: &Address, basis_points: u32) { SetDefaultRoyalty { receiver: receiver.clone(), basis_points }.publish(e); } @@ -178,6 +185,7 @@ pub struct SetTokenRoyalty { /// * `receiver` - The royalty receiver address. /// * `token_id` - The token identifier. /// * `basis_points` - The royalty basis points. +#[cfg(not(feature = "certora"))] pub fn emit_set_token_royalty(e: &Env, receiver: &Address, token_id: u32, basis_points: u32) { SetTokenRoyalty { receiver: receiver.clone(), token_id, basis_points }.publish(e); } @@ -196,6 +204,7 @@ pub struct RemoveTokenRoyalty { /// /// * `e` - The Soroban environment. /// * `token_id` - The token identifier. +#[cfg(not(feature = "certora"))] pub fn emit_remove_token_royalty(e: &Env, token_id: u32) { RemoveTokenRoyalty { token_id }.publish(e); } diff --git a/packages/tokens/src/non_fungible/extensions/royalties/storage.rs b/packages/tokens/src/non_fungible/extensions/royalties/storage.rs index c42b4308..e81c5680 100644 --- a/packages/tokens/src/non_fungible/extensions/royalties/storage.rs +++ b/packages/tokens/src/non_fungible/extensions/royalties/storage.rs @@ -1,7 +1,12 @@ use soroban_sdk::{contracttype, panic_with_error, Address, Env}; +#[cfg(not(feature = "certora"))] use crate::non_fungible::{ royalties::{emit_set_default_royalty, emit_set_token_royalty}, +}; + + +use crate::non_fungible::{ Base, NonFungibleTokenError, OWNER_EXTEND_AMOUNT, OWNER_TTL_THRESHOLD, }; @@ -56,7 +61,7 @@ impl Base { let key = NFTRoyaltiesStorageKey::DefaultRoyalty; let royalty_info = RoyaltyInfo { receiver: receiver.clone(), basis_points }; e.storage().instance().set(&key, &royalty_info); - + #[cfg(not(feature = "certora"))] emit_set_default_royalty(e, receiver, basis_points); } @@ -101,7 +106,7 @@ impl Base { let key = NFTRoyaltiesStorageKey::TokenRoyalty(token_id); let royalty_info = RoyaltyInfo { receiver: receiver.clone(), basis_points }; e.storage().persistent().set(&key, &royalty_info); - + #[cfg(not(feature = "certora"))] emit_set_token_royalty(e, receiver, token_id, basis_points); } @@ -134,7 +139,7 @@ impl Base { // Remove the token royalty information let key = NFTRoyaltiesStorageKey::TokenRoyalty(token_id); e.storage().persistent().remove(&key); - + #[cfg(not(feature = "certora"))] super::emit_remove_token_royalty(e, token_id); } diff --git a/packages/tokens/src/non_fungible/mod.rs b/packages/tokens/src/non_fungible/mod.rs index 9f1c7cd3..f980c188 100644 --- a/packages/tokens/src/non_fungible/mod.rs +++ b/packages/tokens/src/non_fungible/mod.rs @@ -72,10 +72,19 @@ mod utils; #[cfg(test)] mod test; +#[cfg(feature = "certora")] +pub mod specs; + +#[cfg(not(feature = "certora"))] +use soroban_sdk::{contractevent}; + +#[cfg(feature = "certora")] +use cvlr_soroban_derive::contractevent; + pub use extensions::{burnable, consecutive, enumerable, royalties}; pub use overrides::{Base, ContractOverrides}; // ################## TRAIT ################## -use soroban_sdk::{contracterror, contractevent, Address, Env, String}; +use soroban_sdk::{contracterror, Address, Env, String}; pub use storage::{ApprovalData, NFTStorageKey}; pub use utils::sequential; @@ -420,6 +429,7 @@ pub struct Transfer { /// * `from` - The sender address. /// * `to` - The recipient address. /// * `token_id` - The token identifier. +#[cfg(not(feature = "certora"))] pub fn emit_transfer(e: &Env, from: &Address, to: &Address, token_id: u32) { Transfer { from: from.clone(), to: to.clone(), token_id }.publish(e); } @@ -446,6 +456,7 @@ pub struct Approve { /// * `approved` - The approved address. /// * `token_id` - The token identifier. /// * `live_until_ledger` - The ledger number until which the approval is valid. +#[cfg(not(feature = "certora"))] pub fn emit_approve( e: &Env, approver: &Address, @@ -476,6 +487,7 @@ pub struct ApproveForAll { /// * `owner` - The owner address. /// * `operator` - The operator address. /// * `live_until_ledger` - The ledger number until which the approval is valid. +#[cfg(not(feature = "certora"))] pub fn emit_approve_for_all(e: &Env, owner: &Address, operator: &Address, live_until_ledger: u32) { ApproveForAll { owner: owner.clone(), operator: operator.clone(), live_until_ledger } .publish(e); @@ -497,6 +509,7 @@ pub struct Mint { /// * `e` - The Soroban environment. /// * `to` - The recipient address. /// * `token_id` - The token identifier. +#[cfg(not(feature = "certora"))] pub fn emit_mint(e: &Env, to: &Address, token_id: u32) { Mint { to: to.clone(), token_id }.publish(e); } diff --git a/packages/tokens/src/non_fungible/specs/burnable_nft_contract.rs b/packages/tokens/src/non_fungible/specs/burnable_nft_contract.rs new file mode 100644 index 00000000..71152143 --- /dev/null +++ b/packages/tokens/src/non_fungible/specs/burnable_nft_contract.rs @@ -0,0 +1,70 @@ +use soroban_sdk::{Env, Address}; + +use crate::non_fungible::{Base, NonFungibleToken, burnable::NonFungibleBurnable}; + +pub struct BurnableNft; + + +impl NonFungibleToken for BurnableNft { + type ContractType = Base; + + fn balance(e: &Env, account: Address) -> u32 { + Base::balance(e, &account) + } + + fn owner_of(e: &Env, token_id: u32) -> Address { + Base::owner_of(e, token_id) + } + + fn transfer(e: &Env, from: Address, to: Address, token_id: u32) { + Base::transfer(e, &from, &to, token_id); + } + + fn transfer_from(e: &Env, spender: Address, from: Address, to: Address, token_id: u32) { + Base::transfer_from(e, &spender, &from, &to, token_id); + } + + fn approve( + e: &Env, + approver: Address, + approved: Address, + token_id: u32, + live_until_ledger: u32, + ) { + Base::approve(e, &approver, &approved, token_id, live_until_ledger); + } + + fn approve_for_all(e: &Env, owner: Address, operator: Address, live_until_ledger: u32) { + Base::approve_for_all(e, &owner, &operator, live_until_ledger); + } + + fn get_approved(e: &Env, token_id: u32) -> Option
{ + Base::get_approved(e, token_id) + } + + fn is_approved_for_all(e: &Env, owner: Address, operator: Address) -> bool { + Base::is_approved_for_all(e, &owner, &operator) + } + + fn name(e: &Env) -> soroban_sdk::String { + Base::name(e) + } + + fn symbol(e: &Env) -> soroban_sdk::String { + Base::symbol(e) + } + + fn token_uri(e: &Env, token_id: u32) -> soroban_sdk::String { + Base::token_uri(e, token_id) + } +} + +impl NonFungibleBurnable for BurnableNft { + fn burn(e: &Env, from: Address, token_id: u32) { + Base::burn(e, &from, token_id); + } + + fn burn_from(e: &Env, spender: Address, from: Address, token_id: u32) { + Base::burn_from(e, &spender, &from, token_id); + } +} \ No newline at end of file diff --git a/packages/tokens/src/non_fungible/specs/burnable_nft_sanity.rs b/packages/tokens/src/non_fungible/specs/burnable_nft_sanity.rs new file mode 100644 index 00000000..7cc93420 --- /dev/null +++ b/packages/tokens/src/non_fungible/specs/burnable_nft_sanity.rs @@ -0,0 +1,110 @@ +use cvlr::{cvlr_satisfy, nondet::*}; +use cvlr_soroban::nondet_address; +use cvlr_soroban_derive::rule; +use soroban_sdk::{Env, Address}; + +use crate::non_fungible::specs::burnable_nft_contract::BurnableNft; +use crate::non_fungible::{NonFungibleToken, burnable::NonFungibleBurnable}; + +#[rule] +pub fn burnable_nft_balance_sanity(e: Env) { + let account: Address = nondet_address(); + let _ = BurnableNft::balance(&e, account); + cvlr_satisfy!(true); +} + +#[rule] +pub fn burnable_nft_owner_of_sanity(e: Env) { + let token_id: u32 = nondet(); + let _ = BurnableNft::owner_of(&e, token_id); + cvlr_satisfy!(true); +} + +#[rule] +pub fn burnable_nft_transfer_sanity(e: Env) { + let from: Address = nondet_address(); + let to: Address = nondet_address(); + let token_id: u32 = nondet(); + BurnableNft::transfer(&e, from, to, token_id); + cvlr_satisfy!(true); +} + +#[rule] +pub fn burnable_nft_transfer_from_sanity(e: Env) { + let spender: Address = nondet_address(); + let from: Address = nondet_address(); + let to: Address = nondet_address(); + let token_id: u32 = nondet(); + BurnableNft::transfer_from(&e, spender, from, to, token_id); + cvlr_satisfy!(true); +} + +#[rule] +pub fn burnable_nft_approve_sanity(e: Env) { + let approver: Address = nondet_address(); + let approved: Address = nondet_address(); + let token_id: u32 = nondet(); + let live_until_ledger: u32 = nondet(); + BurnableNft::approve(&e, approver, approved, token_id, live_until_ledger); + cvlr_satisfy!(true); +} + +#[rule] +pub fn burnable_nft_approve_for_all_sanity(e: Env) { + let owner: Address = nondet_address(); + let operator: Address = nondet_address(); + let live_until_ledger: u32 = nondet(); + BurnableNft::approve_for_all(&e, owner, operator, live_until_ledger); + cvlr_satisfy!(true); +} + +#[rule] +pub fn burnable_nft_get_approved_sanity(e: Env) { + let token_id: u32 = nondet(); + let _ = BurnableNft::get_approved(&e, token_id); + cvlr_satisfy!(true); +} + +#[rule] +pub fn burnable_nft_is_approved_for_all_sanity(e: Env) { + let owner: Address = nondet_address(); + let operator: Address = nondet_address(); + let _ = BurnableNft::is_approved_for_all(&e, owner, operator); + cvlr_satisfy!(true); +} + +#[rule] +pub fn burnable_nft_name_sanity(e: Env) { + let _ = BurnableNft::name(&e); + cvlr_satisfy!(true); +} + +#[rule] +pub fn burnable_nft_symbol_sanity(e: Env) { + let _ = BurnableNft::symbol(&e); + cvlr_satisfy!(true); +} + +#[rule] +pub fn burnable_nft_token_uri_sanity(e: Env) { + let token_id: u32 = nondet(); + let _ = BurnableNft::token_uri(&e, token_id); + cvlr_satisfy!(true); +} + +#[rule] +pub fn burnable_nft_burn_sanity(e: Env) { + let from: Address = nondet_address(); + let token_id: u32 = nondet(); + BurnableNft::burn(&e, from, token_id); + cvlr_satisfy!(true); +} + +#[rule] +pub fn burnable_nft_burn_from_sanity(e: Env) { + let spender: Address = nondet_address(); + let from: Address = nondet_address(); + let token_id: u32 = nondet(); + BurnableNft::burn_from(&e, spender, from, token_id); + cvlr_satisfy!(true); +} diff --git a/packages/tokens/src/non_fungible/specs/consecutive_nft_contract.rs b/packages/tokens/src/non_fungible/specs/consecutive_nft_contract.rs new file mode 100644 index 00000000..e8d0640a --- /dev/null +++ b/packages/tokens/src/non_fungible/specs/consecutive_nft_contract.rs @@ -0,0 +1,79 @@ +use soroban_sdk::{Address, Env}; + +use crate::non_fungible::{ContractOverrides, NonFungibleToken, burnable::NonFungibleBurnable, consecutive::{Consecutive, NonFungibleConsecutive}}; + +pub struct ConsecutiveNft; + +impl NonFungibleToken for ConsecutiveNft { + type ContractType = Consecutive; + + fn balance(e: &Env, account: Address) -> u32 { + Consecutive::balance(e, &account) + } + + fn owner_of(e: &Env, token_id: u32) -> Address { + Consecutive::owner_of(e, token_id) + } + + fn transfer(e: &Env, from: Address, to: Address, token_id: u32) { + Consecutive::transfer(e, &from, &to, token_id); + } + + fn transfer_from(e: &Env, spender: Address, from: Address, to: Address, token_id: u32) { + Consecutive::transfer_from(e, &spender, &from, &to, token_id); + } + + fn approve( + e: &Env, + approver: Address, + approved: Address, + token_id: u32, + live_until_ledger: u32, + ) { + Consecutive::approve(e, &approver, &approved, token_id, live_until_ledger); + } + + fn approve_for_all(e: &Env, owner: Address, operator: Address, live_until_ledger: u32) { + Consecutive::approve_for_all(e, &owner, &operator, live_until_ledger); + } + + fn get_approved(e: &Env, token_id: u32) -> Option
{ + Consecutive::get_approved(e, token_id) + } + + fn is_approved_for_all(e: &Env, owner: Address, operator: Address) -> bool { + Consecutive::is_approved_for_all(e, &owner, &operator) + } + + fn name(e: &Env) -> soroban_sdk::String { + Consecutive::name(e) + } + + fn symbol(e: &Env) -> soroban_sdk::String { + Consecutive::symbol(e) + } + + fn token_uri(e: &Env, token_id: u32) -> soroban_sdk::String { + Consecutive::token_uri(e, token_id) + } +} + +impl NonFungibleBurnable for ConsecutiveNft { + fn burn(e: &Env, from: Address, token_id: u32) { + Consecutive::burn(e, &from, token_id); + } + + fn burn_from(e: &Env, spender: Address, from: Address, token_id: u32) { + Consecutive::burn_from(e, &spender, &from, token_id); + } +} +// TODO: This is just a basic call to batch_mint. May need additional setup depending on rule. +impl ConsecutiveNft { + pub fn batch_mint(e: &Env, to: Address, amount: u32) -> u32 { + Consecutive::batch_mint(e, &to, amount) + } +} + +impl NonFungibleConsecutive for ConsecutiveNft { + +} \ No newline at end of file diff --git a/packages/tokens/src/non_fungible/specs/consecutive_nft_sanity.rs b/packages/tokens/src/non_fungible/specs/consecutive_nft_sanity.rs new file mode 100644 index 00000000..082c6ed3 --- /dev/null +++ b/packages/tokens/src/non_fungible/specs/consecutive_nft_sanity.rs @@ -0,0 +1,118 @@ +use cvlr::{cvlr_satisfy, nondet::*}; +use cvlr_soroban::nondet_address; +use cvlr_soroban_derive::rule; +use soroban_sdk::{Env, Address}; + +use crate::non_fungible::specs::consecutive_nft_contract::ConsecutiveNft; +use crate::non_fungible::{NonFungibleToken, burnable::NonFungibleBurnable, consecutive::NonFungibleConsecutive}; + +#[rule] +pub fn consecutive_nft_balance_sanity(e: Env) { + let account: Address = nondet_address(); + let _ = ConsecutiveNft::balance(&e, account); + cvlr_satisfy!(true); +} + +#[rule] +pub fn consecutive_nft_owner_of_sanity(e: Env) { + let token_id: u32 = nondet(); + let _ = ConsecutiveNft::owner_of(&e, token_id); + cvlr_satisfy!(true); +} + +#[rule] +pub fn consecutive_nft_transfer_sanity(e: Env) { + let from: Address = nondet_address(); + let to: Address = nondet_address(); + let token_id: u32 = nondet(); + ConsecutiveNft::transfer(&e, from, to, token_id); + cvlr_satisfy!(true); +} + +#[rule] +pub fn consecutive_nft_transfer_from_sanity(e: Env) { + let spender: Address = nondet_address(); + let from: Address = nondet_address(); + let to: Address = nondet_address(); + let token_id: u32 = nondet(); + ConsecutiveNft::transfer_from(&e, spender, from, to, token_id); + cvlr_satisfy!(true); +} + +#[rule] +pub fn consecutive_nft_approve_sanity(e: Env) { + let approver: Address = nondet_address(); + let approved: Address = nondet_address(); + let token_id: u32 = nondet(); + let live_until_ledger: u32 = nondet(); + ConsecutiveNft::approve(&e, approver, approved, token_id, live_until_ledger); + cvlr_satisfy!(true); +} + +#[rule] +pub fn consecutive_nft_approve_for_all_sanity(e: Env) { + let owner: Address = nondet_address(); + let operator: Address = nondet_address(); + let live_until_ledger: u32 = nondet(); + ConsecutiveNft::approve_for_all(&e, owner, operator, live_until_ledger); + cvlr_satisfy!(true); +} + +#[rule] +pub fn consecutive_nft_get_approved_sanity(e: Env) { + let token_id: u32 = nondet(); + let _ = ConsecutiveNft::get_approved(&e, token_id); + cvlr_satisfy!(true); +} + +#[rule] +pub fn consecutive_nft_is_approved_for_all_sanity(e: Env) { + let owner: Address = nondet_address(); + let operator: Address = nondet_address(); + let _ = ConsecutiveNft::is_approved_for_all(&e, owner, operator); + cvlr_satisfy!(true); +} + +#[rule] +pub fn consecutive_nft_name_sanity(e: Env) { + let _ = ConsecutiveNft::name(&e); + cvlr_satisfy!(true); +} + +#[rule] +pub fn consecutive_nft_symbol_sanity(e: Env) { + let _ = ConsecutiveNft::symbol(&e); + cvlr_satisfy!(true); +} + +#[rule] +pub fn consecutive_nft_token_uri_sanity(e: Env) { + let token_id: u32 = nondet(); + let _ = ConsecutiveNft::token_uri(&e, token_id); + cvlr_satisfy!(true); +} + +#[rule] +pub fn consecutive_nft_burn_sanity(e: Env) { + let from: Address = nondet_address(); + let token_id: u32 = nondet(); + ConsecutiveNft::burn(&e, from, token_id); + cvlr_satisfy!(true); +} + +#[rule] +pub fn consecutive_nft_burn_from_sanity(e: Env) { + let spender: Address = nondet_address(); + let from: Address = nondet_address(); + let token_id: u32 = nondet(); + ConsecutiveNft::burn_from(&e, spender, from, token_id); + cvlr_satisfy!(true); +} + +#[rule] +pub fn consecutive_nft_batch_mint_sanity(e: Env) { + let to: Address = nondet_address(); + let amount: u32 = nondet(); + let _ = ConsecutiveNft::batch_mint(&e, to, amount); + cvlr_satisfy!(true); +} diff --git a/packages/tokens/src/non_fungible/specs/enumerable_nft_contract.rs b/packages/tokens/src/non_fungible/specs/enumerable_nft_contract.rs new file mode 100644 index 00000000..2a4a2a37 --- /dev/null +++ b/packages/tokens/src/non_fungible/specs/enumerable_nft_contract.rs @@ -0,0 +1,94 @@ +use soroban_sdk::{Address, Env, xdr::Enum}; + +use crate::{non_fungible::Base, non_fungible::{ContractOverrides, NonFungibleToken, burnable::NonFungibleBurnable, enumerable::{Enumerable, NonFungibleEnumerable}}}; + +pub struct EnumerableNft; + +impl NonFungibleToken for EnumerableNft { + type ContractType = Enumerable; + + fn balance(e: &Env, account: Address) -> u32 { + Enumerable::balance(e, &account) + } + + fn owner_of(e: &Env, token_id: u32) -> Address { + Enumerable::owner_of(e, token_id) + } + + fn transfer(e: &Env, from: Address, to: Address, token_id: u32) { + Enumerable::transfer(e, &from, &to, token_id); + } + + fn transfer_from(e: &Env, spender: Address, from: Address, to: Address, token_id: u32) { + Enumerable::transfer_from(e, &spender, &from, &to, token_id); + } + + fn approve( + e: &Env, + approver: Address, + approved: Address, + token_id: u32, + live_until_ledger: u32, + ) { + Enumerable::approve(e, &approver, &approved, token_id, live_until_ledger); + } + + fn approve_for_all(e: &Env, owner: Address, operator: Address, live_until_ledger: u32) { + Enumerable::approve_for_all(e, &owner, &operator, live_until_ledger); + } + + fn get_approved(e: &Env, token_id: u32) -> Option
{ + Enumerable::get_approved(e, token_id) + } + + fn is_approved_for_all(e: &Env, owner: Address, operator: Address) -> bool { + Enumerable::is_approved_for_all(e, &owner, &operator) + } + + fn name(e: &Env) -> soroban_sdk::String { + Enumerable::name(e) + } + + fn symbol(e: &Env) -> soroban_sdk::String { + Enumerable::symbol(e) + } + + fn token_uri(e: &Env, token_id: u32) -> soroban_sdk::String { + Enumerable::token_uri(e, token_id) + } +} + +impl NonFungibleBurnable for EnumerableNft { + fn burn(e: &Env, from: Address, token_id: u32) { + Enumerable::burn(e, &from, token_id); + } + + fn burn_from(e: &Env, spender: Address, from: Address, token_id: u32) { + Enumerable::burn_from(e, &spender, &from, token_id); + } +} + +impl NonFungibleEnumerable for EnumerableNft { + fn total_supply(e: &Env) -> u32 { + Enumerable::total_supply(e) + } + + fn get_owner_token_id(e: &Env, owner: Address, index: u32) -> u32 { + Enumerable::get_owner_token_id(e, &owner, index) + } + + fn get_token_id(e: &Env, index: u32) -> u32 { + Enumerable::get_token_id(e, index) + } +} + +// TODO: Just a basic calls to `sequential_mint` and `non_sequential_mint`. May need additional setup depending on rule. +impl EnumerableNft { + pub fn seq_mint(e: &Env, to: Address) -> u32 { + Enumerable::sequential_mint(e, &to) + } + + pub fn nonseq_mint(e: &Env, to: Address, token_id: u32) { + Enumerable::non_sequential_mint(e, &to, token_id) + } +} diff --git a/packages/tokens/src/non_fungible/specs/enumerable_nft_sanity.rs b/packages/tokens/src/non_fungible/specs/enumerable_nft_sanity.rs new file mode 100644 index 00000000..568d8b52 --- /dev/null +++ b/packages/tokens/src/non_fungible/specs/enumerable_nft_sanity.rs @@ -0,0 +1,146 @@ +use cvlr::{cvlr_satisfy, nondet::*}; +use cvlr_soroban::nondet_address; +use cvlr_soroban_derive::rule; +use soroban_sdk::{Env, Address}; + +use crate::non_fungible::specs::enumerable_nft_contract::EnumerableNft; +use crate::non_fungible::{NonFungibleToken, burnable::NonFungibleBurnable, enumerable::NonFungibleEnumerable}; + +#[rule] +pub fn enumerable_nft_balance_sanity(e: Env) { + let account: Address = nondet_address(); + let _ = EnumerableNft::balance(&e, account); + cvlr_satisfy!(true); +} + +#[rule] +pub fn enumerable_nft_owner_of_sanity(e: Env) { + let token_id: u32 = nondet(); + let _ = EnumerableNft::owner_of(&e, token_id); + cvlr_satisfy!(true); +} + +#[rule] +pub fn enumerable_nft_transfer_sanity(e: Env) { + let from: Address = nondet_address(); + let to: Address = nondet_address(); + let token_id: u32 = nondet(); + EnumerableNft::transfer(&e, from, to, token_id); + cvlr_satisfy!(true); +} + +#[rule] +pub fn enumerable_nft_transfer_from_sanity(e: Env) { + let spender: Address = nondet_address(); + let from: Address = nondet_address(); + let to: Address = nondet_address(); + let token_id: u32 = nondet(); + EnumerableNft::transfer_from(&e, spender, from, to, token_id); + cvlr_satisfy!(true); +} + +#[rule] +pub fn enumerable_nft_approve_sanity(e: Env) { + let approver: Address = nondet_address(); + let approved: Address = nondet_address(); + let token_id: u32 = nondet(); + let live_until_ledger: u32 = nondet(); + EnumerableNft::approve(&e, approver, approved, token_id, live_until_ledger); + cvlr_satisfy!(true); +} + +#[rule] +pub fn enumerable_nft_approve_for_all_sanity(e: Env) { + let owner: Address = nondet_address(); + let operator: Address = nondet_address(); + let live_until_ledger: u32 = nondet(); + EnumerableNft::approve_for_all(&e, owner, operator, live_until_ledger); + cvlr_satisfy!(true); +} + +#[rule] +pub fn enumerable_nft_get_approved_sanity(e: Env) { + let token_id: u32 = nondet(); + let _ = EnumerableNft::get_approved(&e, token_id); + cvlr_satisfy!(true); +} + +#[rule] +pub fn enumerable_nft_is_approved_for_all_sanity(e: Env) { + let owner: Address = nondet_address(); + let operator: Address = nondet_address(); + let _ = EnumerableNft::is_approved_for_all(&e, owner, operator); + cvlr_satisfy!(true); +} + +#[rule] +pub fn enumerable_nft_name_sanity(e: Env) { + let _ = EnumerableNft::name(&e); + cvlr_satisfy!(true); +} + +#[rule] +pub fn enumerable_nft_symbol_sanity(e: Env) { + let _ = EnumerableNft::symbol(&e); + cvlr_satisfy!(true); +} + +#[rule] +pub fn enumerable_nft_token_uri_sanity(e: Env) { + let token_id: u32 = nondet(); + let _ = EnumerableNft::token_uri(&e, token_id); + cvlr_satisfy!(true); +} + +#[rule] +pub fn enumerable_nft_burn_sanity(e: Env) { + let from: Address = nondet_address(); + let token_id: u32 = nondet(); + EnumerableNft::burn(&e, from, token_id); + cvlr_satisfy!(true); +} + +#[rule] +pub fn enumerable_nft_burn_from_sanity(e: Env) { + let spender: Address = nondet_address(); + let from: Address = nondet_address(); + let token_id: u32 = nondet(); + EnumerableNft::burn_from(&e, spender, from, token_id); + cvlr_satisfy!(true); +} + +#[rule] +pub fn enumerable_nft_total_supply_sanity(e: Env) { + let _ = EnumerableNft::total_supply(&e); + cvlr_satisfy!(true); +} + +#[rule] +pub fn enumerable_nft_get_owner_token_id_sanity(e: Env) { + let owner: Address = nondet_address(); + let index: u32 = nondet(); + let _ = EnumerableNft::get_owner_token_id(&e, owner, index); + cvlr_satisfy!(true); +} + +#[rule] +pub fn enumerable_nft_get_token_id_sanity(e: Env) { + let index: u32 = nondet(); + let _ = EnumerableNft::get_token_id(&e, index); + cvlr_satisfy!(true); +} + +#[rule] +pub fn enumerable_nft_seq_mint_sanity(e: Env) { + let to: Address = nondet_address(); + let _ = EnumerableNft::seq_mint(&e, to); + cvlr_satisfy!(true); +} + +#[rule] +pub fn enumerable_nft_nonseq_mint_sanity(e: Env) { + let to: Address = nondet_address(); + let token_id: u32 = nondet(); + EnumerableNft::nonseq_mint(&e, to, token_id); + cvlr_satisfy!(true); +} diff --git a/packages/tokens/src/non_fungible/specs/helper.rs b/packages/tokens/src/non_fungible/specs/helper.rs new file mode 100644 index 00000000..93449bc6 --- /dev/null +++ b/packages/tokens/src/non_fungible/specs/helper.rs @@ -0,0 +1,22 @@ +use soroban_sdk::{Env, Address}; +use crate::non_fungible::Base; +use cvlr::clog; + +pub fn is_approved_for_token(e: &Env, owner: &Address, operator: &Address, token_id: u32) -> bool { + let get_approved_result = Base::get_approved(e, token_id); + if let Some(ref approved) = get_approved_result { + clog!(cvlr_soroban::Addr(approved)); + } + let is_approved_for_all_result = Base::is_approved_for_all(e, owner, operator); + clog!(is_approved_for_all_result); + if owner == operator { + return true; + } + if get_approved_result.as_ref() == Some(operator) { + return true; + } + if is_approved_for_all_result { + return true; + } + false +} \ No newline at end of file diff --git a/packages/tokens/src/non_fungible/specs/mod.rs b/packages/tokens/src/non_fungible/specs/mod.rs new file mode 100644 index 00000000..24f912f6 --- /dev/null +++ b/packages/tokens/src/non_fungible/specs/mod.rs @@ -0,0 +1,16 @@ +pub mod helper; +pub mod non_fungible_integrity; +pub mod non_fungible_panics; +pub mod non_fungible_non_panics; + +pub mod burnable_nft_contract; +pub mod consecutive_nft_contract; +pub mod enumerable_nft_contract; +pub mod royalties_nft_contract; + +pub mod burnable_nft_sanity; +pub mod consecutive_nft_sanity; +pub mod enumerable_nft_sanity; +pub mod royalties_nft_sanity; + +// TODO: anything to do for `utils/sequential`? Raz: this is part of consecutive. \ No newline at end of file diff --git a/packages/tokens/src/non_fungible/specs/non_fungible_integrity.rs b/packages/tokens/src/non_fungible/specs/non_fungible_integrity.rs new file mode 100644 index 00000000..749a3597 --- /dev/null +++ b/packages/tokens/src/non_fungible/specs/non_fungible_integrity.rs @@ -0,0 +1,160 @@ +use cvlr::{cvlr_satisfy, nondet::*, cvlr_assert, cvlr_assume}; +use cvlr_soroban::nondet_address; +use cvlr_soroban_derive::rule; +use soroban_sdk::{Env, Address}; +use cvlr::clog; + +use crate::non_fungible::Base; +use crate::non_fungible::specs::helper::is_approved_for_token; + +#[rule] +// after transfer the token owner is set to the to address +// updates balances correctly +// status: verified +pub fn nft_transfer_integrity(e: Env) { + let to = nondet_address(); + clog!(cvlr_soroban::Addr(&to)); + let from = nondet_address(); + clog!(cvlr_soroban::Addr(&from)); + let token_id = u32::nondet(); + clog!(token_id); + let owner_pre = Base::owner_of(&e, token_id); + clog!(cvlr_soroban::Addr(&owner_pre)); + let balance_from_pre = Base::balance(&e, &from); + clog!(balance_from_pre); + let balance_to_pre = Base::balance(&e, &to); + clog!(balance_to_pre); + Base::transfer(&e, &from, &to, token_id); + let owner_post = Base::owner_of(&e, token_id); + clog!(cvlr_soroban::Addr(&owner_post)); + cvlr_assert!(owner_post == to); + let balance_from_post = Base::balance(&e, &from); + clog!(balance_from_post); + let balance_to_post = Base::balance(&e, &to); + clog!(balance_to_post); + if to != from { + cvlr_assert!(balance_from_post == balance_from_pre - 1); + cvlr_assert!(balance_to_post == balance_to_pre + 1); + } else { + cvlr_assert!(balance_to_post == balance_to_pre); + cvlr_assert!(balance_from_post == balance_from_pre); + } +} + +#[rule] +// after transfer_from the token owner is to +// updates balances correctly +// removes approval +// status: verified +pub fn nft_transfer_from_integrity(e: Env) { + let spender = nondet_address(); + clog!(cvlr_soroban::Addr(&spender)); + let from = nondet_address(); + clog!(cvlr_soroban::Addr(&from)); + let to = nondet_address(); + clog!(cvlr_soroban::Addr(&to)); + let token_id = u32::nondet(); + clog!(token_id); + let owner_pre = Base::owner_of(&e, token_id); + clog!(cvlr_soroban::Addr(&owner_pre)); + let balance_from_pre = Base::balance(&e, &from); + clog!(balance_from_pre); + let balance_to_pre = Base::balance(&e, &to); + clog!(balance_to_pre); + Base::transfer_from(&e, &spender, &from, &to, token_id); + let owner_post = Base::owner_of(&e, token_id); + clog!(cvlr_soroban::Addr(&owner_post)); + cvlr_assert!(owner_post == to); + let balance_from_post = Base::balance(&e, &from); + clog!(balance_from_post); + let balance_to_post = Base::balance(&e, &to); + clog!(balance_to_post); + if to != from { + cvlr_assert!(balance_from_post == balance_from_pre - 1); + cvlr_assert!(balance_to_post == balance_to_pre + 1); + } else { + cvlr_assert!(balance_to_post == balance_to_pre); + cvlr_assert!(balance_from_post == balance_from_pre); + } + let approval_post = Base::get_approved(&e, token_id); + cvlr_assert!(approval_post.is_none()); +} + +#[rule] +// after approve the token owner is approved +// status: verified +pub fn nft_approve_integrity(e: Env) { + let approver = nondet_address(); + clog!(cvlr_soroban::Addr(&approver)); + let approved = nondet_address(); + clog!(cvlr_soroban::Addr(&approved)); + let token_id = u32::nondet(); + clog!(token_id); + let owner_pre = Base::owner_of(&e, token_id); + clog!(cvlr_soroban::Addr(&owner_pre)); + let live_until_ledger = u32::nondet(); + clog!(live_until_ledger); + cvlr_assume!(live_until_ledger > 0); + Base::approve(&e, &approver, &approved, token_id, live_until_ledger); + let is_approved_for_token_post = is_approved_for_token(&e, &approver, &approved, token_id); + clog!(is_approved_for_token_post); + cvlr_assert!(is_approved_for_token_post); +} + +#[rule] +// after approve_for_all the token owner is approved +// status: verified +pub fn nft_approve_for_all_integrity(e: Env) { + let owner = nondet_address(); + clog!(cvlr_soroban::Addr(&owner)); + let operator = nondet_address(); + clog!(cvlr_soroban::Addr(&operator)); + let live_until_ledger = u32::nondet(); + clog!(live_until_ledger); + let token_id = u32::nondet(); // some token + clog!(token_id); + cvlr_assume!(live_until_ledger > 0); + Base::approve_for_all(&e, &owner, &operator, live_until_ledger); + let is_approved_for_token_post = is_approved_for_token(&e, &owner, &operator, token_id); + clog!(is_approved_for_token_post); + cvlr_assert!(is_approved_for_token_post); +} + +// TODO: sequential mint here? + +#[rule] +// after mint the token owner is set to the to address +// updates balances correctly +// status: verified +pub fn nft_mint_integrity(e: Env) { + let to = nondet_address(); + clog!(cvlr_soroban::Addr(&to)); + let token_id = u32::nondet(); + clog!(token_id); + let owner_pre = Base::owner_of(&e, token_id); + clog!(cvlr_soroban::Addr(&owner_pre)); + let balance_pre = Base::balance(&e, &to); + clog!(balance_pre); + Base::mint(&e, &to, token_id); + let owner_post = Base::owner_of(&e, token_id); + clog!(cvlr_soroban::Addr(&owner_post)); + cvlr_assert!(owner_post == to); + let balance_post = Base::balance(&e, &to); + clog!(balance_post); + cvlr_assert!(balance_post == balance_pre + 1); +} + + +#[rule] +// token_uri of two different tokens is different +// status: tool issue +pub fn nft_token_uri_injective(e: Env) { + let token_id1 = u32::nondet(); + clog!(token_id1); + let token_id2 = u32::nondet(); + clog!(token_id2); + cvlr_assume!(token_id1 != token_id2); + let uri1 = Base::token_uri(&e, token_id1); + let uri2 = Base::token_uri(&e, token_id2); + cvlr_assert!(uri1 != uri2); +} \ No newline at end of file diff --git a/packages/tokens/src/non_fungible/specs/non_fungible_invariants.rs b/packages/tokens/src/non_fungible/specs/non_fungible_invariants.rs new file mode 100644 index 00000000..386f25ec --- /dev/null +++ b/packages/tokens/src/non_fungible/specs/non_fungible_invariants.rs @@ -0,0 +1,10 @@ +use cvlr::{cvlr_satisfy, nondet::*, cvlr_assert, cvlr_assume}; +use cvlr_soroban::nondet_address; +use cvlr_soroban_derive::rule; +use soroban_sdk::{Env, Address}; +use cvlr::clog; + +use crate::non_fungible::Base; + +// invariant: token_owner exists +// invariant: token_owner -> balance >= 1 (can't do iff) \ No newline at end of file diff --git a/packages/tokens/src/non_fungible/specs/non_fungible_non_panics.rs b/packages/tokens/src/non_fungible/specs/non_fungible_non_panics.rs new file mode 100644 index 00000000..b321c4c3 --- /dev/null +++ b/packages/tokens/src/non_fungible/specs/non_fungible_non_panics.rs @@ -0,0 +1,114 @@ +use cvlr::{cvlr_satisfy, nondet::*, cvlr_assert, cvlr_assume}; +use cvlr_soroban::nondet_address; +use cvlr_soroban_derive::rule; +use soroban_sdk::{Env, Address}; +use cvlr::clog; +use crate::non_fungible::Base; +use crate::non_fungible::specs::helper::is_approved_for_token; +use cvlr_soroban::is_auth; +use crate::non_fungible::storage::NFTStorageKey; + +// These rules require the prover arg "prover_args": ["-trapAsAssert true"] to consider also panicking paths. + +// helpers - storage setup + +pub fn storage_setup_owner(e: Env, token_id: u32) { + let owner = nondet_address(); + e.storage().persistent().set(&NFTStorageKey::Owner(token_id), &owner); +} + +// return to this after doing invariants. + +#[rule] +// requires +// owner = from +// from auth +// status: violated - see decrease_balance +pub fn nft_transfer_non_panic(e: Env) { + let to: Address = nondet_address(); + clog!(cvlr_soroban::Addr(&to)); + let from = nondet_address(); + clog!(cvlr_soroban::Addr(&from)); + let token_id: u32 = nondet(); + clog!(token_id); + cvlr_assume!(is_auth(from.clone())); + storage_setup_owner(e.clone(), token_id); + let owner = Base::owner_of(&e, token_id); + clog!(cvlr_soroban::Addr(&owner)); + cvlr_assume!(owner == from); + Base::transfer(&e, &from, &to, token_id); + cvlr_assert!(true); +} + +#[rule] +// requires +// owner = from +// spender auth +// spender is approved +// status: violated - storage? +pub fn nft_transfer_from_non_panic(e: Env) { + let to: Address = nondet_address(); + clog!(cvlr_soroban::Addr(&to)); + let from = nondet_address(); + clog!(cvlr_soroban::Addr(&from)); + let token_id: u32 = nondet(); + clog!(token_id); + storage_setup_owner(e.clone(), token_id); + let owner = Base::owner_of(&e, token_id); + clog!(cvlr_soroban::Addr(&owner)); + cvlr_assume!(owner == from); + let spender = nondet_address(); + clog!(cvlr_soroban::Addr(&spender)); + cvlr_assume!(is_auth(spender.clone())); + cvlr_assume!(is_approved_for_token(&e, &from, &spender, token_id)); + Base::transfer_from(&e, &spender, &from, &to, token_id); + cvlr_assert!(true); +} + +#[rule] +// requires +// owner auth +// live until ledger is appropriate +// status: violated - storage +pub fn nft_approve_non_panic(e: Env) { + let owner = nondet_address(); + clog!(cvlr_soroban::Addr(&owner)); + let spender = nondet_address(); + clog!(cvlr_soroban::Addr(&spender)); + let token_id: u32 = nondet(); + clog!(token_id); + let live_until_ledger = u32::nondet(); + clog!(live_until_ledger); + let current_ledger = e.ledger().sequence(); + let max_live_until_ledger = e.ledger().max_live_until_ledger(); + let ledger_leq_max = live_until_ledger <= max_live_until_ledger; + let ledger_above_currnet = live_until_ledger > current_ledger; + let live_until_ledger_is_zero = live_until_ledger == 0; + cvlr_assume!(live_until_ledger_is_zero || (ledger_leq_max && ledger_above_currnet)); + cvlr_assume!(is_auth(owner.clone())); + Base::approve(&e, &owner, &spender, token_id, live_until_ledger); + cvlr_assert!(true); +} + +#[rule] +// requires +// owner auth +// live until ledger is appropriate +// status: +pub fn nft_approve_for_all_non_panic(e: Env) { + let owner = nondet_address(); + clog!(cvlr_soroban::Addr(&owner)); + let operator = nondet_address(); + clog!(cvlr_soroban::Addr(&operator)); + let live_until_ledger = u32::nondet(); + clog!(live_until_ledger); + let current_ledger = e.ledger().sequence(); + let max_live_until_ledger = e.ledger().max_live_until_ledger(); + let ledger_leq_max = live_until_ledger <= max_live_until_ledger; + let ledger_above_currnet = live_until_ledger > current_ledger; + let live_until_ledger_is_zero = live_until_ledger == 0; + cvlr_assume!(live_until_ledger_is_zero || (ledger_leq_max && ledger_above_currnet)); + cvlr_assume!(is_auth(owner.clone())); + Base::approve_for_all(&e, &owner, &operator, live_until_ledger); + cvlr_assert!(true); +} \ No newline at end of file diff --git a/packages/tokens/src/non_fungible/specs/non_fungible_panics.rs b/packages/tokens/src/non_fungible/specs/non_fungible_panics.rs new file mode 100644 index 00000000..91562611 --- /dev/null +++ b/packages/tokens/src/non_fungible/specs/non_fungible_panics.rs @@ -0,0 +1,151 @@ +use cvlr::{cvlr_satisfy, nondet::*, cvlr_assert, cvlr_assume}; +use cvlr_soroban::{nondet_address, is_auth}; +use cvlr_soroban_derive::rule; +use soroban_sdk::{Env, Address}; +use cvlr::clog; +use crate::non_fungible::Base; +use crate::non_fungible::specs::helper::is_approved_for_token; + +#[rule] +// transfer_panics if not auth by from +// status: verified +pub fn nft_transfer_panics_if_unauthorized(e: Env) { + let to = nondet_address(); + clog!(cvlr_soroban::Addr(&to)); + let from = nondet_address(); + clog!(cvlr_soroban::Addr(&from)); + let token_id = u32::nondet(); + clog!(token_id); + cvlr_assume!(!is_auth(from.clone())); + Base::transfer(&e, &from, &to, token_id); + cvlr_assert!(false); +} + +#[rule] +// transfer_panics if from doesn't own token +// status: verified +pub fn nft_transfer_panics_if_not_owner(e: Env) { + let to = nondet_address(); + clog!(cvlr_soroban::Addr(&to)); + let from = nondet_address(); + clog!(cvlr_soroban::Addr(&from)); + let token_id = u32::nondet(); + clog!(token_id); + let owner = Base::owner_of(&e, token_id); + clog!(cvlr_soroban::Addr(&owner)); + cvlr_assume!(owner != from); + Base::transfer(&e, &from, &to, token_id); + cvlr_assert!(false); +} + + +#[rule] +// transfer_from_panics if spender does not auth +// status: verified +pub fn nft_transfer_from_panics_if_unauthorized(e: Env) { + let to = nondet_address(); + clog!(cvlr_soroban::Addr(&to)); + let spender = nondet_address(); + clog!(cvlr_soroban::Addr(&spender)); + let from = nondet_address(); + clog!(cvlr_soroban::Addr(&from)); + let token_id = u32::nondet(); + clog!(token_id); + cvlr_assume!(!is_auth(spender.clone())); + Base::transfer_from(&e, &spender, &from, &to, token_id); + cvlr_assert!(false); +} + +#[rule] +// transfer_from_panics if from doesn't own token +// status: verified +pub fn nft_transfer_from_panics_if_not_owner(e: Env) { + let to = nondet_address(); + clog!(cvlr_soroban::Addr(&to)); + let spender = nondet_address(); + clog!(cvlr_soroban::Addr(&spender)); + let from = nondet_address(); + clog!(cvlr_soroban::Addr(&from)); + let token_id = u32::nondet(); + clog!(token_id); + let owner = Base::owner_of(&e, token_id); + clog!(cvlr_soroban::Addr(&owner)); + cvlr_assume!(owner != from); + Base::transfer_from(&e, &spender, &from, &to, token_id); + cvlr_assert!(false); +} + +#[rule] +// transfer_from panics if is_approved_for_token returns false +// status: verified +pub fn nft_transfer_from_panics_if_not_approved(e: Env) { + let to = nondet_address(); + clog!(cvlr_soroban::Addr(&to)); + let spender = nondet_address(); + clog!(cvlr_soroban::Addr(&spender)); + let from = nondet_address(); + clog!(cvlr_soroban::Addr(&from)); + let token_id = u32::nondet(); + clog!(token_id); + cvlr_assume!(!is_approved_for_token(&e, &from, &spender, token_id)); + Base::transfer_from(&e, &spender, &from, &to, token_id); + cvlr_assert!(false); +} + +#[rule] +// approve_panics if owner does not auth +// status: verified +pub fn nft_approve_panics_if_unauthorized(e: Env) { + let owner = nondet_address(); + clog!(cvlr_soroban::Addr(&owner)); + let spender = nondet_address(); + clog!(cvlr_soroban::Addr(&spender)); + let token_id = u32::nondet(); + clog!(token_id); + let live_until_ledger = u32::nondet(); + clog!(live_until_ledger); + cvlr_assume!(!is_auth(owner.clone())); + Base::approve(&e, &owner, &spender, token_id, live_until_ledger); + cvlr_assert!(false); +} + +#[rule] +// approve_panics if live_until_ledger > max_ledger +// status: violated - bug? +pub fn nft_approve_panics_if_live_until_ledger_greater_than_max_ledger(e: Env) { + let owner = nondet_address(); + clog!(cvlr_soroban::Addr(&owner)); + let spender = nondet_address(); + clog!(cvlr_soroban::Addr(&spender)); + let token_id = u32::nondet(); + clog!(token_id); + let live_until_ledger = u32::nondet(); + clog!(live_until_ledger); + cvlr_assume!(live_until_ledger > e.ledger().max_live_until_ledger()); + clog!(e.ledger().max_live_until_ledger()); + Base::approve(&e, &owner, &spender, token_id, live_until_ledger); + cvlr_assert!(false); +} + +#[rule] +// approve_panics if live_until_ledger < current_ledger and non-zero +// status: verified +pub fn nft_approve_panics_if_live_until_ledger_less_than_current_ledger(e: Env) { + let owner = nondet_address(); + clog!(cvlr_soroban::Addr(&owner)); + let spender = nondet_address(); + clog!(cvlr_soroban::Addr(&spender)); + let token_id = u32::nondet(); + clog!(token_id); + let live_until_ledger = u32::nondet(); + clog!(live_until_ledger); + let current_ledger = e.ledger().sequence(); + clog!(current_ledger); + cvlr_assume!(live_until_ledger < current_ledger); + cvlr_assume!(live_until_ledger > 0); + Base::approve(&e, &owner, &spender, token_id, live_until_ledger); + cvlr_assert!(false); +} + + + diff --git a/packages/tokens/src/non_fungible/specs/royalties_nft_contract.rs b/packages/tokens/src/non_fungible/specs/royalties_nft_contract.rs new file mode 100644 index 00000000..aa4ff85d --- /dev/null +++ b/packages/tokens/src/non_fungible/specs/royalties_nft_contract.rs @@ -0,0 +1,83 @@ +use soroban_sdk::{Address, Env, xdr::Enum}; + +use crate::{non_fungible::Base, non_fungible::{ContractOverrides, NonFungibleToken, burnable::NonFungibleBurnable, enumerable::{Enumerable, NonFungibleEnumerable}, royalties::NonFungibleRoyalties}}; + +pub struct RoyaltiesNft; + +impl NonFungibleToken for RoyaltiesNft { + type ContractType = Base; + + fn balance(e: &Env, account: Address) -> u32 { + Base::balance(e, &account) + } + + fn owner_of(e: &Env, token_id: u32) -> Address { + Base::owner_of(e, token_id) + } + + fn transfer(e: &Env, from: Address, to: Address, token_id: u32) { + Base::transfer(e, &from, &to, token_id); + } + + fn transfer_from(e: &Env, spender: Address, from: Address, to: Address, token_id: u32) { + Base::transfer_from(e, &spender, &from, &to, token_id); + } + + fn approve( + e: &Env, + approver: Address, + approved: Address, + token_id: u32, + live_until_ledger: u32, + ) { + Base::approve(e, &approver, &approved, token_id, live_until_ledger); + } + + fn approve_for_all(e: &Env, owner: Address, operator: Address, live_until_ledger: u32) { + Base::approve_for_all(e, &owner, &operator, live_until_ledger); + } + + fn get_approved(e: &Env, token_id: u32) -> Option
{ + Base::get_approved(e, token_id) + } + + fn is_approved_for_all(e: &Env, owner: Address, operator: Address) -> bool { + Base::is_approved_for_all(e, &owner, &operator) + } + + fn name(e: &Env) -> soroban_sdk::String { + Base::name(e) + } + + fn symbol(e: &Env) -> soroban_sdk::String { + Base::symbol(e) + } + + fn token_uri(e: &Env, token_id: u32) -> soroban_sdk::String { + Base::token_uri(e, token_id) + } +} + +impl NonFungibleRoyalties for RoyaltiesNft { + fn set_default_royalty(e: &Env, receiver: Address, basis_points: u32, operator: Address) { + Base::set_default_royalty(e, &receiver, basis_points); + } + + fn set_token_royalty( + e: &Env, + token_id: u32, + receiver: Address, + basis_points: u32, + operator: Address, + ) { + Base::set_token_royalty(e, token_id, &receiver, basis_points); + } + + fn remove_token_royalty(e: &Env, token_id: u32, operator: Address) { + Base::remove_token_royalty(e, token_id); + } + + fn royalty_info(e: &Env, token_id: u32, sale_price: i128) -> (Address, i128) { + Base::royalty_info(e, token_id, sale_price) + } +} \ No newline at end of file diff --git a/packages/tokens/src/non_fungible/specs/royalties_nft_sanity.rs b/packages/tokens/src/non_fungible/specs/royalties_nft_sanity.rs new file mode 100644 index 00000000..6c9ea52c --- /dev/null +++ b/packages/tokens/src/non_fungible/specs/royalties_nft_sanity.rs @@ -0,0 +1,128 @@ +use cvlr::{cvlr_satisfy, nondet::*}; +use cvlr_soroban::nondet_address; +use cvlr_soroban_derive::rule; +use soroban_sdk::{Env, Address}; + +use crate::non_fungible::specs::royalties_nft_contract::RoyaltiesNft; +use crate::non_fungible::{NonFungibleToken, royalties::NonFungibleRoyalties}; + +#[rule] +pub fn royalties_nft_balance_sanity(e: Env) { + let account: Address = nondet_address(); + let _ = RoyaltiesNft::balance(&e, account); + cvlr_satisfy!(true); +} + +#[rule] +pub fn royalties_nft_owner_of_sanity(e: Env) { + let token_id: u32 = nondet(); + let _ = RoyaltiesNft::owner_of(&e, token_id); + cvlr_satisfy!(true); +} + +#[rule] +pub fn royalties_nft_transfer_sanity(e: Env) { + let from: Address = nondet_address(); + let to: Address = nondet_address(); + let token_id: u32 = nondet(); + RoyaltiesNft::transfer(&e, from, to, token_id); + cvlr_satisfy!(true); +} + +#[rule] +pub fn royalties_nft_transfer_from_sanity(e: Env) { + let spender: Address = nondet_address(); + let from: Address = nondet_address(); + let to: Address = nondet_address(); + let token_id: u32 = nondet(); + RoyaltiesNft::transfer_from(&e, spender, from, to, token_id); + cvlr_satisfy!(true); +} + +#[rule] +pub fn royalties_nft_approve_sanity(e: Env) { + let approver: Address = nondet_address(); + let approved: Address = nondet_address(); + let token_id: u32 = nondet(); + let live_until_ledger: u32 = nondet(); + RoyaltiesNft::approve(&e, approver, approved, token_id, live_until_ledger); + cvlr_satisfy!(true); +} + +#[rule] +pub fn royalties_nft_approve_for_all_sanity(e: Env) { + let owner: Address = nondet_address(); + let operator: Address = nondet_address(); + let live_until_ledger: u32 = nondet(); + RoyaltiesNft::approve_for_all(&e, owner, operator, live_until_ledger); + cvlr_satisfy!(true); +} + +#[rule] +pub fn royalties_nft_get_approved_sanity(e: Env) { + let token_id: u32 = nondet(); + let _ = RoyaltiesNft::get_approved(&e, token_id); + cvlr_satisfy!(true); +} + +#[rule] +pub fn royalties_nft_is_approved_for_all_sanity(e: Env) { + let owner: Address = nondet_address(); + let operator: Address = nondet_address(); + let _ = RoyaltiesNft::is_approved_for_all(&e, owner, operator); + cvlr_satisfy!(true); +} + +#[rule] +pub fn royalties_nft_name_sanity(e: Env) { + let _ = RoyaltiesNft::name(&e); + cvlr_satisfy!(true); +} + +#[rule] +pub fn royalties_nft_symbol_sanity(e: Env) { + let _ = RoyaltiesNft::symbol(&e); + cvlr_satisfy!(true); +} + +#[rule] +pub fn royalties_nft_token_uri_sanity(e: Env) { + let token_id: u32 = nondet(); + let _ = RoyaltiesNft::token_uri(&e, token_id); + cvlr_satisfy!(true); +} + +#[rule] +pub fn royalties_nft_set_default_royalty_sanity(e: Env) { + let receiver: Address = nondet_address(); + let basis_points: u32 = nondet(); + let operator: Address = nondet_address(); + RoyaltiesNft::set_default_royalty(&e, receiver, basis_points, operator); + cvlr_satisfy!(true); +} + +#[rule] +pub fn royalties_nft_set_token_royalty_sanity(e: Env) { + let token_id: u32 = nondet(); + let receiver: Address = nondet_address(); + let basis_points: u32 = nondet(); + let operator: Address = nondet_address(); + RoyaltiesNft::set_token_royalty(&e, token_id, receiver, basis_points, operator); + cvlr_satisfy!(true); +} + +#[rule] +pub fn royalties_nft_remove_token_royalty_sanity(e: Env) { + let token_id: u32 = nondet(); + let operator: Address = nondet_address(); + RoyaltiesNft::remove_token_royalty(&e, token_id, operator); + cvlr_satisfy!(true); +} + +#[rule] +pub fn royalties_nft_royalty_info_sanity(e: Env) { + let token_id: u32 = nondet(); + let sale_price: i128 = nondet(); + let _ = RoyaltiesNft::royalty_info(&e, token_id, sale_price); + cvlr_satisfy!(true); +} diff --git a/packages/tokens/src/non_fungible/storage.rs b/packages/tokens/src/non_fungible/storage.rs index b724de28..0fb2498d 100644 --- a/packages/tokens/src/non_fungible/storage.rs +++ b/packages/tokens/src/non_fungible/storage.rs @@ -1,7 +1,12 @@ use soroban_sdk::{contracttype, panic_with_error, Address, Env, String}; +#[cfg(not(feature = "certora"))] use crate::non_fungible::{ - emit_approve, emit_approve_for_all, emit_mint, emit_transfer, sequential::increment_token_id, + emit_approve, emit_approve_for_all, emit_mint, emit_transfer, +}; + +use crate::non_fungible::{ + sequential::increment_token_id, Base, NonFungibleTokenError, BALANCE_EXTEND_AMOUNT, BALANCE_TTL_THRESHOLD, MAX_BASE_URI_LEN, MAX_NUM_DIGITS, OWNER_EXTEND_AMOUNT, OWNER_TTL_THRESHOLD, }; @@ -253,6 +258,7 @@ impl Base { pub fn transfer(e: &Env, from: &Address, to: &Address, token_id: u32) { from.require_auth(); Base::update(e, Some(from), Some(to), token_id); + #[cfg(not(feature = "certora"))] emit_transfer(e, from, to, token_id); } @@ -286,6 +292,7 @@ impl Base { spender.require_auth(); Base::check_spender_approval(e, spender, from, token_id); Base::update(e, Some(from), Some(to), token_id); + #[cfg(not(feature = "certora"))] emit_transfer(e, from, to, token_id); } @@ -365,6 +372,7 @@ impl Base { // If revoking approval (live_until_ledger == 0) if live_until_ledger == 0 { e.storage().temporary().remove(&key); + #[cfg(not(feature = "certora"))] emit_approve_for_all(e, owner, operator, live_until_ledger); return; } @@ -383,7 +391,7 @@ impl Base { // Update the TTL based on the expiration ledger let live_for = live_until_ledger - current_ledger; e.storage().temporary().extend_ttl(&key, live_for, live_for); - + #[cfg(not(feature = "certora"))] emit_approve_for_all(e, owner, operator, live_until_ledger); } @@ -472,7 +480,7 @@ impl Base { if live_until_ledger == 0 { e.storage().temporary().remove(&key); - + #[cfg(not(feature = "certora"))] emit_approve(e, approver, approved, token_id, live_until_ledger); return; } @@ -488,7 +496,7 @@ impl Base { let live_for = live_until_ledger - e.ledger().sequence(); e.storage().temporary().extend_ttl(&key, live_for, live_for); - + #[cfg(not(feature = "certora"))] emit_approve(e, approver, approved, token_id, live_until_ledger); } @@ -659,6 +667,7 @@ impl Base { pub fn sequential_mint(e: &Env, to: &Address) -> u32 { let token_id = increment_token_id(e, 1); Base::update(e, None, Some(to), token_id); + #[cfg(not(feature = "certora"))] emit_mint(e, to, token_id); token_id @@ -705,6 +714,7 @@ impl Base { /// implemented accordingly. pub fn mint(e: &Env, to: &Address, token_id: u32) { Base::update(e, None, Some(to), token_id); + #[cfg(not(feature = "certora"))] emit_mint(e, to, token_id); } } diff --git a/packages/tokens/src/rwa/claim_issuer/mod.rs b/packages/tokens/src/rwa/claim_issuer/mod.rs index 819244ab..75adedb5 100644 --- a/packages/tokens/src/rwa/claim_issuer/mod.rs +++ b/packages/tokens/src/rwa/claim_issuer/mod.rs @@ -143,7 +143,13 @@ mod storage; #[cfg(test)] mod test; -use soroban_sdk::{contractclient, contracterror, contractevent, Address, Bytes, Env}; +#[cfg(not(feature = "certora"))] +use soroban_sdk::{contractevent}; + +#[cfg(feature = "certora")] +use cvlr_soroban_derive::contractevent; + +use soroban_sdk::{contractclient, contracterror, Address, Bytes, Env}; pub use storage::{ allow_key, build_claim_identifier, decode_claim_data_expiration, encode_claim_data_expiration, get_current_nonce_for, get_keys_for_topic, get_registries, invalidate_claim_signatures, @@ -261,6 +267,7 @@ pub struct KeyRemoved { /// * `registry` - The address of the `claim_topics_and_issuers` registry. /// * `scheme` - The signature scheme used. /// * `claim_topic` - Optional claim topic for topic-specific operations. +#[cfg(not(feature = "certora"))] pub fn emit_key_allowed( e: &Env, public_key: &Bytes, @@ -281,6 +288,7 @@ pub fn emit_key_allowed( /// * `registry` - The address of the `claim_topics_and_issuers` registry. /// * `scheme` - The signature scheme used. /// * `claim_topic` - Optional claim topic for topic-specific operations. +#[cfg(not(feature = "certora"))] pub fn emit_key_removed( e: &Env, public_key: &Bytes, @@ -314,6 +322,7 @@ pub struct ClaimRevoked { /// * `claim_topic` - The topic of the claim. /// * `claim_data` - The claim data. /// * `revoked` - Whether the claim should be marked as revoked. +#[cfg(not(feature = "certora"))] pub fn emit_revocation_event( e: &Env, identity: &Address, @@ -350,6 +359,7 @@ pub struct SignaturesInvalidated { /// * `identity` - The identity address whose signatures are invalidated. /// * `claim_topic` - The claim topic for which signatures are invalidated. /// * `nonce` - The nonce value before invalidation. +#[cfg(not(feature = "certora"))] pub fn emit_signatures_invalidated(e: &Env, identity: &Address, claim_topic: u32, nonce: u32) { SignaturesInvalidated { identity: identity.clone(), claim_topic, nonce }.publish(e); } @@ -368,22 +378,19 @@ pub enum ClaimIssuerError { KeyAlreadyAllowed = 352, /// The specified key was not found in the allowed keys. KeyNotFound = 353, - /// The claim issuer is not registered at the claim topics and issuers - /// registry. - IssuerNotRegistered = 354, /// The claim issuer is not allowed to sign claims about the specified /// claim topic. - ClaimTopicNotAllowed = 355, - /// Maximum number of signing keys per topic exceeded. - MaxKeysPerTopicExceeded = 356, - /// Maximum number of registries per signing key exceeded. - MaxRegistriesPerKeyExceeded = 357, + NotAllowed = 354, + /// Maximum limit exceeded (keys per topic or registries per key). + LimitExceeded = 355, /// No signing keys found for the specified claim topic. - NoKeysForTopic = 358, + NoKeysForTopic = 356, /// Invalid claim data encoding. - InvalidClaimDataExpiration = 359, + InvalidClaimDataExpiration = 357, /// Recovery of the Secp256k1 public key failed. - Secp256k1RecoveryFailed = 360, + Secp256k1RecoveryFailed = 358, + /// Indicates overflow when adding two values. + MathOverflow = 359, } // ################## CONSTANTS ################## diff --git a/packages/tokens/src/rwa/claim_issuer/storage.rs b/packages/tokens/src/rwa/claim_issuer/storage.rs index fdf291d5..f6223110 100644 --- a/packages/tokens/src/rwa/claim_issuer/storage.rs +++ b/packages/tokens/src/rwa/claim_issuer/storage.rs @@ -75,13 +75,19 @@ use soroban_sdk::{contracttype, panic_with_error, xdr::ToXdr, Address, Bytes, By use crate::rwa::{ claim_issuer::{ - emit_key_allowed, emit_key_removed, emit_revocation_event, emit_signatures_invalidated, ClaimIssuerError, SignatureVerifier, CLAIMS_EXTEND_AMOUNT, CLAIMS_TTL_THRESHOLD, KEYS_EXTEND_AMOUNT, KEYS_TTL_THRESHOLD, MAX_KEYS_PER_TOPIC, MAX_REGISTRIES_PER_KEY, }, claim_topics_and_issuers::ClaimTopicsAndIssuersClient, }; +#[cfg(not(feature = "certora"))] +use crate::rwa::{ + claim_issuer::{ + emit_key_allowed, emit_key_removed, emit_revocation_event, emit_signatures_invalidated, + }, +}; + #[contracttype] #[derive(Clone, Debug, Eq, PartialEq)] pub struct SigningKey { @@ -93,7 +99,7 @@ pub struct SigningKey { #[contracttype] #[derive(Clone)] pub enum ClaimIssuerStorageKey { - /// Maps Topic -> Vec + /// Maps Topic -> `Vec` Topics(u32), /// Maps SigningKey -> Vec<(Topic, Registry)> Pairs(SigningKey), @@ -316,7 +322,17 @@ pub fn get_registries(e: &Env, signing_key: &SigningKey) -> Vec
{ .iter() .map(|(_, addr)| addr); - Vec::from_iter(e, iter) + #[cfg(not(feature = "certora"))] + return Vec::from_iter(e, iter); + + #[cfg(feature = "certora")] + { + let mut v: Vec
= Vec::new(e); + for addr in iter { + v.push_back(addr); + } + v + } } /// Checks if a public key and its scheme are allowed to sign claims for a @@ -427,12 +443,12 @@ pub fn is_authorized_for(e: &Env, registry: &Address, claim_topic: u32) -> bool /// # Errors /// /// * [`ClaimIssuerError::KeyIsEmpty`] - If attempting to allow an empty key. -/// * [`ClaimIssuerError::IssuerNotRegistered`] - If this claim issuer is not -/// registered at the `claim_topics_and_issuers` registry. -/// * [`ClaimIssuerError::ClaimTopicNotAllowed`] - If this claim issuer is not -/// allowed to sign claims about the `claim_topic`. +/// * [`ClaimIssuerError::NotAllowed`] - If this claim issuer is not allowed to +/// sign claims about the `claim_topic`. /// * [`ClaimIssuerError::KeyAlreadyAllowed`] - If this exact (key, topic, /// registry) combination is already registered. +/// * [`ClaimIssuerError::LimitExceeded`] - If maximum keys per topic or +/// registries per key limit is exceeded. /// /// # Events /// @@ -455,14 +471,9 @@ pub fn allow_key(e: &Env, public_key: &Bytes, registry: &Address, scheme: u32, c let registry_client = ClaimTopicsAndIssuersClient::new(e, registry); - // Check claim issuer is registered at claim_topics_and_issuers registry - if !registry_client.is_trusted_issuer(&e.current_contract_address()) { - panic_with_error!(e, ClaimIssuerError::IssuerNotRegistered) - } - // Check claim issuer can sign claim about a specific topic if !registry_client.has_claim_topic(&e.current_contract_address(), &claim_topic) { - panic_with_error!(e, ClaimIssuerError::ClaimTopicNotAllowed) + panic_with_error!(e, ClaimIssuerError::NotAllowed) } let signing_key = SigningKey { public_key: public_key.clone(), scheme }; @@ -474,7 +485,7 @@ pub fn allow_key(e: &Env, public_key: &Bytes, registry: &Address, scheme: u32, c e.storage().persistent().get(&key).unwrap_or_else(|| Vec::new(e)); if topic_keys.len() >= MAX_KEYS_PER_TOPIC { - panic_with_error!(e, ClaimIssuerError::MaxKeysPerTopicExceeded) + panic_with_error!(e, ClaimIssuerError::LimitExceeded) } topic_keys.push_back(signing_key.clone()); @@ -494,11 +505,11 @@ pub fn allow_key(e: &Env, public_key: &Bytes, registry: &Address, scheme: u32, c pairs.push_back((claim_topic, registry.clone())); if pairs.len() >= MAX_REGISTRIES_PER_KEY { - panic_with_error!(e, ClaimIssuerError::MaxRegistriesPerKeyExceeded) + panic_with_error!(e, ClaimIssuerError::LimitExceeded) } e.storage().persistent().set(&pairs_storage_key, &pairs); - + #[cfg(not(feature = "certora"))] emit_key_allowed(e, public_key, registry, scheme, claim_topic); } @@ -570,7 +581,7 @@ pub fn remove_key(e: &Env, public_key: &Bytes, registry: &Address, scheme: u32, e.storage().persistent().set(&topics_storage_key, &topic_keys); } } - + #[cfg(not(feature = "certora"))] emit_key_removed(e, public_key, registry, scheme, claim_topic); } @@ -624,6 +635,11 @@ pub fn get_current_nonce_for(e: &Env, identity: &Address, claim_topic: u32) -> u /// * `identity` - The identity address to invalidate signatures for. /// * `claim_topic` - The claim topic to invalidate signatures for. /// +/// # Errors +/// +/// * [`ClaimIssuerError::MathOverflow`] - If the nonce has reached `u32::MAX` +/// and cannot be incremented further. +/// /// # Events /// /// * topics - `["signatures_invalidated", identity: Address, claim_topic: u32]` @@ -642,9 +658,12 @@ pub fn invalidate_claim_signatures(e: &Env, identity: &Address, claim_topic: u32 let nonce_key = ClaimIssuerStorageKey::ClaimNonce(identity.clone(), claim_topic); let mut nonce: u32 = e.storage().persistent().get(&nonce_key).unwrap_or(0); + #[cfg(not(feature = "certora"))] emit_signatures_invalidated(e, identity, claim_topic, nonce); - nonce += 1; + nonce = nonce + .checked_add(1) + .unwrap_or_else(|| panic_with_error!(e, ClaimIssuerError::MathOverflow)); e.storage().persistent().set(&nonce_key, &nonce); } @@ -686,6 +705,7 @@ pub fn set_claim_revoked( e.storage().persistent().set(&ClaimIssuerStorageKey::RevokedClaim(claim_digest), &revoked); + #[cfg(not(feature = "certora"))] emit_revocation_event(e, identity, claim_topic, claim_data, revoked); } diff --git a/packages/tokens/src/rwa/claim_issuer/test.rs b/packages/tokens/src/rwa/claim_issuer/test.rs index 64c5aee7..eb8e0653 100644 --- a/packages/tokens/src/rwa/claim_issuer/test.rs +++ b/packages/tokens/src/rwa/claim_issuer/test.rs @@ -337,7 +337,7 @@ fn secp256k1_verify_success() { } #[test] -#[should_panic(expected = "Error(Contract, #360)")] +#[should_panic(expected = "Error(Contract, #358)")] fn secp256k1_verify_fails() { let e = Env::default(); let contract_id = e.register(MockContract, ()); @@ -867,7 +867,7 @@ fn bidirectional_mapping_same_key_different_topics() { } #[test] -#[should_panic(expected = "Error(Contract, #356)")] +#[should_panic(expected = "Error(Contract, #355)")] fn max_keys_per_topic_exceeded() { let e = Env::default(); let contract_id = e.register(MockContract, ()); @@ -890,7 +890,7 @@ fn max_keys_per_topic_exceeded() { } #[test] -#[should_panic(expected = "Error(Contract, #357)")] +#[should_panic(expected = "Error(Contract, #355)")] fn max_registries_per_key_exceeded() { let e = Env::default(); let contract_id = e.register(MockContract, ()); @@ -913,23 +913,6 @@ fn max_registries_per_key_exceeded() { #[test] #[should_panic(expected = "Error(Contract, #354)")] -fn allow_key_issuer_not_registered() { - let e = Env::default(); - let contract_id = e.register(MockContract, ()); - let registry_id = e.register(MockClaimTopicsAndIssuersContract, ()); - - let public_key = Bytes::from_array(&e, &[1u8; 32]); - let scheme = 1u32; - let topic = 42u32; - - e.as_contract(&contract_id, || { - // Try to allow key without being registered - should panic - allow_key(&e, &public_key, ®istry_id, scheme, topic); - }); -} - -#[test] -#[should_panic(expected = "Error(Contract, #355)")] fn allow_key_topic_not_allowed() { let e = Env::default(); let contract_id = e.register(MockContract, ()); @@ -957,7 +940,7 @@ fn allow_key_topic_not_allowed() { } #[test] -#[should_panic(expected = "Error(Contract, #358)")] +#[should_panic(expected = "Error(Contract, #356)")] fn get_keys_for_topic_panics_when_no_keys() { let e = Env::default(); let contract_id = e.register(MockContract, ()); @@ -1184,6 +1167,27 @@ fn signature_invalidation_vs_per_claim_revocation() { }); } +#[test] +#[should_panic(expected = "Error(Contract, #359)")] +fn invalidate_claim_signatures_nonce_overflow() { + let e = Env::default(); + let contract_id = e.register(MockContract, ()); + let identity = Address::generate(&e); + let claim_topic = 42u32; + + e.as_contract(&contract_id, || { + // Set nonce to u32::MAX + let nonce_key = ClaimIssuerStorageKey::ClaimNonce(identity.clone(), claim_topic); + e.storage().persistent().set(&nonce_key, &u32::MAX); + + // Verify nonce is at max + assert_eq!(get_current_nonce_for(&e, &identity, claim_topic), u32::MAX); + + // Attempt to invalidate signatures - should panic with MathOverflow (361) + invalidate_claim_signatures(&e, &identity, claim_topic); + }); +} + // ======= EXPIRATION TESTS ======= #[test] @@ -1262,7 +1266,7 @@ fn is_claim_expired_returns_true_for_current_timestamp() { } #[test] -#[should_panic(expected = "Error(Contract, #359)")] +#[should_panic(expected = "Error(Contract, #357)")] fn decode_claim_data_expiration_fails_on_short_data() { let e = Env::default(); let contract_id = e.register(MockContract, ()); @@ -1274,7 +1278,7 @@ fn decode_claim_data_expiration_fails_on_short_data() { } #[test] -#[should_panic(expected = "Error(Contract, #359)")] +#[should_panic(expected = "Error(Contract, #357)")] fn encode_claim_data_fails_when_valid_until_equals_created_at() { let e = Env::default(); let contract_id = e.register(MockContract, ()); @@ -1288,7 +1292,7 @@ fn encode_claim_data_fails_when_valid_until_equals_created_at() { } #[test] -#[should_panic(expected = "Error(Contract, #359)")] +#[should_panic(expected = "Error(Contract, #357)")] fn encode_claim_data_fails_when_valid_until_less_than_created_at() { let e = Env::default(); let contract_id = e.register(MockContract, ()); diff --git a/packages/tokens/src/rwa/claim_topics_and_issuers/mod.rs b/packages/tokens/src/rwa/claim_topics_and_issuers/mod.rs index bb3d1a7e..aacc0d46 100644 --- a/packages/tokens/src/rwa/claim_topics_and_issuers/mod.rs +++ b/packages/tokens/src/rwa/claim_topics_and_issuers/mod.rs @@ -3,7 +3,13 @@ pub mod storage; #[cfg(test)] mod test; -use soroban_sdk::{contractclient, contracterror, contractevent, Address, Env, Map, Vec}; +#[cfg(not(feature = "certora"))] +use soroban_sdk::{contractevent}; + +#[cfg(feature = "certora")] +use cvlr_soroban_derive::contractevent; + +use soroban_sdk::{contractclient, contracterror, Address, Env, Map, Vec}; /// Trait for managing claim topics and trusted issuers for RWA tokens. /// @@ -284,6 +290,7 @@ pub struct ClaimTopicAdded { /// /// * `e` - The Soroban environment. /// * `claim_topic` - The claim topic that was added. +#[cfg(not(feature = "certora"))] pub fn emit_claim_topic_added(e: &Env, claim_topic: u32) { ClaimTopicAdded { claim_topic }.publish(e); } @@ -302,6 +309,7 @@ pub struct ClaimTopicRemoved { /// /// * `e` - The Soroban environment. /// * `claim_topic` - The claim topic that was removed. +#[cfg(not(feature = "certora"))] pub fn emit_claim_topic_removed(e: &Env, claim_topic: u32) { ClaimTopicRemoved { claim_topic }.publish(e); } @@ -322,6 +330,7 @@ pub struct TrustedIssuerAdded { /// * `e` - The Soroban environment. /// * `trusted_issuer` - The trusted issuer that was added. /// * `claim_topics` - The claim topics associated with the trusted issuer. +#[cfg(not(feature = "certora"))] pub fn emit_trusted_issuer_added(e: &Env, trusted_issuer: &Address, claim_topics: Vec) { TrustedIssuerAdded { trusted_issuer: trusted_issuer.clone(), claim_topics }.publish(e); } @@ -340,6 +349,7 @@ pub struct TrustedIssuerRemoved { /// /// * `e` - The Soroban environment. /// * `trusted_issuer` - The trusted issuer that was removed. +#[cfg(not(feature = "certora"))] pub fn emit_trusted_issuer_removed(e: &Env, trusted_issuer: &Address) { TrustedIssuerRemoved { trusted_issuer: trusted_issuer.clone() }.publish(e); } @@ -361,6 +371,7 @@ pub struct IssuerTopicsUpdated { /// * `e` - The Soroban environment. /// * `trusted_issuer` - The trusted issuer whose claim topics were updated. /// * `claim_topics` - The updated claim topics. +#[cfg(not(feature = "certora"))] pub fn emit_issuer_topics_updated(e: &Env, trusted_issuer: &Address, claim_topics: Vec) { IssuerTopicsUpdated { trusted_issuer: trusted_issuer.clone(), claim_topics }.publish(e); } diff --git a/packages/tokens/src/rwa/claim_topics_and_issuers/storage.rs b/packages/tokens/src/rwa/claim_topics_and_issuers/storage.rs index 4cea2a96..8d7fc3f1 100644 --- a/packages/tokens/src/rwa/claim_topics_and_issuers/storage.rs +++ b/packages/tokens/src/rwa/claim_topics_and_issuers/storage.rs @@ -1,5 +1,6 @@ use soroban_sdk::{contracttype, panic_with_error, Address, Env, Map, Vec}; +#[cfg(not(feature = "certora"))] use super::{ emit_claim_topic_added, emit_claim_topic_removed, emit_issuer_topics_updated, emit_trusted_issuer_added, emit_trusted_issuer_removed, @@ -195,7 +196,7 @@ pub fn add_claim_topic(e: &Env, claim_topic: u32) { // initializing ClaimTopicIssuers for this topic let key = ClaimTopicsAndIssuersStorageKey::ClaimTopicIssuers(claim_topic); e.storage().persistent().set(&key, &Vec::
::new(e)); - + #[cfg(not(feature = "certora"))] emit_claim_topic_added(e, claim_topic); } } @@ -252,7 +253,7 @@ pub fn remove_claim_topic(e: &Env, claim_topic: u32) { // removing ClaimTopicIssuers for this topic let key = ClaimTopicsAndIssuersStorageKey::ClaimTopicIssuers(claim_topic); e.storage().persistent().remove(&key); - + #[cfg(not(feature = "certora"))] emit_claim_topic_removed(e, claim_topic); } else { panic_with_error!(e, ClaimTopicsAndIssuersError::ClaimTopicDoesNotExist); @@ -340,7 +341,7 @@ pub fn add_trusted_issuer(e: &Env, trusted_issuer: &Address, claim_topics: &Vec< let topic_key = ClaimTopicsAndIssuersStorageKey::ClaimTopicIssuers(topic); e.storage().persistent().set(&topic_key, &topic_issuers); } - + #[cfg(not(feature = "certora"))] emit_trusted_issuer_added(e, trusted_issuer, claim_topics.clone()); } @@ -400,7 +401,7 @@ pub fn remove_trusted_issuer(e: &Env, trusted_issuer: &Address) { e.storage().persistent().set(&topic_key, &topic_issuers); } } - + #[cfg(not(feature = "certora"))] emit_trusted_issuer_removed(e, trusted_issuer); } else { panic_with_error!(e, ClaimTopicsAndIssuersError::IssuerDoesNotExist); @@ -466,13 +467,33 @@ pub fn update_issuer_claim_topics(e: &Env, trusted_issuer: &Address, claim_topic let old_topics = get_trusted_issuer_claim_topics(e, trusted_issuer); // Calculate topics to remove (in old but not in new) + #[cfg(not(feature = "certora"))] let topics_to_remove: Vec = Vec::from_iter(e, old_topics.iter().filter(|old_topic| !claim_topics.contains(old_topic))); + #[cfg(feature = "certora")] + let topics_to_remove: Vec = { + let mut v: Vec = Vec::new(e); + for old_topic in old_topics.iter().filter(|old_topic| !claim_topics.contains(old_topic)) { + v.push_back(old_topic); + } + v + }; + // Calculate topics to add (in new but not in old) + #[cfg(not(feature = "certora"))] let topics_to_add: Vec = Vec::from_iter(e, claim_topics.iter().filter(|new_topic| !old_topics.contains(new_topic))); + #[cfg(feature = "certora")] + let topics_to_add: Vec = { + let mut v: Vec = Vec::new(e); + for claim_topic in claim_topics.iter().filter(|new_topic| !old_topics.contains(new_topic)) { + v.push_back(claim_topic); + } + v + }; + // Update issuer's claim topics let topics_key = ClaimTopicsAndIssuersStorageKey::IssuerClaimTopics(trusted_issuer.clone()); e.storage().persistent().set(&topics_key, claim_topics); @@ -496,7 +517,7 @@ pub fn update_issuer_claim_topics(e: &Env, trusted_issuer: &Address, claim_topic let topic_key = ClaimTopicsAndIssuersStorageKey::ClaimTopicIssuers(topic_to_add); e.storage().persistent().set(&topic_key, &topic_issuers); } - + #[cfg(not(feature = "certora"))] emit_issuer_topics_updated(e, trusted_issuer, claim_topics.clone()); } diff --git a/packages/tokens/src/rwa/compliance/mod.rs b/packages/tokens/src/rwa/compliance/mod.rs index 0b52940b..98a04a5c 100644 --- a/packages/tokens/src/rwa/compliance/mod.rs +++ b/packages/tokens/src/rwa/compliance/mod.rs @@ -1,7 +1,13 @@ use soroban_sdk::{ - contractclient, contracterror, contractevent, contracttype, Address, Env, String, Vec, + contractclient, contracterror, contracttype, Address, Env, String, Vec, }; +#[cfg(not(feature = "certora"))] +use soroban_sdk::{contractevent}; + +#[cfg(feature = "certora")] +use cvlr_soroban_derive::contractevent; + use crate::rwa::utils::token_binder::TokenBinder; pub mod storage; @@ -236,6 +242,7 @@ pub struct ModuleAdded { /// * `e` - Access to the Soroban environment. /// * `hook` - The hook type the module is registered for. /// * `module` - The address of the module. +#[cfg(not(feature = "certora"))] pub fn emit_module_added(e: &Env, hook: ComplianceHook, module: Address) { ModuleAdded { hook, module }.publish(e); } @@ -257,6 +264,7 @@ pub struct ModuleRemoved { /// * `e` - Access to the Soroban environment. /// * `hook` - The hook type the module is registered for. /// * `module` - The address of the module. +#[cfg(not(feature = "certora"))] pub fn emit_module_removed(e: &Env, hook: ComplianceHook, module: Address) { ModuleRemoved { hook, module }.publish(e); } diff --git a/packages/tokens/src/rwa/compliance/storage.rs b/packages/tokens/src/rwa/compliance/storage.rs index 3e39e80f..d298aeaa 100644 --- a/packages/tokens/src/rwa/compliance/storage.rs +++ b/packages/tokens/src/rwa/compliance/storage.rs @@ -2,17 +2,24 @@ use soroban_sdk::{contracttype, panic_with_error, Address, Env, Vec}; use crate::rwa::{ compliance::{ - emit_module_added, emit_module_removed, ComplianceError, ComplianceHook, + ComplianceError, ComplianceHook, ComplianceModuleClient, COMPLIANCE_EXTEND_AMOUNT, COMPLIANCE_TTL_THRESHOLD, MAX_MODULES, }, utils::token_binder::is_token_bound, }; +#[cfg(not(feature = "certora"))] +use crate::rwa::{ + compliance::{ + emit_module_added, emit_module_removed, + } +}; + /// Storage keys for the modular compliance contract. #[contracttype] #[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] -pub enum DataKey { - /// Maps ComplianceHook -> Vec
for registered modules +pub enum ComplianceDataKey { + /// Maps ComplianceHook -> `Vec
` for registered modules HookModules(ComplianceHook), } @@ -33,7 +40,7 @@ pub enum DataKey { /// A vector of module addresses registered for the specified hook. /// Returns an empty vector if no modules are registered. pub fn get_modules_for_hook(e: &Env, hook: ComplianceHook) -> Vec
{ - let key = DataKey::HookModules(hook); + let key = ComplianceDataKey::HookModules(hook); if let Some(existing_modules) = e.storage().persistent().get(&key) { e.storage().persistent().extend_ttl( &key, @@ -110,11 +117,12 @@ pub fn add_module_to(e: &Env, hook: ComplianceHook, module: Address) { } // Add the module - let key = DataKey::HookModules(hook.clone()); + let key = ComplianceDataKey::HookModules(hook.clone()); modules.push_back(module.clone()); e.storage().persistent().set(&key, &modules); // Emit event + #[cfg(not(feature = "certora"))] emit_module_added(e, hook, module); } @@ -159,10 +167,11 @@ pub fn remove_module_from(e: &Env, hook: ComplianceHook, module: Address) { modules.remove(index); // Update storage - let key = DataKey::HookModules(hook.clone()); + let key = ComplianceDataKey::HookModules(hook.clone()); e.storage().persistent().set(&key, &modules); // Emit event + #[cfg(not(feature = "certora"))] emit_module_removed(e, hook, module); } @@ -186,7 +195,7 @@ pub fn remove_module_from(e: &Env, hook: ComplianceHook, module: Address) { /// /// # Errors /// -/// * refer to [`require_auth_from_bound_contract`] +/// * refer to [`require_auth_from_bound_token`] /// /// # Cross-Contract Calls /// @@ -216,7 +225,7 @@ pub fn transferred(e: &Env, from: Address, to: Address, amount: i128, token: Add /// /// # Errors /// -/// * refer to [`require_auth_from_bound_contract`] +/// * refer to [`require_auth_from_bound_token`] /// /// # Cross-Contract Calls /// @@ -246,7 +255,7 @@ pub fn created(e: &Env, to: Address, amount: i128, token: Address) { /// /// # Errors /// -/// * refer to [`require_auth_from_bound_contract`] +/// * refer to [`require_auth_from_bound_token`] /// /// # Cross-Contract Calls /// diff --git a/packages/tokens/src/rwa/extensions/doc_manager/mod.rs b/packages/tokens/src/rwa/extensions/doc_manager/mod.rs index fa167ad5..c5022daa 100644 --- a/packages/tokens/src/rwa/extensions/doc_manager/mod.rs +++ b/packages/tokens/src/rwa/extensions/doc_manager/mod.rs @@ -29,9 +29,15 @@ mod storage; #[cfg(test)] mod test; -use soroban_sdk::{contracterror, contractevent, Address, BytesN, Env, String, Vec}; +#[cfg(not(feature = "certora"))] +use soroban_sdk::{contractevent}; + +#[cfg(feature = "certora")] +use cvlr_soroban_derive::contractevent; + +use soroban_sdk::{contracterror, Address, BytesN, Env, String, Vec}; pub use storage::{ - get_all_documents, get_document, get_document_by_index, get_document_count, remove_document, + get_document, get_document_by_index, get_document_count, get_documents, remove_document, set_document, Document, DocumentStorageKey, }; @@ -100,12 +106,15 @@ pub trait DocumentManager: RWAToken { /// * data - `[]` fn remove_document(e: &Env, name: BytesN<32>, operator: Address); - /// Retrieves a full list of all documents attached to the contract. + /// Retrieves documents from a specific bucket. + /// + /// Returns an empty vector if the bucket is empty or doesn't exist. /// /// # Arguments /// /// * `e` - The Soroban environment. - fn get_all_documents(e: &Env) -> Vec<(BytesN<32>, Document)>; + /// * `bucket_index` - The index of the bucket to retrieve documents from. + fn get_documents(e: &Env, bucket_index: u32) -> Vec<(BytesN<32>, Document)>; } // ################## ERRORS ################## @@ -119,6 +128,8 @@ pub enum DocumentError { DocumentNotFound = 380, /// Maximum number of documents has been reached. MaxDocumentsReached = 381, + /// The URI exceeds the maximum allowed length. + UriTooLong = 382, } // ################## CONSTANTS ################## @@ -133,6 +144,8 @@ pub const MAX_BUCKETS: u32 = 100; pub const BUCKET_SIZE: u32 = 50; /// Maximum number of documents that can be stored. pub const MAX_DOCUMENTS: u32 = BUCKET_SIZE * MAX_BUCKETS; // 5_000 +/// Maximum length for document URI. +pub const MAX_URI_LEN: u32 = 200; // ################## EVENTS ################## @@ -156,6 +169,7 @@ pub struct DocumentUpdated { /// * `uri` - The document URI. /// * `document_hash` - The document hash. /// * `timestamp` - The timestamp of the operation. +#[cfg(not(feature = "certora"))] pub fn emit_document_updated( e: &Env, name: &BytesN<32>, @@ -186,6 +200,7 @@ pub struct DocumentRemoved { /// /// * `e` - The Soroban environment. /// * `name` - The document name. +#[cfg(not(feature = "certora"))] pub fn emit_document_removed(e: &Env, name: &BytesN<32>) { DocumentRemoved { name: name.clone() }.publish(e); } diff --git a/packages/tokens/src/rwa/extensions/doc_manager/storage.rs b/packages/tokens/src/rwa/extensions/doc_manager/storage.rs index ab34050a..eb0fbff5 100644 --- a/packages/tokens/src/rwa/extensions/doc_manager/storage.rs +++ b/packages/tokens/src/rwa/extensions/doc_manager/storage.rs @@ -28,8 +28,13 @@ use soroban_sdk::{contracttype, panic_with_error, BytesN, Env, String, TryFromVal, Val, Vec}; use super::{ - emit_document_removed, emit_document_updated, DocumentError, BUCKET_SIZE, - DOCUMENT_EXTEND_AMOUNT, DOCUMENT_TTL_THRESHOLD, MAX_DOCUMENTS, + DocumentError, BUCKET_SIZE, + DOCUMENT_EXTEND_AMOUNT, DOCUMENT_TTL_THRESHOLD, MAX_DOCUMENTS, MAX_URI_LEN, +}; + +#[cfg(not(feature = "certora"))] +use super::{ + emit_document_removed, emit_document_updated, }; /// Represents a document with its metadata. @@ -112,30 +117,17 @@ pub fn get_document_by_index(e: &Env, index: u32) -> (BytesN<32>, Document) { bucket.get(offset_in_bucket).expect("document entry to be present in bucket") } -/// Retrieves a full list of all documents. +/// Retrieves documents from a specific bucket. +/// +/// Returns an empty vector if the bucket is empty or doesn't exist. /// /// # Arguments /// /// * `e` - The Soroban environment. -pub fn get_all_documents(e: &Env) -> Vec<(BytesN<32>, Document)> { - let count = get_document_count(e); - let mut documents = Vec::new(e); - - if count == 0 { - return documents; - } - - let last_bucket = (count - 1) / BUCKET_SIZE; - - for bucket_idx in 0..=last_bucket { - let bucket_key = DocumentStorageKey::Bucket(bucket_idx); - let bucket: Vec<(BytesN<32>, Document)> = - e.storage().persistent().get(&bucket_key).unwrap_or_else(|| Vec::new(e)); - - documents.append(&bucket); - } - - documents +/// * `bucket_index` - The index of the bucket to retrieve documents from. +pub fn get_documents(e: &Env, bucket_index: u32) -> Vec<(BytesN<32>, Document)> { + let bucket_key = DocumentStorageKey::Bucket(bucket_index); + get_persistent_entry(e, &bucket_key).unwrap_or_else(|| Vec::new(e)) } // ################## UPDATE STATE ################## @@ -153,6 +145,8 @@ pub fn get_all_documents(e: &Env) -> Vec<(BytesN<32>, Document)> { /// /// * [`DocumentError::MaxDocumentsReached`] - If the maximum number of /// documents has been reached. +/// * [`DocumentError::UriTooLong`] - If the URI exceeds the maximum allowed +/// length of 200 characters. /// /// # Events /// @@ -166,6 +160,11 @@ pub fn get_all_documents(e: &Env) -> Vec<(BytesN<32>, Document)> { /// - During contract initialization/construction /// - In functions that implement their own authorization logic pub fn set_document(e: &Env, name: &BytesN<32>, uri: &String, document_hash: &BytesN<32>) { + // Validate URI length + if uri.len() > MAX_URI_LEN { + panic_with_error!(e, DocumentError::UriTooLong) + } + let timestamp = e.ledger().timestamp(); let document = Document { uri: uri.clone(), document_hash: document_hash.clone(), timestamp }; @@ -209,7 +208,7 @@ pub fn set_document(e: &Env, name: &BytesN<32>, uri: &String, document_hash: &By e.storage().persistent().set(&DocumentStorageKey::Count, &(count + 1)); } - + #[cfg(not(feature = "certora"))] emit_document_updated(e, name, uri, document_hash, timestamp); } @@ -291,7 +290,7 @@ pub fn remove_document(e: &Env, name: &BytesN<32>) { e.storage().persistent().remove(&index_key); e.storage().persistent().set(&DocumentStorageKey::Count, &last_index); - + #[cfg(not(feature = "certora"))] emit_document_removed(e, name); } diff --git a/packages/tokens/src/rwa/extensions/doc_manager/test.rs b/packages/tokens/src/rwa/extensions/doc_manager/test.rs index d8a97cf8..cf5b3851 100644 --- a/packages/tokens/src/rwa/extensions/doc_manager/test.rs +++ b/packages/tokens/src/rwa/extensions/doc_manager/test.rs @@ -4,10 +4,10 @@ use soroban_sdk::{contract, Bytes, BytesN, Env, String, Vec}; use crate::rwa::extensions::doc_manager::{ storage::{ - get_all_documents, get_document, get_document_by_index, get_document_count, - remove_document, set_document, + get_document, get_document_by_index, get_document_count, get_documents, remove_document, + set_document, }, - DocumentStorageKey, BUCKET_SIZE, MAX_DOCUMENTS, + DocumentStorageKey, BUCKET_SIZE, MAX_DOCUMENTS, MAX_URI_LEN, }; #[contract] @@ -128,18 +128,19 @@ fn remove_document_not_found() { } #[test] -fn get_all_documents_empty() { +fn get_documents_empty() { let e = Env::default(); let contract_id = e.register(MockContract, ()); e.as_contract(&contract_id, || { - let documents = get_all_documents(&e); + // Should return empty vector when bucket is empty + let documents = get_documents(&e, 0); assert_eq!(documents.len(), 0); }); } #[test] -fn get_all_documents_multiple() { +fn get_documents_multiple() { let e = Env::default(); let contract_id = e.register(MockContract, ()); @@ -161,7 +162,8 @@ fn get_all_documents_multiple() { set_document(&e, &name2, &uri2, &hash2); set_document(&e, &name3, &uri3, &hash3); - let documents = get_all_documents(&e); + // Get documents from bucket 0 + let documents = get_documents(&e, 0); assert_eq!(documents.len(), 3); // Verify all documents are present @@ -177,7 +179,7 @@ fn get_all_documents_multiple() { } #[test] -fn get_all_documents_after_removal() { +fn get_documents_after_removal() { let e = Env::default(); let contract_id = e.register(MockContract, ()); @@ -198,7 +200,8 @@ fn get_all_documents_after_removal() { // Remove one document remove_document(&e, &name1); - let documents = get_all_documents(&e); + // Get documents from bucket 0 + let documents = get_documents(&e, 0); assert_eq!(documents.len(), 1); let (doc_name, _doc) = documents.get(0).unwrap(); assert_eq!(doc_name, name2); @@ -479,3 +482,20 @@ fn remove_last_document_in_bucket() { assert_eq!(first_name, create_test_name(&e, "doc_0")); }); } + +#[test] +#[should_panic(expected = "Error(Contract, #382)")] +fn set_document_uri_too_long() { + let e = Env::default(); + let contract_id = e.register(MockContract, ()); + + e.as_contract(&contract_id, || { + let name = create_test_name(&e, "test_doc"); + // Create a URI that exceeds MAX_URI_LEN (200 characters) + let long_uri = String::from_str(&e, &"a".repeat((MAX_URI_LEN + 1) as usize)); + let hash = create_test_hash(&e, "document content"); + + // This should panic with UriTooLong error (382) + set_document(&e, &name, &long_uri, &hash); + }); +} diff --git a/packages/tokens/src/rwa/identity_claims/mod.rs b/packages/tokens/src/rwa/identity_claims/mod.rs index e3abca90..4fce7e81 100644 --- a/packages/tokens/src/rwa/identity_claims/mod.rs +++ b/packages/tokens/src/rwa/identity_claims/mod.rs @@ -2,8 +2,14 @@ mod storage; #[cfg(test)] mod test; +#[cfg(not(feature = "certora"))] +use soroban_sdk::{contractevent}; + +#[cfg(feature = "certora")] +use cvlr_soroban_derive::contractevent; + use soroban_sdk::{ - contractclient, contracterror, contractevent, Address, Bytes, BytesN, Env, String, Vec, + contractclient, contracterror, Address, Bytes, BytesN, Env, String, Vec, }; pub use storage::{ add_claim, generate_claim_id, get_claim, get_claim_ids_by_topic, remove_claim, Claim, @@ -127,6 +133,7 @@ pub struct ClaimChanged { /// * `e` - The Soroban environment. /// * `event_type` - The type of claim event (Added, Removed, or Changed). /// * `claim` - The claim data. +#[cfg(not(feature = "certora"))] pub fn emit_claim_event(e: &Env, event_type: ClaimEvent, claim: Claim) { match event_type { ClaimEvent::Added => ClaimAdded { claim }.publish(e), diff --git a/packages/tokens/src/rwa/identity_claims/storage.rs b/packages/tokens/src/rwa/identity_claims/storage.rs index 9d48cf9c..e72aff78 100644 --- a/packages/tokens/src/rwa/identity_claims/storage.rs +++ b/packages/tokens/src/rwa/identity_claims/storage.rs @@ -28,8 +28,14 @@ use soroban_sdk::{ }; use super::{ - emit_claim_event, ClaimEvent, ClaimsError, CLAIMS_EXTEND_AMOUNT, CLAIMS_TTL_THRESHOLD, + ClaimEvent, ClaimsError, CLAIMS_EXTEND_AMOUNT, CLAIMS_TTL_THRESHOLD, }; + +#[cfg(not(feature = "certora"))] +use super::{ + emit_claim_event, +}; + use crate::rwa::claim_issuer::ClaimIssuerClient; /// Represents a claim stored on-chain. @@ -120,8 +126,10 @@ pub fn add_claim( // Emit appropriate event if is_new_claim { add_claim_to_topic_index(e, topic, &claim_id); + #[cfg(not(feature = "certora"))] emit_claim_event(e, ClaimEvent::Added, claim); } else { + #[cfg(not(feature = "certora"))] emit_claim_event(e, ClaimEvent::Changed, claim); } @@ -206,7 +214,7 @@ pub fn remove_claim(e: &Env, claim_id: &BytesN<32>) { e.storage().persistent().remove(&claim_key); remove_claim_from_topic_index(e, claim.topic, claim_id); - + #[cfg(not(feature = "certora"))] emit_claim_event(e, ClaimEvent::Removed, claim); } diff --git a/packages/tokens/src/rwa/identity_registry_storage/mod.rs b/packages/tokens/src/rwa/identity_registry_storage/mod.rs index c98ae351..d7de7512 100644 --- a/packages/tokens/src/rwa/identity_registry_storage/mod.rs +++ b/packages/tokens/src/rwa/identity_registry_storage/mod.rs @@ -276,7 +276,13 @@ mod storage; #[cfg(test)] mod test; -use soroban_sdk::{contracterror, contractevent, Address, Env, FromVal, Val, Vec}; +#[cfg(not(feature = "certora"))] +use soroban_sdk::{contractevent}; + +#[cfg(feature = "certora")] +use cvlr_soroban_derive::contractevent; + +use soroban_sdk::{contracterror, Address, Env, FromVal, Val, Vec}; pub use storage::{ add_country_data_entries, add_identity, delete_country_data, get_country_data, get_country_data_entries, get_identity_profile, get_recovered_to, modify_country_data, @@ -528,6 +534,7 @@ pub struct IdentityStored { /// * `e` - The Soroban environment. /// * `account` - The account address associated with the identity. /// * `identity` - The identity address that was stored. +#[cfg(not(feature = "certora"))] pub fn emit_identity_stored(e: &Env, account: &Address, identity: &Address) { IdentityStored { account: account.clone(), identity: identity.clone() }.publish(e); } @@ -549,6 +556,7 @@ pub struct IdentityUnstored { /// * `e` - The Soroban environment. /// * `account` - The account address that had its identity removed. /// * `identity` - The identity address that was removed. +#[cfg(not(feature = "certora"))] pub fn emit_identity_unstored(e: &Env, account: &Address, identity: &Address) { IdentityUnstored { account: account.clone(), identity: identity.clone() }.publish(e); } @@ -570,6 +578,7 @@ pub struct IdentityModified { /// * `e` - The Soroban environment. /// * `old_identity` - The previous identity address. /// * `new_identity` - The new identity address. +#[cfg(not(feature = "certora"))] pub fn emit_identity_modified(e: &Env, old_identity: &Address, new_identity: &Address) { IdentityModified { old_identity: old_identity.clone(), new_identity: new_identity.clone() } .publish(e); @@ -592,6 +601,7 @@ pub struct IdentityRecovered { /// * `e` - The Soroban environment. /// * `old_account` - The previous account address. /// * `new_account` - The new account address. +#[cfg(not(feature = "certora"))] pub fn emit_identity_recovered(e: &Env, old_account: &Address, new_account: &Address) { IdentityRecovered { old_account: old_account.clone(), new_account: new_account.clone() } .publish(e); @@ -633,6 +643,7 @@ pub struct CountryDataModified { /// * `event_type` - The type of country data event. /// * `account` - The account address associated with the country data. /// * `country_data` - The country data that was affected. +#[cfg(not(feature = "certora"))] pub fn emit_country_data_event( e: &Env, event_type: CountryDataEvent, diff --git a/packages/tokens/src/rwa/identity_registry_storage/storage.rs b/packages/tokens/src/rwa/identity_registry_storage/storage.rs index 94036941..92a0a355 100644 --- a/packages/tokens/src/rwa/identity_registry_storage/storage.rs +++ b/packages/tokens/src/rwa/identity_registry_storage/storage.rs @@ -125,11 +125,16 @@ use soroban_sdk::{ }; use crate::rwa::identity_registry_storage::{ - emit_country_data_event, emit_identity_modified, emit_identity_recovered, emit_identity_stored, - emit_identity_unstored, CountryDataEvent, IRSError, IDENTITY_EXTEND_AMOUNT, + CountryDataEvent, IRSError, IDENTITY_EXTEND_AMOUNT, IDENTITY_TTL_THRESHOLD, MAX_COUNTRY_ENTRIES, }; +#[cfg(not(feature = "certora"))] +use crate::rwa::identity_registry_storage::{ + emit_country_data_event, emit_identity_modified, emit_identity_recovered, emit_identity_stored, + emit_identity_unstored, +}; + /// Represents the type of identity holder #[contracttype] #[derive(Clone, Debug, Eq, PartialEq)] @@ -361,7 +366,7 @@ pub fn add_identity( panic_with_error!(e, IRSError::IdentityOverwrite) } e.storage().persistent().set(&identity_key, identity); - + #[cfg(not(feature = "certora"))] emit_identity_stored(e, account, identity); let profile = IdentityProfile { identity_type, countries: initial_countries.clone() }; @@ -369,6 +374,7 @@ pub fn add_identity( e.storage().persistent().set(&IRSStorageKey::IdentityProfile(account.clone()), &profile); for country_data in initial_countries.iter() { + #[cfg(not(feature = "certora"))] emit_country_data_event(e, CountryDataEvent::Added, account, &country_data); } } @@ -411,7 +417,7 @@ pub fn modify_identity(e: &Env, account: &Address, new_identity: &Address) { .unwrap_or_else(|| panic_with_error!(e, IRSError::IdentityNotFound)); e.storage().persistent().set(&key, new_identity); - + #[cfg(not(feature = "certora"))] emit_identity_modified(e, &old_identity, new_identity); } @@ -455,7 +461,7 @@ pub fn remove_identity(e: &Env, account: &Address) { .get(&identity_key) .unwrap_or_else(|| panic_with_error!(e, IRSError::IdentityNotFound)); e.storage().persistent().remove(&identity_key); - + #[cfg(not(feature = "certora"))] emit_identity_unstored(e, account, &identity); // Remove all associated identity profile @@ -465,6 +471,7 @@ pub fn remove_identity(e: &Env, account: &Address) { e.storage().persistent().remove(&profile_key); for country_data in profile.countries { + #[cfg(not(feature = "certora"))] emit_country_data_event(e, CountryDataEvent::Removed, account, &country_data); } } @@ -547,7 +554,7 @@ pub fn recover_identity(e: &Env, old_account: &Address, new_account: &Address) { // Mark old account as recovered to new account e.storage().persistent().set(&IRSStorageKey::RecoveredTo(old_account.clone()), new_account); - + #[cfg(not(feature = "certora"))] emit_identity_recovered(e, old_account, new_account); } @@ -601,6 +608,7 @@ pub fn add_country_data_entries(e: &Env, account: &Address, country_data_list: & e.storage().persistent().set(&key, &profile); for country_data in country_data_list.iter() { + #[cfg(not(feature = "certora"))] emit_country_data_event(e, CountryDataEvent::Added, account, &country_data); } } @@ -643,7 +651,7 @@ pub fn modify_country_data(e: &Env, account: &Address, index: u32, country_data: let key = IRSStorageKey::IdentityProfile(account.clone()); e.storage().persistent().set(&key, &profile); - + #[cfg(not(feature = "certora"))] emit_country_data_event(e, CountryDataEvent::Modified, account, country_data); } @@ -692,7 +700,7 @@ pub fn delete_country_data(e: &Env, account: &Address, index: u32) { let key = IRSStorageKey::IdentityProfile(account.clone()); e.storage().persistent().set(&key, &profile); - + #[cfg(not(feature = "certora"))] emit_country_data_event(e, CountryDataEvent::Removed, account, &country_data_to_remove); } diff --git a/packages/tokens/src/rwa/identity_verifier/storage.rs b/packages/tokens/src/rwa/identity_verifier/storage.rs index dbedafbe..9d3da6a5 100644 --- a/packages/tokens/src/rwa/identity_verifier/storage.rs +++ b/packages/tokens/src/rwa/identity_verifier/storage.rs @@ -3,11 +3,15 @@ use soroban_sdk::{contractclient, contracttype, panic_with_error, Address, Env}; use crate::rwa::{ claim_issuer::ClaimIssuerClient, claim_topics_and_issuers::ClaimTopicsAndIssuersClient, - emit_claim_topics_and_issuers_set, identity_claims::{generate_claim_id, Claim, IdentityClaimsClient}, RWAError, }; +#[cfg(not(feature = "certora"))] +use crate::rwa::{ + emit_claim_topics_and_issuers_set, +}; + /// Storage keys for the data associated with `RWA` token #[contracttype] pub enum IdentityVerifierStorageKey { @@ -75,7 +79,7 @@ pub fn identity_registry_storage(e: &Env) -> Address { /// /// # Errors /// -/// * [`RWAError::IdentityVefificationFailed`] - When the identity of the +/// * [`RWAError::IdentityVerificationFailed`] - When the identity of the /// account cannot be verified. pub fn verify_identity(e: &Env, account: &Address) { let irs_addr = identity_registry_storage(e); @@ -187,6 +191,7 @@ pub fn set_claim_topics_and_issuers(e: &Env, claim_topics_and_issuers: &Address) e.storage() .instance() .set(&IdentityVerifierStorageKey::ClaimTopicsAndIssuers, claim_topics_and_issuers); + #[cfg(not(feature = "certora"))] emit_claim_topics_and_issuers_set(e, claim_topics_and_issuers); } diff --git a/packages/tokens/src/rwa/mod.rs b/packages/tokens/src/rwa/mod.rs index 062a1633..422a51b8 100644 --- a/packages/tokens/src/rwa/mod.rs +++ b/packages/tokens/src/rwa/mod.rs @@ -104,7 +104,13 @@ pub mod utils; #[cfg(test)] mod test; -use soroban_sdk::{contracterror, contractevent, Address, Env, String}; +#[cfg(not(feature = "certora"))] +use soroban_sdk::{contractevent}; + +#[cfg(feature = "certora")] +use cvlr_soroban_derive::contractevent; + +use soroban_sdk::{contracterror, Address, Env, String}; use stellar_contract_utils::pausable::Pausable; pub use storage::{RWAStorageKey, RWA}; @@ -166,10 +172,11 @@ pub trait RWAToken: Pausable + FungibleToken { /// /// # Errors /// - /// * [`RWAError::IdentityVefificationFailed`] - When the identity of the + /// * [`RWAError::IdentityVerificationFailed`] - When the identity of the /// recipient address cannot be verified. /// * [`RWAError::AddressFrozen`] - When the recipient address is frozen. - /// * [`PausableError::EnforcedPause`] - When the contract is paused. + /// * [`stellar_contract_utils::pausable::PausableError::EnforcedPause`] - + /// When the contract is paused. /// /// # Events /// @@ -214,7 +221,7 @@ pub trait RWAToken: Pausable + FungibleToken { /// /// # Errors /// - /// * [`RWAError::IdentityVefificationFailed`] - When the identity of the + /// * [`RWAError::IdentityVerificationFailed`] - When the identity of the /// new account cannot be verified. /// /// # Events @@ -443,6 +450,7 @@ pub struct TokenOnchainIdUpdated { /// /// * `e` - Access to the Soroban environment. /// * `onchain_id` - The address of the onchain ID. +#[cfg(not(feature = "certora"))] pub fn emit_token_onchain_id_updated(e: &Env, onchain_id: &Address) { TokenOnchainIdUpdated { onchain_id: onchain_id.clone() }.publish(e); } @@ -464,6 +472,7 @@ pub struct RecoverySuccess { /// * `e` - Access to the Soroban environment. /// * `old_account` - The address of the old account. /// * `new_account` - The address of the new account. +#[cfg(not(feature = "certora"))] pub fn emit_recovery_success(e: &Env, old_account: &Address, new_account: &Address) { RecoverySuccess { old_account: old_account.clone(), new_account: new_account.clone() } .publish(e); @@ -486,6 +495,7 @@ pub struct AddressFrozen { /// * `e` - Access to the Soroban environment. /// * `user_address` - The wallet address that is affected. /// * `is_frozen` - The freezing status of the wallet. +#[cfg(not(feature = "certora"))] pub fn emit_address_frozen(e: &Env, user_address: &Address, is_frozen: bool) { AddressFrozen { user_address: user_address.clone(), is_frozen }.publish(e); } @@ -506,6 +516,7 @@ pub struct TokensFrozen { /// * `e` - Access to the Soroban environment. /// * `user_address` - The wallet address where tokens are frozen. /// * `amount` - The amount of tokens that are frozen. +#[cfg(not(feature = "certora"))] pub fn emit_tokens_frozen(e: &Env, user_address: &Address, amount: i128) { TokensFrozen { user_address: user_address.clone(), amount }.publish(e); } @@ -526,6 +537,7 @@ pub struct TokensUnfrozen { /// * `e` - Access to the Soroban environment. /// * `user_address` - The wallet address where tokens are unfrozen. /// * `amount` - The amount of tokens that are unfrozen. +#[cfg(not(feature = "certora"))] pub fn emit_tokens_unfrozen(e: &Env, user_address: &Address, amount: i128) { TokensUnfrozen { user_address: user_address.clone(), amount }.publish(e); } @@ -546,6 +558,7 @@ pub struct Mint { /// * `e` - Access to the Soroban environment. /// * `to` - The address receiving the new tokens. /// * `amount` - The amount of tokens minted. +#[cfg(not(feature = "certora"))] pub fn emit_mint(e: &Env, to: &Address, amount: i128) { Mint { to: to.clone(), amount }.publish(e); } @@ -566,6 +579,7 @@ pub struct Burn { /// * `e` - Access to the Soroban environment. /// * `from` - The address from which tokens were burned. /// * `amount` - The amount of tokens burned. +#[cfg(not(feature = "certora"))] pub fn emit_burn(e: &Env, from: &Address, amount: i128) { Burn { from: from.clone(), amount }.publish(e); } @@ -584,6 +598,7 @@ pub struct ComplianceSet { /// /// * `e` - Access to the Soroban environment. /// * `compliance` - The address of the Compliance contract. +#[cfg(not(feature = "certora"))] pub fn emit_compliance_set(e: &Env, compliance: &Address) { ComplianceSet { compliance: compliance.clone() }.publish(e); } @@ -604,6 +619,7 @@ pub struct ClaimTopicsAndIssuersSet { /// * `e` - Access to the Soroban environment. /// * `claim_topics_and_issuers` - The address of the Claim Topics and Issuers /// contract. +#[cfg(not(feature = "certora"))] pub fn emit_claim_topics_and_issuers_set(e: &Env, claim_topics_and_issuers: &Address) { ClaimTopicsAndIssuersSet { claim_topics_and_issuers: claim_topics_and_issuers.clone() } .publish(e); @@ -623,6 +639,7 @@ pub struct IdentityVerifierSet { /// /// * `e` - Access to the Soroban environment. /// * `identity_verifier` - The address of the Identity Verifier contract. +#[cfg(not(feature = "certora"))] pub fn emit_identity_verifier_set(e: &Env, identity_verifier: &Address) { IdentityVerifierSet { identity_verifier: identity_verifier.clone() }.publish(e); } diff --git a/packages/tokens/src/rwa/storage.rs b/packages/tokens/src/rwa/storage.rs index 13cce272..7928ee28 100644 --- a/packages/tokens/src/rwa/storage.rs +++ b/packages/tokens/src/rwa/storage.rs @@ -2,16 +2,24 @@ use soroban_sdk::{contracttype, panic_with_error, Address, Env, String}; use stellar_contract_utils::pausable::{paused, PausableError}; use crate::{ - fungible::{emit_transfer, Base, ContractOverrides}, + fungible::{Base, ContractOverrides}, rwa::{ - compliance::ComplianceClient, emit_address_frozen, emit_burn, emit_compliance_set, - emit_identity_verifier_set, emit_mint, emit_recovery_success, - emit_token_onchain_id_updated, emit_tokens_frozen, emit_tokens_unfrozen, + compliance::ComplianceClient, identity_verifier::IdentityVerifierClient, RWAError, FROZEN_EXTEND_AMOUNT, FROZEN_TTL_THRESHOLD, }, }; +#[cfg(not(feature = "certora"))] +use crate::{ + fungible::{emit_transfer,}, + rwa::{ + emit_address_frozen, emit_burn, emit_compliance_set, + emit_identity_verifier_set, emit_mint, emit_recovery_success, + emit_token_onchain_id_updated, emit_tokens_frozen, emit_tokens_unfrozen, + }, +}; + /// Storage keys for the data associated with `RWA` token #[contracttype] pub enum RWAStorageKey { @@ -210,6 +218,7 @@ impl RWA { let new_frozen = current_frozen - tokens_to_unfreeze; e.storage().persistent().set(&RWAStorageKey::FrozenTokens(from.clone()), &new_frozen); + #[cfg(not(feature = "certora"))] emit_tokens_unfrozen(e, from, tokens_to_unfreeze); } @@ -218,7 +227,7 @@ impl RWA { let compliance_addr = Self::compliance(e); let compliance_client = ComplianceClient::new(e, &compliance_addr); compliance_client.transferred(from, to, &amount, &e.current_contract_address()); - + #[cfg(not(feature = "certora"))] emit_transfer(e, from, to, amount); } @@ -276,7 +285,7 @@ impl RWA { Base::update(e, None, Some(to), amount); compliance_client.created(to, &amount, &e.current_contract_address()); - + #[cfg(not(feature = "certora"))] emit_mint(e, to, amount); } @@ -318,6 +327,7 @@ impl RWA { e.storage() .persistent() .set(&RWAStorageKey::FrozenTokens(user_address.clone()), &new_frozen); + #[cfg(not(feature = "certora"))] emit_tokens_unfrozen(e, user_address, tokens_to_unfreeze); } @@ -326,7 +336,7 @@ impl RWA { let compliance_addr = Self::compliance(e); let compliance_client = ComplianceClient::new(e, &compliance_addr); compliance_client.destroyed(user_address, &amount, &e.current_contract_address()); - + #[cfg(not(feature = "certora"))] emit_burn(e, user_address, amount); } @@ -410,7 +420,7 @@ impl RWA { if is_address_frozen { Self::set_address_frozen(e, new_account, true); } - + #[cfg(not(feature = "certora"))] emit_recovery_success(e, old_account, new_account); true @@ -437,7 +447,7 @@ impl RWA { /// authorization logic. pub fn set_address_frozen(e: &Env, user_address: &Address, freeze: bool) { e.storage().persistent().set(&RWAStorageKey::AddressFrozen(user_address.clone()), &freeze); - + #[cfg(not(feature = "certora"))] emit_address_frozen(e, user_address, freeze); } @@ -481,6 +491,7 @@ impl RWA { e.storage() .persistent() .set(&RWAStorageKey::FrozenTokens(user_address.clone()), &new_frozen); + #[cfg(not(feature = "certora"))] emit_tokens_frozen(e, user_address, amount); } @@ -522,6 +533,7 @@ impl RWA { e.storage() .persistent() .set(&RWAStorageKey::FrozenTokens(user_address.clone()), &new_frozen); + #[cfg(not(feature = "certora"))] emit_tokens_unfrozen(e, user_address, amount); } @@ -545,7 +557,7 @@ impl RWA { /// authorization logic. pub fn set_onchain_id(e: &Env, onchain_id: &Address) { e.storage().instance().set(&RWAStorageKey::OnchainId, onchain_id); - + #[cfg(not(feature = "certora"))] emit_token_onchain_id_updated(e, onchain_id); } @@ -568,6 +580,7 @@ impl RWA { /// authorization logic. pub fn set_compliance(e: &Env, compliance: &Address) { e.storage().instance().set(&RWAStorageKey::Compliance, compliance); + #[cfg(not(feature = "certora"))] emit_compliance_set(e, compliance); } @@ -590,6 +603,7 @@ impl RWA { /// authorization logic. pub fn set_identity_verifier(e: &Env, identity_verifier: &Address) { e.storage().instance().set(&RWAStorageKey::IdentityVerifier, identity_verifier); + #[cfg(not(feature = "certora"))] emit_identity_verifier_set(e, identity_verifier); } @@ -649,7 +663,7 @@ impl RWA { Base::update(e, Some(from), Some(to), amount); compliance_client.transferred(from, to, &amount, &e.current_contract_address()); - + #[cfg(not(feature = "certora"))] emit_transfer(e, from, to, amount); } diff --git a/packages/tokens/src/rwa/utils/token_binder/mod.rs b/packages/tokens/src/rwa/utils/token_binder/mod.rs index 33005c8b..a3da3d53 100644 --- a/packages/tokens/src/rwa/utils/token_binder/mod.rs +++ b/packages/tokens/src/rwa/utils/token_binder/mod.rs @@ -3,7 +3,13 @@ mod storage; #[cfg(test)] mod test; -use soroban_sdk::{contractclient, contracterror, contractevent, Address, Env, Vec}; +#[cfg(not(feature = "certora"))] +use soroban_sdk::{contractevent}; + +#[cfg(feature = "certora")] +use cvlr_soroban_derive::contractevent; + +use soroban_sdk::{contractclient, contracterror, Address, Env, Vec}; pub use storage::{ bind_token, bind_tokens, get_token_by_index, get_token_index, is_token_bound, linked_tokens, unbind_token, @@ -125,6 +131,7 @@ pub struct TokenBound { /// /// * `e` - The Soroban environment /// * `token` - The token address that was bound +#[cfg(not(feature = "certora"))] pub fn emit_token_bound(e: &Env, token: &Address) { TokenBound { token: token.clone() }.publish(e); } @@ -143,6 +150,7 @@ pub struct TokenUnbound { /// /// * `e` - The Soroban environment /// * `token` - The token address that was unbound +#[cfg(not(feature = "certora"))] fn emit_token_unbound(e: &Env, token: &Address) { TokenUnbound { token: token.clone() }.publish(e); } diff --git a/packages/tokens/src/rwa/utils/token_binder/storage.rs b/packages/tokens/src/rwa/utils/token_binder/storage.rs index 0bd745e7..ca0c0f8a 100644 --- a/packages/tokens/src/rwa/utils/token_binder/storage.rs +++ b/packages/tokens/src/rwa/utils/token_binder/storage.rs @@ -1,10 +1,15 @@ use soroban_sdk::{contracttype, panic_with_error, Address, Env, Map, TryFromVal, Val, Vec}; use crate::rwa::utils::token_binder::{ - emit_token_bound, emit_token_unbound, TokenBinderError, BUCKET_SIZE, MAX_TOKENS, + TokenBinderError, BUCKET_SIZE, MAX_TOKENS, TOKEN_BINDER_EXTEND_AMOUNT, TOKEN_BINDER_TTL_THRESHOLD, }; +#[cfg(not(feature = "certora"))] +use crate::rwa::utils::token_binder::{ + emit_token_bound, emit_token_unbound, +}; + /// Storage keys for the token binder system. /// /// - Tokens are stored in buckets of 100 addresses each @@ -193,7 +198,7 @@ pub fn bind_token(e: &Env, token: &Address) { count += 1; e.storage().persistent().set(&TokenBinderStorageKey::TotalCount, &count); - + #[cfg(not(feature = "certora"))] emit_token_bound(e, token); } @@ -281,6 +286,7 @@ pub fn bind_tokens(e: &Env, tokens: &Vec
) { panic_with_error!(e, TokenBinderError::TokenAlreadyBound) } bucket.push_back(token.clone()); + #[cfg(not(feature = "certora"))] emit_token_bound(e, &token); i += 1; count += 1; @@ -355,7 +361,7 @@ pub fn unbind_token(e: &Env, token: &Address) { // Update total count e.storage().persistent().set(&TokenBinderStorageKey::TotalCount, &last_index); - + #[cfg(not(feature = "certora"))] emit_token_unbound(e, token); } diff --git a/packages/tokens/src/fungible/extensions/vault/mod.rs b/packages/tokens/src/vault/mod.rs similarity index 72% rename from packages/tokens/src/fungible/extensions/vault/mod.rs rename to packages/tokens/src/vault/mod.rs index e04b42b0..0a1aad32 100644 --- a/packages/tokens/src/fungible/extensions/vault/mod.rs +++ b/packages/tokens/src/vault/mod.rs @@ -3,7 +3,16 @@ pub mod storage; #[cfg(test)] mod test; -use soroban_sdk::{contractevent, Address, Env}; +#[cfg(feature = "certora")] +pub mod specs; + +#[cfg(not(feature = "certora"))] +use soroban_sdk::{contractevent}; + +#[cfg(feature = "certora")] +use cvlr_soroban_derive::contractevent; + +use soroban_sdk::{contracterror, Address, Env}; pub use storage::Vault; use crate::fungible::FungibleToken; @@ -48,8 +57,8 @@ pub trait FungibleVault: FungibleToken { /// /// # Errors /// - /// * [`crate::fungible::FungibleTokenError::VaultAssetAddressNotSet`] - - /// When the vault's underlying asset address has not been initialized. + /// * [`crate::vault::VaultTokenError::VaultAssetAddressNotSet`] - When the + /// vault's underlying asset address has not been initialized. fn query_asset(e: &Env) -> Address; /// Returns the total amount of underlying assets held by the vault. @@ -63,8 +72,8 @@ pub trait FungibleVault: FungibleToken { /// /// # Errors /// - /// * [`crate::fungible::FungibleTokenError::VaultAssetAddressNotSet`] - - /// When the vault's underlying asset address has not been initialized. + /// * [`crate::vault::VaultTokenError::VaultAssetAddressNotSet`] - When the + /// vault's underlying asset address has not been initialized. fn total_assets(e: &Env) -> i128; /// Converts an amount of underlying assets to the equivalent amount of @@ -77,10 +86,10 @@ pub trait FungibleVault: FungibleToken { /// /// # Errors /// - /// * [`crate::fungible::FungibleTokenError::VaultInvalidAssetsAmount`] - - /// When assets < 0. - /// * [`crate::fungible::FungibleTokenError::MathOverflow`] - When - /// mathematical operations result in overflow. + /// * [`crate::vault::VaultTokenError::VaultInvalidAssetsAmount`] - When + /// assets < 0. + /// * [`crate::vault::VaultTokenError::MathOverflow`] - When mathematical + /// operations result in overflow. fn convert_to_shares(e: &Env, assets: i128) -> i128; /// Converts an amount of vault shares to the equivalent amount of @@ -93,10 +102,10 @@ pub trait FungibleVault: FungibleToken { /// /// # Errors /// - /// * [`crate::fungible::FungibleTokenError::VaultInvalidSharesAmount`] - - /// When shares < 0. - /// * [`crate::fungible::FungibleTokenError::MathOverflow`] - When - /// mathematical operations result in overflow. + /// * [`crate::vault::VaultTokenError::VaultInvalidSharesAmount`] - When + /// shares < 0. + /// * [`crate::vault::VaultTokenError::MathOverflow`] - When mathematical + /// operations result in overflow. fn convert_to_assets(e: &Env, shares: i128) -> i128; /// Returns the maximum amount of underlying assets that can be deposited @@ -118,10 +127,10 @@ pub trait FungibleVault: FungibleToken { /// /// # Errors /// - /// * [`crate::fungible::FungibleTokenError::VaultInvalidAssetsAmount`] - - /// When assets < 0. - /// * [`crate::fungible::FungibleTokenError::MathOverflow`] - When - /// mathematical operations result in overflow. + /// * [`crate::vault::VaultTokenError::VaultInvalidAssetsAmount`] - When + /// assets < 0. + /// * [`crate::vault::VaultTokenError::MathOverflow`] - When mathematical + /// operations result in overflow. fn preview_deposit(e: &Env, assets: i128) -> i128; /// Deposits underlying assets into the vault and mints vault shares @@ -137,13 +146,13 @@ pub trait FungibleVault: FungibleToken { /// /// # Errors /// - /// * [`crate::fungible::FungibleTokenError::VaultExceededMaxDeposit`] - - /// When attempting to deposit more assets than the maximum allowed for - /// the receiver. - /// * [`crate::fungible::FungibleTokenError::VaultInvalidAssetsAmount`] - - /// When `assets < 0`. - /// * [`crate::fungible::FungibleTokenError::MathOverflow`] - When - /// mathematical operations result in overflow. + /// * [`crate::vault::VaultTokenError::VaultExceededMaxDeposit`] - When + /// attempting to deposit more assets than the maximum allowed for the + /// receiver. + /// * [`crate::vault::VaultTokenError::VaultInvalidAssetsAmount`] - When + /// `assets < 0`. + /// * [`crate::vault::VaultTokenError::MathOverflow`] - When mathematical + /// operations result in overflow. /// /// # Events /// @@ -177,10 +186,10 @@ pub trait FungibleVault: FungibleToken { /// /// # Errors /// - /// * [`crate::fungible::FungibleTokenError::VaultInvalidSharesAmount`] - - /// When shares < 0. - /// * [`crate::fungible::FungibleTokenError::MathOverflow`] - When - /// mathematical operations result in overflow. + /// * [`crate::vault::VaultTokenError::VaultInvalidSharesAmount`] - When + /// shares < 0. + /// * [`crate::vault::VaultTokenError::MathOverflow`] - When mathematical + /// operations result in overflow. fn preview_mint(e: &Env, shares: i128) -> i128; /// Mints a specific amount of vault shares to the receiver by depositing @@ -197,13 +206,13 @@ pub trait FungibleVault: FungibleToken { /// /// # Errors /// - /// * [`crate::fungible::FungibleTokenError::VaultExceededMaxMint`] - When + /// * [`crate::vault::VaultTokenError::VaultExceededMaxMint`] - When /// attempting to mint more shares than the maximum allowed for the /// receiver. - /// * [`crate::fungible::FungibleTokenError::VaultInvalidSharesAmount`] - - /// When `shares < 0`. - /// * [`crate::fungible::FungibleTokenError::MathOverflow`] - When - /// mathematical operations result in overflow. + /// * [`crate::vault::VaultTokenError::VaultInvalidSharesAmount`] - When + /// `shares < 0`. + /// * [`crate::vault::VaultTokenError::MathOverflow`] - When mathematical + /// operations result in overflow. /// /// # Events /// @@ -228,10 +237,10 @@ pub trait FungibleVault: FungibleToken { /// /// # Errors /// - /// * [`crate::fungible::FungibleTokenError::VaultInvalidSharesAmount`] - - /// When shares < 0. - /// * [`crate::fungible::FungibleTokenError::MathOverflow`] - When - /// mathematical operations result in overflow. + /// * [`crate::vault::VaultTokenError::VaultInvalidSharesAmount`] - When + /// shares < 0. + /// * [`crate::vault::VaultTokenError::MathOverflow`] - When mathematical + /// operations result in overflow. fn max_withdraw(e: &Env, owner: Address) -> i128; /// Simulates and returns the amount of vault shares that would be burned @@ -244,10 +253,10 @@ pub trait FungibleVault: FungibleToken { /// /// # Errors /// - /// * [`crate::fungible::FungibleTokenError::VaultInvalidAssetsAmount`] - - /// When assets < 0. - /// * [`crate::fungible::FungibleTokenError::MathOverflow`] - When - /// mathematical operations result in overflow. + /// * [`crate::vault::VaultTokenError::VaultInvalidAssetsAmount`] - When + /// assets < 0. + /// * [`crate::vault::VaultTokenError::MathOverflow`] - When mathematical + /// operations result in overflow. fn preview_withdraw(e: &Env, assets: i128) -> i128; /// Withdraws a specific amount of underlying assets from the vault @@ -264,9 +273,9 @@ pub trait FungibleVault: FungibleToken { /// /// # Errors /// - /// * [`crate::fungible::FungibleTokenError::VaultExceededMaxWithdraw`] - - /// When attempting to withdraw more assets than the maximum allowed for - /// the owner. + /// * [`crate::vault::VaultTokenError::VaultExceededMaxWithdraw`] - When + /// attempting to withdraw more assets than the maximum allowed for the + /// owner. /// /// # Events /// @@ -306,10 +315,10 @@ pub trait FungibleVault: FungibleToken { /// /// # Errors /// - /// * [`crate::fungible::FungibleTokenError::VaultInvalidSharesAmount`] - - /// When shares < 0. - /// * [`crate::fungible::FungibleTokenError::MathOverflow`] - When - /// mathematical operations result in overflow. + /// * [`crate::vault::VaultTokenError::VaultInvalidSharesAmount`] - When + /// shares < 0. + /// * [`crate::vault::VaultTokenError::MathOverflow`] - When mathematical + /// operations result in overflow. fn preview_redeem(e: &Env, shares: i128) -> i128; /// Redeems a specific amount of vault shares for underlying assets, @@ -325,13 +334,13 @@ pub trait FungibleVault: FungibleToken { /// /// # Errors /// - /// * [`crate::fungible::FungibleTokenError::VaultExceededMaxRedeem`] - When + /// * [`crate::vault::VaultTokenError::VaultExceededMaxRedeem`] - When /// attempting to redeem more shares than the maximum allowed for the /// owner. - /// * [`crate::fungible::FungibleTokenError::VaultInvalidSharesAmount`] - - /// When `shares < 0`. - /// * [`crate::fungible::FungibleTokenError::MathOverflow`] - When - /// mathematical operations result in overflow. + /// * [`crate::vault::VaultTokenError::VaultInvalidSharesAmount`] - When + /// `shares < 0`. + /// * [`crate::vault::VaultTokenError::MathOverflow`] - When mathematical + /// operations result in overflow. /// /// # Events /// @@ -347,6 +356,41 @@ pub trait FungibleVault: FungibleToken { fn redeem(e: &Env, shares: i128, receiver: Address, owner: Address, operator: Address) -> i128; } +// ################## ERRORS ################## + +#[contracterror] +#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] +#[repr(u32)] +pub enum VaultTokenError { + /// Indicates access to uninitialized vault asset address. + VaultAssetAddressNotSet = 400, + /// Indicates that vault asset address is already set. + VaultAssetAddressAlreadySet = 401, + /// Indicates that vault virtual decimals offset is already set. + VaultVirtualDecimalsOffsetAlreadySet = 402, + /// Indicates the amount is not a valid vault assets value. + VaultInvalidAssetsAmount = 403, + /// Indicates the amount is not a valid vault shares value. + VaultInvalidSharesAmount = 404, + /// Attempted to deposit more assets than the max amount for address. + VaultExceededMaxDeposit = 405, + /// Attempted to mint more shares than the max amount for address. + VaultExceededMaxMint = 406, + /// Attempted to withdraw more assets than the max amount for address. + VaultExceededMaxWithdraw = 407, + /// Attempted to redeem more shares than the max amount for address. + VaultExceededMaxRedeem = 408, + /// Maximum number of decimals offset exceeded + VaultMaxDecimalsOffsetExceeded = 409, + /// Indicates overflow due to mathematical operations + MathOverflow = 410, +} + +// ################## CONSTANTS ################## + +// Suggested upper-bound for decimals to maximize both security and UX +pub const MAX_DECIMALS_OFFSET: u32 = 10; + // ################## EVENTS ################## /// Event emitted when underlying assets are deposited into the vault. @@ -375,6 +419,7 @@ pub struct Deposit { /// * `assets` - The amount of underlying assets being deposited into the vault. /// * `shares` - The amount of vault shares being minted in exchange for the /// assets. +#[cfg(not(feature = "certora"))] pub fn emit_deposit( e: &Env, operator: &Address, @@ -420,6 +465,7 @@ pub struct Withdraw { /// * `assets` - The amount of underlying assets being withdrawn from the vault. /// * `shares` - The amount of vault shares being burned in exchange for the /// assets. +#[cfg(not(feature = "certora"))] pub fn emit_withdraw( e: &Env, operator: &Address, diff --git a/packages/tokens/src/vault/specs/basic_token.rs b/packages/tokens/src/vault/specs/basic_token.rs new file mode 100644 index 00000000..929508f0 --- /dev/null +++ b/packages/tokens/src/vault/specs/basic_token.rs @@ -0,0 +1,53 @@ +use soroban_sdk::{contract, contractimpl, Address, Env, String}; + +use crate::fungible::{Base, FungibleToken}; + +pub struct BasicToken<'a> { + pub asset: &'a Address +} + +impl<'a> BasicToken<'a> { + pub fn __constructor (e: &Env, addr: &'a Address) -> BasicToken<'a> { + return BasicToken { asset: addr }; + } +} + +impl<'a> FungibleToken for BasicToken<'a> { + type ContractType = Base; + + fn total_supply(e: &Env) -> i128 { + Base::total_supply(e) + } + + fn balance(e: &Env, account: Address) -> i128 { + Base::balance(e, &account) + } + + fn allowance(e: &Env, owner: Address, spender: Address) -> i128 { + Base::allowance(e, &owner, &spender) + } + + fn transfer(e: &Env, from: Address, to: Address, amount: i128) { + Base::transfer(e, &from, &to, amount); + } + + fn transfer_from(e: &Env, spender: Address, from: Address, to: Address, amount: i128) { + Base::transfer_from(e, &spender, &from, &to, amount); + } + + fn approve(e: &Env, owner: Address, spender: Address, amount: i128, live_until_ledger: u32) { + Base::approve(e, &owner, &spender, amount, live_until_ledger); + } + + fn decimals(e: &Env) -> u32 { + Base::decimals(e) + } + + fn name(e: &Env) -> String { + Base::name(e) + } + + fn symbol(e: &Env) -> String { + Base::symbol(e) + } +} \ No newline at end of file diff --git a/packages/tokens/src/vault/specs/mod.rs b/packages/tokens/src/vault/specs/mod.rs new file mode 100644 index 00000000..8b2d5c61 --- /dev/null +++ b/packages/tokens/src/vault/specs/mod.rs @@ -0,0 +1,3 @@ +pub mod basic_token; +pub mod vault; +pub mod vault_sanity; \ No newline at end of file diff --git a/packages/tokens/src/vault/specs/vault.rs b/packages/tokens/src/vault/specs/vault.rs new file mode 100644 index 00000000..d09d38d6 --- /dev/null +++ b/packages/tokens/src/vault/specs/vault.rs @@ -0,0 +1,117 @@ +use crate::{fungible::{ContractOverrides, FungibleToken}, vault::{FungibleVault, Vault}}; +use soroban_sdk::{Address, Env}; +pub struct BasicVault; + +impl FungibleToken for BasicVault { + type ContractType = Vault; + + fn total_supply(e: &Env) -> i128 { + Vault::total_supply(e) + } + + fn balance(e: &Env, account: Address) -> i128 { + Vault::balance(e, &account) + } + + fn allowance(e: &Env, owner: Address, spender: Address) -> i128 { + Vault::allowance(e, &owner, &spender) + } + + fn transfer(e: &Env, from: Address, to: Address, amount: i128) { + Vault::transfer(e, &from, &to, amount); + } + + fn transfer_from(e: &Env, spender: Address, from: Address, to: Address, amount: i128) { + Vault::transfer_from(e, &spender, &from, &to, amount); + } + + fn approve(e: &Env, owner: Address, spender: Address, amount: i128, live_until_ledger: u32) { + Vault::approve(e, &owner, &spender, amount, live_until_ledger); + } + + fn decimals(e: &Env) -> u32 { + Vault::decimals(e) + } + + fn name(e: &Env) -> soroban_sdk::String { + Vault::name(e) + } + + fn symbol(e: &Env) -> soroban_sdk::String { + Vault::symbol(e) + } +} + +// TODO: do we need require_auth? The `fungible_vault` example has it. + +impl FungibleVault for BasicVault { + fn query_asset(e: &Env) -> Address { + Vault::query_asset(e) + } + + fn total_assets(e: &Env) -> i128 { + Vault::total_assets(e) + } + + fn convert_to_shares(e: &Env, assets: i128) -> i128 { + Vault::convert_to_shares(e, assets) + } + + fn convert_to_assets(e: &Env, shares: i128) -> i128 { + Vault::convert_to_assets(e, shares) + } + + fn max_deposit(e: &Env, receiver: Address) -> i128 { + Vault::max_deposit(e, receiver) + } + + fn preview_deposit(e: &Env, assets: i128) -> i128 { + Vault::preview_deposit(e, assets) + } + + fn deposit(e: &Env, assets: i128, receiver: Address, from: Address, operator: Address) -> i128 { + Vault::deposit(e, assets, receiver, from, operator) + } + + fn max_mint(e: &Env, receiver: Address) -> i128 { + Vault::max_mint(e, receiver) + } + + fn preview_mint(e: &Env, shares: i128) -> i128 { + Vault::preview_mint(e, shares) + } + + fn mint(e: &Env, shares: i128, receiver: Address, from: Address, operator: Address) -> i128 { + Vault::mint(e, shares, receiver, from, operator) + } + + fn max_withdraw(e: &Env, owner: Address) -> i128 { + Vault::max_withdraw(e, owner) + } + + fn preview_withdraw(e: &Env, assets: i128) -> i128 { + Vault::preview_withdraw(e, assets) + } + + fn withdraw( + e: &Env, + assets: i128, + receiver: Address, + owner: Address, + operator: Address, + ) -> i128 { + Vault::withdraw(e, assets, receiver, owner, operator) + } + + fn max_redeem(e: &Env, owner: Address) -> i128 { + Vault::max_redeem(e, owner) + } + + fn preview_redeem(e: &Env, shares: i128) -> i128 { + Vault::preview_redeem(e, shares) + } + + fn redeem(e: &Env, shares: i128, receiver: Address, owner: Address, operator: Address) -> i128 { + Vault::redeem(e, shares, receiver, owner, operator) + } +} \ No newline at end of file diff --git a/packages/tokens/src/vault/specs/vault_sanity.rs b/packages/tokens/src/vault/specs/vault_sanity.rs new file mode 100644 index 00000000..1e94b93b --- /dev/null +++ b/packages/tokens/src/vault/specs/vault_sanity.rs @@ -0,0 +1,188 @@ +use cvlr::{cvlr_satisfy, nondet::*}; +use cvlr_soroban::nondet_address; +use cvlr_soroban_derive::rule; +use soroban_sdk::{Env, Address}; + +use stellar_contract_utils::math::fixed_point::Rounding; + +use crate::vault::{FungibleVault, Vault}; +use crate::vault::specs::vault::BasicVault; + +// Note: we are currently not depending on `query_asset`. Could be something to fix + +#[rule] +pub fn vault_query_asset_sanity(e: Env) { + let _ = BasicVault::query_asset(&e); + cvlr_satisfy!(true); +} + +#[rule] +pub fn vault_total_assets_sanity(e: Env) { + let _ = BasicVault::total_assets(&e); + cvlr_satisfy!(true); +} + +#[rule] +pub fn vault_convert_to_shares_sanity(e: Env) { + let assets: i128 = nondet(); + let _ = BasicVault::convert_to_shares(&e, assets); + cvlr_satisfy!(true); +} + +#[rule] +pub fn vault_convert_to_assets_sanity(e: Env) { + let shares: i128 = nondet(); + let _ = BasicVault::convert_to_assets(&e, shares); + cvlr_satisfy!(true); +} + +#[rule] +pub fn vault_max_deposit_sanity(e: Env) { + let receiver: Address = nondet_address(); + let _ = BasicVault::max_deposit(&e, receiver); + cvlr_satisfy!(true); +} + +#[rule] +pub fn vault_preview_deposit_sanity(e: Env) { + let assets: i128 = nondet(); + let _ = BasicVault::preview_deposit(&e, assets); + cvlr_satisfy!(true); +} + +#[rule] +pub fn vault_max_mint_sanity(e: Env) { + let receiver: Address = nondet_address(); + let _ = BasicVault::max_mint(&e, receiver); + cvlr_satisfy!(true); +} + +#[rule] +pub fn vault_preview_mint_sanity(e: Env) { + let shares: i128 = nondet(); + let _ = BasicVault::preview_mint(&e, shares); + cvlr_satisfy!(true); +} + +#[rule] +pub fn vault_max_withdraw_sanity(e: Env) { + let owner: Address = nondet_address(); + let _ = BasicVault::max_withdraw(&e, owner); + cvlr_satisfy!(true); +} + +#[rule] +pub fn vault_preview_withdraw_sanity(e: Env) { + let assets: i128 = nondet(); + let _ = BasicVault::preview_withdraw(&e, assets); + cvlr_satisfy!(true); +} + +#[rule] +pub fn vault_max_redeem_sanity(e: Env) { + let owner: Address = nondet_address(); + let _ = BasicVault::max_redeem(&e, owner); + cvlr_satisfy!(true); +} + +#[rule] +pub fn vault_preview_redeem_sanity(e: Env) { + let shares: i128 = nondet(); + let _ = BasicVault::preview_redeem(&e, shares); + cvlr_satisfy!(true); +} + +#[rule] +pub fn vault_deposit_sanity(e: Env) { + let assets: i128 = nondet(); + let receiver: Address = nondet_address(); + let from: Address = nondet_address(); + let operator: Address = nondet_address(); + let _ = BasicVault::deposit(&e, assets, receiver, from, operator); + cvlr_satisfy!(true); +} + +#[rule] +pub fn vault_mint_sanity(e: Env) { + let shares: i128 = nondet(); + let receiver: Address = nondet_address(); + let from: Address = nondet_address(); + let operator: Address = nondet_address(); + let _ = BasicVault::mint(&e, shares, receiver, from, operator); + cvlr_satisfy!(true); +} + +#[rule] +pub fn vault_withdraw_sanity(e: Env) { + let assets: i128 = nondet(); + let receiver: Address = nondet_address(); + let owner: Address = nondet_address(); + let operator: Address = nondet_address(); + let _ = BasicVault::withdraw(&e, assets, receiver, owner, operator); + cvlr_satisfy!(true); +} + +#[rule] +pub fn vault_redeem_sanity(e: Env) { + let shares: i128 = nondet(); + let receiver: Address = nondet_address(); + let owner: Address = nondet_address(); + let operator: Address = nondet_address(); + let _ = BasicVault::redeem(&e, shares, receiver, owner, operator); + cvlr_satisfy!(true); +} + +#[rule] +pub fn vault_decimals_sanity(e: Env) { + let _ = Vault::decimals(&e); + cvlr_satisfy!(true); +} + +#[rule] +pub fn vault_set_assset_sanity(e: Env) { + let asset = nondet_address(); + Vault::set_asset(&e, asset); + cvlr_satisfy!(true); +} + +#[rule] +pub fn vault_set_decimals_offset_sanity(e: Env) { + let offset: u32 = nondet(); + Vault::set_decimals_offset(&e, offset); + cvlr_satisfy!(true); +} + +#[rule] +pub fn vault_deposit_internal_sanity(e: Env) { + let receiver: Address = nondet_address(); + let assets: i128 = nondet(); + let shares: i128 = nondet(); + let from: Address = nondet_address(); + let operator: Address = nondet_address(); + Vault::deposit_internal(&e, &receiver, assets, shares, &from, &operator); + cvlr_satisfy!(true); +} + +#[rule] +pub fn vault_withdraw_internal_sanity(e: Env) { + let receiver: Address = nondet_address(); + let owner: Address = nondet_address(); + let assets: i128 = nondet(); + let shares: i128 = nondet(); + let operator: Address = nondet_address(); + Vault::withdraw_internal(&e, &receiver, &owner, assets, shares, &operator); + cvlr_satisfy!(true); +} + +#[rule] +pub fn vault_get_decimals_offset_sanity(e: Env) { + let _ = Vault::get_decimals_offset(&e); + cvlr_satisfy!(true); +} + +#[rule] +pub fn vault_get_underlying_asset_decimals_sanity(e: Env) { + let _ = Vault::get_underlying_asset_decimals(&e); + cvlr_satisfy!(true); +} + diff --git a/packages/tokens/src/fungible/extensions/vault/storage.rs b/packages/tokens/src/vault/storage.rs similarity index 80% rename from packages/tokens/src/fungible/extensions/vault/storage.rs rename to packages/tokens/src/vault/storage.rs index d9d17af1..37704590 100644 --- a/packages/tokens/src/fungible/extensions/vault/storage.rs +++ b/packages/tokens/src/vault/storage.rs @@ -1,9 +1,14 @@ use soroban_sdk::{contracttype, panic_with_error, token, Address, Env}; use stellar_contract_utils::math::fixed_point::{muldiv, Rounding}; -use crate::fungible::{ - vault::{emit_deposit, emit_withdraw}, - Base, ContractOverrides, FungibleTokenError, MAX_DECIMALS_OFFSET, +use crate::{ + fungible::{Base, ContractOverrides, FungibleToken}, + vault::{MAX_DECIMALS_OFFSET, VaultTokenError, specs::basic_token::{self, BasicToken}}, +}; + +#[cfg(not(feature = "certora"))] +use crate::{ + vault::{emit_deposit, emit_withdraw,}, }; pub struct Vault; @@ -23,6 +28,60 @@ pub enum VaultStorageKey { VirtualDecimalsOffset, } +/// # Inflation Attack (Donation Attack) Mitigation +/// +/// ## Vulnerability Overview +/// +/// In empty (or nearly empty) vaults, deposits are at high risk of being stolen +/// through a "donation" to the vault that inflates the price of a share. +/// This is variously known as a **donation attack** or **inflation attack** and +/// is essentially a problem of slippage. +/// +/// ## Attack Mechanism +/// +/// 1. Attacker observes a pending deposit transaction in the mempool +/// 2. Attacker frontruns by directly transferring assets to the vault +/// (donation) +/// 3. This inflates the share price before the victim's deposit is processed +/// 4. Victim receives fewer shares than expected due to inflated price +/// 5. Attacker redeems their shares, capturing value from the victim's deposit +/// +/// ## Mitigation Strategies +/// +/// ### 1. Initial Deposit Protection +/// +/// Vault deployers can protect against this attack by making an initial deposit +/// of a non-trivial amount of the asset, such that price manipulation becomes +/// infeasible. This "dead shares" approach makes the attack economically +/// unviable. +/// +/// ### 2. Virtual Assets and Shares (Configurable Decimals Offset) +/// +/// This implementation introduces configurable virtual assets and shares to +/// help developers mitigate the risk. The decimals offset (accessible via +/// [`Vault::get_decimals_offset()`]) corresponds to an offset in the decimal +/// representation between the underlying asset's decimals and the vault +/// decimals. +/// +/// While not fully preventing the attack, analysis shows that the default +/// offset (0) makes it non-profitable even if an attacker is able to capture +/// value from multiple user deposits, as a result of the value being captured +/// by the virtual shares (out of the attacker's donation) matching the +/// attacker's expected gains. With a larger offset, the attack becomes orders +/// of magnitude more expensive than it is profitable. +/// +/// The drawback of this approach is that the virtual shares do capture (a very +/// small) part of the value being accrued to the vault. Also, if the vault +/// experiences losses, the users try to exit the vault, the virtual shares and +/// assets will cause the first user to exit to experience reduced losses in +/// detriment to the last users that will experience bigger losses. +/// +/// If this is not the preferred solution, implementers can still use the +/// default offset of 0 and implement their own safeguards. +/// +/// ## References +/// +/// impl Vault { // ################## QUERY STATE ################## @@ -35,7 +94,7 @@ impl Vault { /// /// # Errors /// - /// * [`FungibleTokenError::VaultAssetAddressNotSet`] - When the vault's + /// * [`VaultTokenError::VaultAssetAddressNotSet`] - When the vault's /// underlying asset address has not been initialized. /// /// # ERC-4626 Compliance Note @@ -64,7 +123,7 @@ impl Vault { e.storage() .instance() .get(&VaultStorageKey::AssetAddress) - .unwrap_or_else(|| panic_with_error!(e, FungibleTokenError::VaultAssetAddressNotSet)) + .unwrap_or_else(|| panic_with_error!(e, VaultTokenError::VaultAssetAddressNotSet)) } /// Returns the total amount of underlying assets held by the vault. @@ -86,8 +145,12 @@ impl Vault { /// See the ERC-4626 Compliance Note in that function's documentation for /// details on the deviation from the standard. pub fn total_assets(e: &Env) -> i128 { + #[cfg(not(feature = "certora"))] let token_client = token::Client::new(e, &Self::query_asset(e)); - token_client.balance(&e.current_contract_address()) + #[cfg(not(feature = "certora"))] + return token_client.balance(&e.current_contract_address()); + #[cfg(feature = "certora")] + BasicToken::balance(e, e.current_contract_address()) } /// Converts an amount of underlying assets to the equivalent amount of @@ -262,7 +325,7 @@ impl Vault { /// /// # Errors /// - /// * [`FungibleTokenError::VaultExceededMaxDeposit`] - When attempting to + /// * [`VaultTokenError::VaultExceededMaxDeposit`] - When attempting to /// deposit more assets than the maximum allowed for the receiver. /// * also refer to [`Self::preview_deposit()`] errors. /// @@ -289,7 +352,7 @@ impl Vault { ) -> i128 { let max_assets = Self::max_deposit(e, receiver.clone()); if assets > max_assets { - panic_with_error!(e, FungibleTokenError::VaultExceededMaxDeposit); + panic_with_error!(e, VaultTokenError::VaultExceededMaxDeposit); } let shares: i128 = Self::preview_deposit(e, assets); Self::deposit_internal(e, &receiver, assets, shares, &from, &operator); @@ -310,7 +373,7 @@ impl Vault { /// /// # Errors /// - /// * [`FungibleTokenError::VaultExceededMaxMint`] - When attempting to mint + /// * [`VaultTokenError::VaultExceededMaxMint`] - When attempting to mint /// more shares than the maximum allowed for the receiver. /// * also refer to [`Self::preview_mint()`] errors. /// @@ -337,7 +400,7 @@ impl Vault { ) -> i128 { let max_shares = Self::max_mint(e, receiver.clone()); if shares > max_shares { - panic_with_error!(e, FungibleTokenError::VaultExceededMaxMint); + panic_with_error!(e, VaultTokenError::VaultExceededMaxMint); } let assets: i128 = Self::preview_mint(e, shares); Self::deposit_internal(e, &receiver, assets, shares, &from, &operator); @@ -358,7 +421,7 @@ impl Vault { /// /// # Errors /// - /// * [`FungibleTokenError::VaultExceededMaxWithdraw`] - When attempting to + /// * [`VaultTokenError::VaultExceededMaxWithdraw`] - When attempting to /// withdraw more assets than the maximum allowed for the owner. /// /// # Events @@ -384,7 +447,7 @@ impl Vault { ) -> i128 { let max_assets = Self::max_withdraw(e, owner.clone()); if assets > max_assets { - panic_with_error!(e, FungibleTokenError::VaultExceededMaxWithdraw); + panic_with_error!(e, VaultTokenError::VaultExceededMaxWithdraw); } let shares: i128 = Self::preview_withdraw(e, assets); Self::withdraw_internal(e, &receiver, &owner, assets, shares, &operator); @@ -404,7 +467,7 @@ impl Vault { /// /// # Errors /// - /// * [`FungibleTokenError::VaultExceededMaxRedeem`] - When attempting to + /// * [`VaultTokenError::VaultExceededMaxRedeem`] - When attempting to /// redeem more shares than the maximum allowed for the owner. /// * also refer to [`Self::preview_redeem()`] errors. /// @@ -431,7 +494,7 @@ impl Vault { ) -> i128 { let max_shares = Self::max_redeem(e, owner.clone()); if shares > max_shares { - panic_with_error!(e, FungibleTokenError::VaultExceededMaxRedeem); + panic_with_error!(e, VaultTokenError::VaultExceededMaxRedeem); } let assets = Self::preview_redeem(e, shares); Self::withdraw_internal(e, &receiver, &owner, assets, shares, &operator); @@ -453,12 +516,12 @@ impl Vault { /// /// # Errors /// - /// * [`FungibleTokenError::MathOverflow`] - When the sum of underlying - /// asset decimals and offset exceeds the maximum value. + /// * [`VaultTokenError::MathOverflow`] - When the sum of underlying asset + /// decimals and offset exceeds the maximum value. pub fn decimals(e: &Env) -> u32 { Self::get_underlying_asset_decimals(e) .checked_add(Self::get_decimals_offset(e)) - .unwrap_or_else(|| panic_with_error!(e, FungibleTokenError::MathOverflow)) + .unwrap_or_else(|| panic_with_error!(e, VaultTokenError::MathOverflow)) } // ################## LOW-LEVEL HELPERS ################## @@ -479,8 +542,8 @@ impl Vault { /// /// # Errors /// - /// * [`FungibleTokenError::VaultAssetAddressAlreadySet`] - When attempting - /// to set the asset address after it has already been initialized. + /// * [`VaultTokenError::VaultAssetAddressAlreadySet`] - When attempting to + /// set the asset address after it has already been initialized. /// /// # Security Warning /// @@ -494,7 +557,7 @@ impl Vault { pub fn set_asset(e: &Env, asset: Address) { // Check if asset is already set if e.storage().instance().has(&VaultStorageKey::AssetAddress) { - panic_with_error!(e, FungibleTokenError::VaultAssetAddressAlreadySet); + panic_with_error!(e, VaultTokenError::VaultAssetAddressAlreadySet); } e.storage().instance().set(&VaultStorageKey::AssetAddress, &asset); @@ -525,11 +588,11 @@ impl Vault { /// /// # Errors /// - /// * [`FungibleTokenError::VaultVirtualDecimalsOffsetAlreadySet`] - When + /// * [`VaultTokenError::VaultVirtualDecimalsOffsetAlreadySet`] - When /// attempting to set the offset after it has already been initialized. - /// * [`FungibleTokenError::VaultMaxDecimalsOffsetExceeded`] - When - /// attempting to set the offset to a value higher than the suggested - /// maximum allowed. + /// * [`VaultTokenError::VaultMaxDecimalsOffsetExceeded`] - When attempting + /// to set the offset to a value higher than the suggested maximum + /// allowed. /// /// # Security Warning /// @@ -542,11 +605,11 @@ impl Vault { /// pattern. pub fn set_decimals_offset(e: &Env, offset: u32) { if offset > MAX_DECIMALS_OFFSET { - panic_with_error!(e, FungibleTokenError::VaultMaxDecimalsOffsetExceeded); + panic_with_error!(e, VaultTokenError::VaultMaxDecimalsOffsetExceeded); } // Check if virtual decimals offset is already set if e.storage().instance().has(&VaultStorageKey::VirtualDecimalsOffset) { - panic_with_error!(e, FungibleTokenError::VaultVirtualDecimalsOffsetAlreadySet); + panic_with_error!(e, VaultTokenError::VaultVirtualDecimalsOffsetAlreadySet); } e.storage().instance().set(&VaultStorageKey::VirtualDecimalsOffset, &offset); } @@ -565,12 +628,12 @@ impl Vault { /// /// # Errors /// - /// * [`FungibleTokenError::VaultInvalidAssetsAmount`] - When `assets < 0`. - /// * [`FungibleTokenError::MathOverflow`] - When mathematical operations + /// * [`VaultTokenError::VaultInvalidAssetsAmount`] - When `assets < 0`. + /// * [`VaultTokenError::MathOverflow`] - When mathematical operations /// result in overflow. pub fn convert_to_shares_with_rounding(e: &Env, assets: i128, rounding: Rounding) -> i128 { if assets < 0 { - panic_with_error!(e, FungibleTokenError::VaultInvalidAssetsAmount); + panic_with_error!(e, VaultTokenError::VaultInvalidAssetsAmount); } if assets == 0 { return 0; @@ -582,17 +645,17 @@ impl Vault { // Virtual offset = 10^offset let pow = 10_i128 .checked_pow(Self::get_decimals_offset(e)) - .unwrap_or_else(|| panic_with_error!(e, FungibleTokenError::MathOverflow)); + .unwrap_or_else(|| panic_with_error!(e, VaultTokenError::MathOverflow)); // Effective total supply = totalSupply + virtual offset let y = Self::total_supply(e) .checked_add(pow) - .unwrap_or_else(|| panic_with_error!(e, FungibleTokenError::MathOverflow)); + .unwrap_or_else(|| panic_with_error!(e, VaultTokenError::MathOverflow)); // Effective total assets = totalAssets + 1 (prevents division by zero) let denominator = Self::total_assets(e) .checked_add(1_i128) - .unwrap_or_else(|| panic_with_error!(e, FungibleTokenError::MathOverflow)); + .unwrap_or_else(|| panic_with_error!(e, VaultTokenError::MathOverflow)); // (assets × (totalSupply + 10^offset)) / (totalAssets + 1) muldiv(e, x, y, denominator, rounding) @@ -613,12 +676,12 @@ impl Vault { /// /// # Errors /// - /// * [`FungibleTokenError::VaultInvalidSharesAmount`] - When `shares < 0`. - /// * [`FungibleTokenError::MathOverflow`] - When mathematical operations + /// * [`VaultTokenError::VaultInvalidSharesAmount`] - When `shares < 0`. + /// * [`VaultTokenError::MathOverflow`] - When mathematical operations /// result in overflow. pub fn convert_to_assets_with_rounding(e: &Env, shares: i128, rounding: Rounding) -> i128 { if shares < 0 { - panic_with_error!(e, FungibleTokenError::VaultInvalidSharesAmount); + panic_with_error!(e, VaultTokenError::VaultInvalidSharesAmount); } if shares == 0 { return 0; @@ -630,17 +693,17 @@ impl Vault { // Effective total assets = totalAssets + 1 (prevents division by zero) let y = Self::total_assets(e) .checked_add(1_i128) - .unwrap_or_else(|| panic_with_error!(e, FungibleTokenError::MathOverflow)); + .unwrap_or_else(|| panic_with_error!(e, VaultTokenError::MathOverflow)); // Virtual offset = 10^offset let pow = 10_i128 .checked_pow(Self::get_decimals_offset(e)) - .unwrap_or_else(|| panic_with_error!(e, FungibleTokenError::MathOverflow)); + .unwrap_or_else(|| panic_with_error!(e, VaultTokenError::MathOverflow)); // Effective total supply = totalSupply + virtual offset let denominator = Self::total_supply(e) .checked_add(pow) - .unwrap_or_else(|| panic_with_error!(e, FungibleTokenError::MathOverflow)); + .unwrap_or_else(|| panic_with_error!(e, VaultTokenError::MathOverflow)); // (shares × (totalAssets + 1)) / (totalSupply + 10^offset) muldiv(e, x, y, denominator, rounding) @@ -683,20 +746,27 @@ impl Vault { ) { // This function assumes prior authorization of the operator and validation of // amounts. + #[cfg(not(feature = "certora"))] let token_client = token::Client::new(e, &Self::query_asset(e)); // `safeTransfer` mechanism is not present in the base module, (will be provided // as an extension) - if operator == from { // Direct transfer: `operator` is depositing their own assets - token_client.transfer(from, e.current_contract_address(), &assets); + #[cfg(not(feature = "certora"))] + token_client.transfer(from, &e.current_contract_address(), &assets); + #[cfg(feature = "certora")] + BasicToken::transfer(e, from.clone(), e.current_contract_address(), assets); } else { // Allowance-based transfer: `operator` is depositing on behalf of `from` // This requires that `from` has approved `operator` on the underlying asset + #[cfg(not(feature = "certora"))] token_client.transfer_from(operator, from, &e.current_contract_address(), &assets); + #[cfg(feature = "certora")] + BasicToken::transfer_from(e, operator.clone(), from.clone(), e.current_contract_address(), assets); } Base::mint(e, receiver, shares); + #[cfg(not(feature = "certora"))] emit_deposit(e, operator, from, receiver, assets, shares); } @@ -740,10 +810,15 @@ impl Vault { Base::spend_allowance(e, owner, operator, shares); } Base::update(e, Some(owner), None, shares); + #[cfg(not(feature = "certora"))] let token_client = token::Client::new(e, &Self::query_asset(e)); // `safeTransfer` mechanism is not present in the base module, (will be provided // as an extension) + #[cfg(not(feature = "certora"))] token_client.transfer(&e.current_contract_address(), receiver, &assets); + #[cfg(feature = "certora")] + BasicToken::transfer(e, e.current_contract_address(), receiver.clone(), assets); + #[cfg(not(feature = "certora"))] emit_withdraw(e, operator, receiver, owner, assets, shares); } @@ -760,8 +835,10 @@ impl Vault { /// /// # Notes /// - /// For more information about virtual decimals offset, see: - /// https://docs.openzeppelin.com/contracts/5.x/erc4626 + /// For more information about virtual decimals offset and its role in + /// mitigating inflation attacks, see the implementation-level + /// documentation: [Inflation Attack (Donation Attack) + /// Mitigation](Vault) pub fn get_decimals_offset(e: &Env) -> u32 { e.storage().instance().get(&VaultStorageKey::VirtualDecimalsOffset).unwrap_or(0) } @@ -779,7 +856,11 @@ impl Vault { /// /// * refer to [`Self::query_asset()`] errors. pub fn get_underlying_asset_decimals(e: &Env) -> u32 { + #[cfg(not(feature = "certora"))] let token_client = token::Client::new(e, &Self::query_asset(e)); - token_client.decimals() + #[cfg(not(feature = "certora"))] + return token_client.decimals(); + #[cfg(feature = "certora")] + BasicToken::decimals(e) } } diff --git a/packages/tokens/src/fungible/extensions/vault/test.rs b/packages/tokens/src/vault/test.rs similarity index 94% rename from packages/tokens/src/fungible/extensions/vault/test.rs rename to packages/tokens/src/vault/test.rs index 21aec2b6..9e078c4d 100644 --- a/packages/tokens/src/fungible/extensions/vault/test.rs +++ b/packages/tokens/src/vault/test.rs @@ -2,7 +2,10 @@ extern crate std; use soroban_sdk::{contract, contractimpl, testutils::Address as _, Address, Env}; -use crate::fungible::{vault::Vault, Base, MAX_DECIMALS_OFFSET}; +use crate::{ + fungible::Base, + vault::{Vault, MAX_DECIMALS_OFFSET}, +}; // Simple mock contract for vault testing #[contract] @@ -348,7 +351,7 @@ fn conversion_with_existing_assets() { } #[test] -#[should_panic(expected = "Error(Contract, #122)")] +#[should_panic(expected = "Error(Contract, #407)")] fn withdraw_exceeds_max() { let e = Env::default(); let admin = Address::generate(&e); @@ -377,7 +380,7 @@ fn withdraw_exceeds_max() { } #[test] -#[should_panic(expected = "Error(Contract, #123)")] +#[should_panic(expected = "Error(Contract, #408)")] fn redeem_exceeds_max() { let e = Env::default(); let admin = Address::generate(&e); @@ -405,7 +408,7 @@ fn redeem_exceeds_max() { } #[test] -#[should_panic(expected = "Error(Contract, #116)")] +#[should_panic(expected = "Error(Contract, #401)")] fn asset_address_already_set() { let e = Env::default(); let asset_address1 = Address::generate(&e); @@ -419,7 +422,7 @@ fn asset_address_already_set() { } #[test] -#[should_panic(expected = "Error(Contract, #117)")] +#[should_panic(expected = "Error(Contract, #402)")] fn decimals_offset_already_set() { let e = Env::default(); let asset_address = Address::generate(&e); @@ -432,7 +435,7 @@ fn decimals_offset_already_set() { } #[test] -#[should_panic(expected = "Error(Contract, #124)")] +#[should_panic(expected = "Error(Contract, #409)")] fn decimals_offset_exceeded() { let e = Env::default(); let asset_address = Address::generate(&e); @@ -442,7 +445,7 @@ fn decimals_offset_exceeded() { } #[test] -#[should_panic(expected = "Error(Contract, #115)")] +#[should_panic(expected = "Error(Contract, #400)")] fn query_asset_not_set() { let e = Env::default(); let contract_address = e.register(MockVaultContract, ()); @@ -454,7 +457,26 @@ fn query_asset_not_set() { } #[test] -#[should_panic(expected = "Error(Contract, #118)")] +fn convert_zero_assets() { + let e = Env::default(); + let admin = Address::generate(&e); + let initial_supply = 1_000_000_000_000_000_000i128; + let decimals_offset = 6; + + // Create contracts + let asset_address = create_asset_contract(&e, initial_supply, &admin); + let vault_address = create_vault_contract(&e, &asset_address, decimals_offset); + + e.as_contract(&vault_address, || { + // Converting 0 assets should return 0 shares + assert_eq!(Vault::convert_to_shares(&e, 0), 0); + assert_eq!(Vault::preview_deposit(&e, 0), 0); + assert_eq!(Vault::preview_withdraw(&e, 0), 0); + }); +} + +#[test] +#[should_panic(expected = "Error(Contract, #403)")] fn invalid_assets_amount() { let e = Env::default(); let admin = Address::generate(&e); @@ -472,7 +494,7 @@ fn invalid_assets_amount() { } #[test] -#[should_panic(expected = "Error(Contract, #119)")] +#[should_panic(expected = "Error(Contract, #404)")] fn invalid_shares_amount() { let e = Env::default(); let admin = Address::generate(&e); diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 11bcc00a..b432d827 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,4 +1,4 @@ [toolchain] -channel = "stable" -targets = ["wasm32v1-none"] +channel = "nightly-2024-11-22" +targets = ["wasm32-unknown-unknown"] components = ["rustfmt", "clippy", "rust-src"]