diff --git a/app/src/components/admin/ConfigurationSection.test.tsx b/app/src/components/admin/ConfigurationSection.test.tsx index b001b6fe..cee4e54b 100644 --- a/app/src/components/admin/ConfigurationSection.test.tsx +++ b/app/src/components/admin/ConfigurationSection.test.tsx @@ -72,7 +72,7 @@ const READY: ConfigurationState = { freshness: DAY, cooldown: 3_600n, maxUpBps: 1_000, - maxDownBps: null, + maxDownBps: 500, min: PRICE_ONE / 2n, max: 2n * PRICE_ONE, }, @@ -157,7 +157,7 @@ describe("ConfigurationSection", () => { freshness_duration: DAY, cooldown_secs: 3_600n, max_up_bps: 500, - max_down_bps: null, + max_down_bps: 500, min_answer: PRICE_ONE / 2n, max_answer: 2n * PRICE_ONE, }, diff --git a/app/src/components/admin/CycleActionRail.test.tsx b/app/src/components/admin/CycleActionRail.test.tsx index eec99204..46b3c101 100644 --- a/app/src/components/admin/CycleActionRail.test.tsx +++ b/app/src/components/admin/CycleActionRail.test.tsx @@ -422,15 +422,15 @@ describe("CycleActionRail", () => { it("states an action's note outside the conditions", () => { renderRail([ - { ...attestAction, note: "Ripcord raised — attesting does not lift it." }, + { ...attestAction, note: "NAV paused — attesting does not unpause it." }, ]) expand("Attest price") const list = screen.getByRole("list", { name: "Attest price conditions" }) expect( - screen.getByText("Ripcord raised — attesting does not lift it."), + screen.getByText("NAV paused — attesting does not unpause it."), ).toBeTruthy() - expect(within(list).queryByText(/Ripcord/)).toBeNull() + expect(within(list).queryByText(/NAV paused/)).toBeNull() }) }) diff --git a/app/src/components/admin/CycleSection.test.tsx b/app/src/components/admin/CycleSection.test.tsx index 519bfc14..926dcd63 100644 --- a/app/src/components/admin/CycleSection.test.tsx +++ b/app/src/components/admin/CycleSection.test.tsx @@ -140,13 +140,13 @@ const ready = (overrides: Partial = {}): CycleState => ({ price: price(1n), attestedAt: 1_700_050_000n, expiresAt: 1_700_136_000n, - ripcord: false, + navPaused: false, recorded: true, limits: { freshness: 86_400n, cooldown: 3_600n, maxUpBps: 500, - maxDownBps: null, + maxDownBps: 1_000, min: price(1n) / 2n, max: price(2n), }, diff --git a/app/src/components/admin/EmergencySection.test.tsx b/app/src/components/admin/EmergencySection.test.tsx index 7af8efe0..ec451db8 100644 --- a/app/src/components/admin/EmergencySection.test.tsx +++ b/app/src/components/admin/EmergencySection.test.tsx @@ -16,8 +16,8 @@ const { useOracleStateMock, pauseVaultMock, unpauseVaultMock, - raiseRipcordMock, - setRipcordMock, + pauseNavMock, + setNavPausedMock, clearLatestMock, pauseTokenMock, unpauseTokenMock, @@ -33,8 +33,8 @@ const { useOracleStateMock: vi.fn(), pauseVaultMock: tx(), unpauseVaultMock: tx(), - raiseRipcordMock: tx(), - setRipcordMock: tx(), + pauseNavMock: tx(), + setNavPausedMock: tx(), clearLatestMock: tx(), pauseTokenMock: tx(), unpauseTokenMock: tx(), @@ -58,8 +58,8 @@ vi.mock("../../hooks/useOracleState", () => ({ vi.mock("../../hooks/useEmergencyTransactions", () => ({ usePauseVault: () => pauseVaultMock, useUnpauseVault: () => unpauseVaultMock, - useRaiseRipcord: () => raiseRipcordMock, - useSetRipcord: () => setRipcordMock, + usePauseNav: () => pauseNavMock, + useSetNavPaused: () => setNavPausedMock, useClearLatest: () => clearLatestMock, usePauseToken: () => pauseTokenMock, useUnpauseToken: () => unpauseTokenMock, @@ -103,8 +103,8 @@ describe("EmergencySection", () => { for (const mock of [ pauseVaultMock, unpauseVaultMock, - raiseRipcordMock, - setRipcordMock, + pauseNavMock, + setNavPausedMock, clearLatestMock, pauseTokenMock, unpauseTokenMock, @@ -122,7 +122,7 @@ describe("EmergencySection", () => { expect(within(overview).getByText("Vault")).toBeTruthy() expect(within(overview).getAllByText("Paused")).toHaveLength(2) expect(within(overview).getByText("Open")).toBeTruthy() - expect(within(overview).getByText("Ripcord raised")).toBeTruthy() + expect(within(overview).getByText("NAV paused")).toBeTruthy() }) it("offers the vault guardian only the pause, saying what it stops and what stays open", () => { @@ -155,36 +155,36 @@ describe("EmergencySection", () => { ).toBeTruthy() }) - it("raises the ripcord for an oracle admin through set_ripcord, shown as raising", () => { + it("pauses the NAV for an oracle admin through set_nav_paused, shown as pausing", () => { const { rerender } = render( , ) - fireEvent.click(button("Raise the ripcord")) - expect(setRipcordMock.submit).toHaveBeenCalledWith(true, oracleAdmin) - expect(raiseRipcordMock.submit).not.toHaveBeenCalled() + fireEvent.click(button("Pause the NAV")) + expect(setNavPausedMock.submit).toHaveBeenCalledWith(true, oracleAdmin) + expect(pauseNavMock.submit).not.toHaveBeenCalled() - setRipcordMock.status = { status: "preparing" } + setNavPausedMock.status = { status: "preparing" } rerender() expect( - screen.getByRole("heading", { name: "Preparing to raise the ripcord" }), + screen.getByRole("heading", { name: "Preparing to pause the NAV" }), ).toBeTruthy() }) - it("lowers the ripcord, and clears the latest price once raised", () => { + it("unpauses the NAV, and clears the latest price once paused", () => { setState("open", "open", "paused") const { rerender } = render( , ) - fireEvent.click(button("Lower the ripcord")) - expect(setRipcordMock.submit).toHaveBeenCalledWith(false, oracleAdmin) + fireEvent.click(button("Unpause the NAV")) + expect(setNavPausedMock.submit).toHaveBeenCalledWith(false, oracleAdmin) - setRipcordMock.status = { status: "preparing" } + setNavPausedMock.status = { status: "preparing" } rerender() expect( - screen.getByRole("heading", { name: "Preparing to lower the ripcord" }), + screen.getByRole("heading", { name: "Preparing to unpause the NAV" }), ).toBeTruthy() fireEvent.click(button("Clear the latest price")) diff --git a/app/src/components/admin/EmergencySection.tsx b/app/src/components/admin/EmergencySection.tsx index ba755d02..09ce1f7d 100644 --- a/app/src/components/admin/EmergencySection.tsx +++ b/app/src/components/admin/EmergencySection.tsx @@ -5,8 +5,8 @@ import { useClearLatest, usePauseToken, usePauseVault, - useRaiseRipcord, - useSetRipcord, + usePauseNav, + useSetNavPaused, useUnpauseToken, useUnpauseVault, } from "../../hooks/useEmergencyTransactions" @@ -29,8 +29,8 @@ import CycleActionRail from "./CycleActionRail" import SurfaceLayout from "./SurfaceLayout" import layoutStyles from "./SurfaceLayout.module.css" -const RAISE = "raise" -const LOWER = "lower" +const PAUSE = "pause" +const UNPAUSE = "unpause" const EmergencySection: React.FC<{ grants: Grant[] @@ -43,8 +43,8 @@ const EmergencySection: React.FC<{ const { oracle } = useOracleState() const pauseVault = usePendingTransaction(usePauseVault()) const unpauseVault = usePendingTransaction(useUnpauseVault()) - const raiseRipcord = usePendingTransaction(useRaiseRipcord()) - const setRipcord = usePendingTransaction(useSetRipcord()) + const pauseNav = usePendingTransaction(usePauseNav()) + const setNavPaused = usePendingTransaction(useSetNavPaused()) const clearLatest = usePendingTransaction(useClearLatest()) const pauseToken = usePendingTransaction(usePauseToken()) const unpauseToken = usePendingTransaction(useUnpauseToken()) @@ -60,12 +60,12 @@ const EmergencySection: React.FC<{ case "unpause-vault": unpauseVault.submit(undefined, "", action.grant) return - case "raise-ripcord": - if (action.viaAdmin) setRipcord.submit(true, RAISE, action.grant) - else raiseRipcord.submit(undefined, "", action.grant) + case "pause-nav": + if (action.viaAdmin) setNavPaused.submit(true, PAUSE, action.grant) + else pauseNav.submit(undefined, "", action.grant) return - case "lower-ripcord": - setRipcord.submit(false, LOWER, action.grant) + case "unpause-nav": + setNavPaused.submit(false, UNPAUSE, action.grant) return case "clear-latest": clearLatest.submit(undefined, "", action.grant) @@ -95,10 +95,10 @@ const EmergencySection: React.FC<{ const modalFlows: TransactionModalProps[] = [ flow("pause-vault", pauseVault), flow("unpause-vault", unpauseVault), - flow("raise-ripcord", raiseRipcord), + flow("pause-nav", pauseNav), flow( - setRipcord.amountLabel === RAISE ? "raise-ripcord" : "lower-ripcord", - setRipcord, + setNavPaused.amountLabel === PAUSE ? "pause-nav" : "unpause-nav", + setNavPaused, ), flow("clear-latest", clearLatest), flow("pause-token", pauseToken), diff --git a/app/src/components/vault/TransactionModal.test.tsx b/app/src/components/vault/TransactionModal.test.tsx index 83ce5b65..d387e3bc 100644 --- a/app/src/components/vault/TransactionModal.test.tsx +++ b/app/src/components/vault/TransactionModal.test.tsx @@ -1601,6 +1601,7 @@ describe("TransactionModal, attesting a price", () => { [3005, /already have expired/], [3003, /cooldown .* has not elapsed/], [3004, /moves further from the last attested price/], + [3010, /^Unpause the NAV first\.$/], ])("names the oracle's reason for code %i", (code, sentence) => { renderAttestModal({ status: "failed", @@ -2105,8 +2106,8 @@ describe("TransactionModal, emergency actions", () => { it.each<[EmergencyModalAction, string, string]>([ ["pause-vault", "Preparing to pause the vault", "Vault paused"], ["unpause-vault", "Preparing to unpause the vault", "Vault unpaused"], - ["raise-ripcord", "Preparing to raise the ripcord", "Ripcord raised"], - ["lower-ripcord", "Preparing to lower the ripcord", "Ripcord lowered"], + ["pause-nav", "Preparing to pause the NAV", "NAV paused"], + ["unpause-nav", "Preparing to unpause the NAV", "NAV unpaused"], [ "clear-latest", "Preparing to clear the latest price", @@ -2177,8 +2178,8 @@ describe("TransactionModal, emergency actions", () => { ["unpause-vault", 1001, "vault", /The vault is not paused/], ["pause-token", 1000, "share token", /The share token is already paused/], ["unpause-token", 1001, "share token", /The share token is not paused/], - ["raise-ripcord", 2000, "oracle", /does not hold the role/], - ["clear-latest", 3008, "oracle", /The ripcord is not raised/], + ["pause-nav", 2000, "oracle", /does not hold the role/], + ["clear-latest", 3008, "oracle", /The NAV is not paused/], ])( "names the reason %s was refused with code %i", (action, code, refuser, sentence) => { @@ -2209,7 +2210,7 @@ describe("TransactionModal, emergency actions", () => { }) it("sends an unknown outcome to the Emergency tab", () => { - renderEmergencyModal("raise-ripcord", { + renderEmergencyModal("pause-nav", { status: "failed", failure: { kind: "unknown" }, }) diff --git a/app/src/components/vault/TransactionModal.tsx b/app/src/components/vault/TransactionModal.tsx index 98ec47ff..6458d82c 100644 --- a/app/src/components/vault/TransactionModal.tsx +++ b/app/src/components/vault/TransactionModal.tsx @@ -38,8 +38,8 @@ import styles from "./TransactionModal.module.css" export type EmergencyModalAction = | "pause-vault" | "unpause-vault" - | "raise-ripcord" - | "lower-ripcord" + | "pause-nav" + | "unpause-nav" | "clear-latest" | "pause-token" | "unpause-token" @@ -331,6 +331,8 @@ const attestContractErrorReason = (code: number): string => { return "The cooldown since the last attestation has not elapsed yet." case 3004: return "That price moves further from the last attested price than the oracle allows." + case 3010: + return "Unpause the NAV first." default: return GENERIC_REFUSAL } @@ -1026,19 +1028,19 @@ const EMERGENCY_COPY: Record = { done: "Vault unpaused", doneBody: "Deposits, pricing and upgrades are open again.", }, - "raise-ripcord": { + "pause-nav": { refuser: "oracle", - verb: "raise the ripcord", - signing: "Signing stops pricing until the ripcord is lowered.", - done: "Ripcord raised", + verb: "pause the NAV", + signing: "Signing stops pricing until the NAV is unpaused.", + done: "NAV paused", doneBody: - "Pricing is stopped. No epoch can be fulfilled until the ripcord is lowered.", + "Pricing is stopped. No epoch can be fulfilled until the NAV is unpaused.", }, - "lower-ripcord": { + "unpause-nav": { refuser: "oracle", - verb: "lower the ripcord", + verb: "unpause the NAV", signing: "Signing resumes pricing.", - done: "Ripcord lowered", + done: "NAV unpaused", doneBody: "Epochs can be fulfilled again while the price is valid.", }, "clear-latest": { @@ -1047,7 +1049,7 @@ const EMERGENCY_COPY: Record = { signing: "Signing removes the recorded price.", done: "Latest price cleared", doneBody: - "The next attestation is not limited by its distance from the last. Pricing stays stopped until the ripcord is lowered.", + "The next attestation is not limited by its distance from the last. Pricing stays stopped until the NAV is unpaused.", }, "pause-token": { refuser: "share token", @@ -1339,7 +1341,7 @@ const adminContractErrorReason = case 3001: return "One of the oracle limits is out of range." case 3008: - return "The ripcord is not raised, so the latest price cannot be cleared." + return "The NAV is not paused, so the latest price cannot be cleared." case 6007: return "The deposit cap cannot be negative." case 6045: @@ -1443,8 +1445,8 @@ const describeStatus = ( return describeFinalizeRoundStatus(props.status, props.round) case "pause-vault": case "unpause-vault": - case "raise-ripcord": - case "lower-ripcord": + case "pause-nav": + case "unpause-nav": case "clear-latest": case "pause-token": case "unpause-token": diff --git a/app/src/hooks/useAdminTransaction.test.ts b/app/src/hooks/useAdminTransaction.test.ts index 7be5ae71..88d1ce27 100644 --- a/app/src/hooks/useAdminTransaction.test.ts +++ b/app/src/hooks/useAdminTransaction.test.ts @@ -32,8 +32,8 @@ import { useClearLatest, usePauseToken, usePauseVault, - useRaiseRipcord, - useSetRipcord, + usePauseNav, + useSetNavPaused, useUnpauseToken, useUnpauseVault, } from "./useEmergencyTransactions" @@ -115,8 +115,8 @@ const { verifier: { allow: vi.fn() }, verifierWriterMock: vi.fn(), oracle: { - raise_ripcord: vi.fn(), - set_ripcord: vi.fn(), + pause_nav: vi.fn(), + set_nav_paused: vi.fn(), clear_latest: vi.fn(), set_config: vi.fn(), grant_role: vi.fn(), @@ -144,7 +144,7 @@ const ORACLE_CONFIG = { freshness_duration: 86_400n, cooldown_secs: 3_600n, max_up_bps: 1_000, - max_down_bps: null, + max_down_bps: 500, min_answer: 1n, max_answer: 10n, } @@ -200,9 +200,9 @@ const cases: Case[] = [ refusal: 1001, }, { - name: "useRaiseRipcord", - useHook: useRaiseRipcord, - method: oracle.raise_ripcord, + name: "usePauseNav", + useHook: usePauseNav, + method: oracle.pause_nav, arg: undefined, args: { caller: investorAddress }, keys: oracleWriteKeys, @@ -218,9 +218,9 @@ const cases: Case[] = [ refusal: 3008, }, { - name: "useSetRipcord", - useHook: useSetRipcord, - method: oracle.set_ripcord, + name: "useSetNavPaused", + useHook: useSetNavPaused, + method: oracle.set_nav_paused, arg: false, args: { paused: false, caller: investorAddress }, keys: oracleWriteKeys, diff --git a/app/src/hooks/useConfigurationState.test.ts b/app/src/hooks/useConfigurationState.test.ts index 01a3ec0e..e5459732 100644 --- a/app/src/hooks/useConfigurationState.test.ts +++ b/app/src/hooks/useConfigurationState.test.ts @@ -30,7 +30,7 @@ const LIMITS = { freshness: 86_400n, cooldown: 3_600n, maxUpBps: 1_000, - maxDownBps: null, + maxDownBps: 500, min: 1n, max: 10n, } diff --git a/app/src/hooks/useCycleState.test.ts b/app/src/hooks/useCycleState.test.ts index ee5f77f5..f1497479 100644 --- a/app/src/hooks/useCycleState.test.ts +++ b/app/src/hooks/useCycleState.test.ts @@ -77,7 +77,7 @@ const config = { freshness_duration: 86_400n, cooldown_secs: 3_600n, max_up_bps: 500, - max_down_bps: null, + max_down_bps: 500, min_answer: 5n * 10n ** 17n, max_answer: 2n * 10n ** 18n, } @@ -86,19 +86,19 @@ const state = (tag: OracleState["tag"]): ContractRead => value({ tag, values: undefined } as OracleState) describe("toOracle", () => { - it("carries the report, the limits and no ripcord while the price is valid", () => { + it("carries the report, the limits and no NAV pause while the price is valid", () => { expect(toOracle(state("Valid"), value(report), value(config))).toEqual({ state: "valid", price: (10n ** 18n) as Price, attestedAt: 1_700_050_000n, expiresAt: 1_700_136_000n, - ripcord: false, + navPaused: false, recorded: true, limits: { freshness: 86_400n, cooldown: 3_600n, maxUpBps: 500, - maxDownBps: null, + maxDownBps: 500, min: 5n * 10n ** 17n, max: 2n * 10n ** 18n, }, @@ -112,9 +112,9 @@ describe("toOracle", () => { expect(oracle.expiresAt).toBe(1_700_136_000n) }) - it("reads a paused oracle as a raised ripcord", () => { + it("reads a paused oracle as a paused NAV", () => { expect( - toOracle(state("Paused"), value(report), value(config)).ripcord, + toOracle(state("Paused"), value(report), value(config)).navPaused, ).toBe(true) }) @@ -133,7 +133,7 @@ describe("toOracle", () => { }) }) - it("knows there is no record even while the ripcord is raised", () => { + it("knows there is no record even while the NAV is paused", () => { const oracle = toOracle( state("Paused"), { kind: "contract-error", code: 3006 }, @@ -142,7 +142,7 @@ describe("toOracle", () => { expect(oracle).toMatchObject({ state: "paused", - ripcord: true, + navPaused: true, recorded: false, price: null, attestedAt: null, @@ -155,7 +155,7 @@ describe("toOracle", () => { price: null, attestedAt: null, expiresAt: null, - ripcord: null, + navPaused: null, recorded: null, limits: null, }) diff --git a/app/src/hooks/useCycleState.ts b/app/src/hooks/useCycleState.ts index ed3e02e5..728e69ef 100644 --- a/app/src/hooks/useCycleState.ts +++ b/app/src/hooks/useCycleState.ts @@ -48,7 +48,7 @@ export type OracleLimits = { freshness: bigint cooldown: bigint maxUpBps: number - maxDownBps: number | null + maxDownBps: number min: bigint max: bigint } @@ -58,7 +58,7 @@ export type CycleOracle = { price: Price | null attestedAt: bigint | null expiresAt: bigint | null - ripcord: boolean | null + navPaused: boolean | null recorded: boolean | null limits: OracleLimits | null } @@ -128,7 +128,7 @@ export function toOracle( freshness_duration: bigint cooldown_secs: bigint max_up_bps: number - max_down_bps: number | null + max_down_bps: number min_answer: bigint max_answer: bigint }>, @@ -141,7 +141,7 @@ export function toOracle( price: report === null ? null : (report.nav_per_share as Price), attestedAt: report?.timestamp ?? null, expiresAt: report?.expires_at ?? null, - ripcord: nav.status === "unreadable" ? null : nav.status === "paused", + navPaused: nav.status === "unreadable" ? null : nav.status === "paused", recorded: recordedOf(latest), limits: config.kind === "value" diff --git a/app/src/hooks/useEmergencyTransactions.ts b/app/src/hooks/useEmergencyTransactions.ts index d78e1a22..0057a248 100644 --- a/app/src/hooks/useEmergencyTransactions.ts +++ b/app/src/hooks/useEmergencyTransactions.ts @@ -19,15 +19,14 @@ const tokenCall = (method: "pause" | "unpause") => (signer: Signer) => token[method]({ caller: signer.publicKey }), ) -const oracleCall = - (method: "raise_ripcord" | "clear_latest") => (signer: Signer) => - navOracleWriter(signer).then((oracle) => - oracle[method]({ caller: signer.publicKey }), - ) +const oracleCall = (method: "pause_nav" | "clear_latest") => (signer: Signer) => + navOracleWriter(signer).then((oracle) => + oracle[method]({ caller: signer.publicKey }), + ) -const setRipcordCall = (signer: Signer, paused: boolean) => +const setNavPausedCall = (signer: Signer, paused: boolean) => navOracleWriter(signer).then((oracle) => - oracle.set_ripcord({ paused, caller: signer.publicKey }), + oracle.set_nav_paused({ paused, caller: signer.publicKey }), ) export const usePauseVault = adminTransaction(vaultCall("pause"), [ @@ -42,12 +41,15 @@ export const usePauseToken = adminTransaction(tokenCall("pause"), [ export const useUnpauseToken = adminTransaction(tokenCall("unpause"), [ tokenPausedKey, ]) -export const useRaiseRipcord = adminTransaction( - oracleCall("raise_ripcord"), +export const usePauseNav = adminTransaction( + oracleCall("pause_nav"), oracleWriteKeys, ) export const useClearLatest = adminTransaction( oracleCall("clear_latest"), oracleWriteKeys, ) -export const useSetRipcord = adminTransaction(setRipcordCall, oracleWriteKeys) +export const useSetNavPaused = adminTransaction( + setNavPausedCall, + oracleWriteKeys, +) diff --git a/app/src/hooks/useOracleState.test.ts b/app/src/hooks/useOracleState.test.ts index 8220bb6b..ecfbe479 100644 --- a/app/src/hooks/useOracleState.test.ts +++ b/app/src/hooks/useOracleState.test.ts @@ -34,7 +34,7 @@ describe("useOracleState", () => { vi.clearAllMocks() }) - it("reports the oracle as paused while the ripcord is raised", async () => { + it("reports the oracle as paused while the NAV is paused", async () => { oracleMock.state.mockResolvedValue({ result: { tag: "Paused", values: undefined }, }) diff --git a/app/src/pages/configuration.test.ts b/app/src/pages/configuration.test.ts index d07515d2..cd01a808 100644 --- a/app/src/pages/configuration.test.ts +++ b/app/src/pages/configuration.test.ts @@ -42,7 +42,7 @@ const ready = ( freshness: DAY, cooldown: 3_600n, maxUpBps: 1_000, - maxDownBps: null, + maxDownBps: 500, min: PRICE_ONE / 2n, max: 2n * PRICE_ONE, }, @@ -195,7 +195,7 @@ describe("toConfigurationActions", () => { expect(uncapped.enabled).toBe(false) }) - it("holds a deposit cap and an oracle duration at their contract width", () => { + it("holds a deposit cap and the oracle limits at their contract edges", () => { const cap = (text: string) => find("set-deposit-cap", [governance], { depositCap: text }) const oracle = (inputs: ConfigurationInputs) => @@ -214,16 +214,19 @@ describe("toConfigurationActions", () => { ), ).toBe(false) expect( - condition( - oracle({ freshness: "18446744073709551615" }), - "Freshness window ≤ 2^64−1 s", - ), + condition(oracle({ freshness: "35d" }), "Freshness window ≤ 35d"), ).toBe(true) expect( - condition( - oracle({ cooldown: "18446744073709551615" }), - "Cooldown ≤ 2^64−1 s", - ), + condition(oracle({ cooldown: "1d" }), "Cooldown ≤ freshness window"), + ).toBe(true) + expect( + condition(oracle({ maxFall: "0.01" }), "Largest fall 0.01% to 100%"), + ).toBe(true) + expect( + condition(oracle({ maxFall: "100" }), "Largest fall 0.01% to 100%"), + ).toBe(true) + expect( + condition(oracle({ ceiling: "1000000" }), "Price ceiling ≤ 1,000,000"), ).toBe(true) }) @@ -307,7 +310,7 @@ describe("toConfigurationActions", () => { ["Freshness window", "1d"], ["Cooldown", "1h"], ["Largest rise", "5"], - ["Largest fall", ""], + ["Largest fall", "5"], ["Price floor", "0.50"], ["Price ceiling", "2.00"], ]) @@ -316,25 +319,36 @@ describe("toConfigurationActions", () => { freshness_duration: DAY, cooldown_secs: 3_600n, max_up_bps: 500, - max_down_bps: null, + max_down_bps: 500, min_answer: PRICE_ONE / 2n, max_answer: 2n * PRICE_ONE, }) }) + it("requires a largest fall", () => { + const config = find("set-oracle-config", [oracleAdmin], { maxFall: "" }) + + expect(config.arg).toBeUndefined() + expect(config.enabled).toBe(false) + expect(config.fields?.[3]).toMatchObject({ + label: "Largest fall", + placeholder: "10", + next: null, + }) + }) + it.each<[ConfigurationInputs, string]>([ - [{ freshness: "0" }, "Freshness window > 0"], - [{ freshness: "18446744073709551616" }, "Freshness window ≤ 2^64−1 s"], - [{ cooldown: "18446744073709551616" }, "Cooldown ≤ 2^64−1 s"], - [{ maxRise: "0" }, "Largest rise > 0"], - [{ maxRise: "100.01" }, "Largest rise ≤ 100%"], - [{ maxFall: "101%" }, "Largest fall ≤ 100%"], + [{ freshness: "0" }, "Freshness window ≥ 1s"], + [{ freshness: "35d 1s" }, "Freshness window ≤ 35d"], + [{ cooldown: "0" }, "Cooldown ≥ 1s"], + [{ cooldown: "1d 1s" }, "Cooldown ≤ freshness window"], + [{ maxRise: "0" }, "Largest rise 0.01% to 100%"], + [{ maxRise: "100.01" }, "Largest rise 0.01% to 100%"], + [{ maxFall: "0" }, "Largest fall 0.01% to 100%"], + [{ maxFall: "100.01%" }, "Largest fall 0.01% to 100%"], [{ floor: "0" }, "Price floor > 0"], [{ ceiling: "0.4" }, "Price ceiling ≥ floor"], - [ - { ceiling: "17014118346046924" }, - "Price ceiling ≤ 17,014,118,346,046,923", - ], + [{ ceiling: "1000000.01" }, "Price ceiling ≤ 1,000,000"], ])("names the oracle limit %o breaks", (inputs, label) => { const config = find("set-oracle-config", [oracleAdmin], inputs) @@ -386,7 +400,7 @@ describe("toConfigurationGroups", () => { expect( groups[1]?.strips.map((strip) => strip.map((tile) => tile.value)), ).toEqual([ - ["1d", "1h", "10%", "No limit"], + ["1d", "1h", "10%", "5%"], ["0.5000", "2.0000"], ]) expect(groups[2]?.strips).toEqual([]) diff --git a/app/src/pages/configuration.ts b/app/src/pages/configuration.ts index 515e73ec..3771ac6b 100644 --- a/app/src/pages/configuration.ts +++ b/app/src/pages/configuration.ts @@ -79,10 +79,12 @@ export const EFFECTS = { const MAX_NOTICE = 30n * DAY const MAX_WIND_DOWN_DELAY = 90n * DAY +const MIN_SECS = 1n +const MAX_FRESHNESS = 35n * DAY +const MIN_BPS = 1 const MAX_BPS = 10_000 -const MAX_U64 = (1n << 64n) - 1n const MAX_I128 = (1n << 127n) - 1n -const MAX_CEILING = MAX_I128 / 10_000n +const MAX_CEILING = 1_000_000n * 10n ** BigInt(PRICE_DECIMALS) const PERCENT = /^(\d+)(?:\.(\d{1,2}))?\s*%?$/ type Kind = { @@ -139,20 +141,13 @@ const PRICE: Kind = { placeholder: "0.00", } -const RISE: Kind = { +const MOVE: Kind = { parse: parseBps, show: formatPercent, prefill: (bps) => String(bps / 100), placeholder: "10", } -const FALL: Kind = { - parse: orNone(parseBps), - show: (bps) => (bps === null ? "No limit" : formatPercent(bps)), - prefill: (bps) => (bps === null ? "" : String(bps / 100)), - placeholder: "No limit", -} - type Parsed = { arg: T | undefined; field: ActionField } function parsed( @@ -367,8 +362,8 @@ function oracleFields( DURATION, ), cooldown_secs: field("cooldown_secs", "cooldown", "Cooldown", DURATION), - max_up_bps: field("max_up_bps", "maxRise", "Largest rise", RISE), - max_down_bps: field("max_down_bps", "maxFall", "Largest fall", FALL), + max_up_bps: field("max_up_bps", "maxRise", "Largest rise", MOVE), + max_down_bps: field("max_down_bps", "maxFall", "Largest fall", MOVE), min_answer: field("min_answer", "floor", "Price floor", PRICE), max_answer: field("max_answer", "ceiling", "Price ceiling", PRICE), } @@ -398,36 +393,40 @@ const configOf = ({ max_answer: ceiling.arg, } +const withinBps = (value: number): boolean => + value >= MIN_BPS && value <= MAX_BPS + const oracleBounds = (fields: ParsedConfig): Condition[] => { + const freshness = fields.freshness_duration.arg + const cooldown = fields.cooldown_secs.arg const floor = fields.min_answer.arg const ceiling = fields.max_answer.arg + const bpsRange = `${formatPercent(MIN_BPS)} to ${formatPercent(MAX_BPS)}` return [ bound( - "Freshness window > 0", - fields.freshness_duration.arg, - (value) => value > 0n, - ), - bound( - "Freshness window ≤ 2^64−1 s", - fields.freshness_duration.arg, - (value) => value <= MAX_U64, + `Freshness window ≥ ${formatDuration(MIN_SECS)}`, + freshness, + (value) => value >= MIN_SECS, ), bound( - "Cooldown ≤ 2^64−1 s", - fields.cooldown_secs.arg, - (value) => value <= MAX_U64, + `Freshness window ≤ ${formatDuration(MAX_FRESHNESS)}`, + freshness, + (value) => value <= MAX_FRESHNESS, ), - bound("Largest rise > 0", fields.max_up_bps.arg, (value) => value > 0), bound( - "Largest rise ≤ 100%", - fields.max_up_bps.arg, - (value) => value <= MAX_BPS, - ), - bound( - "Largest fall ≤ 100%", - fields.max_down_bps.arg, - (value) => value === null || value <= MAX_BPS, + `Cooldown ≥ ${formatDuration(MIN_SECS)}`, + cooldown, + (value) => value >= MIN_SECS, ), + { + label: "Cooldown ≤ freshness window", + met: + freshness === undefined || cooldown === undefined + ? null + : cooldown <= freshness, + }, + bound(`Largest rise ${bpsRange}`, fields.max_up_bps.arg, withinBps), + bound(`Largest fall ${bpsRange}`, fields.max_down_bps.arg, withinBps), bound("Price floor > 0", floor, (value) => value > 0n), { label: "Price ceiling ≥ floor", diff --git a/app/src/pages/cycle.test.ts b/app/src/pages/cycle.test.ts index 1f8a70ff..cc7f5645 100644 --- a/app/src/pages/cycle.test.ts +++ b/app/src/pages/cycle.test.ts @@ -78,13 +78,13 @@ const oracle: CycleOracle = { price: price(1n), attestedAt: 1_700_050_000n, expiresAt: 1_700_136_000n, - ripcord: false, + navPaused: false, recorded: true, limits: { freshness: 86_400n, cooldown: 3_600n, maxUpBps: 500, - maxDownBps: null, + maxDownBps: 1_000, min: price(1n) / 2n, max: price(2n), }, @@ -167,7 +167,7 @@ describe("toCycleRows", () => { ]) expect(groups.map((group) => group.rows.map((row) => row.label))).toEqual([ ["Vault", "Notice period", "Sealed at", "Notice"], - ["Ripcord"], + ["NAV paused"], ["Committed", "Liquid reserve"], ["Activates at", "Delay", "Supply"], ]) @@ -258,11 +258,11 @@ describe("toCycleRows", () => { it.each<[string, CycleOracle["state"], string]>([ ["a valid price", "valid", "Valid"], ["a stale price", "stale", "Stale"], - ["a raised ripcord", "paused", "Paused"], + ["a paused NAV", "paused", "Paused"], ])("notes %s under the share price", (_name, state, expected) => { const groups = toCycleRows( ready({ - oracle: { ...oracle, state, ripcord: state === "paused" }, + oracle: { ...oracle, state, navPaused: state === "paused" }, }), ) @@ -303,15 +303,15 @@ describe("toCycleRows", () => { ) }) - it("shows the ripcord, the share price and the oracle limits in app units", () => { + it("shows the NAV pause, the share price and the oracle limits in app units", () => { const groups = toCycleRows(ready({})) expect(tileOf(groups, "Price", "Share price")?.value).toBe("1.0000") - expect(valueOf(groups, "Price", "Ripcord")).toBe("Not raised") + expect(valueOf(groups, "Price", "NAV paused")).toBe("Not paused") expect(valueOf(groups, "Price", "Cooldown")).toBe("1h") expect(valueOf(groups, "Price", "Freshness window")).toBe("1d") expect(valueOf(groups, "Price", "Largest rise")).toBe("5%") - expect(valueOf(groups, "Price", "Largest fall")).toBe("No limit") + expect(valueOf(groups, "Price", "Largest fall")).toBe("10%") expect(valueOf(groups, "Price", "Price floor")).toBe("0.5000") }) @@ -946,9 +946,6 @@ const attestOf = ( "attest", ) -const withLimits = (limits: Partial>) => - ({ ...oracle, limits: { ...oracle.limits!, ...limits } }) as CycleOracle - describe("toCycleActions, attesting", () => { it("offers the attestation only to the oracle attester, in the Price group", () => { expect( @@ -1070,47 +1067,27 @@ describe("toCycleActions, attesting", () => { ).toBe(false) }) - it("lets any fall through when the oracle sets no largest fall", () => { - expect(conditionsOf(attestOf("0.5"))["Within the allowed move"]).toBe(true) - }) - - it("caps a fall at the largest allowed when the oracle sets one", () => { - const limited = { oracle: withLimits({ maxDownBps: 1_000 }) } - - expect( - conditionsOf(attestOf("0.9", limited))["Within the allowed move"], - ).toBe(true) + it("caps a fall at the largest allowed, letting an equal move through", () => { + expect(conditionsOf(attestOf("0.9"))["Within the allowed move"]).toBe(true) expect( - conditionsOf(attestOf("0.899999999999999999", limited))[ - "Within the allowed move" - ], + conditionsOf(attestOf("0.899999999999999999"))["Within the allowed move"], ).toBe(false) }) - it("shows the ceiling when a rise exceeds the cap and no fall cap exists", () => { - const move = attestOf("1.06")?.conditions.find( - (condition) => condition.label === "Within the allowed move", - ) - - expect(move).toEqual({ - label: "Within the allowed move", - met: false, - detail: "≤ 1.0500", - }) - }) - - it("shows the allowed range when both caps exist and the move exceeds one", () => { - const limited = { oracle: withLimits({ maxDownBps: 1_000 }) } - const move = attestOf("0.8", limited)?.conditions.find( - (condition) => condition.label === "Within the allowed move", - ) + it.each(["1.06", "0.8"])( + "shows the allowed range when %s exceeds a cap", + (input) => { + const move = attestOf(input)?.conditions.find( + (condition) => condition.label === "Within the allowed move", + ) - expect(move).toEqual({ - label: "Within the allowed move", - met: false, - detail: "0.9000 – 1.0500", - }) - }) + expect(move).toEqual({ + label: "Within the allowed move", + met: false, + detail: "0.9000 – 1.0500", + }) + }, + ) it("skips the cooldown and the move cap for the first attestation", () => { const attest = attestOf("1.9", { @@ -1131,13 +1108,13 @@ describe("toCycleActions, attesting", () => { expect(attest?.enabled).toBe(true) }) - it("skips the cooldown and the move cap with the ripcord raised and no stored record", () => { + it("skips the cooldown and the move cap with the NAV paused and no stored record", () => { const attest = attestOf("1.9", { ledgerTime: oracle.attestedAt! - 1n, oracle: { ...oracle, state: "paused", - ripcord: true, + navPaused: true, recorded: false, price: null, attestedAt: null, @@ -1185,12 +1162,12 @@ describe("toCycleActions, attesting", () => { expect(attest?.enabled).toBe(false) }) - it("notes a raised ripcord without making it a condition", () => { + it("notes a paused NAV without making it a condition", () => { const attest = attestOf("1.04", { - oracle: { ...oracle, state: "paused", ripcord: true }, + oracle: { ...oracle, state: "paused", navPaused: true }, }) - expect(attest?.note).toBe("Ripcord raised — attesting does not lift it.") + expect(attest?.note).toBe("NAV paused — attesting does not unpause it.") expect(Object.keys(conditionsOf(attest))).toHaveLength(3) expect(attest?.enabled).toBe(true) }) diff --git a/app/src/pages/cycle.ts b/app/src/pages/cycle.ts index 6c6c1c2a..2da96102 100644 --- a/app/src/pages/cycle.ts +++ b/app/src/pages/cycle.ts @@ -67,7 +67,7 @@ const GROUP_LAYOUT: { { title: "Price", tiles: ["Share price", "Attested", "Valid until"], - rows: ["Ripcord"], + rows: ["NAV paused"], limits: [ "Freshness window", "Cooldown", @@ -250,13 +250,7 @@ export const oracleLimitRows = (limits: OracleLimits | null): FigureRow[] => [ row("Freshness window", limits && formatDuration(limits.freshness)), row("Cooldown", limits && formatDuration(limits.cooldown)), row("Largest rise", limits && formatPercent(limits.maxUpBps)), - row( - "Largest fall", - limits && - (limits.maxDownBps === null - ? "No limit" - : formatPercent(limits.maxDownBps)), - ), + row("Largest fall", limits && formatPercent(limits.maxDownBps)), row("Price floor", limits && formatPriceValue(limits.min)), row("Price ceiling", limits && formatPriceValue(limits.max)), ] @@ -281,12 +275,12 @@ function priceGroup(oracle: CycleOracle): CycleGroup { ], rows: [ row( - "Ripcord", - oracle.ripcord === null + "NAV paused", + oracle.navPaused === null ? null - : oracle.ripcord - ? "Raised" - : "Not raised", + : oracle.navPaused + ? "Paused" + : "Not paused", ), ], limits: oracleLimitRows(limits), @@ -528,17 +522,14 @@ const withinAllowedMove = ( if (oracle.recorded === false) return { label, met: true } if (limits === null || previous === null) return { label, met: false } const ceiling = previous + capped(previous, limits.maxUpBps) - const floor = - limits.maxDownBps === null - ? null - : previous - capped(previous, limits.maxDownBps) - const met = price <= ceiling && (floor === null || price >= floor) + const floor = previous - capped(previous, limits.maxDownBps) + const met = price >= floor && price <= ceiling if (met) return { label, met } const top = formatPriceValue(ceiling) return { label, met, - detail: floor === null ? `≤ ${top}` : `${formatPriceValue(floor)} – ${top}`, + detail: `${formatPriceValue(floor)} – ${top}`, } } @@ -564,8 +555,8 @@ function toAttestAction( price === null || freshness === null ? null : `Records ${formatSignedPrice(price)} as the share price, valid for ${formatDuration(freshness)}.`, - ...(oracle.ripcord === true - ? { note: "Ripcord raised — attesting does not lift it." } + ...(oracle.navPaused === true + ? { note: "NAV paused — attesting does not unpause it." } : {}), field: { label: "Share price", diff --git a/app/src/pages/emergency.test.ts b/app/src/pages/emergency.test.ts index 96708e99..795b97e1 100644 --- a/app/src/pages/emergency.test.ts +++ b/app/src/pages/emergency.test.ts @@ -84,8 +84,8 @@ describe("toEmergencyActions", () => { expect(ids(calm, everyRole)).toEqual([ "pause-vault", "unpause-vault", - "raise-ripcord", - "lower-ripcord", + "pause-nav", + "unpause-nav", "clear-latest", "pause-token", "unpause-token", @@ -95,32 +95,32 @@ describe("toEmergencyActions", () => { it.each<[string, string[]]>([ ["vault guardian", ["pause-vault"]], ["vault governance", ["unpause-vault"]], - ["oracle guardian", ["raise-ripcord"]], - ["oracle admin", ["raise-ripcord", "lower-ripcord", "clear-latest"]], + ["oracle guardian", ["pause-nav"]], + ["oracle admin", ["pause-nav", "unpause-nav", "clear-latest"]], ["share-token admin", ["pause-token", "unpause-token"]], ])("offers %s only what its guard permits", (role, expected) => { expect(ids(calm, [grantOf(role)])).toEqual(expected) }) - it("raises through raise_ripcord for the oracle guardian and set_ripcord for the oracle admin", () => { + it("pauses through pause_nav for the oracle guardian and set_nav_paused for the oracle admin", () => { expect( - actionOf(calm, [grantOf("oracle guardian")], "raise-ripcord"), + actionOf(calm, [grantOf("oracle guardian")], "pause-nav"), ).toMatchObject({ viaAdmin: false }) expect( - actionOf(calm, [grantOf("oracle admin")], "raise-ripcord"), + actionOf(calm, [grantOf("oracle admin")], "pause-nav"), ).toMatchObject({ viaAdmin: true }) - expect(actionOf(calm, everyRole, "raise-ripcord")).toMatchObject({ + expect(actionOf(calm, everyRole, "pause-nav")).toMatchObject({ viaAdmin: false, }) }) - it("raises through the oracle admin when the guardian grant needs more signatures", () => { + it("pauses through the oracle admin when the guardian grant needs more signatures", () => { const grants = [ grantOf("oracle guardian", { standing: { needs: 2 } }), grantOf("oracle admin"), ] - expect(actionOf(calm, grants, "raise-ripcord")).toMatchObject({ + expect(actionOf(calm, grants, "pause-nav")).toMatchObject({ viaAdmin: true, enabled: true, }) @@ -134,13 +134,13 @@ describe("toEmergencyActions", () => { expect(enabled).toEqual([ "pause-vault", - "raise-ripcord", - "lower-ripcord", + "pause-nav", + "unpause-nav", "pause-token", ]) }) - it("enables unpausing, lowering and clearing once everything is paused", () => { + it("enables unpausing the vault, unpausing the NAV and clearing once everything is paused", () => { const actions = toEmergencyActions(alarmed, everyRole, wallet) const enabled = actions .filter((action) => action.enabled) @@ -148,8 +148,8 @@ describe("toEmergencyActions", () => { expect(enabled).toEqual([ "unpause-vault", - "raise-ripcord", - "lower-ripcord", + "pause-nav", + "unpause-nav", "clear-latest", "unpause-token", ]) @@ -163,9 +163,9 @@ describe("toEmergencyActions", () => { { label: "Vault is paused", met: false }, ]) expect(actionOf(calm, everyRole, "clear-latest").conditions).toEqual([ - { label: "Ripcord raised", met: false }, + { label: "NAV paused", met: false }, ]) - expect(actionOf(calm, everyRole, "lower-ripcord").conditions).toEqual([]) + expect(actionOf(calm, everyRole, "unpause-nav").conditions).toEqual([]) expect(actionOf(alarmed, everyRole, "unpause-token").conditions).toEqual([ { label: "Share token is paused", met: true }, ]) @@ -185,13 +185,13 @@ describe("toEmergencyActions", () => { expect(actions.every((action) => action.enabled)).toBe(true) }) - it("keeps raising the ripcord available once raised, noting it already is", () => { - const raise = actionOf(alarmed, everyRole, "raise-ripcord") + it("keeps pausing the NAV available once paused, noting it already is", () => { + const pause = actionOf(alarmed, everyRole, "pause-nav") - expect(raise.enabled).toBe(true) - expect(raise.conditions).toEqual([]) - expect(raise.note).toBe("Ripcord already raised.") - expect(actionOf(calm, everyRole, "raise-ripcord").note).toBeUndefined() + expect(pause.enabled).toBe(true) + expect(pause.conditions).toEqual([]) + expect(pause.note).toBe("NAV already paused.") + expect(actionOf(calm, everyRole, "pause-nav").note).toBeUndefined() }) it("says what each action blocks and what stays open", () => { @@ -204,8 +204,8 @@ describe("toEmergencyActions", () => { expect(actionOf(alarmed, everyRole, "unpause-token").outcome).toBe( "Resumes holder transfers and returns of escrowed shares.", ) - expect(actionOf(calm, everyRole, "raise-ripcord").outcome).toBe( - "Stops pricing: no epoch can be fulfilled until it is lowered. Attesting stays open.", + expect(actionOf(calm, everyRole, "pause-nav").outcome).toBe( + "Stops pricing: no epoch can be fulfilled until it is unpaused. Attesting stays open.", ) }) @@ -220,8 +220,8 @@ describe("toEmergencyActions", () => { expect(tones).toEqual({ "pause-vault": "danger", "unpause-vault": undefined, - "raise-ripcord": "danger", - "lower-ripcord": undefined, + "pause-nav": "danger", + "unpause-nav": undefined, "clear-latest": "danger", "pause-token": "danger", "unpause-token": undefined, @@ -252,26 +252,26 @@ describe("toEmergencyTiles", () => { ]) }) - it("states each contract's current pause and the ripcord", () => { + it("states each contract's current pause and the NAV pause", () => { expect(toEmergencyTiles(calm)).toEqual([ { label: "Vault", value: "Open" }, { label: "Share token", value: "Open" }, - { label: "Oracle", value: "Valid", note: "Ripcord not raised" }, + { label: "Oracle", value: "Valid", note: "NAV not paused" }, ]) expect(toEmergencyTiles(alarmed)).toEqual([ { label: "Vault", value: "Paused" }, { label: "Share token", value: "Paused" }, - { label: "Oracle", value: "Paused", note: "Ripcord raised" }, + { label: "Oracle", value: "Paused", note: "NAV paused" }, ]) }) - it("shows a stale oracle as stale with the ripcord down, and an unreadable state as unknown", () => { + it("shows a stale oracle as stale with the NAV not paused, and an unreadable state as unknown", () => { expect( toEmergencyTiles({ vault: "unreadable", token: "open", oracle: "stale" }), ).toEqual([ { label: "Vault", value: null }, { label: "Share token", value: "Open" }, - { label: "Oracle", value: "Stale", note: "Ripcord not raised" }, + { label: "Oracle", value: "Stale", note: "NAV not paused" }, ]) expect(toEmergencyTiles({ ...calm, oracle: "unreadable" })[2]).toEqual({ label: "Oracle", diff --git a/app/src/pages/emergency.ts b/app/src/pages/emergency.ts index fa225e43..13cb005a 100644 --- a/app/src/pages/emergency.ts +++ b/app/src/pages/emergency.ts @@ -20,13 +20,13 @@ export type EmergencyState = { type PlainId = | "pause-vault" | "unpause-vault" - | "lower-ripcord" + | "unpause-nav" | "clear-latest" | "pause-token" | "unpause-token" export type EmergencyAction = RailAction & - ({ id: PlainId } | { id: "raise-ripcord"; viaAdmin: boolean }) + ({ id: PlainId } | { id: "pause-nav"; viaAdmin: boolean }) const PAUSE_LABELS = { open: "Open", paused: "Paused" } as const @@ -48,8 +48,8 @@ const pauseIs = ( met: isKnown(state) ? state === wanted : null, }) -const ripcordRaised = (oracle: OracleStatus): Condition => ({ - label: "Ripcord raised", +const navPaused = (oracle: OracleStatus): Condition => ({ + label: "NAV paused", met: isKnown(oracle) ? oracle === "paused" : null, }) @@ -75,8 +75,8 @@ const oracleAdminSpecs = ( oracle: OracleStatus, ): [Spec, Spec] => [ { - id: "lower-ripcord", - label: "Lower the ripcord", + id: "unpause-nav", + label: "Unpause the NAV", conditions: [], outcome: "Resumes pricing: epochs can be fulfilled again while the price is valid.", @@ -84,9 +84,9 @@ const oracleAdminSpecs = ( { id: "clear-latest", label: "Clear the latest price", - conditions: [ripcordRaised(oracle)], + conditions: [navPaused(oracle)], outcome: - "Removes the recorded price, so the next attestation is not limited by its distance from the last. Pricing stays stopped until the ripcord is lowered.", + "Removes the recorded price, so the next attestation is not limited by its distance from the last. Pricing stays stopped until the NAV is unpaused.", danger: true, }, ] @@ -108,7 +108,7 @@ const tokenSpecs = (token: PauseState): [Spec, Spec] => [ }, ] -const raiseGrant = ( +const pauseGrant = ( guardian: Grant | undefined, admin: Grant | undefined, ): Grant | undefined => { @@ -117,13 +117,13 @@ const raiseGrant = ( return guardian ?? admin } -const raiseSpec = (oracle: OracleStatus): Spec<"raise-ripcord"> => ({ - id: "raise-ripcord", - label: "Raise the ripcord", +const pauseSpec = (oracle: OracleStatus): Spec<"pause-nav"> => ({ + id: "pause-nav", + label: "Pause the NAV", conditions: [], outcome: - "Stops pricing: no epoch can be fulfilled until it is lowered. Attesting stays open.", - ...(oracle === "paused" ? { note: "Ripcord already raised." } : {}), + "Stops pricing: no epoch can be fulfilled until it is unpaused. Attesting stays open.", + ...(oracle === "paused" ? { note: "NAV already paused." } : {}), danger: true, }) @@ -135,24 +135,24 @@ export function toEmergencyActions( if (grants.length === 0) return [] const held = (role: string) => grantFor(grants, role, wallet) const [pauseVault, unpauseVault] = vaultSpecs(state.vault) - const [lower, clear] = oracleAdminSpecs(state.oracle) + const [unpause, clear] = oracleAdminSpecs(state.oracle) const [pauseToken, unpauseToken] = tokenSpecs(state.token) const oracleAdmin = held(ROLES.oracleAdmin) const tokenAdmin = held(ROLES.tokenAdmin) - const raiser = raiseGrant(held(ROLES.oracleGuardian), oracleAdmin) + const pauser = pauseGrant(held(ROLES.oracleGuardian), oracleAdmin) const actions: EmergencyAction[] = [] const guardian = held(ROLES.vaultGuardian) if (guardian !== undefined) actions.push(toAction(pauseVault, guardian)) const governance = held(ROLES.governance) if (governance !== undefined) actions.push(toAction(unpauseVault, governance)) - if (raiser !== undefined) + if (pauser !== undefined) actions.push({ - ...toAction(raiseSpec(state.oracle), raiser), - viaAdmin: raiser.role === ROLES.oracleAdmin, + ...toAction(pauseSpec(state.oracle), pauser), + viaAdmin: pauser.role === ROLES.oracleAdmin, }) if (oracleAdmin !== undefined) - actions.push(toAction(lower, oracleAdmin), toAction(clear, oracleAdmin)) + actions.push(toAction(unpause, oracleAdmin), toAction(clear, oracleAdmin)) if (tokenAdmin !== undefined) actions.push( toAction(pauseToken, tokenAdmin), @@ -173,7 +173,7 @@ const oracleTile = (oracle: OracleStatus): Metric => { return { label, value: ORACLE_LABELS[oracle], - note: oracle === "paused" ? "Ripcord raised" : "Ripcord not raised", + note: oracle === "paused" ? "NAV paused" : "NAV not paused", } } diff --git a/app/src/pages/sign.test.ts b/app/src/pages/sign.test.ts index 4eb9915f..761c62f8 100644 --- a/app/src/pages/sign.test.ts +++ b/app/src/pages/sign.test.ts @@ -96,19 +96,55 @@ describe("describePending", () => { }) }) - it("maps the ripcord flag to raise or lower", () => { - const raise = describePending( - pending(addresses.oracle, "set_ripcord", [true, account]), + it("maps the paused flag to pause or unpause", () => { + const pause = describePending( + pending(addresses.oracle, "set_nav_paused", [true, account]), addresses, ) - const lower = describePending( - pending(addresses.oracle, "set_ripcord", [false, account]), + const unpause = describePending( + pending(addresses.oracle, "set_nav_paused", [false, account]), addresses, ) - expect(raise.action).toBe("raise-ripcord") - expect(lower.action).toBe("lower-ripcord") - expect(lower.label).toBe("Lower the ripcord") + expect(pause.action).toBe("pause-nav") + expect(pause.label).toBe("Pause the NAV") + expect(unpause.action).toBe("unpause-nav") + expect(unpause.label).toBe("Unpause the NAV") + }) + + it("describes the guardian's pause_nav as pausing the NAV", () => { + expect( + describePending( + pending(addresses.oracle, "pause_nav", [account]), + addresses, + ), + ).toMatchObject({ action: "pause-nav", label: "Pause the NAV", args: [] }) + }) + + it("shows every oracle limit of set_config, the largest fall as a percent", () => { + const { args } = describePending( + pending(addresses.oracle, "set_config", [ + { + freshness_duration: 86_400n, + cooldown_secs: 3_600n, + max_up_bps: 500, + max_down_bps: 250, + min_answer: 500_000_000_000_000_000n, + max_answer: 2_000_000_000_000_000_000n, + }, + account, + ]), + addresses, + ) + + expect(args).toEqual([ + { label: "Freshness window", value: "1d" }, + { label: "Cooldown", value: "1h" }, + { label: "Largest rise", value: "5%" }, + { label: "Largest fall", value: "2.5%" }, + { label: "Price floor", value: "0.50" }, + { label: "Price ceiling", value: "2.00" }, + ]) }) it("formats an attestation price and expiry", () => { diff --git a/app/src/pages/sign.ts b/app/src/pages/sign.ts index 29e31904..5c5e1210 100644 --- a/app/src/pages/sign.ts +++ b/app/src/pages/sign.ts @@ -182,22 +182,16 @@ const oracleLimits = (config: Record): SignArg[] => [ duration("Freshness window", config.freshness_duration), duration("Cooldown", config.cooldown_secs), { label: "Largest rise", value: formatPercent(count(config.max_up_bps)) }, - { - label: "Largest fall", - value: - config.max_down_bps === null - ? "No limit" - : formatPercent(count(config.max_down_bps)), - }, + { label: "Largest fall", value: formatPercent(count(config.max_down_bps)) }, price("Price floor", config.min_answer), price("Price ceiling", config.max_answer), ] const ORACLE: Record = { ...governed("oracle"), - raise_ripcord: only("raise-ripcord"), - set_ripcord: ([paused]) => ({ - action: flag(paused) ? "raise-ripcord" : "lower-ripcord", + pause_nav: only("pause-nav"), + set_nav_paused: ([paused]) => ({ + action: flag(paused) ? "pause-nav" : "unpause-nav", args: [], }), clear_latest: only("clear-latest"), @@ -445,8 +439,8 @@ type ModalHandlers = { onClose: () => void; onRetry: () => void } const EMERGENCY_ACTIONS: ReadonlySet = new Set([ "pause-vault", "unpause-vault", - "raise-ripcord", - "lower-ripcord", + "pause-nav", + "unpause-nav", "clear-latest", "pause-token", "unpause-token",