From 16efdd225935831c0c02e56a79c8e12d9298a41b Mon Sep 17 00:00:00 2001 From: Andrey Date: Thu, 6 Aug 2026 20:31:50 -0700 Subject: [PATCH 1/7] ci: exclude TruffleHog Lob detector to unblock secret scanning TruffleHog's Lob detector matches `test_` followed by 35 alphanumerics, which is exactly the shape of many Foundry test names recorded in snapshots/gas.txt (e.g. test_AddAdapterUsesAdapterWhitelistEntry). That produced 56 findings on PR #135, all in snapshots/gas.txt and all test names. Because the bullfrog egress filter blocks api.lob.com, they could not be verified and so surfaced under `--results=verified,unknown`, failing the scan with exit 183. Reproduced with trufflehog 3.96.0 over the same commit range: 56 Lob findings, 25 unique, every one a test function name. With `--exclude-detectors=lob`: 0 findings. A postal-mail API key cannot legitimately appear in this repository, so no coverage is lost. Co-Authored-By: Claude Opus 5 --- .github/workflows/trufflehog.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/trufflehog.yml b/.github/workflows/trufflehog.yml index 1b393afd..ae6c07f3 100644 --- a/.github/workflows/trufflehog.yml +++ b/.github/workflows/trufflehog.yml @@ -33,4 +33,8 @@ jobs: path: . base: ${{ github.event.pull_request.base.sha }} head: ${{ github.event.pull_request.head.sha }} - extra_args: --results=verified,unknown + # Lob is excluded: its key pattern is `test_` + 35 alphanumerics, which collides with + # Foundry test names recorded in snapshots/gas.txt (e.g. test_AddAdapterUsesAdapterWhitelistEntry). + # That produced 56 false positives and, because egress to api.lob.com is blocked, they surfaced + # as unverifiable and failed the scan. A postal-mail API key cannot legitimately live in this repo. + extra_args: --results=verified,unknown --exclude-detectors=lob From 66602a746e8b285a6064ea51023f462343f41968 Mon Sep 17 00:00:00 2001 From: Andrey Date: Thu, 6 Aug 2026 21:32:09 -0700 Subject: [PATCH 2/7] ci: bump mainnet fork block past LL3 deploy and throttle fork RPC Two distinct causes behind the fork-job failures, both infrastructural. 1. Stale fork pin. MAINNET_FORK_BLOCK was 25422678, set in 78536491 and never moved. Liquid Lane 3 was deployed in blocks 25697401-25697403, ~38 days later, so the suites asserted live state that does not exist at the fork block. Four tests reverted for this reason: testDeploysCompleteCurrentMainnetConfiguration testAllTokenAccountsUseRealMainnetTokens testVBILLMainnetTopology testVBILLMainnetCloseRedemptionSequence Pinned to 25700000: after the deployment, and finalized at the time of writing. 2. Provider rate limiting. The three suites run concurrently against one key; 70 of the 78 failures in the last run were HTTP 429, both at fork creation and mid-execution. Each suite is now throttled and retries with backoff. Verified locally: forge fmt --check passes and the non-fork suite is unaffected (1110 passed, 1 skipped). The fork suites cannot be run locally - they need archive state at the pinned block, and no free provider serves it - so CI is the verification for this change. Co-Authored-By: Claude Opus 5 --- .github/workflows/test.yaml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index ea328ecf..3f482a49 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -59,7 +59,13 @@ jobs: env: FOUNDRY_PROFILE: pr ETH_RPC_URL: ${{ secrets.ETH_RPC_URL }} - MAINNET_FORK_BLOCK: 25422678 + # Must be at or after the Liquid Lane 3 deployment (blocks 25697401-25697403), otherwise + # the mainnet suites assert against contracts that do not yet exist at the fork block. + # Bump this whenever a deployment adds live state that new fork tests depend on. + MAINNET_FORK_BLOCK: 25700000 + # The three suites run concurrently against a single provider key, so throttle each one + # to keep the combined request rate inside the plan limit and retry patiently on 429s. + FORK_RPC_FLAGS: --compute-units-per-second 100 --fork-retries 10 --fork-retry-backoff 4 steps: - uses: bullfrogsec/bullfrog@1831f79cce8ad602eef14d2163873f27081ebfb3 # v0.8.4 - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 @@ -80,4 +86,4 @@ jobs: ${{ runner.os }}-foundry-rpc-mainnet-${{ env.MAINNET_FORK_BLOCK }}- - name: Run Forge mainnet tests - run: ${{ matrix.suite.command }} + run: ${{ matrix.suite.command }} $FORK_RPC_FLAGS From 821b6a792b3226a04d1222c204a1ebf5a6887b15 Mon Sep 17 00:00:00 2001 From: Andrey Date: Thu, 6 Aug 2026 23:58:18 -0700 Subject: [PATCH 3/7] ci: fix fork job invocation broken by unusable throttle flags 66602a74 appended --compute-units-per-second/--fork-retries/--fork-retry-backoff to the fork suites. Those flags are gated behind --rpc-url in forge 1.7.1, so every fork job died at argument parsing with exit code 2 before running a single test: error: the following required arguments were not provided: --rpc-url Supplying --rpc-url is not a fix: it is an alias for --fork-url, which would fork the whole suite instead of only the tests that call vm.createSelectFork. The three settings are also not exposed in foundry.toml or via FOUNDRY_* env vars in this version, so the throttle cannot be expressed that way at all. Rate limiting is instead addressed with two levers that need no fork flag: - max-parallel: 1, so the three suites stop sharing the provider key concurrently (the 429s were observed with all three in flight) - --threads 2, capping in-job test parallelism The MAINNET_FORK_BLOCK bump to 25700000 from 66602a74 is retained; it was correct and is what lets the Liquid Lane 3 tests see the deployed contracts. Verified locally with forge 1.7.1 (same version CI installs): all three matrix commands parse with --threads 2, and the previous flag set reproduces the exact CI parse error. Co-Authored-By: Claude Opus 5 --- .github/workflows/test.yaml | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 3f482a49..280a4a66 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -45,6 +45,9 @@ jobs: timeout-minutes: 180 strategy: fail-fast: false + # The suites share one provider key. Running them one at a time keeps the combined + # request rate inside the plan limit; concurrently they trip HTTP 429. + max-parallel: 1 matrix: suite: - name: mainnet forks @@ -63,9 +66,11 @@ jobs: # the mainnet suites assert against contracts that do not yet exist at the fork block. # Bump this whenever a deployment adds live state that new fork tests depend on. MAINNET_FORK_BLOCK: 25700000 - # The three suites run concurrently against a single provider key, so throttle each one - # to keep the combined request rate inside the plan limit and retry patiently on 429s. - FORK_RPC_FLAGS: --compute-units-per-second 100 --fork-retries 10 --fork-retry-backoff 4 + # Cap in-job test parallelism as a second brake on the provider request rate. + # Note: --compute-units-per-second and --fork-retries cannot be used here. They are + # gated behind --rpc-url, and --rpc-url is an alias for --fork-url, which would fork + # every test rather than only the ones that call vm.createSelectFork. + FORK_TEST_FLAGS: --threads 2 steps: - uses: bullfrogsec/bullfrog@1831f79cce8ad602eef14d2163873f27081ebfb3 # v0.8.4 - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 @@ -86,4 +91,4 @@ jobs: ${{ runner.os }}-foundry-rpc-mainnet-${{ env.MAINNET_FORK_BLOCK }}- - name: Run Forge mainnet tests - run: ${{ matrix.suite.command }} $FORK_RPC_FLAGS + run: ${{ matrix.suite.command }} $FORK_TEST_FLAGS From e022e6131e5a74dc1eca8904af88d3eb4e154188 Mon Sep 17 00:00:00 2001 From: Andrey Date: Fri, 7 Aug 2026 00:14:01 -0700 Subject: [PATCH 4/7] ci: throttle fork RPC via foundry.toml profile instead of CLI flags max-parallel and --threads were not enough: the last run still hit 40 HTTP 429s in the mainnet suite alone. The 11 apparent EvmError: Revert failures were artifacts of the same thing - each is preceded in the log by `sharedbackend: Failed to send/recv basic/storage`, i.e. the fork backend failed to fetch state and the EVM then reverted against empty state. The real throttle can be set in foundry.toml, which the earlier attempt missed. Placed under [profile.pr] because that is the profile both CI test jobs use. Confirmed the keys are recognised (forge warns on unknown keys, and warns for these when they are misplaced, but not here) and confirmed they take effect: the same fork test runs in ~23s unthrottled, 78s at compute_units_per_second=1, and 53s at 200. 200 leaves headroom under Alchemy's 330 CUPS tier while staying near baseline speed. fork_retries/backoff make transient 429s recoverable instead of fatal. Co-Authored-By: Claude Opus 5 --- foundry.toml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/foundry.toml b/foundry.toml index da6c6b9d..7cff0a11 100644 --- a/foundry.toml +++ b/foundry.toml @@ -52,6 +52,16 @@ depth = 64 max_time_delay = 1209600 max_block_delay = 2 +# Fork CI runs every mainnet suite against a single provider key and was tripping HTTP 429, +# which surfaces as `sharedbackend: Failed to send/recv` and then bare `EvmError: Revert` +# rather than as an obvious rate-limit error. These must live here rather than on the command +# line: forge gates --compute-units-per-second/--fork-retries/--fork-retry-backoff behind +# --rpc-url, and --rpc-url is an alias for --fork-url, which would fork every test. +[profile.pr] +compute_units_per_second = 200 +fork_retries = 10 +fork_retry_backoff = 4 + [profile.pr.fuzz] runs = 100 max_test_rejects = 4294967295 From 7968f1039cf5d679f4510693aad0b2cc6e29484b Mon Sep 17 00:00:00 2001 From: Andrey Date: Fri, 7 Aug 2026 00:22:44 -0700 Subject: [PATCH 5/7] Revert "ci: throttle fork RPC via foundry.toml profile instead of CLI flags" This reverts commit e022e6131e5a74dc1eca8904af88d3eb4e154188. --- foundry.toml | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/foundry.toml b/foundry.toml index 7cff0a11..da6c6b9d 100644 --- a/foundry.toml +++ b/foundry.toml @@ -52,16 +52,6 @@ depth = 64 max_time_delay = 1209600 max_block_delay = 2 -# Fork CI runs every mainnet suite against a single provider key and was tripping HTTP 429, -# which surfaces as `sharedbackend: Failed to send/recv` and then bare `EvmError: Revert` -# rather than as an obvious rate-limit error. These must live here rather than on the command -# line: forge gates --compute-units-per-second/--fork-retries/--fork-retry-backoff behind -# --rpc-url, and --rpc-url is an alias for --fork-url, which would fork every test. -[profile.pr] -compute_units_per_second = 200 -fork_retries = 10 -fork_retry_backoff = 4 - [profile.pr.fuzz] runs = 100 max_test_rejects = 4294967295 From 0cfd95cce6c1e0c0c6b0af2d99bb4d71c9b39bcb Mon Sep 17 00:00:00 2001 From: Andrey Date: Fri, 7 Aug 2026 00:23:37 -0700 Subject: [PATCH 6/7] ci: persist fork RPC cache across failed runs The fork jobs were stuck in a self-sustaining loop. actions/cache only saves in its post step when the job succeeds, so with the suites failing the save was skipped every time: success Cache Foundry RPC responses failure Run Forge mainnet tests skipped Post Cache Foundry RPC responses Every run therefore started cold, re-fetched all fork state, and tripped the provider rate limit - which failed the job, which skipped the save again. The MAINNET_FORK_BLOCK bump made this worse by invalidating the existing cache. Split into cache/restore plus cache/save with if: always(), so even a failing run leaves the cache warmer for the next one. The save key includes run_id and run_attempt because cache/save errors on an existing key; restore-keys matches on prefix and picks up the newest entry. Also drops the [profile.pr] throttle added in e022e613 (reverted in 7968f103). Those keys are not real: forge prints "Found unknown compute_units_per_second config for profile pr", they are absent from all 118 keys in forge config --json, and a controlled A/B on one test showed no timing difference (60s/58s with, 59s/54s without). compute_units_per_second is CLI-only in 1.7.1 and gated behind --rpc-url, so it cannot be applied here at all. Co-Authored-By: Claude Opus 5 --- .github/workflows/test.yaml | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 280a4a66..0548820c 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -82,13 +82,28 @@ jobs: with: version: v1.7.1 - - name: Cache Foundry RPC responses - uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 + # Restore and save are split so the cache is written even when the suite fails. + # actions/cache skips its post-run save on job failure, so while the suites were + # failing every run started cold, re-fetched all state, and tripped the provider + # rate limit - which failed the job, which skipped the save. A partial cache from + # a failed run still removes most of the requests the next run would make. + - name: Restore Foundry RPC cache + uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 with: path: ~/.foundry/cache/rpc key: ${{ runner.os }}-foundry-rpc-mainnet-${{ env.MAINNET_FORK_BLOCK }}-${{ matrix.suite.id }} restore-keys: | + ${{ runner.os }}-foundry-rpc-mainnet-${{ env.MAINNET_FORK_BLOCK }}-${{ matrix.suite.id }} ${{ runner.os }}-foundry-rpc-mainnet-${{ env.MAINNET_FORK_BLOCK }}- - name: Run Forge mainnet tests run: ${{ matrix.suite.command }} $FORK_TEST_FLAGS + + - name: Save Foundry RPC cache + if: always() + uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 + with: + path: ~/.foundry/cache/rpc + # Unique per run: cache/save errors on an existing key, and restore-keys above + # matches the prefix so the newest entry is picked up regardless. + key: ${{ runner.os }}-foundry-rpc-mainnet-${{ env.MAINNET_FORK_BLOCK }}-${{ matrix.suite.id }}-${{ github.run_id }}-${{ github.run_attempt }} From c9b0eafa6789a30ca0efc49497967297e67ced2d Mon Sep 17 00:00:00 2001 From: Andrey Date: Fri, 7 Aug 2026 00:43:09 -0700 Subject: [PATCH 7/7] fix: msyrupUSDp redeems in USDT, the only token its Midas vault accepts msyrupUSDp_Account was configured with USDC as its redemption token, but the Midas redemption vault it points at (0x71EFa7AF1686C5c04AA34a120a91cb4262679C44) lists only USDT as a payment token, and tokensConfig(USDC) is zeroed. Verified on mainnet at fork block 25700000 and at head: msyrupUSD getPaymentTokens() -> [USDC] mTBILL getPaymentTokens() -> [USDC, ...] mAPOLLO getPaymentTokens() -> [USDC] msyrupUSDp getPaymentTokens() -> [USDT] <- the outlier This is not only a test failure. MidasAccount._requestRedeem passes `dataFeed == address(0) ? REDEMPTION_TOKEN : _asset`, and since the vault has no config for the USDC vault asset it falls back to REDEMPTION_TOKEN. With USDC that is a token the vault rejects, so every redemption through this account would have reverted on mainnet. The account is not deployed yet, so correcting the constant is sufficient. USDT is handled the same way as the Pareto USDT/USDC case: MidasAccount._totalAssets already values REDEMPTION_TOKEN != _asset, and the CoW converter settles it back to the vault asset. The vault asset stays USDC, so testMidasTokenAccountsUseUsdcVaultAsset is unaffected. Verified against a real fork at the CI block (drpc serves archive state there): testOnboardsEthereumMainnetMidasTokensToRedeem, testAllTokenAccountsUseRealMainnetTokens and testMidasTokenAccountsUseUsdcVaultAsset all pass, having failed with [FAIL: msyrupUSDp] before. Co-Authored-By: Claude Opus 5 --- .../ll-adapter/tokens-to-redeem/msyrupUSDp_Account.sol | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/contracts/adapters/ll-adapter/tokens-to-redeem/msyrupUSDp_Account.sol b/src/contracts/adapters/ll-adapter/tokens-to-redeem/msyrupUSDp_Account.sol index 89ec1030..e326d458 100644 --- a/src/contracts/adapters/ll-adapter/tokens-to-redeem/msyrupUSDp_Account.sol +++ b/src/contracts/adapters/ll-adapter/tokens-to-redeem/msyrupUSDp_Account.sol @@ -11,7 +11,11 @@ import {IMidasTokenAccount} from "../../../../interfaces/adapters/ll-adapter/mid contract msyrupUSDp_Account is MidasCompAccount, IMidasTokenAccount { uint48 internal constant TOKEN_COOLDOWN = 1 days; - address internal constant MAINNET_USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; + /// @dev Unlike the other Midas USD vaults, this redemption vault lists only USDT as a payment + /// token, so USDC would be rejected. MidasAccount._requestRedeem falls back to + /// REDEMPTION_TOKEN when the vault has no config for the vault asset, and the USDT is + /// settled back to the vault asset through the CoW converter. + address internal constant MAINNET_USDT = 0xdAC17F958D2ee523a2206206994597C13D831ec7; address internal constant TOKEN_ADDRESS = 0x2fE058CcF29f123f9dd2aEC0418AA66a877d8E50; address internal constant REDEMPTION_VAULT_ADDRESS = 0x71EFa7AF1686C5c04AA34a120a91cb4262679C44; @@ -27,7 +31,7 @@ contract msyrupUSDp_Account is MidasCompAccount, IMidasTokenAccount { factory, TOKEN_COOLDOWN, TOKEN_ADDRESS, - MAINNET_USDC, + MAINNET_USDT, REDEMPTION_VAULT_ADDRESS, cowSwapSettlement )