-
Notifications
You must be signed in to change notification settings - Fork 1
test(credible-block): live-anvil scripts for credible block guard upgrades #94
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
makemake-kbo
merged 3 commits into
master
from
makemake/eng-4069-featstd-create-test-scripts-for-testing-contract-updates
Jul 27, 2026
Merged
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
24f390d
test(credible-block): live-anvil scripts for credible block guard upg…
makemake-kbo 380b169
fix(credible-block): guard anvil startup against a foreign RPC listener
makemake-kbo 6afb544
docs(credible-block): correct fixture description and seed credible b…
makemake-kbo File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| # Credible Block guard — upgrade tests | ||
|
|
||
| Integration scripts that exercise [`CredibleBlockGuard`](../../src/protection/credible_block/CredibleBlockGuard.sol)'s | ||
| `onlyCredibleBlock` modifier against a **live anvil node**, so we can validate credible-layer | ||
| contract upgrades end to end. | ||
|
|
||
| The forge unit tests in | ||
| [`test/protection/credible_block/`](../../test/protection/credible_block/CredibleBlockGuard.t.sol) | ||
| fake block state with `vm.roll` / `vm.prank`. That covers the pure decision logic, but it cannot | ||
| reproduce the one thing that only exists on a real chain: a builder's *credible block marker* | ||
| transaction and a *guarded* transaction landing in the **same block** (a bundle). These scripts seed | ||
| a real node and drive mining manually so we can test exactly that. | ||
|
|
||
| ## Contents | ||
|
|
||
| | File | Role | | ||
| | ---- | ---- | | ||
| | [`src/CredibleRegistry.sol`](./src/CredibleRegistry.sol) | Minimal deployable registry: a single immutable builder (set at construction) can mark the current block credible; implements [`ICredibleRegistry`](../../src/protection/credible_block/ICredibleRegistry.sol). The production registry ([`phylaxsystems/credible-registry`](https://github.com/phylaxsystems/credible-registry)) additionally has a timelocked admin, a builder whitelist, and timestamp slot-binding — none needed to exercise the guard. | | ||
| | [`src/GuardedCounter.sol`](./src/GuardedCounter.sol) | A concrete `CredibleBlockGuard` standing in for an upgraded credible-layer contract; its `bump()` entrypoint is `onlyCredibleBlock`. | | ||
| | [`script/test-credible-upgrades.sh`](./script/test-credible-upgrades.sh) | Orchestrator that boots anvil, deploys, and runs the three cases. | | ||
|
|
||
| ## Cases | ||
|
|
||
| | Case | Scenario | Expectation | | ||
| | ---- | -------- | ----------- | | ||
| | **1. Credible block** | Bundle `[markCurrentBlockCredible, bump]` into one block via manual mining | Both txs succeed; counter increments | | ||
| | **2. Non-credible block** | Send only `bump()` with no marker | Reverts with `NonCredibleBlock()`; counter unchanged | | ||
| | **3. Fail-open** | Builder stops marking for `> failOpenBlockThreshold` blocks | Still reverts at the boundary (gap == threshold); passes once gap > threshold | | ||
|
|
||
| ## How the bundle is simulated | ||
|
|
||
| Anvil auto-mines each tx into its own block by default, which would put the marker and the guarded | ||
| call in different blocks. The script turns automine **off** (`evm_setAutomine false`), submits both | ||
| txs with `cast send --async` (returns immediately without waiting for a receipt), then seals exactly | ||
| one block with `evm_mine`. Both queued txs land together; `--order fifo` guarantees the marker | ||
| executes first, so the guarded call sees the block already marked credible. | ||
|
|
||
| ## Running | ||
|
|
||
| From the repo root: | ||
|
|
||
| ```shell | ||
| ./examples/credible-block/script/test-credible-upgrades.sh | ||
| ``` | ||
|
|
||
| Requires `anvil`, `cast`, `forge`, and `jq` on `PATH`. Exits non-zero if any check fails. Env | ||
| overrides: `RPC_PORT` (default `8545`), `FAIL_OPEN_THRESHOLD` (default `10`, kept small so the | ||
| fail-open case runs quickly). | ||
|
|
||
| The script uses the `credible-block` foundry profile (see `foundry.toml`); it sets | ||
| `FOUNDRY_PROFILE=credible-block` itself. | ||
|
|
||
| > On macOS, `cast`/`forge` read system proxy configuration at startup, which the Claude Code Bash | ||
| > sandbox blocks (the process aborts with a NULL-object panic). Run this script with the sandbox | ||
| > disabled. |
226 changes: 226 additions & 0 deletions
226
examples/credible-block/script/test-credible-upgrades.sh
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,226 @@ | ||
| #!/usr/bin/env bash | ||
| # | ||
| # Integration test for the Credible Layer block guard against a live anvil node. | ||
| # | ||
| # Unlike the forge unit tests (which fake block state with vm.roll/vm.prank), this drives a real | ||
| # node so we can exercise the one thing that only matters on a real chain: whether a builder's | ||
| # "credible block marker" tx and a guarded tx land in the SAME block (a bundle). | ||
| # | ||
| # It seeds anvil with: | ||
| # - a CredibleRegistry (examples/credible-block/src/CredibleRegistry.sol) with a single immutable | ||
| # builder baked in at construction (the production registry additionally gates builders behind a | ||
| # timelocked admin and a whitelist; none of that is needed to exercise the guard), | ||
| # - a "builder" account we control, passed in as that immutable builder, | ||
| # - a GuardedCounter (a concrete CredibleBlockGuard) standing in for an upgraded credible contract. | ||
| # | ||
| # Then it verifies three cases: | ||
| # 1. Credible block — bundle [marker tx, guarded tx] into one block; both must succeed. | ||
| # 2. Non-credible — send only the guarded tx (no marker); it must revert (NonCredibleBlock). | ||
| # 3. Fail-open — after the builder stops marking for > failOpenBlockThreshold blocks, the | ||
| # guarded tx must start passing again (and must still revert at the boundary). | ||
| # | ||
| # Usage: ./examples/credible-block/script/test-credible-upgrades.sh (run from the repo root) | ||
| # Requires: anvil, cast, forge, jq on PATH. | ||
|
|
||
| set -euo pipefail | ||
|
|
||
| # -------------------------------------------------------------------------------------------------- | ||
| # Config | ||
| # -------------------------------------------------------------------------------------------------- | ||
| RPC_PORT="${RPC_PORT:-8545}" | ||
| RPC_URL="http://127.0.0.1:${RPC_PORT}" | ||
| FAIL_OPEN_THRESHOLD="${FAIL_OPEN_THRESHOLD:-10}" # kept small so the fail-open case runs quickly | ||
|
|
||
| export FOUNDRY_PROFILE=credible-block | ||
|
|
||
| # Deterministic anvil dev accounts (default mnemonic). | ||
| ADMIN_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 # account 0 (deployer/admin) | ||
| BUILDER_KEY=0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d # account 1 (credible builder) | ||
| USER_KEY=0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a # account 2 (unprivileged caller) | ||
| BUILDER_ADDR=$(cast wallet address "$BUILDER_KEY") | ||
| USER_ADDR=$(cast wallet address "$USER_KEY") | ||
|
|
||
| GAS_LIMIT=2000000 # explicit so `cast send` skips eth_estimateGas (which would fail on reverting txs) | ||
|
|
||
| SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" | ||
| REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)" | ||
|
|
||
| RED=$'\033[0;31m'; GREEN=$'\033[0;32m'; BLUE=$'\033[0;34m'; BOLD=$'\033[1m'; NC=$'\033[0m' | ||
| PASS_COUNT=0; FAIL_COUNT=0 | ||
| ANVIL_PID="" | ||
|
|
||
| cleanup() { [[ -n "$ANVIL_PID" ]] && kill "$ANVIL_PID" 2>/dev/null || true; } | ||
| trap cleanup EXIT | ||
|
|
||
| info() { echo "${BLUE}==>${NC} $*"; } | ||
| section() { echo; echo "${BOLD}$*${NC}"; } | ||
|
|
||
| # check <description> <actual> <expected> | ||
| check() { | ||
| local desc="$1" actual="$2" expected="$3" | ||
| if [[ "$actual" == "$expected" ]]; then | ||
| echo " ${GREEN}PASS${NC} $desc" | ||
| PASS_COUNT=$((PASS_COUNT + 1)) | ||
| else | ||
| echo " ${RED}FAIL${NC} $desc (expected '$expected', got '$actual')" | ||
| FAIL_COUNT=$((FAIL_COUNT + 1)) | ||
| fi | ||
| } | ||
|
|
||
| rpc() { cast rpc --rpc-url "$RPC_URL" "$@" >/dev/null; } | ||
| mine_blocks() { rpc anvil_mine "$(cast to-hex "$1")"; } # mine N blocks instantly | ||
| receipt_status() { cast receipt --rpc-url "$RPC_URL" --async "$1" --json 2>/dev/null | jq -r '.status'; } | ||
| receipt_block() { cast to-dec "$(cast receipt --rpc-url "$RPC_URL" --async "$1" --json | jq -r '.blockNumber')"; } | ||
| counter() { cast call --rpc-url "$RPC_URL" "$GUARD" "count()(uint256)"; } | ||
|
|
||
| # Submit a tx WITHOUT waiting for it to be mined; echoes the tx hash. | ||
| send_async() { | ||
| local key="$1"; shift | ||
| cast send --rpc-url "$RPC_URL" --private-key "$key" --gas-limit "$GAS_LIMIT" --async "$@" | ||
| } | ||
|
|
||
| # Reset chain state to the post-deployment snapshot and re-arm the snapshot for the next case. | ||
| reset_state() { | ||
| rpc evm_revert "$SNAPSHOT" | ||
| SNAPSHOT=$(cast rpc --rpc-url "$RPC_URL" evm_snapshot | tr -d '"') | ||
| rpc evm_setAutomine false # manual mining for deterministic block composition | ||
| } | ||
|
|
||
| # -------------------------------------------------------------------------------------------------- | ||
| # Boot anvil + deploy | ||
| # -------------------------------------------------------------------------------------------------- | ||
| section "Booting anvil (fifo mempool ordering) and deploying contracts" | ||
|
|
||
| # Refuse to run against a pre-existing RPC listener: if the port is taken, our anvil would exit and | ||
| # the readiness loop below would happily adopt (and snapshot/revert/disable-automine) a foreign node. | ||
| if cast block-number --rpc-url "$RPC_URL" >/dev/null 2>&1; then | ||
| echo "${RED}ERROR${NC} something is already listening on $RPC_URL; refusing to run. Set RPC_PORT to a free port." >&2 | ||
| exit 1 | ||
| fi | ||
|
|
||
| # fifo ordering guarantees the marker tx (submitted first) is included before the guarded tx when | ||
| # both share a block, so the guarded call sees the block already marked credible. | ||
| anvil --port "$RPC_PORT" --order fifo --silent & | ||
| ANVIL_PID=$! | ||
|
|
||
| # Poll for readiness, but bail if our anvil child died (e.g. the port was grabbed between the check | ||
| # above and launch) rather than proceeding against whatever else may be answering on the port. | ||
| for _ in $(seq 1 50); do | ||
| if ! kill -0 "$ANVIL_PID" 2>/dev/null; then | ||
| echo "${RED}ERROR${NC} anvil (pid $ANVIL_PID) exited before becoming ready; check the port is free." >&2 | ||
| exit 1 | ||
| fi | ||
| cast block-number --rpc-url "$RPC_URL" >/dev/null 2>&1 && break | ||
|
makemake-kbo marked this conversation as resolved.
|
||
| sleep 0.1 | ||
| done | ||
|
|
||
| if ! cast block-number --rpc-url "$RPC_URL" >/dev/null 2>&1; then | ||
| echo "${RED}ERROR${NC} anvil did not become ready on $RPC_URL within the timeout." >&2 | ||
| exit 1 | ||
| fi | ||
|
|
||
| pushd "$REPO_ROOT" >/dev/null | ||
|
|
||
| # Deploy the registry with the builder account we control baked in as its sole marker. | ||
| REGISTRY=$(forge create examples/credible-block/src/CredibleRegistry.sol:CredibleRegistry \ | ||
| --rpc-url "$RPC_URL" --private-key "$ADMIN_KEY" --broadcast --json \ | ||
| --constructor-args "$BUILDER_ADDR" | jq -r '.deployedTo') | ||
| info "CredibleRegistry: $REGISTRY (builder=$BUILDER_ADDR)" | ||
|
|
||
| GUARD=$(forge create examples/credible-block/src/GuardedCounter.sol:GuardedCounter \ | ||
| --rpc-url "$RPC_URL" --private-key "$ADMIN_KEY" --broadcast --json \ | ||
| --constructor-args "$REGISTRY" "$FAIL_OPEN_THRESHOLD" | jq -r '.deployedTo') | ||
| info "GuardedCounter: $GUARD (failOpenBlockThreshold=$FAIL_OPEN_THRESHOLD)" | ||
|
|
||
| popd >/dev/null | ||
|
|
||
| check "registry builder is the account we control" \ | ||
| "$(cast call --rpc-url "$RPC_URL" "$REGISTRY" "builder()(address)")" \ | ||
| "$BUILDER_ADDR" | ||
|
|
||
| SNAPSHOT=$(cast rpc --rpc-url "$RPC_URL" evm_snapshot | tr -d '"') | ||
|
|
||
| # -------------------------------------------------------------------------------------------------- | ||
| # Case 1: credible block — bundle [marker, guarded] into one block; both succeed. | ||
| # -------------------------------------------------------------------------------------------------- | ||
| section "Case 1: credible block (marker + guarded tx bundled in one block)" | ||
| reset_state | ||
|
|
||
| # The marker must be submitted first (fifo) so it executes before the guarded call in the block. | ||
| MARKER_TX=$(send_async "$BUILDER_KEY" "$REGISTRY" "markCurrentBlockCredible()") | ||
| GUARDED_TX=$(send_async "$USER_KEY" "$GUARD" "bump()") | ||
| rpc evm_mine # seal ONE block containing both queued txs | ||
|
|
||
| info "marker tx: $MARKER_TX" | ||
| info "guarded tx: $GUARDED_TX" | ||
| check "marker tx succeeded" "$(receipt_status "$MARKER_TX")" "0x1" | ||
| check "guarded tx succeeded" "$(receipt_status "$GUARDED_TX")" "0x1" | ||
| check "both txs in the same block" "$(receipt_block "$MARKER_TX")" "$(receipt_block "$GUARDED_TX")" | ||
| check "counter incremented to 1" "$(counter)" "1" | ||
|
|
||
| # -------------------------------------------------------------------------------------------------- | ||
| # Case 2: non-credible block — guarded tx alone must revert. | ||
| # -------------------------------------------------------------------------------------------------- | ||
| section "Case 2: non-credible block (guarded tx with no marker reverts)" | ||
| reset_state | ||
|
|
||
| # Seed one credible block first. Without it lastCredibleBlock stays 0, and the blocks already mined | ||
| # by deployment would put block.number - 0 past a small FAIL_OPEN_THRESHOLD override — tripping | ||
| # fail-open, so the guarded tx would wrongly pass and this case would report a false failure. With a | ||
| # recent credible block M seeded, the guarded tx below lands at M+1 (gap 1); since the guard rejects | ||
| # a zero threshold, every deployable threshold is >= 1, so gap 1 is never > threshold and fail-open | ||
| # stays inactive — leaving the block correctly non-credible. | ||
| SEED_TX=$(send_async "$BUILDER_KEY" "$REGISTRY" "markCurrentBlockCredible()") | ||
| rpc evm_mine | ||
| info "seeded credible block at $(receipt_block "$SEED_TX")" | ||
|
|
||
| # The guarded tx's own block carries no marker, so it must revert. | ||
| GUARDED_TX=$(send_async "$USER_KEY" "$GUARD" "bump()") | ||
| rpc evm_mine | ||
| check "guarded tx reverted on-chain" "$(receipt_status "$GUARDED_TX")" "0x0" | ||
| check "counter still 0" "$(counter)" "0" | ||
|
|
||
| # Static call at the now-current (non-credible) tip surfaces the revert reason. cast can't decode the | ||
| # custom error without the ABI, so we match its 4-byte selector directly. | ||
| NON_CREDIBLE_SIG=$(cast sig "NonCredibleBlock()") # 0x95ad9b59 | ||
| CALL_OUT=$(cast call --rpc-url "$RPC_URL" --from "$USER_ADDR" "$GUARD" "bump()" 2>&1 || true) | ||
| if echo "$CALL_OUT" | grep -qi "$NON_CREDIBLE_SIG"; then | ||
| echo " ${GREEN}PASS${NC} static call reverts with NonCredibleBlock()"; PASS_COUNT=$((PASS_COUNT + 1)) | ||
| else | ||
| echo " ${RED}FAIL${NC} static call did not revert with NonCredibleBlock() (got: $CALL_OUT)"; FAIL_COUNT=$((FAIL_COUNT + 1)) | ||
| fi | ||
|
|
||
| # -------------------------------------------------------------------------------------------------- | ||
| # Case 3: fail-open — after the builder stops marking for > threshold blocks, guarded tx passes. | ||
| # -------------------------------------------------------------------------------------------------- | ||
| section "Case 3: fail-open once no credible block for > failOpenBlockThreshold blocks" | ||
| reset_state | ||
|
|
||
| THRESHOLD=$(cast call --rpc-url "$RPC_URL" "$GUARD" "failOpenBlockThreshold()(uint256)") | ||
| info "failOpenBlockThreshold = $THRESHOLD" | ||
|
|
||
| # Mark one block credible so lastCredibleBlock is set to a concrete block M. | ||
| MARKER_TX=$(send_async "$BUILDER_KEY" "$REGISTRY" "markCurrentBlockCredible()") | ||
| rpc evm_mine | ||
| M=$(receipt_block "$MARKER_TX") | ||
| info "last credible block M = $M" | ||
|
|
||
| # Advance so the NEXT sealed block is M+THRESHOLD (gap == threshold, NOT > threshold => still guarded). | ||
| mine_blocks $(( THRESHOLD - 1 )) | ||
| BOUNDARY_TX=$(send_async "$USER_KEY" "$GUARD" "bump()") | ||
| rpc evm_mine | ||
| check "at gap == threshold (block $(receipt_block "$BOUNDARY_TX")), guarded tx still reverts" \ | ||
| "$(receipt_status "$BOUNDARY_TX")" "0x0" | ||
|
|
||
| # One more block: gap == threshold+1 (> threshold) => fail-open active, guarded tx passes. | ||
| OPEN_TX=$(send_async "$USER_KEY" "$GUARD" "bump()") | ||
| rpc evm_mine | ||
| check "at gap > threshold (block $(receipt_block "$OPEN_TX")), fail-open lets guarded tx pass" \ | ||
| "$(receipt_status "$OPEN_TX")" "0x1" | ||
| check "counter incremented to 1 via fail-open" "$(counter)" "1" | ||
|
|
||
| # -------------------------------------------------------------------------------------------------- | ||
| # Summary | ||
| # -------------------------------------------------------------------------------------------------- | ||
| section "Summary: ${GREEN}${PASS_COUNT} passed${NC}, ${RED}${FAIL_COUNT} failed${NC}" | ||
| [[ "$FAIL_COUNT" -eq 0 ]] | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| // SPDX-License-Identifier: MIT | ||
| pragma solidity ^0.8.13; | ||
|
|
||
| import {ICredibleRegistry} from "credible-std/protection/credible_block/ICredibleRegistry.sol"; | ||
|
|
||
| /// @notice Minimal deployable Credible Registry for the upgrade-test scripts: just enough to seed a | ||
| /// live anvil node with "a builder we control" and let the {CredibleBlockGuard} read block | ||
| /// credibility through {ICredibleRegistry}. | ||
| /// @dev Trimmed to the essentials — a single immutable builder set at construction, one marker | ||
| /// entrypoint, and the two interface reads. The production registry | ||
| /// (`phylaxsystems/credible-registry`) adds a timelocked admin, a whitelist of builders, and | ||
| /// slot-binding by timestamp; none of that is needed to exercise the guard. | ||
| contract CredibleRegistry is ICredibleRegistry { | ||
| /// @notice The only account allowed to mark blocks credible. | ||
| address public immutable builder; | ||
|
|
||
| mapping(uint256 blockNumber => bool credible) internal _credible; | ||
| uint256 internal _lastCredibleBlock; | ||
|
|
||
| error NotBuilder(); | ||
|
|
||
| constructor(address builder_) { | ||
| builder = builder_; | ||
| } | ||
|
|
||
| /// @notice Marks the current block credible. Callable only by the builder. | ||
| function markCurrentBlockCredible() external { | ||
| if (msg.sender != builder) revert NotBuilder(); | ||
|
|
||
| _credible[block.number] = true; | ||
| _lastCredibleBlock = block.number; | ||
| } | ||
|
|
||
| function isCredibleBlock(uint256 blockNumber) external view returns (bool) { | ||
| return _credible[blockNumber]; | ||
| } | ||
|
|
||
| function lastCredibleBlock() external view returns (uint256) { | ||
| return _lastCredibleBlock; | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| // SPDX-License-Identifier: MIT | ||
| pragma solidity ^0.8.13; | ||
|
|
||
| import {CredibleBlockGuard} from "credible-std/protection/credible_block/CredibleBlockGuard.sol"; | ||
| import {ICredibleRegistry} from "credible-std/protection/credible_block/ICredibleRegistry.sol"; | ||
|
|
||
| /// @notice A minimal contract that adopts the {CredibleBlockGuard} `onlyCredibleBlock` modifier, | ||
| /// standing in for a real credible-layer contract upgrade. Its guarded entrypoint (`bump`) | ||
| /// only executes inside a block a whitelisted builder marked credible, unless the guard is | ||
| /// failing open because the builder set has gone silent past `failOpenBlockThreshold`. | ||
| contract GuardedCounter is CredibleBlockGuard { | ||
| uint256 public count; | ||
|
|
||
| constructor(ICredibleRegistry credibleRegistry_, uint256 failOpenBlockThreshold_) | ||
| CredibleBlockGuard(credibleRegistry_, failOpenBlockThreshold_) | ||
| {} | ||
|
|
||
| /// @notice Guarded state mutation: reverts with `NonCredibleBlock` outside a credible block. | ||
| function bump() external onlyCredibleBlock { | ||
| count++; | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.