Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/require-multirewarddistributor-evm-config.md
Original file line number Diff line number Diff line change
@@ -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.
49 changes: 49 additions & 0 deletions src/environments/contracts-invariants.test.ts
Original file line number Diff line number Diff line change
@@ -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}$/);
}
});
});
20 changes: 19 additions & 1 deletion src/environments/types/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -236,9 +236,27 @@ export const createMorphoMarketConfig = <const tokens, const markets>(config: {
markets: MorphoMarketsConfig<markets, tokens>;
}) => config.markets as Prettify<markets>;

// 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> = contracts extends {
comptroller: Address;
}
? contracts extends { governor: Address }
? unknown
: { multiRewardDistributor: Address }
: unknown;

export const createContractsConfig = <const tokens, const contracts>(config: {
tokens: TokensConfig<tokens>;
contracts: ContractsConfig<contracts, tokens>;
contracts: ContractsConfig<contracts, tokens> &
RequireMultiRewardDistributor<contracts>;
}) => {
return config.contracts as Prettify<contracts>;
};
Expand Down
1 change: 1 addition & 0 deletions test/vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down