From a8414b01e2d1b62d0ec465550c42cb22f4d6c4dd Mon Sep 17 00:00:00 2001 From: shivam kalra Date: Wed, 19 Aug 2026 13:40:30 +0530 Subject: [PATCH] fix: bind Incentives to the vault's live wiring in SystemSealer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit INVARIANT 8 checked Incentives.owner() == ROOT_TIMELOCK but never that config.incentives was the address CoreVault actually reads from — the same decoy gap already closed for feeCollector/router/bufferManager/ healthRegistry, just missed for this component. A correctly-governed Incentives contract the vault doesn't point to could still pass canSeal()/verifyAndSeal(). _verifyLiveState now checks vault.incentives() == config.incentives before the ownership check, matching the existing pattern. Added test_canSeal_and_verifyAndSeal_agree_whenIncentivesIsADecoy, wiring a real Incentives contract into the vault and proving both canSeal() and verifyAndSeal() reject a separate, correctly-owned decoy. --- src/core/SystemSealer.sol | 9 +++++++- .../SystemSealer_CanSealAgreement.t.sol | 23 +++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/src/core/SystemSealer.sol b/src/core/SystemSealer.sol index 7e5bec0..ee5ac48 100644 --- a/src/core/SystemSealer.sol +++ b/src/core/SystemSealer.sol @@ -358,9 +358,16 @@ contract SystemSealer { } // ───────────────────────────────────────────────────────────────────────── - // INVARIANT 8: Incentives ownership (if deployed) + // INVARIANT 8: Incentives is bound to the vault, and its ownership (if deployed) // ───────────────────────────────────────────────────────────────────────── + // Live-wiring bind (review §24), same pattern as feeCollector/router/ + // bufferManager/healthRegistry above: without this, a correctly-owned + // decoy Incentives contract the vault does not actually read from + // still satisfies the ownership check below and passes the seal. if (config.incentives != address(0)) { + if (address(vault.incentives()) != config.incentives) { + return (false, "Incentives not bound to vault"); + } if (Incentives(config.incentives).owner() != config.rootTimelock) { return (false, "Incentives.owner != ROOT_TIMELOCK"); } diff --git a/test/sprint-test/SystemSealer_CanSealAgreement.t.sol b/test/sprint-test/SystemSealer_CanSealAgreement.t.sol index cd85942..ea3ee4f 100644 --- a/test/sprint-test/SystemSealer_CanSealAgreement.t.sol +++ b/test/sprint-test/SystemSealer_CanSealAgreement.t.sol @@ -38,6 +38,8 @@ import { GlobalConfig } from "../../src/core/config/GlobalConfig.sol"; import { SelectorRegistry } from "../../src/core/libraries/SelectorRegistry.sol"; import { SelectorLib } from "../../src/core/libraries/SelectorLib.sol"; import { SystemSealer } from "../../src/core/SystemSealer.sol"; +import { Incentives } from "../../src/core/modules/Incentives.sol"; +import { IIncentives } from "../../src/interfaces/IIncentives.sol"; import { IAdminModule } from "../../src/interfaces/IAdminModule.sol"; import { IBufferManager } from "../../src/interfaces/IBufferManager.sol"; import { IncentivesTimelock } from "../../src/governance/IncentivesTimelock.sol"; @@ -228,6 +230,27 @@ contract SystemSealer_CanSealAgreement_Test is Test { assertFalse(vault.isSystemSealed()); } + function test_canSeal_and_verifyAndSeal_agree_whenIncentivesIsADecoy() public { + IIncentives.Params memory p = + IIncentives.Params({ cliffDays: 30, fullDays: 180, bmaxWad: 3e16, vestingDays: 180 }); + + Incentives realIncentives = new Incentives(address(rootTimelock), address(vault), treasury, p); + vm.prank(address(rootTimelock)); + IAdminModule(address(vault)).setIncentives(address(realIncentives)); + + Incentives decoy = new Incentives(address(rootTimelock), address(vault), treasury, p); + + SystemSealer.SealConfig memory decoyConfig = sealConfig; + decoyConfig.incentives = address(decoy); + + (bool ok, string memory reason) = systemSealer.canSeal(decoyConfig); + assertFalse(ok, "canSeal must reject a correctly-governed Incentives the vault does not read from"); + assertEq(reason, "Incentives not bound to vault"); + + _scheduleAndExpectRevert(decoyConfig, "incentives-decoy-salt"); + assertFalse(vault.isSystemSealed()); + } + function test_canSeal_and_verifyAndSeal_agree_whenFeeCollectorIsADecoy() public { FeeCollector decoy = new FeeCollector(address(rootTimelock), treasury, treasury, treasury, 7000, 200, 3000);