diff --git a/.github/workflows/release-readiness.yml b/.github/workflows/release-readiness.yml index 759f94d4..cf428da6 100644 --- a/.github/workflows/release-readiness.yml +++ b/.github/workflows/release-readiness.yml @@ -88,6 +88,7 @@ jobs: - name: Test deployment lanes run: | corepack yarn test:intent-guardian-fee-update + corepack yarn test:retire-dispute-risk-windows corepack yarn test:v3-groups-deployment corepack yarn test:dispute-lifecycle-deployment corepack yarn test:method-scoped-deployment diff --git a/AGENTS.md b/AGENTS.md index 14215723..179f0d66 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -104,6 +104,19 @@ Recording checkpoints: commit live records before any artifact generation, pin lanes 39/40 after their first live execution, propose the Base cutover at the live Safe nonce, and flip manifests/package only after execution. Lane 40 is immutable and pinned after its 2026-08-28 Base execution. +- `deploy/42_retire_dispute_risk_windows.ts` removes payment methods from dispute protection by zeroing their risk + windows on `DisputeProtectionPolicyMethodScopedStaked`: a zero window makes `onIntentSignaled` a no-op and + `isDisputeProtectionEnabled` false for that method, and PayPal/Venmo are untouched. Its target is + `RETIRED_DISPUTABLE_PAYMENT_METHODS` (`cashapp`, decided 2026-08-28), not `DISPUTABLE_PAYMENT_METHODS`, which + #311 already trimmed to PayPal/Venmo. Live networks run only under `DEPLOY_ACTIVE_TAG=42_retire_dispute_risk_windows` + (`yarn deploy:dispute-risk-windows:base_staging` executes the deployer-EOA write; `:base` queues one + `setRiskWindow(cashapp, 0)` Safe call that `deploy_summary` writes under `deployments/outputs/safe-batches/`); + local networks zero the window right after lane 39, and the lane skips wherever every retired window already reads + zero. Base executed the call at Safe nonce 80 on 2026-08-28 (safeTxHash `0x16a7…4650`, tx `0x4824…6f8a`, artifacts + `base_retire_cashapp_risk_window*`, whose Safe origin/description say "lane 41" — the retirement lane's number + before lane 41 was claimed by the UPI payment method), so Base evidence is already `0`. Base staging is still pending: the package + extractor's `LIVE_DISPUTABLE_PAYMENT_METHODS.base_staging` override, the staging evidence, and the spec's staging + window keep Cash App at 1209600 until the staging write executes, and that recording PR also pins lane 42. - `deployments/predecessorDisputeStack.ts` keeps two pinned maps: `PREDECESSOR_DISPUTE_STACKS` describes the predecessor of the currently selected stack and feeds the lane-30 wrapper, the package's recognized-predecessor identities, and lane-34 tooling; `METHOD_SCOPED_PREDECESSOR_DISPUTE_STACKS` describes what lane 37 replaces (the diff --git a/CLAUDE.md b/CLAUDE.md index 1b830b1c..cfb983be 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -149,8 +149,12 @@ Current numbered lanes include: - `39` (immutable): method-scoped stack on a dedicated `StakeVaultMethodScoped` (deploy-only on live networks; deploys and activates locally); - `40` (immutable): dedicated-vault activation (tag-only; staging EOA steps, - one guarded Base cutover batch, deferred predecessor writer removal). -- `41`: generic UPI/INR payment method (tag-only, Base staging only). + one guarded Base cutover batch, deferred predecessor writer removal); +- `41`: generic UPI/INR payment method (tag-only, Base staging only); +- `42`: dispute risk-window retirement (tag-only on live networks; zeroes the + `RETIRED_DISPUTABLE_PAYMENT_METHODS` windows on the lane-39 policy; Base + executed at Safe nonce 80 on 2026-08-28, staging EOA write and its evidence + flip still pending; pin after that execution). There is no `26` script. Numbered files are identities, not proof that every script should execute. A numbered script is immutable after any production diff --git a/README.md b/README.md index c976040d..bdb53ef4 100644 --- a/README.md +++ b/README.md @@ -797,6 +797,17 @@ Dispute-evidence issuance in `attestation-service` remains a separate follow-up by these contract lanes. Only PayPal and Venmo receive non-zero onchain risk windows; Cash App and every other active payment method have a zero window and do not require stake. +`deploy/42_retire_dispute_risk_windows.ts` is the governed write behind that statement (decided 2026-08-28). It zeroes +the `RETIRED_DISPUTABLE_PAYMENT_METHODS` risk windows on `DisputeProtectionPolicyMethodScopedStaked`, which makes +`onIntentSignaled` a no-op and `isDisputeProtectionEnabled` false for the method while PayPal and Venmo keep their +14-day windows. Base executed it at Safe nonce 80 on 2026-08-28 (safeTxHash `0x16a7…4650`, tx +`0x4824a78931e3785cd69e4aa1c669c2f86cf0ccbaf22066d54f7f7ace55156f8a`; batch and chunk plan under +`deployments/outputs/safe-batches/base_retire_cashapp_risk_window*`; their "lane 41" label predates the UPI +payment method claiming that number), so the lane's tagged Base run now skips. Base +staging still carries the 14-day Cash App window until `yarn deploy:dispute-risk-windows:base_staging` executes the +deployer-EOA write; after that, the recording PR drops Cash App from the package extractor's staging +`LIVE_DISPUTABLE_PAYMENT_METHODS` override, flips the staging evidence and spec windows, and pins lane 42. + ### Whitelist Bootstrap `yarn whitelist:bootstrap` discovers active deposits from a configurable raw GraphQL endpoint and diff --git a/deploy/42_retire_dispute_risk_windows.ts b/deploy/42_retire_dispute_risk_windows.ts new file mode 100644 index 00000000..9fd722e4 --- /dev/null +++ b/deploy/42_retire_dispute_risk_windows.ts @@ -0,0 +1,157 @@ +// Lane 42 retires dispute protection for governance-selected payment methods by zeroing their risk windows. +// Local and EOA-owned policies execute directly; Safe-owned policies emit queued governance calls. +// The lane skips once every retired window reads zero; per-network evidence flips in recording PRs after each execution. + +import { ethers } from "ethers"; +import type { BigNumber, Contract } from "ethers"; +import { HardhatRuntimeEnvironment } from "hardhat/types"; +import { DeployFunction } from "hardhat-deploy/types"; + +import { + MULTI_SIG, + RETIRED_DISPUTABLE_PAYMENT_METHODS, + getActivePaymentMethods, +} from "../deployments/parameters"; +import { + getDeployedContractAddress, + waitForDeploymentDelay, +} from "../deployments/helpers"; +import { safeBatchCollector } from "../deployments/safeBatchCollector"; + +export const TAG = "42_retire_dispute_risk_windows"; +export const POLICY_DEPLOYMENT_NAME = + "DisputeProtectionPolicyMethodScopedStaked"; + +const SUPPORTED_NETWORKS = new Set([ + "localhost", + "hardhat", + "base_staging", + "base", +]); + +function isLiveNetwork(network: string): boolean { + return network === "base" || network === "base_staging"; +} + +function sameAddress(left: string, right: string): boolean { + return left.toLowerCase() === right.toLowerCase(); +} + +export function paymentMethodHash(method: string): string { + return ethers.utils.keccak256(ethers.utils.toUtf8Bytes(method)); +} + +export function assertRetiredMethodsActive( + network: string, + retired: readonly string[] = RETIRED_DISPUTABLE_PAYMENT_METHODS +): void { + if (retired.length === 0) { + throw new Error("Retired disputable payment methods must not be empty"); + } + const activeMethods = getActivePaymentMethods(network); + for (const method of retired) { + if (!activeMethods.includes(method)) { + throw new Error( + `Retired disputable payment method is not active on ${network}: ${method}` + ); + } + } +} + +export async function readRetiredRiskWindows( + policy: Contract, + network: string +): Promise> { + assertRetiredMethodsActive(network); + return Promise.all( + RETIRED_DISPUTABLE_PAYMENT_METHODS.map(async (method) => { + const hash = paymentMethodHash(method); + return { + method, + hash, + window: await policy.getRiskWindow(hash), + }; + }) + ); +} + +export async function retireDisputeRiskWindows( + hre: HardhatRuntimeEnvironment, + policy: Contract +): Promise { + const network = hre.deployments.getNetworkName(); + const windows = await readRetiredRiskWindows(policy, network); + const drifted = windows.filter(({ window }) => !window.isZero()); + if (drifted.length === 0) return; + + const accounts = await hre.getUnnamedAccounts(); + const deployer = accounts[0]; + const expectedOwner = MULTI_SIG[network] || deployer; + const owner = await policy.owner(); + if (!sameAddress(owner, expectedOwner)) { + throw new Error( + `${POLICY_DEPLOYMENT_NAME} owner mismatch: expected ${expectedOwner}, found ${owner}` + ); + } + + if (accounts.some((account) => sameAddress(account, owner))) { + const signer = await hre.ethers.getSigner(owner); + for (const { hash } of drifted) { + await (await policy.connect(signer).setRiskWindow(hash, 0)).wait(); + await waitForDeploymentDelay(hre); + } + for (const { method, hash } of drifted) { + if (!(await policy.getRiskWindow(hash)).isZero()) { + throw new Error( + `${POLICY_DEPLOYMENT_NAME} risk window still set for ${method}` + ); + } + } + return; + } + + for (const { method, hash } of drifted) { + safeBatchCollector.add( + policy.address, + policy.interface.encodeFunctionData("setRiskWindow", [hash, 0]), + `${POLICY_DEPLOYMENT_NAME}.setRiskWindow(${method}, 0)` + ); + } +} + +export async function retiredRiskWindowsCleared( + hre: HardhatRuntimeEnvironment +): Promise { + const network = hre.deployments.getNetworkName(); + const policy = await hre.ethers.getContractAt( + "DisputeProtectionPolicy", + getDeployedContractAddress(network, POLICY_DEPLOYMENT_NAME) + ); + const windows = await readRetiredRiskWindows(policy, network); + return windows.every(({ window }) => window.isZero()); +} + +const func: DeployFunction = async function ( + hre: HardhatRuntimeEnvironment +): Promise { + const network = hre.deployments.getNetworkName(); + const policy = await hre.ethers.getContractAt( + "DisputeProtectionPolicy", + getDeployedContractAddress(network, POLICY_DEPLOYMENT_NAME) + ); + await retireDisputeRiskWindows(hre, policy); +}; + +func.skip = async (hre: HardhatRuntimeEnvironment): Promise => { + const network = hre.deployments.getNetworkName(); + if (!SUPPORTED_NETWORKS.has(network)) return true; + if (isLiveNetwork(network) && process.env.DEPLOY_ACTIVE_TAG !== TAG) { + return true; + } + return retiredRiskWindowsCleared(hre); +}; + +func.tags = [TAG, "DisputeRiskWindowRetirement"]; +func.dependencies = ["39_deploy_method_scoped_vault_stack"]; + +export default func; diff --git a/deploy/deploy_summary.ts b/deploy/deploy_summary.ts index e8f76f8d..50846e05 100644 --- a/deploy/deploy_summary.ts +++ b/deploy/deploy_summary.ts @@ -106,6 +106,8 @@ func.tags = [ "V3DisputeMethodScopedStack", "38_activate_method_scoped_dispute_lifecycle_stack", "V3DisputeMethodScopedActivation", + "42_retire_dispute_risk_windows", + "DisputeRiskWindowRetirement", ]; export default func; diff --git a/deployments/outputs/safe-batches/base_retire_cashapp_risk_window.json b/deployments/outputs/safe-batches/base_retire_cashapp_risk_window.json new file mode 100644 index 00000000..ffa0f11f --- /dev/null +++ b/deployments/outputs/safe-batches/base_retire_cashapp_risk_window.json @@ -0,0 +1,21 @@ +{ + "version": "1.0", + "chainId": "8453", + "createdAt": 1787912115441, + "meta": { + "name": "Base Cash App dispute risk-window retirement", + "description": "DisputeProtectionPolicyMethodScopedStaked.setRiskWindow(keccak256(\"cashapp\"), 0) — governance decision 2026-08-28: Cash App leaves the dispute-protected set (zero window = onIntentSignaled no-op, isDisputeProtectionEnabled false); PayPal and Venmo keep 1209600. Lane 41 (PR #310) encoding; simulated from the Safe at Base block 50561373.", + "txBuilderVersion": "1.16.5", + "createdFromSafeAddress": "0x0bC26FF515411396DD588Abd6Ef6846E04470227", + "createdFromOwnerAddress": "" + }, + "transactions": [ + { + "to": "0xbF4B769dB70DBEc89b6b2c44988304a7aD2de4Fc", + "value": "0", + "data": "0x0aaaa5b410940ee67cfb3c6c064569ec92c0ee934cd7afa18dd2ca2d6a2254fcb009c17d0000000000000000000000000000000000000000000000000000000000000000", + "contractMethod": null, + "contractInputsValues": null + } + ] +} diff --git a/deployments/outputs/safe-batches/base_retire_cashapp_risk_window.json.chunks.json b/deployments/outputs/safe-batches/base_retire_cashapp_risk_window.json.chunks.json new file mode 100644 index 00000000..d49b648b --- /dev/null +++ b/deployments/outputs/safe-batches/base_retire_cashapp_risk_window.json.chunks.json @@ -0,0 +1,37 @@ +{ + "version": 1, + "chainId": 8453, + "safe": "0x0bC26FF515411396DD588Abd6Ef6846E04470227", + "sourceFile": "/Users/kartikeychauhan/Documents/zkp2p/zkp2p-contracts/deployments/outputs/safe-batches/base_retire_cashapp_risk_window.json", + "maxGas": "30000000", + "callOverhead": "60000", + "chunkOverhead": "100000", + "chunks": [ + { + "safeTx": { + "to": "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D", + "value": "0", + "data": "0x8d80ff0a0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000009900bf4b769db70dbec89b6b2c44988304a7ad2de4fc000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000440aaaa5b410940ee67cfb3c6c064569ec92c0ee934cd7afa18dd2ca2d6a2254fcb009c17d000000000000000000000000000000000000000000000000000000000000000000000000000000", + "operation": 1, + "safeTxGas": "0", + "baseGas": "0", + "gasPrice": "0", + "gasToken": "0x0000000000000000000000000000000000000000", + "refundReceiver": "0x0000000000000000000000000000000000000000", + "nonce": 80 + }, + "safeTxHash": "0x16a751458a0f011fecf113c6c8dbbf26eecd930a91c594c9f3ba66266e7a4650", + "estimatedGas": "190904", + "callCount": 1, + "calls": [ + { + "target": "0xbf4b769db70dbec89b6b2c44988304a7ad2de4fc", + "selector": "0x0aaaa5b4", + "estimatedGas": "30904" + } + ], + "firstCall": "0x0aaaa5b4@0xbf4b769db70dbec89b6b2c44988304a7ad2de4fc", + "lastCall": "0x0aaaa5b4@0xbf4b769db70dbec89b6b2c44988304a7ad2de4fc" + } + ] +} diff --git a/deployments/outputs/safe-batches/base_retire_cashapp_risk_window.sha256.json b/deployments/outputs/safe-batches/base_retire_cashapp_risk_window.sha256.json new file mode 100644 index 00000000..ab30907c --- /dev/null +++ b/deployments/outputs/safe-batches/base_retire_cashapp_risk_window.sha256.json @@ -0,0 +1,4 @@ +{ + "file": "base_retire_cashapp_risk_window.json", + "sha256": "c72ccdc3442511e06554dae42f65f808f55dc0f505ddc1649abc1b1df333524e" +} diff --git a/deployments/parameters.ts b/deployments/parameters.ts index dfd69b79..0174e9cd 100644 --- a/deployments/parameters.ts +++ b/deployments/parameters.ts @@ -168,6 +168,13 @@ export const DISPUTABLE_PAYMENT_METHODS: string[] = [ "venmo", ]; +// Payment methods governance removed from dispute protection after lane 39 configured them. Lane 42 zeroes +// their risk windows wherever they are still set (Base executed at Safe nonce 80 on 2026-08-28; Base staging +// pending). DISPUTABLE_PAYMENT_METHODS already excludes them; staging evidence flips after the staging write. +export const RETIRED_DISPUTABLE_PAYMENT_METHODS: string[] = [ + "cashapp", +]; + export const ORCHESTRATOR_V3_PROTOCOL_FEE: any = { "localhost": ether(.001), "hardhat": ether(.001), diff --git a/package.json b/package.json index 1d999455..3e47f6b0 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,8 @@ "deploy:dispute-method-scoped-vault:base": "ts-node --transpile-only scripts/deployActive.ts base 39_deploy_method_scoped_vault_stack", "deploy:dispute-method-scoped-vault-activation:base_staging": "ts-node --transpile-only scripts/deployActive.ts base_staging 40_activate_method_scoped_vault_stack", "deploy:dispute-method-scoped-vault-activation:base": "ts-node --transpile-only scripts/deployActive.ts base 40_activate_method_scoped_vault_stack", + "deploy:dispute-risk-windows:base_staging": "ts-node --transpile-only scripts/deployActive.ts base_staging 42_retire_dispute_risk_windows", + "deploy:dispute-risk-windows:base": "ts-node --transpile-only scripts/deployActive.ts base 42_retire_dispute_risk_windows", "whitelist:bootstrap": "ts-node --transpile-only scripts/bootstrapWhitelistPolicy.ts", "safe:propose-chunks": "ts-node --transpile-only scripts/proposeSafeBatchChunks.ts", "etherscan:base": "yarn hardhat --network base etherscan-verify-with-delay --delay 600", @@ -59,6 +61,7 @@ "test:method-scoped-deployment": "node scripts/test-method-scoped-deployment.cjs", "test:method-scoped-vault-deployment": "node scripts/test-method-scoped-vault-deployment.cjs", "test:method-scoped-activation": "node scripts/test-method-scoped-activation.cjs && node scripts/test-method-scoped-activation-rehearsal.cjs && node scripts/test-method-scoped-vault-activation.cjs && node scripts/test-method-scoped-vault-activation-rehearsal.cjs && node scripts/test-method-scoped-runner.cjs", + "test:retire-dispute-risk-windows": "node --test scripts/retire-dispute-risk-windows.spec.cjs", "test:propose-safe-batch-chunks": "node --test scripts/test-propose-safe-batch-chunks.cjs", "test:v3-groups-deployment": "node scripts/test-v3-groups-base-deployment.cjs prepare-resume && node scripts/test-v3-groups-base-deployment.cjs reject-mismatch && node scripts/test-v3-groups-base-deployment.cjs managed-hook-guard && node scripts/test-v3-groups-base-deployment.cjs managed-hook-no-rollback && node scripts/test-v3-groups-base-deployment.cjs managed-hook-successor-policy && node scripts/test-v3-groups-base-deployment.cjs managed-hook-bytecode-mismatch && node scripts/test-v3-groups-base-deployment.cjs managed-hook-historical-successor", "test:release-policy": "node --test scripts/verify-github-environment.spec.mjs scripts/npm-release.spec.mjs", diff --git a/scripts/retire-dispute-risk-windows.spec.cjs b/scripts/retire-dispute-risk-windows.spec.cjs new file mode 100644 index 00000000..c5a77f24 --- /dev/null +++ b/scripts/retire-dispute-risk-windows.spec.cjs @@ -0,0 +1,253 @@ +#!/usr/bin/env node + +process.env.DEPLOY_TX_DELAY_MS = "0"; +process.env.ALCHEMY_API_KEY ||= "offline"; +process.env.BASE_DEPLOY_PRIVATE_KEY ||= + "1111111111111111111111111111111111111111111111111111111111111111"; +process.env.TESTNET_DEPLOY_PRIVATE_KEY ||= + "2222222222222222222222222222222222222222222222222222222222222222"; + +require(require.resolve("ts-node/register/transpile-only")); +require(require.resolve("module-alias/register")); + +const moduleAlias = require(require.resolve("module-alias")); +moduleAlias.reset(); +moduleAlias.addAlias("@utils", process.cwd() + "/utils"); + +const assert = require("node:assert/strict"); +const { test } = require("node:test"); +const { BigNumber, utils } = require("ethers"); + +const { + DISPUTABLE_PAYMENT_METHODS, + MULTI_SIG, + RETIRED_DISPUTABLE_PAYMENT_METHODS, +} = require("../deployments/parameters.ts"); +const { safeBatchCollector } = require("../deployments/safeBatchCollector.ts"); +const lane41 = require("../deploy/42_retire_dispute_risk_windows.ts"); +const skipLane41 = /** @type {(hre: any) => Promise} */ ( + lane41.default.skip +); + +const DEPLOYER = "0x1000000000000000000000000000000000000001"; +const OTHER = "0x2000000000000000000000000000000000000002"; +const POLICY = "0x3000000000000000000000000000000000000003"; +const RISK_WINDOW = BigNumber.from(1_209_600); +const policyInterface = new utils.Interface([ + "function setRiskWindow(bytes32,uint64)", + "function getRiskWindow(bytes32) view returns (uint64)", + "function owner() view returns (address)", +]); + +/** + * @param {string} network + * @param {string[]} [accounts] + * @param {ReturnType["policy"]} [policy] + * @param {boolean} [throwOnGetContractAt] + */ +function fakeHre( + network, + accounts = [DEPLOYER], + policy = fakePolicy(DEPLOYER).policy, + throwOnGetContractAt = false +) { + return { + deployments: { getNetworkName: () => network }, + ethers: { + getSigner: async (/** @type {string} */ address) => ({ address }), + getContractAt: async () => { + if (throwOnGetContractAt) throw new Error("unexpected chain read"); + return policy; + }, + }, + getUnnamedAccounts: async () => accounts, + }; +} + +/** + * @param {string} owner + * @param {Partial>} [overrides] + */ +function fakePolicy(owner, overrides = {}) { + /** @type {Map} */ + const windows = new Map(); + for (const method of ["paypal", "venmo", "cashapp"]) { + windows.set(utils.id(method), RISK_WINDOW); + } + for (const [method, window] of Object.entries(overrides)) { + windows.set(utils.id(method), BigNumber.from(window)); + } + + /** @type {Array<{ hash: string; window: import("ethers").BigNumber }>} */ + const setterCalls = []; + const policy = { + address: POLICY, + interface: policyInterface, + getRiskWindow: async (/** @type {string} */ hash) => + windows.get(hash) ?? BigNumber.from(0), + owner: async () => owner, + connect: () => ({ + setRiskWindow: async ( + /** @type {string} */ hash, + /** @type {import("ethers").BigNumberish} */ window + ) => { + const value = BigNumber.from(window); + setterCalls.push({ hash, window: value }); + windows.set(hash, value); + return { wait: async () => undefined }; + }, + }), + }; + + return { + policy, + setterCalls, + window: (/** @type {string} */ method) => + windows.get(utils.id(method)) ?? BigNumber.from(0), + }; +} + +test("exports its identity", () => { + assert.deepEqual(lane41.default.tags, [ + "42_retire_dispute_risk_windows", + "DisputeRiskWindowRetirement", + ]); + assert.deepEqual(lane41.default.dependencies, [ + "39_deploy_method_scoped_vault_stack", + ]); + assert.deepEqual(RETIRED_DISPUTABLE_PAYMENT_METHODS, ["cashapp"]); + // Main already excludes the retired methods; the lane must never target a disputable one. + assert.deepEqual(DISPUTABLE_PAYMENT_METHODS, ["paypal", "venmo"]); + for (const method of RETIRED_DISPUTABLE_PAYMENT_METHODS) { + assert.ok(!DISPUTABLE_PAYMENT_METHODS.includes(method), method); + } +}); + +test("retires only the retired window when a local account owns the policy", async () => { + const state = fakePolicy(DEPLOYER); + const queuedBefore = safeBatchCollector.count(); + + await lane41.retireDisputeRiskWindows( + /** @type {any} */ (fakeHre("hardhat", [DEPLOYER], state.policy)), + /** @type {any} */ (state.policy) + ); + + assert.equal(state.window("cashapp").toString(), "0"); + assert.equal(state.window("paypal").toString(), RISK_WINDOW.toString()); + assert.equal(state.window("venmo").toString(), RISK_WINDOW.toString()); + assert.equal(state.setterCalls.length, 1); + assert.equal(state.setterCalls[0].hash, utils.id("cashapp")); + assert.equal(state.setterCalls[0].window.toString(), "0"); + assert.equal(safeBatchCollector.count(), queuedBefore); +}); + +test("queues exactly one Safe call when governance owns the policy", async () => { + const state = fakePolicy(MULTI_SIG.base); + const queuedBefore = safeBatchCollector.count(); + + await lane41.retireDisputeRiskWindows( + /** @type {any} */ (fakeHre("base", [DEPLOYER], state.policy)), + /** @type {any} */ (state.policy) + ); + + const queued = safeBatchCollector.getTransactionsSince(queuedBefore); + assert.equal(queued.length, 1); + assert.equal(queued[0].to, POLICY); + assert.equal( + queued[0].data, + policyInterface.encodeFunctionData("setRiskWindow", [utils.id("cashapp"), 0]) + ); + assert.equal(state.setterCalls.length, 0); + assert.equal(state.window("cashapp").toString(), RISK_WINDOW.toString()); + assert.equal(state.window("paypal").toString(), RISK_WINDOW.toString()); + assert.equal(state.window("venmo").toString(), RISK_WINDOW.toString()); +}); + +test("does nothing when the retired windows are already zero", async () => { + const state = fakePolicy(OTHER, { cashapp: 0 }); + const queuedBefore = safeBatchCollector.count(); + + await lane41.retireDisputeRiskWindows( + /** @type {any} */ (fakeHre("hardhat", [DEPLOYER], state.policy)), + /** @type {any} */ (state.policy) + ); + + assert.equal(safeBatchCollector.count(), queuedBefore); + assert.equal(state.setterCalls.length, 0); + assert.equal(state.window("cashapp").toString(), "0"); + assert.equal(state.window("paypal").toString(), RISK_WINDOW.toString()); + assert.equal(state.window("venmo").toString(), RISK_WINDOW.toString()); +}); + +test("rejects an unexpected policy owner", async () => { + const state = fakePolicy(OTHER); + const queuedBefore = safeBatchCollector.count(); + + await assert.rejects( + lane41.retireDisputeRiskWindows( + /** @type {any} */ (fakeHre("hardhat", [DEPLOYER], state.policy)), + /** @type {any} */ (state.policy) + ), + /owner mismatch/ + ); + assert.equal(safeBatchCollector.count(), queuedBefore); + assert.equal(state.setterCalls.length, 0); + assert.equal(state.window("cashapp").toString(), RISK_WINDOW.toString()); + assert.equal(state.window("paypal").toString(), RISK_WINDOW.toString()); + assert.equal(state.window("venmo").toString(), RISK_WINDOW.toString()); +}); + +test("rejects retired methods that are not active on the network", () => { + assert.throws( + () => lane41.assertRetiredMethodsActive("base", ["mercury"]), + /not active on base/ + ); + assert.doesNotThrow(() => + lane41.assertRetiredMethodsActive("base_staging", ["mercury"]) + ); + assert.throws( + () => lane41.assertRetiredMethodsActive("base", []), + /must not be empty/ + ); +}); + +test("skips untagged live runs before reading the chain and gates tagged runs on live state", async () => { + const previousTag = process.env.DEPLOY_ACTIVE_TAG; + try { + delete process.env.DEPLOY_ACTIVE_TAG; + assert.equal( + await skipLane41( + /** @type {any} */ (fakeHre("base", [DEPLOYER], undefined, true)) + ), + true + ); + + process.env.DEPLOY_ACTIVE_TAG = lane41.TAG; + const state = fakePolicy(MULTI_SIG.base); + assert.equal( + await skipLane41( + /** @type {any} */ (fakeHre("base", [DEPLOYER], state.policy)) + ), + false + ); + const cleared = fakePolicy(MULTI_SIG.base, { cashapp: 0 }); + assert.equal( + await skipLane41( + /** @type {any} */ (fakeHre("base", [DEPLOYER], cleared.policy)) + ), + true + ); + } finally { + if (previousTag === undefined) delete process.env.DEPLOY_ACTIVE_TAG; + else process.env.DEPLOY_ACTIVE_TAG = previousTag; + } +}); + +test("deploy_summary writes the Safe batch for the lane tag", () => { + const deploySummary = require("../deploy/deploy_summary.ts"); + const tags = /** @type {string[]} */ (deploySummary.default.tags); + assert.ok(tags.includes(lane41.TAG)); + assert.ok( + tags.includes("DisputeRiskWindowRetirement") + ); +}); diff --git a/tsconfig.dispute-deployment.json b/tsconfig.dispute-deployment.json index ba8c5e71..5d0bda8b 100644 --- a/tsconfig.dispute-deployment.json +++ b/tsconfig.dispute-deployment.json @@ -26,6 +26,7 @@ "deploy/38_activate_method_scoped_dispute_lifecycle_stack.ts", "deploy/39_deploy_method_scoped_vault_stack.ts", "deploy/40_activate_method_scoped_vault_stack.ts", + "deploy/42_retire_dispute_risk_windows.ts", "deploy/deploy_summary.ts", "packages/contracts/scripts/extractors/abis.ts", "packages/contracts/scripts/extractors/addresses.ts", @@ -43,6 +44,7 @@ "scripts/test-dispute-lifecycle-deployment.cjs", "scripts/test-method-scoped-deployment.cjs", "scripts/test-method-scoped-vault-deployment.cjs", + "scripts/retire-dispute-risk-windows.spec.cjs", "scripts/test-method-scoped-activation.cjs", "scripts/test-method-scoped-activation-rehearsal.cjs", "scripts/test-method-scoped-vault-activation.cjs",