diff --git a/.github/workflows/contracts-confidential-wrapper-foundry-tests.yml b/.github/workflows/contracts-confidential-wrapper-foundry-tests.yml index 9db7088a..d5d8ddb9 100644 --- a/.github/workflows/contracts-confidential-wrapper-foundry-tests.yml +++ b/.github/workflows/contracts-confidential-wrapper-foundry-tests.yml @@ -28,10 +28,10 @@ concurrency: jobs: tests: - name: contracts-confidential-wrapper-foundry-tests/tests (bpr) + name: contracts-confidential-wrapper-foundry-tests/tests (bpr, ${{ matrix.network }}) # The live archive-node run needs the RPC secret, which GitHub provides on push to - # main, manual dispatch, and PRs from branches within this repo. Gate the whole job - # so fork PRs skip it cleanly (a maintainer pushes the branch to this repo to run it, + # main, manual dispatch, and PRs from branches within this repo. Gate the whole job + # so fork PRs skip it cleanly (a maintainer pushes the branch to this repo to run it, # matching security-analysis-gate.yml). if: >- github.event_name != 'pull_request' || @@ -39,6 +39,17 @@ jobs: runs-on: ubuntu-latest permissions: contents: 'read' # Required to checkout repository code + strategy: + # One chain failing says nothing about the others, so let every leg report. + fail-fast: false + matrix: + # Each network needs a config/fork.json entry and its RPC secret below. Steps with no + # config for a network skip themselves, so no per-network `if:` is needed here. + network: [ethereum, polygon] + env: + NETWORK: ${{ matrix.network }} + ETHEREUM_MAINNET_FORK_RPC_URL: ${{ secrets.ETHEREUM_MAINNET_FORK_RPC_URL }} + POLYGON_MAINNET_FORK_RPC_URL: ${{ secrets.POLYGON_MAINNET_FORK_RPC_URL }} steps: # TODO: remove once GitHub runner images ship the CVE-2026-31431 kernel fix - name: Workaround CVE-2026-31431 (copy.fail) @@ -70,8 +81,9 @@ jobs: working-directory: contracts/confidential-wrapper/test/foundry run: make build - # The batcher suite drives the deployed bytecode at the addresses in config/batchers.json, so - # a redeploy upstream would leave it testing dead contracts. Skips itself without a token. + # The batcher suite drives the deployed bytecode at the addresses in + # config//batchers.json, so a redeploy upstream would leave it testing dead + # contracts. Skips itself without a token, or on a network with no batchers. - name: Check batcher deployment manifest is current working-directory: contracts/confidential-wrapper/test/foundry env: @@ -80,12 +92,8 @@ jobs: - name: Run live fork tests against archive node working-directory: contracts/confidential-wrapper/test/foundry - env: - ETHEREUM_MAINNET_FORK_RPC_URL: ${{ secrets.ETHEREUM_MAINNET_FORK_RPC_URL }} run: make fork-test - name: Run live fork tests against the deployed batchers working-directory: contracts/confidential-wrapper/test/foundry - env: - ETHEREUM_MAINNET_FORK_RPC_URL: ${{ secrets.ETHEREUM_MAINNET_FORK_RPC_URL }} run: make fork-test-batcher diff --git a/contracts/confidential-wrapper/.env.example b/contracts/confidential-wrapper/.env.example index ab54f6e7..c4cca772 100644 --- a/contracts/confidential-wrapper/.env.example +++ b/contracts/confidential-wrapper/.env.example @@ -52,5 +52,6 @@ CONFIDENTIAL_WRAPPER_PAUSER_ADDRESS_1=0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC # ConfidentialWrapper upgrade version label used by upgrade tasks CONFIDENTIAL_WRAPPER_UPGRADE_VERSION_LABEL=v2 -# Archive RPC used by the Foundry live mainnet-fork tests (make fork-test) +# Archive RPCs for the Foundry live fork tests; `make fork-test` picks one by NETWORK (default ethereum) ETHEREUM_MAINNET_FORK_RPC_URL=https://ethereum-rpc.publicnode.com/ +POLYGON_MAINNET_FORK_RPC_URL=https://polygon-bor-rpc.publicnode.com/ diff --git a/contracts/confidential-wrapper/test/ConfidentialWrapperV3.test.ts b/contracts/confidential-wrapper/test/ConfidentialWrapperV3.test.ts index b40cae5e..844490e8 100644 --- a/contracts/confidential-wrapper/test/ConfidentialWrapperV3.test.ts +++ b/contracts/confidential-wrapper/test/ConfidentialWrapperV3.test.ts @@ -1096,6 +1096,26 @@ describe('ConfidentialWrapperV3 DenyList', function () { expect(await wrapper.isBlocked(ethers.ZeroAddress)).to.be.false; }); + it('allows wrap and unwrap when the underlying denies the zero address', async function () { + const token: any = await ethers.deployContract('ERC20MockCUSDC'); + const wrapper: any = await deployV3(token.target as string, SELECTOR_CUSDC); + await token.setDenyListed(ethers.ZeroAddress, true); + + // Sanity: underlying denies zero, wrapper exempts it + expect(await token.isBlacklisted(ethers.ZeroAddress)).to.be.true; + expect(await wrapper.isBlocked(ethers.ZeroAddress)).to.be.false; + + await token.mint(holder.address, ethers.parseUnits('100', 6)); + await token.connect(holder).approve(wrapper.target, ethers.MaxUint256); + + // Mint path: wrap → _update(0, holder, ...) + await expect(wrapper.connect(holder).wrap(holder.address, ethers.parseUnits('100', 6))).not.to.be.reverted; + + // Burn path: unwrap → _update(holder, 0, ...) + const balance = await wrapper.confidentialBalanceOf(holder.address); + await expect(wrapper.connect(holder).unwrap(holder.address, holder.address, balance)).not.to.be.reverted; + }); + it('reverts with UnderlyingDenyListCallFailed when the underlying call reverts', async function () { const token: any = await ethers.deployContract('ERC20MockRevertingDenyList'); const wrapper = await deployV3(token.target as string, SELECTOR_CUSDC); diff --git a/contracts/confidential-wrapper/test/foundry/Makefile b/contracts/confidential-wrapper/test/foundry/Makefile index 6bd9cd59..b64e23e6 100644 --- a/contracts/confidential-wrapper/test/foundry/Makefile +++ b/contracts/confidential-wrapper/test/foundry/Makefile @@ -1,5 +1,10 @@ .PHONY: setup fork-test fork-test-batcher build clean +# The chain to fork: a config/fork.json key, which also names its config// directory. +# Exported so fork-test.sh and the tests (vm.envOr("NETWORK", ...)) read the same value. +NETWORK ?= ethereum +export NETWORK + # Installs the soldeer dependencies pinned in soldeer.toml (incl. forge-fhevm). setup: forge soldeer install --config-location soldeer @@ -7,19 +12,17 @@ setup: build: forge build -# Network-bound test run against a live mainnet fork. Reads -# ETHEREUM_MAINNET_FORK_RPC_URL from the environment (CI secret) or -# contracts/confidential-wrapper/.env (see .env.example). The fork block comes from -# config/fork.json, which is unpinned by default so runs cover latest - 50; -# FORK_BLOCK pins a run ad hoc. +# Network-bound test run against a live fork of $(NETWORK). fork-test.sh makes that network's RPC +# variable visible to Foundry (from the environment in CI, or contracts/confidential-wrapper/.env +# locally — see .env.example) and resolves the fork block from config/fork.json, which is unpinned +# by default so runs cover latest - 50; FORK_BLOCK pins a run ad hoc. Call the script directly to +# forward extra forge arguments, e.g. `./script/utils/fork-test.sh --match-test test_Foo`. fork-test: - @FORK_URL="$$(./script/utils/resolve-fork.sh)" || exit 1; \ - forge test --fork-url "$${FORK_URL}" -vvv + @./script/utils/fork-test.sh # Same fork, but the deployed-batcher suite under the `batcher` profile. fork-test-batcher: - @FORK_URL="$$(./script/utils/resolve-fork.sh)" || exit 1; \ - FOUNDRY_PROFILE=batcher forge test --fork-url "$${FORK_URL}" -vvv + @FOUNDRY_PROFILE=batcher ./script/utils/fork-test.sh clean: forge clean diff --git a/contracts/confidential-wrapper/test/foundry/README.md b/contracts/confidential-wrapper/test/foundry/README.md index 22993b8b..348ba076 100644 --- a/contracts/confidential-wrapper/test/foundry/README.md +++ b/contracts/confidential-wrapper/test/foundry/README.md @@ -1,21 +1,22 @@ -# Foundry Mainnet-Fork Tests: Confidential Wrappers +# Foundry Fork Tests: Confidential Wrappers -Foundry tests that exercise the **live** Confidential Wrappers deployed on Ethereum mainnet. +Foundry tests that exercise the **live** Confidential Wrappers deployed on a supported chain. `BaseForkTest` enumerates every valid wrapper from the on-chain `ConfidentialTokenWrappersRegistry`, and the suite checks: - direct wrap, confidential transfer, unwrap, finalize, and ERC-1363 receiver flows; - per-wrapper deny-list behavior (owner gating, block/unblock, blocked-wrap guard); - configured underlying-token deny-list selectors against the deployed underlying token code; -- underlying-token deny-list gating against real mainnet state, including known - blacklisted mainnet addresses. +- underlying-token deny-list gating against real chain state, including known + blacklisted addresses. A second suite under `test/batcher` drives the **deployed** Confidential DeFi Gateway batchers against the same candidate implementation, so a wrapper upgrade that breaks the batchers fails here. See [Deployed-batcher suite](#deployed-batcher-suite). -Tests run against a **live mainnet fork**: `forge test --fork-url ` reads the code -and storage the tests touch directly from the archive node. +Tests run against a **live fork**: `forge test --fork-url ` reads the code +and storage the tests touch directly from the archive node. The chain is selected with +`NETWORK` (default `ethereum`); see [Networks](#networks). ## Setup @@ -31,43 +32,57 @@ make build # forge build | Task | Command | Notes | | ---- | ------- | ----- | -| Live fork run | `make fork-test` | Forks mainnet at latest - 50 by default (see [Fork block](#fork-block)). Reads the RPC (see below). | +| Live fork run | `make fork-test` | Forks the network at latest - 50 by default (see [Fork block](#fork-block)). Reads the RPC (see below). | | Deployed-batcher run | `make fork-test-batcher` | Same fork, `test/batcher` under the `batcher` profile. | +| Another network | `make fork-test NETWORK=polygon` | Any key in `config/fork.json`. | | Ad-hoc block | `FORK_BLOCK= make fork-test` | Pins one run to a specific block. | Test cases are isolated: each `test_*` starts from its own `setUp()` state; mutations do not leak across tests or files. -`make fork-test` resolves `ETHEREUM_MAINNET_FORK_RPC_URL` via -`script/utils/resolve-fork.sh`: the process environment first (CI injects it from a GitHub -secret), then `contracts/confidential-wrapper/.env` for local dev (see `.env.example`). CI runs -`make fork-test` against the archive node on pushes to `main`, manual dispatch, and PRs from -branches in this repo; fork PRs skip the whole job, since GitHub withholds the secret from them. +Each network is an `[rpc_endpoints]` alias in `foundry.toml` pointing at its RPC variable, e.g. +`ETHEREUM_MAINNET_FORK_RPC_URL`, which `forge` and `cast` resolve from the environment. CI sets +those variables from GitHub secrets; locally `script/utils/fork-test.sh` loads them from +`contracts/confidential-wrapper/.env` (see `.env.example`). + +## Networks + +`NETWORK` (default `ethereum`) selects the chain. It names an entry in `config/fork.json`, the +`[rpc_endpoints]` alias in `foundry.toml` that resolves its RPC URL, and the `config//` +directory holding that chain's deny-list and batcher files. + +To add a network: + +1. Add its entry to `config/fork.json` and its alias to `foundry.toml`'s `[rpc_endpoints]`. +2. Add a `config//` directory for the deny-list tokens and batchers it has, if any. +3. Add it to the matrix in `.github/workflows/contracts-confidential-wrapper-foundry-tests.yml`, + with its RPC variable in the job `env` and the matching repository secret. ## Fork block -The fork block is optional and resolved by `script/utils/resolve-fork.sh`. +The fork block is optional and resolved by `script/utils/fork-test.sh`. -Precedence: `FORK_BLOCK` (ad-hoc override) → `config/fork.json` → latest - 50 when -`ethereumMainnet.block` is `null`, which is the committed default. Set `ethereumMainnet.block` to an integer, or export `FORK_BLOCK`, to pin a run while reproducing a failure. +Precedence: `FORK_BLOCK` (ad-hoc override) → `config/fork.json` → latest - 50 when the selected +network's `block` is `null`, which is the committed default. Set `.block` to an integer, or +export `FORK_BLOCK`, to pin a run while reproducing a failure. ## Deny-list config -USDC, USDT, XAUT, and TGBP carry on-chain deny lists. Two small committed files drive the -deny-list tests: +One committed file per network, `config//blacklist-interfaces.json`, drives the deny-list tests. + +Each token entry carries: -- `config/blacklist-interfaces.json` — the bool-returning `getter` selector per token - (USDC `isBlacklisted(address)`, USDT `isBlackListed(address)`, XAUT `isBlocked(address)`, - TGBP `isBanned(address)`). Read by `test/BaseForkTest.t.sol`. -- `config/blacklist-seeds.json` — a handful of known-denied addresses per token, used as test - vectors. The suite reads each seed's deny-list slot from the live fork and asserts the token - reports it denied. These are real addresses that must still be denied at the forked block. Adding a token is a - one-entry edit to each file. +- `getter` — the bool-returning selector the wrapper staticcalls (USDC `isBlacklisted(address)`, + USDT `isBlackListed(address)`, etc.). +- `setter` / `authority` — used to freshly deny an address by pranking the token's own admin. +- `blacklisted` — a handful of real already-denied addresses used as test vectors. The suite reads + each one's deny-list slot from the live fork and asserts the token still reports it denied, so + they must remain denied at the forked block. ## Deployed-batcher suite `test/batcher` runs the deployed Confidential DeFi batchers against the candidate wrapper -implementation. The batchers are read from mainnet, not deployed by the tests, so the suite checks +implementation. The batchers are read from the chain, not deployed by the tests, so the suite checks the exact non-upgradeable bytecode a wrapper upgrade must support. Run it with: @@ -79,10 +94,10 @@ make fork-test-batcher It uses the `batcher` Foundry profile, which enables `isolate = true` and keeps the regular `make fork-test` target scoped to the wrapper suite. -Addresses live in `config/batchers.json`. +Addresses live in `config//batchers.json`. The harness mutates fork storage to repoint the deployed batchers at the local fhEVM host and clear -mainnet ciphertext handles that cannot be decoded locally. If a storage-layout guard fails, rederive +live ciphertext handles that cannot be decoded locally. If a storage-layout guard fails, rederive the deployed layout before changing any `vm.store` slot. ## Layout @@ -92,30 +107,27 @@ the deployed layout before changing any `vm.store` slot. | `test/BaseForkTest.t.sol` | `FhevmTest` harness: enumerate registry wrappers, repoint FHE config at the local host, shared token/KMS helpers | | `test/WrapperFlows.t.sol` | Per-wrapper wrap, confidential transfer, unwrap/finalize, ERC-1363 receiver path | | `test/DenyList.t.sol` | Local block/unblock, owner gating, blocked wrap guard | -| `test/UnderlyingDenyList.t.sol` | Underlying deny-list selectors vs. token code and known blacklisted mainnet addresses | +| `test/UnderlyingDenyList.t.sol` | Underlying deny-list selectors vs. token code and known blacklisted addresses | | `test/Upgrade.t.sol` | Upgrades every live proxy onto the HEAD impl and asserts storage, enablement and initializer-version invariants | | `test/batcher/IVaultBatcher.sol` | Slice of the deployed batchers' ABI these tests drive | | `test/batcher/BatcherForkBase.t.sol` | Harness for the deployed batchers | | `test/batcher/BatcherFlows.t.sol` | Wiring guard, deposit/redeem round trip, operator join and quit, empty-batch dispatch | | `test/batcher/BatcherDenyList.t.sol` | Deny-list and pause behavior seen through a batcher | -| `script/utils/resolve-fork.sh` | Resolves the fork target: RPC URL from the environment or `.env`, block from `FORK_BLOCK` or `config/fork.json` | -| `script/utils/check-batcher-manifest.sh` | Fails when `config/batchers.json` drifts from the upstream deployment manifest | -| `config/fork.json` | Optional mainnet fork block pin (`null` = chain tip) | -| `config/blacklist-interfaces.json` | Per-token deny-list getter selectors | -| `config/blacklist-seeds.json` | Per-token known-denied test-vector addresses | -| `config/batchers.json` | Deployed batcher, wrapper and vault addresses | +| `script/utils/fork-test.sh` | Runs `forge test` against a fork of `NETWORK`: loads its RPC variable from `.env` when unset, resolves the block from `FORK_BLOCK` or `config/fork.json` | +| `script/utils/check-batcher-manifest.sh` | Fails when `config//batchers.json` drifts from the upstream deployment manifest | +| `config/fork.json` | Per-network registry address and optional fork block pin (`null` = chain tip) | +| `config//blacklist-interfaces.json` | Per-token deny-list selectors and known-denied test-vector addresses | +| `config//batchers.json` | Deployed batcher, wrapper and vault addresses | ## Troubleshooting -- `ETHEREUM_MAINNET_FORK_RPC_URL is not set`: export the archive RPC or set it in - `contracts/confidential-wrapper/.env` (see `.env.example`). - `missing underlying token code`: the archive node did not return code for that address at the forked block; check the RPC and the pinned `FORK_BLOCK`. -- `seeded address not denied by real token state`: a `config/blacklist-seeds.json` address is no - longer denied at the forked block; refresh the seed. Runs default to the chain tip, so this - tracks live mainnet state. +- `seeded address not denied by real token state`: a `blacklisted` address in the deny-list config + is no longer denied at the forked block; refresh the seed. Runs default to the chain tip, so this + tracks live chain state. - `MISMATCH ` from `check-batcher-manifest.sh`: the batchers were redeployed upstream; copy the - new addresses into `config/batchers.json` and re-run `make fork-test-batcher`. + new addresses into `config//batchers.json` and re-run `make fork-test-batcher`. - `batcher: unexpected ACL` / `unexpected batcher layout`: the deployed batcher no longer matches what the harness assumes (FHE config or `BatcherConfidential` storage slots). Re-derive it (see below) rather than relaxing the guard — it is the only thing keeping the `vm.store` writes honest. @@ -140,14 +152,14 @@ drift from the mainnet deployment. ### FHE on a live fork -The deployed wrappers point their FHE config at the real Zama mainnet coprocessor (compute +The deployed wrappers point their FHE config at the real Zama coprocessor for their chain (compute happens off-chain), so a bare fork can't produce usable ciphertext/decryptions. Zama's [`forge-fhevm`](https://github.com/zama-ai/forge-fhevm) closes the gap: - The inherited `FhevmTest.setUp()` deploys the local fhEVM host stack (at canonical addresses) and records executor logs into an in-memory plaintext DB. - `BaseForkTest.setUp()` then repoints each wrapper's three FHE config slots at those local host - addresses and **zeroes the cached total-supply handle** (a mainnet handle has no entry in the + addresses and **zeroes the cached total-supply handle** (a live handle has no entry in the local plaintext DB, so the first local mint/burn rebuilds it against the local executor). - `finalizeUnwrap` verifies a scalar `abi.encode(uint64)` payload, so tests use `buildDecryptionProof(handle, abi.encode(cleartext))` rather than the generic diff --git a/contracts/confidential-wrapper/test/foundry/config/batchers.json b/contracts/confidential-wrapper/test/foundry/config/batchers.json deleted file mode 100644 index e34f89fd..00000000 --- a/contracts/confidential-wrapper/test/foundry/config/batchers.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "_doc": "Deployed Confidential DeFi Gateway batchers on Ethereum mainnet, read by test/batcher/BatcherForkBase.t.sol. Mirrored from contracts/deployments/mainnet.json in zama-ai/confidential-defi; script/utils/check-batcher-manifest.sh fails CI when that manifest moves. The batchers are non-upgradeable, so these tests exercise the deployed bytecode against the candidate wrapper implementation. `depositBatcher` routes cUsdc -> cShare through IERC4626.deposit; `redeemBatcher` routes cShare -> cUsdc through IERC4626.redeem.", - "depositBatcher": "0x324EA89FD3784036673BfE6Ffee2334A088F40Cc", - "redeemBatcher": "0x96Cd3Faa7483783Ac2Eb715f6333361500F1eec9", - "cUsdc": "0xe978F22157048E5DB8E5d07971376e86671672B2", - "cShare": "0x66Bf74E96900D1a19c7070D939D124f2F565C458", - "morphoVault": "0xbEEF00A59B577423653A1526c7009bdE103F542B" -} diff --git a/contracts/confidential-wrapper/test/foundry/config/blacklist-interfaces.json b/contracts/confidential-wrapper/test/foundry/config/blacklist-interfaces.json deleted file mode 100644 index 47079581..00000000 --- a/contracts/confidential-wrapper/test/foundry/config/blacklist-interfaces.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "_doc": "Underlying-token deny-list interfaces, read by test/BaseForkTest.t.sol (_underlyingDenyListInterface). Only `getter` (the bool-returning view selector) is consumed; `token`/`name` identify the entry. Known-denied test vectors live in config/blacklist-seeds.json; the deny-list storage itself is read from the live mainnet fork, so no membership scanning is configured here. Adding a blacklist-bearing token is a one-entry edit.", - "tokens": [ - { - "token": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", - "name": "USDC", - "getter": "isBlacklisted(address)", - "setter": "blacklist(address)", - "authority": "blacklister()" - }, - { - "token": "0xdac17f958d2ee523a2206206994597c13d831ec7", - "name": "USDT", - "getter": "isBlackListed(address)", - "setter": "addBlackList(address)", - "authority": "owner()" - }, - { - "token": "0x68749665ff8d2d112fa859aa293f07a622782f38", - "name": "XAUT", - "getter": "isBlocked(address)", - "setter": "addToBlockedList(address)", - "authority": "owner()" - }, - { - "token": "0x27f6c8289550fCE67f6B50BeD1F519966aFE5287", - "name": "TGBP", - "getter": "isBanned(address)", - "setter": "ban(address)", - "authority": "owner()" - } - ] -} diff --git a/contracts/confidential-wrapper/test/foundry/config/blacklist-seeds.json b/contracts/confidential-wrapper/test/foundry/config/blacklist-seeds.json deleted file mode 100644 index a38eb6d6..00000000 --- a/contracts/confidential-wrapper/test/foundry/config/blacklist-seeds.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "_doc": "Curated known-denied test vectors per underlying, read by test/UnderlyingDenyList.t.sol. These are real mainnet-blacklisted addresses; the suite reads each token's deny-list getter for these addresses on the live fork and asserts they are reported denied. A handful per token is enough (the suite checks at most 5). Fields: token = underlying address; blacklisted = denied addresses. To add a token, add an entry.", - "tokens": [ - { - "token": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", - "name": "USDC", - "blacklisted": [ - "0x002471b8a185f9980708d0eaec5b289714f56f8d", - "0x0078ef2d7baf87bf6c4ca49b19efd1e05f57ac30", - "0x00be5e0c85be35948d97ad37f62d108243f89ae0" - ] - }, - { - "token": "0xdac17f958d2ee523a2206206994597c13d831ec7", - "name": "USDT", - "blacklisted": [ - "0x0009413d525a716c2cc9ddd63626e80f9f9c038d", - "0x002471b8a185f9980708d0eaec5b289714f56f8d", - "0x0027846505cd5e91bdb743770f3d89de9cb7b978" - ] - }, - { - "token": "0x68749665ff8d2d112fa859aa293f07a622782f38", - "name": "XAUT", - "blacklisted": [ - "0x34abe30b9bcb461d051ed1bcd8b5b12320cd0e3b", - "0x53b9b72dc6f96eb4b54143b211b22e2548e4cf5c", - "0x7b5fa035954c948bd2d1e2ffb34ea06832ceb62d" - ] - } - ] -} diff --git a/contracts/confidential-wrapper/test/foundry/config/ethereum/batchers.json b/contracts/confidential-wrapper/test/foundry/config/ethereum/batchers.json new file mode 100644 index 00000000..7fa4eff6 --- /dev/null +++ b/contracts/confidential-wrapper/test/foundry/config/ethereum/batchers.json @@ -0,0 +1,8 @@ +{ + "_doc": "Deployed Confidential DeFi Gateway batchers on the network named by the parent directory, read by test/batcher/BatcherForkBase.t.sol. Mirrored from contracts/deployments/mainnet.json in zama-ai/confidential-defi; script/utils/check-batcher-manifest.sh fails CI when that manifest moves. The batchers are non-upgradeable, so these tests exercise the deployed bytecode against the candidate wrapper implementation. `depositBatcher` routes cUsdc -> cShare through IERC4626.deposit; `redeemBatcher` routes cShare -> cUsdc through IERC4626.redeem.", + "depositBatcher": "0x324EA89FD3784036673BfE6Ffee2334A088F40Cc", + "redeemBatcher": "0x96Cd3Faa7483783Ac2Eb715f6333361500F1eec9", + "cUsdc": "0xe978F22157048E5DB8E5d07971376e86671672B2", + "cShare": "0x66Bf74E96900D1a19c7070D939D124f2F565C458", + "morphoVault": "0xbEEF00A59B577423653A1526c7009bdE103F542B" +} diff --git a/contracts/confidential-wrapper/test/foundry/config/ethereum/blacklist-interfaces.json b/contracts/confidential-wrapper/test/foundry/config/ethereum/blacklist-interfaces.json new file mode 100644 index 00000000..0fc619d9 --- /dev/null +++ b/contracts/confidential-wrapper/test/foundry/config/ethereum/blacklist-interfaces.json @@ -0,0 +1,49 @@ +{ + "_doc": "Underlying-token deny lists for the network named by the parent directory, read by test/BaseForkTest.t.sol (_underlyingDenyListInterface) and test/UnderlyingDenyList.t.sol. Per token: `getter` is the bool-returning view selector the wrapper staticcalls, and is the only field the wrapper itself needs; `setter`/`authority` let the tests freshly deny an address by pranking the token's own admin; `blacklisted` is a handful of real already-denied addresses used as test vectors, which the suite reads off the live fork and asserts are still denied (it checks at most 5). Leave `blacklisted` empty for a token with no known-denied address — the freshly-blacklisted test still covers it. Adding a token is one entry. A network with no deny-list-bearing underlying omits this file and the suite skips itself.", + "tokens": [ + { + "token": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + "name": "USDC", + "getter": "isBlacklisted(address)", + "setter": "blacklist(address)", + "authority": "blacklister()", + "blacklisted": [ + "0x002471b8a185f9980708d0eaec5b289714f56f8d", + "0x0078ef2d7baf87bf6c4ca49b19efd1e05f57ac30", + "0x00be5e0c85be35948d97ad37f62d108243f89ae0" + ] + }, + { + "token": "0xdac17f958d2ee523a2206206994597c13d831ec7", + "name": "USDT", + "getter": "isBlackListed(address)", + "setter": "addBlackList(address)", + "authority": "owner()", + "blacklisted": [ + "0x0009413d525a716c2cc9ddd63626e80f9f9c038d", + "0x002471b8a185f9980708d0eaec5b289714f56f8d", + "0x0027846505cd5e91bdb743770f3d89de9cb7b978" + ] + }, + { + "token": "0x68749665ff8d2d112fa859aa293f07a622782f38", + "name": "XAUT", + "getter": "isBlocked(address)", + "setter": "addToBlockedList(address)", + "authority": "owner()", + "blacklisted": [ + "0x34abe30b9bcb461d051ed1bcd8b5b12320cd0e3b", + "0x53b9b72dc6f96eb4b54143b211b22e2548e4cf5c", + "0x7b5fa035954c948bd2d1e2ffb34ea06832ceb62d" + ] + }, + { + "token": "0x27f6c8289550fCE67f6B50BeD1F519966aFE5287", + "name": "TGBP", + "getter": "isBanned(address)", + "setter": "ban(address)", + "authority": "owner()", + "blacklisted": [] + } + ] +} diff --git a/contracts/confidential-wrapper/test/foundry/config/fork.json b/contracts/confidential-wrapper/test/foundry/config/fork.json index 885fa0ae..f52638f8 100644 --- a/contracts/confidential-wrapper/test/foundry/config/fork.json +++ b/contracts/confidential-wrapper/test/foundry/config/fork.json @@ -1,6 +1,11 @@ { - "_doc": "Optional fork block for `make fork-test`, read by script/utils/resolve-fork.sh. Null (the default) forks latest - 50; set a positive integer to pin a run. FORK_BLOCK overrides this file.", - "ethereumMainnet": { + "_doc": "Per-network fork config, read by script/utils/fork-test.sh (block) and test/BaseForkTest.t.sol (registry). Select one with `make fork-test NETWORK=`; the key also names the config// directory holding that network's deny-list and batcher files, and the foundry.toml [rpc_endpoints] alias resolving to its RPC URL. `block` null (the default) forks latest - 50; a positive integer pins a run, and FORK_BLOCK overrides it.", + "ethereum": { + "registry": "0xeb5015fF021DB115aCe010f23F55C2591059bBA0", + "block": null + }, + "polygon": { + "registry": "0xc8908569868758dAF814B5a8b96bBc44D1653d54", "block": null } } diff --git a/contracts/confidential-wrapper/test/foundry/config/polygon/blacklist-interfaces.json b/contracts/confidential-wrapper/test/foundry/config/polygon/blacklist-interfaces.json new file mode 100644 index 00000000..d6d5d45e --- /dev/null +++ b/contracts/confidential-wrapper/test/foundry/config/polygon/blacklist-interfaces.json @@ -0,0 +1,17 @@ +{ + "_doc": "Underlying-token deny lists for the network named by the parent directory, read by test/BaseForkTest.t.sol (_underlyingDenyListInterface) and test/UnderlyingDenyList.t.sol. Per token: `getter` is the bool-returning view selector the wrapper staticcalls, and is the only field the wrapper itself needs; `setter`/`authority` let the tests freshly deny an address by pranking the token's own admin; `blacklisted` is a handful of real already-denied addresses used as test vectors, which the suite reads off the live fork and asserts are still denied (it checks at most 5). Leave `blacklisted` empty for a token with no known-denied address — the freshly-blacklisted test still covers it. Adding a token is one entry. A network with no deny-list-bearing underlying omits this file and the suite skips itself.", + "tokens": [ + { + "token": "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359", + "name": "USDC", + "getter": "isBlacklisted(address)", + "setter": "blacklist(address)", + "authority": "blacklister()", + "blacklisted": [ + "0x002471b8a185f9980708d0eaec5b289714f56f8d", + "0x0078ef2d7baf87bf6c4ca49b19efd1e05f57ac30", + "0x00be5e0c85be35948d97ad37f62d108243f89ae0" + ] + } + ] +} diff --git a/contracts/confidential-wrapper/test/foundry/foundry.toml b/contracts/confidential-wrapper/test/foundry/foundry.toml index 30d35c09..52c6197a 100644 --- a/contracts/confidential-wrapper/test/foundry/foundry.toml +++ b/contracts/confidential-wrapper/test/foundry/foundry.toml @@ -29,6 +29,12 @@ match_path = "test/batcher/*" no_match_path = "test/batcher/BatcherForkBase.t.sol" isolate = true +# Network aliases usable as `--fork-url `, each resolving to that network's RPC variable. +# The names match the keys in config/fork.json, and `${...}` is interpolated by Foundry at load. +[rpc_endpoints] +ethereum = "${ETHEREUM_MAINNET_FORK_RPC_URL}" +polygon = "${POLYGON_MAINNET_FORK_RPC_URL}" + [fmt] line_length = 120 diff --git a/contracts/confidential-wrapper/test/foundry/script/utils/check-batcher-manifest.sh b/contracts/confidential-wrapper/test/foundry/script/utils/check-batcher-manifest.sh index 433339a2..724d9480 100755 --- a/contracts/confidential-wrapper/test/foundry/script/utils/check-batcher-manifest.sh +++ b/contracts/confidential-wrapper/test/foundry/script/utils/check-batcher-manifest.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# Check that config/batchers.json still matches the live deployment manifest in +# Check that config//batchers.json still matches the live deployment manifest in # zama-ai/confidential-defi. The batcher fork suite drives the deployed bytecode at those # addresses, so a redeploy there silently turns this suite into a test of dead contracts. # @@ -7,14 +7,21 @@ # (or a `gh auth login` session) grants access. Without credentials the check skips rather than # fails, so a missing token never breaks the fork-test job. # -# Run from the foundry package root (test/foundry), where make and CI invoke it. +# Run from the foundry package root (test/foundry), where make and CI invoke it. NETWORK selects +# the local manifest and defaults to ethereum. set -euo pipefail +NETWORK="${NETWORK:-ethereum}" UPSTREAM_REPO="zama-ai/confidential-defi" UPSTREAM_PATH="contracts/deployments/mainnet.json" -LOCAL_MANIFEST="config/batchers.json" +LOCAL_MANIFEST="config/${NETWORK}/batchers.json" KEYS=(depositBatcher redeemBatcher cUsdc cShare morphoVault) +if [ ! -f "${LOCAL_MANIFEST}" ]; then + echo "SKIP: no ${LOCAL_MANIFEST}; ${NETWORK} has no deployed batchers." >&2 + exit 0 +fi + for cmd in gh jq; do if ! command -v "${cmd}" >/dev/null 2>&1; then echo "SKIP: ${cmd} is not installed, cannot verify ${LOCAL_MANIFEST}." >&2 diff --git a/contracts/confidential-wrapper/test/foundry/script/utils/fork-test.sh b/contracts/confidential-wrapper/test/foundry/script/utils/fork-test.sh new file mode 100755 index 00000000..75dea1c0 --- /dev/null +++ b/contracts/confidential-wrapper/test/foundry/script/utils/fork-test.sh @@ -0,0 +1,56 @@ +#!/usr/bin/env bash +# Run `forge test` against a live fork of a network, forwarding any extra arguments. +# +# Network: the NETWORK environment variable (exported by the Makefile, default "ethereum"). It +# selects an entry in config/fork.json, which carries that network's `block`, and names the +# `[rpc_endpoints]` alias in foundry.toml that forge and cast resolve to its RPC URL. +# +# Block: the FORK_BLOCK environment variable (ad-hoc override) first, then the committed +# config/fork.json pin, and finally latest - 50 when the pin is null. +# +# Run from the foundry package root (test/foundry), where make invokes it, so ../../.env, +# ./config/fork.json and ./foundry.toml resolve. +set -euo pipefail + +NETWORK="${NETWORK:-ethereum}" +FORK_CONFIG=config/fork.json +ENV_FILE=../../.env +LATEST_BLOCK_OFFSET=50 + +# Foundry interpolates the ${...} in foundry.toml's [rpc_endpoints] from the process environment. +# CI sets those variables directly; locally they live in the package .env two directories above the +# Foundry root, which Foundry's own dotenv loading never reads. Take just the fork RPC entries from +# it — the rest of that file is deploy config bash cannot source — and never override a variable +# that is already set, so CI always wins. +if [ -f "${ENV_FILE}" ]; then + while IFS='=' read -r key value; do + [ -n "${!key:-}" ] || export "${key}=${value}" + done < <(grep -E '^[A-Z0-9_]+_FORK_RPC_URL=' "${ENV_FILE}" | tr -d "\"'") +fi + +# The whole entry for this network, so the file is parsed once and an unknown NETWORK fails here +# rather than as a confusing empty value further down. +NET="$(jq -er --arg n "${NETWORK}" ' + .[$n] // ("\(input_filename): unknown network \($n); known: \([keys[] | select(startswith("_") | not)] | join(", "))\n" | halt_error(1)) +' "${FORK_CONFIG}")" + +BLOCK="${FORK_BLOCK:-}" + +if [ -z "${BLOCK}" ]; then + # Emits the block, or nothing when it is null; any other value is a config error. + BLOCK="$(printf '%s' "${NET}" | jq -er --arg n "${NETWORK}" ' + .block as $b + | if $b == null then "" + elif ($b | type) == "number" and ($b | floor) == $b and $b > 0 then ($b | tostring) + else "config/fork.json: \($n).block must be a positive integer or null, got \($b | tojson)\n" | halt_error(1) + end + ')" +fi + +if [ -z "${BLOCK}" ]; then + LATEST_BLOCK="$(cast block-number --rpc-url "${NETWORK}")" + BLOCK="$((LATEST_BLOCK - LATEST_BLOCK_OFFSET))" +fi + +echo "Forking ${NETWORK} at block ${BLOCK}" >&2 +exec forge test --fork-url "${NETWORK}" --fork-block-number "${BLOCK}" -vvv "$@" diff --git a/contracts/confidential-wrapper/test/foundry/script/utils/resolve-fork.sh b/contracts/confidential-wrapper/test/foundry/script/utils/resolve-fork.sh deleted file mode 100755 index 9ccaadda..00000000 --- a/contracts/confidential-wrapper/test/foundry/script/utils/resolve-fork.sh +++ /dev/null @@ -1,57 +0,0 @@ -#!/usr/bin/env bash -# Resolve the mainnet fork target and print it to stdout as forge's --fork-url argument, -# i.e. "" or "@". Progress goes to stderr so stdout stays parseable. -# -# RPC URL: the process environment (CI injects ETHEREUM_MAINNET_FORK_RPC_URL from a GitHub -# secret) first, then contracts/confidential-wrapper/.env for local dev. Exits 1 with guidance -# if neither provides it. -# -# Block: the FORK_BLOCK environment variable (ad-hoc override) first, then the committed -# config/fork.json pin (read with jq), and finally latest - 50 when the pin is null. -# -# Run from the foundry package root (test/foundry), where make invokes it, so ../../.env and -# ./config/fork.json resolve. -set -euo pipefail - -URL="${ETHEREUM_MAINNET_FORK_RPC_URL:-}" -LATEST_BLOCK_OFFSET=50 - -if [ -z "${URL}" ] && [ -f ../../.env ]; then - URL="$(. ../../.env && printf '%s' "${ETHEREUM_MAINNET_FORK_RPC_URL:-}")" -fi - -if [ -z "${URL}" ]; then - echo "ETHEREUM_MAINNET_FORK_RPC_URL is not set." >&2 - echo "Set it in the environment (CI secret) or in contracts/confidential-wrapper/.env (see .env.example)." >&2 - exit 1 -fi - -BLOCK="${FORK_BLOCK:-}" - -if [ -z "${BLOCK}" ] && [ -f config/fork.json ]; then - if ! command -v jq >/dev/null 2>&1; then - echo "jq is required to read config/fork.json (install it, or set FORK_BLOCK to bypass)." >&2 - exit 1 - fi - # Emits the block, or nothing when it is null; any other value is a config error. - BLOCK="$(jq -er ' - .ethereumMainnet.block as $b - | if $b == null then "" - elif ($b | type) == "number" and ($b | floor) == $b and $b > 0 then ($b | tostring) - else "config/fork.json: ethereumMainnet.block must be a positive integer or null, got \($b | tojson)\n" | halt_error(1) - end - ' config/fork.json)" -fi - -if [ -z "${BLOCK}" ]; then - if ! command -v cast >/dev/null 2>&1; then - echo "cast is required to resolve latest - ${LATEST_BLOCK_OFFSET} (install Foundry, or set FORK_BLOCK to bypass)." >&2 - exit 1 - fi - - LATEST_BLOCK="$(cast block-number --rpc-url "${URL}")" - BLOCK="$((LATEST_BLOCK - LATEST_BLOCK_OFFSET))" -fi - -echo "Forking Ethereum mainnet at block ${BLOCK}" >&2 -printf '%s@%s' "${URL}" "${BLOCK}" diff --git a/contracts/confidential-wrapper/test/foundry/test/BaseForkTest.t.sol b/contracts/confidential-wrapper/test/foundry/test/BaseForkTest.t.sol index 1f071de7..f98c1545 100644 --- a/contracts/confidential-wrapper/test/foundry/test/BaseForkTest.t.sol +++ b/contracts/confidential-wrapper/test/foundry/test/BaseForkTest.t.sol @@ -12,22 +12,34 @@ import {ConfidentialTokenWrappersRegistry} from "registry/ConfidentialTokenWrapp /** * @title BaseForkTest - * @notice Shared harness for mainnet-fork tests over the live Confidential Wrappers. + * @notice Shared harness for fork tests over the live Confidential Wrappers. * - * @dev The suite runs against a live mainnet fork (`forge test --fork-url`), - * reading real mainnet code and storage from the archive node. The deployed - * wrappers point their FHE config at the real Zama mainnet coprocessor, whose - * compute happens off-chain, so a bare fork cannot produce usable - * ciphertext/decryptions. + * @dev The suite runs against a live fork of the selected network + * (`forge test --fork-url`), reading real code and storage from the archive + * node. The deployed wrappers point their FHE config at the real Zama + * coprocessor, whose compute happens off-chain, so a bare fork cannot produce + * usable ciphertext/decryptions. * * To make FHE satisfiable natively in Solidity, this harness inherits * {FhevmTest}: its `setUp()` deploys the fhEVM host contracts in-process at * their canonical local addresses and records executor logs into an in-memory * plaintext DB. {setUp} then repoints each wrapper's FHE config at that local * host and zeroes the cached total-supply handle (see {_repointFhevmConfig}). + * + * The `NETWORK` environment variable, exported by + * `make fork-test NETWORK=`, names the config/fork.json entry this run + * reads its registry from and the config// directory holding the rest + * of that chain's config. */ abstract contract BaseForkTest is FhevmTest { - address internal constant REGISTRY = 0xeb5015fF021DB115aCe010f23F55C2591059bBA0; + /// @dev Network selected when NETWORK is unset, i.e. a plain `make fork-test`. + string internal constant DEFAULT_NETWORK = "ethereum"; + + /// @notice Per-network fork config: registry address, RPC env variable name and fork block. + string internal constant FORK_CONFIG_PATH = "config/fork.json"; + + /// @notice Network key selecting this run's config/fork.json entry and config// directory. + string internal network; ConfidentialTokenWrappersRegistry internal registry; @@ -49,7 +61,7 @@ abstract contract BaseForkTest is FhevmTest { 0xfbb2c4771bcc77528b8fd58eedad6a4f84fdaf9eea4a56a2752391a0c87eee00; /// @dev forge-fhevm's in-process host addresses (dependencies/forge-fhevm-.../FHEVMHostAddresses.sol), - /// deployed by {FhevmTest.setUp}. The live wrappers instead store Zama's mainnet coprocessor + /// deployed by {FhevmTest.setUp}. The live wrappers instead store Zama's live coprocessor /// addresses, so encrypted ops are repointed here at runtime (see {_repointFhevmConfig}). address internal constant LOCAL_FHEVM_ACL = 0x50157CFfD6bBFA2DECe204a89ec419c23ef5755D; address internal constant LOCAL_FHEVM_COPROCESSOR = 0xe3a9105a3a932253A70F126eb1E3b589C643dD24; @@ -107,9 +119,9 @@ abstract contract BaseForkTest is FhevmTest { /// {_upgradeAllWrappersToLatest}: the version a fresh `initialize` lands on is that constant. uint64 internal reinitializerVersion; - /// @notice Address-keyed underlying deny-list interface config (getter selectors), read by - /// these tests. Known-denied test vectors live separately in config/blacklist-seeds.json. - string internal constant DENY_LIST_INTERFACES_PATH = "config/blacklist-interfaces.json"; + /// @notice Address-keyed underlying deny-list config: the getter/setter/authority selectors per + /// token, plus the known-denied addresses {UnderlyingDenyListTest} uses as test vectors. + string internal constant DENY_LIST_INTERFACES_FILE = "blacklist-interfaces.json"; /// @dev Valid (non-revoked) confidential wrapper proxies enumerated from the registry. address[] internal wrappers; @@ -119,7 +131,11 @@ abstract contract BaseForkTest is FhevmTest { // and starts recording executor logs into the plaintext DB. super.setUp(); - registry = ConfidentialTokenWrappersRegistry(REGISTRY); + network = vm.envOr("NETWORK", DEFAULT_NETWORK); + // Reverts naming the missing JSON path when NETWORK has no config/fork.json entry. + registry = ConfidentialTokenWrappersRegistry( + vm.parseJsonAddress(vm.readFile(FORK_CONFIG_PATH), string.concat(".", network, ".registry")) + ); ConfidentialTokenWrappersRegistry.TokenWrapperPair[] memory pairs = registry.getTokenConfidentialTokenPairs(); @@ -133,14 +149,14 @@ abstract contract BaseForkTest is FhevmTest { } /// @notice Deploys one fresh implementation from repo HEAD and upgrades every enumerated proxy - /// onto it, so the whole suite exercises the candidate impl against live mainnet state. Each + /// onto it, so the whole suite exercises the candidate impl against live on-chain state. Each /// proxy's pre-upgrade state is snapshotted first for {UpgradeTest}. /// @dev Every proxy is repointed at the fresh implementation whatever its live version, so the suite - /// always drives HEAD's bytecode rather than mainnet's. Only the initializer call differs: a proxy + /// always drives HEAD's bytecode rather than the chain's. Only the initializer call differs: a proxy /// behind HEAD runs `reinitializeV4` as part of the swap, while one already at HEAD's version has no /// migration left to replay and is upgraded with empty calldata. A proxy ahead of HEAD's reinitializer version /// is a stale checkout or a missed reinitializer bump, and fails rather than silently pointing the - /// suite at older bytecode than mainnet runs. + /// suite at older bytecode than the chain runs. function _upgradeAllWrappersToLatest() internal { newImplementation = new ConfidentialWrapper(); reinitializerVersion = _initializedVersion(_deployFreshProxy()); @@ -194,9 +210,9 @@ abstract contract BaseForkTest is FhevmTest { } /// @notice Repoints `w`'s FHE config at the in-process forge-fhevm host and zeroes its cached - /// total-supply handle, so encrypted ops resolve locally instead of at Zama's mainnet coprocessor. + /// total-supply handle, so encrypted ops resolve locally instead of at Zama's live coprocessor. /// @dev Runs before {_snapshotPreUpgrade} so the zeroed handle is captured pre-upgrade and - /// {UpgradeTest} still sees it unchanged after the swap. A mainnet handle has no entry in the local + /// {UpgradeTest} still sees it unchanged after the swap. A live handle has no entry in the local /// plaintext DB, so zeroing lets the first local mint/burn rebuild total supply against the in-process /// executor. function _repointFhevmConfig(address w) internal { @@ -300,13 +316,26 @@ abstract contract BaseForkTest is FhevmTest { return bytes32(uint256(CONFIDENTIAL_WRAPPER_V3_STORAGE_BASE) + 2); } - /// @notice Returns the explicit blacklist interface for `token`, read from the shared + /// @notice Path to one of this network's config files, e.g. `config/ethereum/batchers.json`. + function _configPath(string memory file) internal view returns (string memory) { + return string.concat("config/", network, "/", file); + } + + /// @dev True when this network's config lists at least one deny-list-bearing underlying. + /// Networks with none omit the file entirely, and the deny-list suite skips itself. + function _hasDenyListConfig() internal view returns (bool) { + string memory path = _configPath(DENY_LIST_INTERFACES_FILE); + return vm.exists(path) && vm.keyExistsJson(vm.readFile(path), ".tokens[0]"); + } + + /// @notice Returns the explicit blacklist interface for `token`, read from this network's /// config file (not hardcoded). `supported == false` for tokens with no entry. function _underlyingDenyListInterface( address token ) internal view returns (UnderlyingDenyListInterface memory iface) { - if (!vm.exists(DENY_LIST_INTERFACES_PATH)) return iface; - string memory json = vm.readFile(DENY_LIST_INTERFACES_PATH); + string memory path = _configPath(DENY_LIST_INTERFACES_FILE); + if (!vm.exists(path)) return iface; + string memory json = vm.readFile(path); // Foundry JSON cheatcodes are index-addressed here; config tokens are a dense array, so // the first missing `.tokens[i]` marks the end. for (uint256 i = 0; ; i++) { diff --git a/contracts/confidential-wrapper/test/foundry/test/Pause.t.sol b/contracts/confidential-wrapper/test/foundry/test/Pause.t.sol index 27344a37..52c06d7c 100644 --- a/contracts/confidential-wrapper/test/foundry/test/Pause.t.sol +++ b/contracts/confidential-wrapper/test/foundry/test/Pause.t.sol @@ -10,7 +10,7 @@ import {ERC165Checker} from "@openzeppelin/contracts/utils/introspection/ERC165C import {IERC1363} from "@openzeppelin/contracts/interfaces/IERC1363.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -/// @notice Pause behavior across every registered wrapper, running against live mainnet state. +/// @notice Pause behavior across every registered wrapper, running against live chain state. /// @dev The live proxies carry no pauser of their own and the upgrade in {BaseForkTest} seeds /// `address(0)`, so these tests also cover arming it on real V3 state without disturbing the /// deny-list config it shares a slot with. diff --git a/contracts/confidential-wrapper/test/foundry/test/UnderlyingDenyList.t.sol b/contracts/confidential-wrapper/test/foundry/test/UnderlyingDenyList.t.sol index a2d80a16..fea46c0c 100644 --- a/contracts/confidential-wrapper/test/foundry/test/UnderlyingDenyList.t.sol +++ b/contracts/confidential-wrapper/test/foundry/test/UnderlyingDenyList.t.sol @@ -7,16 +7,16 @@ import {externalEuint64} from "encrypted-types/EncryptedTypes.sol"; /** * @notice Exercises configured underlying deny-list selectors against the real - * mainnet token code on the fork. This intentionally does not mock the underlying + * token code on the fork. This intentionally does not mock the underlying * token: the selector must staticcall the live underlying implementation and * return a normal boolean response before the wrapper is allowed to wrap. */ contract UnderlyingDenyListTest is BaseForkTest { function setUp() public override { super.setUp(); - // The null-address test completes a wrap+unwrap (mint then burn), which chains a few - // FHE ops; relax the sequential depth cap. - disableHCUDepthLimit(); + // Every test below floors on exercising at least one deny-list-bearing underlying, so a + // network whose config lists none has nothing to run here. + if (!_hasDenyListConfig()) vm.skip(true); } function test_ConfiguredUnderlyingDenyListSelectors_AllWrappers() public { @@ -47,7 +47,7 @@ contract UnderlyingDenyListTest is BaseForkTest { } /** - * @notice Uses real blacklist membership from mainnet state and checks + * @notice Uses real blacklist membership from live chain state and checks * that the wrapper's direct wrap path rejects a known blacklisted depositor. */ function test_UnderlyingDenyListBlocksKnownBlacklistedWrap() public { @@ -74,7 +74,7 @@ contract UnderlyingDenyListTest is BaseForkTest { } /** - * @notice A known blacklisted mainnet address is reported by {isBlockedOnUnderlying} and by the + * @notice A known blacklisted address is reported by {isBlockedOnUnderlying} and by the * combined {isBlocked}, while {isBlockedOnWrapper} stays false. This is the case the combined * view exists for: an address no wrapper owner ever touched, that the underlying still denies. */ @@ -171,7 +171,7 @@ contract UnderlyingDenyListTest is BaseForkTest { * @notice Freshly blacklists a brand-new user through the underlying token's own admin setter * (pranked as the token's configured authority), then proves every wrapper entry point rejects that * user. This exercises deny-list enforcement even for underlyings that have no pre-existing - * blacklisted address in mainnet state (e.g. TGBP), which the known-blacklist tests skip. + * blacklisted address in live chain state (e.g. TGBP), which the known-blacklist tests skip. */ function test_UnderlyingDenyListBlocksFreshlyBlacklistedUser() public { uint256 exercised; @@ -206,57 +206,11 @@ contract UnderlyingDenyListTest is BaseForkTest { } /** - * @notice A denied null address must NOT block minting or burning. `_requireNotBlocked` - * short-circuits address(0) precisely because mint has from == 0 and burn has to == 0, and - * some underlyings (e.g. USDT) report isBlackListed(address(0)) == true. - */ - function test_UnderlyingDenyListNullAddressDoesNotBlock() public { - uint256 exercised; - - for (uint256 i = 0; i < wrappers.length; i++) { - address w = wrappers[i]; - bytes4 selector = _wrapper(w).getUnderlyingDenyListSelector(); - if (selector == bytes4(0)) continue; - address token = _wrapper(w).underlying(); - - if (!_queryUnderlyingDenyList(token, selector, address(0))) continue; // underlying allows the null address - exercised++; - string memory sym = _label(w); - - // Both views carry the same exemption, so neither contradicts the mint/burn paths below. - assertFalse( - _wrapper(w).isBlockedOnUnderlying(address(0)), - string.concat(sym, ": isBlockedOnUnderlying reports the null address as denied") - ); - assertFalse(_wrapper(w).isBlocked(address(0)), string.concat(sym, ": isBlocked reports the null address")); - - // Mint: wrap does _update(0, holder, ...); a denied null address must not block it. - address holder = makeAddr(string.concat("null-deny-holder-", sym)); - _dealAndWrap(w, holder, _wrapper(w).rate()); - assertEq(_decryptBalance(w, holder), 1, string.concat(sym, ": mint blocked by denied null address")); - - // Burn: unwrap does _update(holder, 0, ...); a denied null address must not block it. - (externalEuint64 enc, bytes memory proof) = encryptUint64(1, holder, w); - vm.prank(holder); - _wrapper(w).unwrap(holder, holder, enc, proof); - assertEq(_decryptTotalSupply(w), 0, string.concat(sym, ": burn blocked by denied null address")); - } - - assertGt(exercised, 0, "no configured wrapper whose underlying denies the null address"); - } - - /** - * @notice If the curated blacklist seed list is present, asserts each seeded address is reported - * denied by the underlying token getter against the real mainnet state on the fork. + * @notice Asserts every curated `blacklisted` seed is still reported denied by its underlying + * token getter against the real chain state on the fork. */ function test_UnderlyingDenyListSeededBlacklist() public { - string memory path = "config/blacklist-seeds.json"; - if (!vm.exists(path)) { - emit log("blacklist-seeds.json absent; skipping known-blacklisted deny-list assertion"); - return; - } - - string memory json = vm.readFile(path); + string memory json = vm.readFile(_configPath(DENY_LIST_INTERFACES_FILE)); uint256 checked; for (uint256 ti = 0; ; ti++) { @@ -281,10 +235,7 @@ contract UnderlyingDenyListTest is BaseForkTest { } function _knownBlacklistedAddress(address token) internal view returns (address) { - string memory path = "config/blacklist-seeds.json"; - if (!vm.exists(path)) return address(0); - - string memory json = vm.readFile(path); + string memory json = vm.readFile(_configPath(DENY_LIST_INTERFACES_FILE)); for (uint256 ti = 0; ; ti++) { string memory base = string.concat(".tokens[", vm.toString(ti), "]"); if (!vm.keyExistsJson(json, base)) break; diff --git a/contracts/confidential-wrapper/test/foundry/test/Upgrade.t.sol b/contracts/confidential-wrapper/test/foundry/test/Upgrade.t.sol index dcb3b232..718795c5 100644 --- a/contracts/confidential-wrapper/test/foundry/test/Upgrade.t.sol +++ b/contracts/confidential-wrapper/test/foundry/test/Upgrade.t.sol @@ -184,7 +184,7 @@ contract UpgradeTest is BaseForkTest { assertGt(uint256(version), 1, "initialize did not advance past version 1"); // reinitializeV4 path: the live proxies already carry the version it targets, so each is - // rewound one below to drive HEAD's reinitializer rather than mainnet's past run of it. + // rewound one below to drive HEAD's reinitializer rather than the chain's past run of it. address[] memory empty = new address[](0); for (uint256 i = 0; i < wrappers.length; i++) { address proxy = wrappers[i]; diff --git a/contracts/confidential-wrapper/test/foundry/test/batcher/BatcherDenyList.t.sol b/contracts/confidential-wrapper/test/foundry/test/batcher/BatcherDenyList.t.sol index f05b4413..43b76321 100644 --- a/contracts/confidential-wrapper/test/foundry/test/batcher/BatcherDenyList.t.sol +++ b/contracts/confidential-wrapper/test/foundry/test/batcher/BatcherDenyList.t.sol @@ -35,7 +35,7 @@ contract BatcherDenyListTest is BatcherForkBase { /** * @notice The other deny-list source, driven through the same join path. * @dev Skips itself when cUSDC has no underlying check configured, or when USDC's deny-list - * interface is absent from `config/blacklist-interfaces.json`, mirroring the guards the wrapper + * interface is absent from `config//blacklist-interfaces.json`, mirroring the guards the wrapper * suite's underlying tests use. */ function test_UnderlyingDeniedUserCannotJoin() public { diff --git a/contracts/confidential-wrapper/test/foundry/test/batcher/BatcherForkBase.t.sol b/contracts/confidential-wrapper/test/foundry/test/batcher/BatcherForkBase.t.sol index 99d64187..0c84a3f6 100644 --- a/contracts/confidential-wrapper/test/foundry/test/batcher/BatcherForkBase.t.sol +++ b/contracts/confidential-wrapper/test/foundry/test/batcher/BatcherForkBase.t.sol @@ -11,14 +11,17 @@ import {IVaultBatcher} from "./IVaultBatcher.sol"; * @notice Harness for driving the live Confidential DeFi Gateway batchers against the candidate * wrapper implementation {BaseForkTest} upgrades every registry proxy onto. * - * @dev The batchers are read from mainnet, not deployed here. Like the wrappers they store their FHE - * config in the `CoprocessorConfig` ERC-7201 slot (`ZamaEthereumConfig`'s constructor calls + * @dev The batchers are read from the chain, not deployed here. Like the wrappers they store their + * FHE config in the `CoprocessorConfig` ERC-7201 slot (`ZamaEthereumConfig`'s constructor calls * `FHE.setCoprocessor`), so {_prepareBatcher} can repoint them at the in-process fhEVM host the same * way {BaseForkTest} repoints the proxies. + * + * Only a network with deployed batchers carries a `config//batchers.json`; the rest skip + * this suite. */ abstract contract BatcherForkBase is BaseForkTest { /// @notice Deployed batcher/wrapper/vault addresses, mirrored from zama-ai/confidential-defi. - string internal constant BATCHERS_PATH = "config/batchers.json"; + string internal constant BATCHERS_FILE = "batchers.json"; /// @dev Canonical mainnet fhEVM addresses (ZamaConfig Ethereum config) the deployed batchers /// must point at. @@ -37,11 +40,17 @@ abstract contract BatcherForkBase is BaseForkTest { address internal morphoVault; function setUp() public virtual override { + // No manifest, no batchers to drive. Checked before `super.setUp()` so the skip costs nothing. + if (!vm.exists(string.concat("config/", vm.envOr("NETWORK", DEFAULT_NETWORK), "/", BATCHERS_FILE))) { + vm.skip(true); + return; + } + super.setUp(); // Batch flows chain far more FHE ops than the direct wrapper flows. disableHCUDepthLimit(); - string memory json = vm.readFile(BATCHERS_PATH); + string memory json = vm.readFile(_configPath(BATCHERS_FILE)); depositBatcher = IVaultBatcher(vm.parseJsonAddress(json, ".depositBatcher")); redeemBatcher = IVaultBatcher(vm.parseJsonAddress(json, ".redeemBatcher")); cUsdc = vm.parseJsonAddress(json, ".cUsdc"); diff --git a/contracts/confidential-wrapper/test/foundry/test/batcher/IVaultBatcher.sol b/contracts/confidential-wrapper/test/foundry/test/batcher/IVaultBatcher.sol index c41d2477..1d21280e 100644 --- a/contracts/confidential-wrapper/test/foundry/test/batcher/IVaultBatcher.sol +++ b/contracts/confidential-wrapper/test/foundry/test/batcher/IVaultBatcher.sol @@ -50,8 +50,11 @@ interface IVaultBatcher { function dispatchBatch() external; - function dispatchBatchCallback(uint256 batchId, uint64 unwrapAmountCleartext, bytes calldata decryptionProof) - external; + function dispatchBatchCallback( + uint256 batchId, + uint64 unwrapAmountCleartext, + bytes calldata decryptionProof + ) external; function claim(uint256 batchId, address account) external returns (euint64);