diff --git a/.changeset/require-multirewarddistributor-evm-config.md b/.changeset/require-multirewarddistributor-evm-config.md new file mode 100644 index 0000000..3fa371d --- /dev/null +++ b/.changeset/require-multirewarddistributor-evm-config.md @@ -0,0 +1,5 @@ +--- +"@moonwell-fi/moonwell-sdk": minor +--- + +Require `multiRewardDistributor` at the type level for EVM comptroller environments. **Type-breaking** for direct callers of `createContractsConfig`: a config with a `comptroller` and no `governor` that omits `multiRewardDistributor` no longer compiles. `createContractsConfig` now rejects a contracts config that defines a `comptroller` but no `governor` (i.e. Base/Optimism/Ethereum) unless it also defines `multiRewardDistributor` — closing the gap that let Ethereum ship without it and silently break reward claims (MOO-413). Moonbeam and Moonriver, which use the on-chain `governor` + WELL precompile reward model, are unaffected and correctly continue to omit it. A runtime invariant test across `publicEnvironments` backstops the type guard. diff --git a/src/environments/contracts-invariants.test.ts b/src/environments/contracts-invariants.test.ts new file mode 100644 index 0000000..045b23f --- /dev/null +++ b/src/environments/contracts-invariants.test.ts @@ -0,0 +1,49 @@ +import { describe, expect, test } from "vitest"; +import { publicEnvironments } from "./index.js"; + +// Runtime companion to the `createContractsConfig` compile-time guard in +// types/config.ts. Every EVM comptroller environment — one that has a +// `comptroller` but no Moonbeam-style on-chain `governor` — must wire a +// `multiRewardDistributor`. Without it the app's reward-claim flow falls back +// to the Moonbeam `0x…0808` precompile `batchAll` path, which reverts on these +// chains (the MOO-413 regression, which shipped on Ethereum). The input type +// enforces this at the `createContractsConfig` call site; this test is the +// universal net — it iterates the wired `publicEnvironments`, so it also covers +// any environment built outside that factory and catches an `as`-cast that +// bypasses the type. Moonbeam/Moonriver use the `governor` + WELL precompile +// reward model and correctly have no MRD, so the `governor` discriminator +// excludes them. +describe("contract config invariants across public environments", () => { + type ContractRef = { address?: string } | undefined; + const envs = Object.entries(publicEnvironments) as Array< + [ + string, + { + contracts?: { + comptroller?: ContractRef; + governor?: ContractRef; + multiRewardDistributor?: ContractRef; + }; + }, + ] + >; + + const evmComptrollerEnvs = envs.filter( + ([, env]) => !!env.contracts?.comptroller && !env.contracts?.governor, + ); + + test("Base, Optimism, and Ethereum are detected as EVM comptroller chains", () => { + expect(evmComptrollerEnvs.map(([key]) => key)).toEqual( + expect.arrayContaining(["base", "optimism", "ethereum"]), + ); + }); + + test("every EVM comptroller environment defines multiRewardDistributor", () => { + for (const [key, env] of evmComptrollerEnvs) { + expect( + env.contracts?.multiRewardDistributor?.address, + `${key} is an EVM comptroller chain and must define multiRewardDistributor`, + ).toMatch(/^0x[0-9a-fA-F]{40}$/); + } + }); +}); diff --git a/src/environments/types/config.ts b/src/environments/types/config.ts index b7d3a11..fce7683 100644 --- a/src/environments/types/config.ts +++ b/src/environments/types/config.ts @@ -236,9 +236,27 @@ export const createMorphoMarketConfig = (config: { markets: MorphoMarketsConfig; }) => config.markets as Prettify; +// Moonwell's EVM comptroller chains (Base, Optimism, Ethereum) distribute WELL +// rewards through a MultiRewardDistributor, so omitting `multiRewardDistributor` +// silently breaks reward claims — the app's claim flow then falls back to the +// Moonbeam `0x…0808` precompile path, which reverts on these chains. That gap +// shipped once already (MOO-413: Ethereum had no MRD). The Moonbeam-family home +// chains (Moonbeam, Moonriver) instead use the on-chain `governor` + WELL +// precompile reward model and legitimately have no MRD. Encode the distinction: +// a config with a `comptroller` but no `governor` is an EVM comptroller chain +// and must define `multiRewardDistributor`, so dropping it fails to compile. +type RequireMultiRewardDistributor = contracts extends { + comptroller: Address; +} + ? contracts extends { governor: Address } + ? unknown + : { multiRewardDistributor: Address } + : unknown; + export const createContractsConfig = (config: { tokens: TokensConfig; - contracts: ContractsConfig; + contracts: ContractsConfig & + RequireMultiRewardDistributor; }) => { return config.contracts as Prettify; }; diff --git a/test/vitest.config.ts b/test/vitest.config.ts index 084b1de..e0dd847 100644 --- a/test/vitest.config.ts +++ b/test/vitest.config.ts @@ -54,6 +54,7 @@ export default defineConfig({ "src/client/createMoonwellClient.test.ts", "src/common/getBlockNumberAtTimestamp.test.ts", "src/environments/definitions/ethereum/environment.test.ts", + "src/environments/contracts-invariants.test.ts", ], setupFiles: [join(__dirname, "./setup.ts")], hookTimeout: 60_000,