Skip to content

Commit 6ae1e8e

Browse files
shivam kalrastefanobotticelli
authored andcommitted
fix: bind Incentives to the vault's live wiring in SystemSealer
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.
1 parent 217971b commit 6ae1e8e

2 files changed

Lines changed: 31 additions & 1 deletion

File tree

‎src/core/SystemSealer.sol‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -358,9 +358,16 @@ contract SystemSealer {
358358
}
359359

360360
// ─────────────────────────────────────────────────────────────────────────
361-
// INVARIANT 8: Incentives ownership (if deployed)
361+
// INVARIANT 8: Incentives is bound to the vault, and its ownership (if deployed)
362362
// ─────────────────────────────────────────────────────────────────────────
363+
// Live-wiring bind (review §24), same pattern as feeCollector/router/
364+
// bufferManager/healthRegistry above: without this, a correctly-owned
365+
// decoy Incentives contract the vault does not actually read from
366+
// still satisfies the ownership check below and passes the seal.
363367
if (config.incentives != address(0)) {
368+
if (address(vault.incentives()) != config.incentives) {
369+
return (false, "Incentives not bound to vault");
370+
}
364371
if (Incentives(config.incentives).owner() != config.rootTimelock) {
365372
return (false, "Incentives.owner != ROOT_TIMELOCK");
366373
}

‎test/sprint-test/SystemSealer_CanSealAgreement.t.sol‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ import { GlobalConfig } from "../../src/core/config/GlobalConfig.sol";
3838
import { SelectorRegistry } from "../../src/core/libraries/SelectorRegistry.sol";
3939
import { SelectorLib } from "../../src/core/libraries/SelectorLib.sol";
4040
import { SystemSealer } from "../../src/core/SystemSealer.sol";
41+
import { Incentives } from "../../src/core/modules/Incentives.sol";
42+
import { IIncentives } from "../../src/interfaces/IIncentives.sol";
4143
import { IAdminModule } from "../../src/interfaces/IAdminModule.sol";
4244
import { IBufferManager } from "../../src/interfaces/IBufferManager.sol";
4345
import { IncentivesTimelock } from "../../src/governance/IncentivesTimelock.sol";
@@ -228,6 +230,27 @@ contract SystemSealer_CanSealAgreement_Test is Test {
228230
assertFalse(vault.isSystemSealed());
229231
}
230232

233+
function test_canSeal_and_verifyAndSeal_agree_whenIncentivesIsADecoy() public {
234+
IIncentives.Params memory p =
235+
IIncentives.Params({ cliffDays: 30, fullDays: 180, bmaxWad: 3e16, vestingDays: 180 });
236+
237+
Incentives realIncentives = new Incentives(address(rootTimelock), address(vault), treasury, p);
238+
vm.prank(address(rootTimelock));
239+
IAdminModule(address(vault)).setIncentives(address(realIncentives));
240+
241+
Incentives decoy = new Incentives(address(rootTimelock), address(vault), treasury, p);
242+
243+
SystemSealer.SealConfig memory decoyConfig = sealConfig;
244+
decoyConfig.incentives = address(decoy);
245+
246+
(bool ok, string memory reason) = systemSealer.canSeal(decoyConfig);
247+
assertFalse(ok, "canSeal must reject a correctly-governed Incentives the vault does not read from");
248+
assertEq(reason, "Incentives not bound to vault");
249+
250+
_scheduleAndExpectRevert(decoyConfig, "incentives-decoy-salt");
251+
assertFalse(vault.isSystemSealed());
252+
}
253+
231254
function test_canSeal_and_verifyAndSeal_agree_whenFeeCollectorIsADecoy() public {
232255
FeeCollector decoy =
233256
new FeeCollector(address(rootTimelock), treasury, treasury, treasury, 7000, 200, 3000);

0 commit comments

Comments
 (0)