From 62eaa1f3bc1283bc17058da69f5b1e08b2c6bc55 Mon Sep 17 00:00:00 2001 From: Tyler Coatsworth Date: Fri, 5 Jun 2026 22:03:05 -0400 Subject: [PATCH 1/2] fix(oo-v3): revalidate cached whitelist entries Signed-off-by: Tyler Coatsworth --- .../implementation/OptimisticOracleV3.sol | 21 +++++---- ...timisticOracleV3.StaleWhitelistCache.t.sol | 43 +++++++++++++++++++ 2 files changed, 55 insertions(+), 9 deletions(-) create mode 100644 packages/core/test/foundry/optimistic-oracle-v3/OptimisticOracleV3.StaleWhitelistCache.t.sol diff --git a/packages/core/contracts/optimistic-oracle-v3/implementation/OptimisticOracleV3.sol b/packages/core/contracts/optimistic-oracle-v3/implementation/OptimisticOracleV3.sol index 7e67a6aee1..cdf22c5a41 100644 --- a/packages/core/contracts/optimistic-oracle-v3/implementation/OptimisticOracleV3.sol +++ b/packages/core/contracts/optimistic-oracle-v3/implementation/OptimisticOracleV3.sol @@ -466,21 +466,24 @@ contract OptimisticOracleV3 is OptimisticOracleV3Interface, Lockable, Ownable, M return EscalationManagerInterface(em).isDisputeAllowed(assertionId, msg.sender); } - // Validates if the identifier is whitelisted by first checking the cache. If not whitelisted in the cache then - // checks it from the identifier whitelist contract and caches result. + // Validates the identifier against the live whitelist and caches the current result. function _validateAndCacheIdentifier(bytes32 identifier) internal returns (bool) { - if (cachedIdentifiers[identifier]) return true; cachedIdentifiers[identifier] = _getIdentifierWhitelist().isIdentifierSupported(identifier); return cachedIdentifiers[identifier]; } - // Validates if the currency is whitelisted by first checking the cache. If not whitelisted in the cache then - // checks it from the collateral whitelist contract and caches whitelist status and final fee. + // Validates the currency against the live whitelist. The final fee is fetched only when adding a currency to cache. function _validateAndCacheCurrency(address currency) internal returns (bool) { - if (cachedCurrencies[currency].isWhitelisted) return true; - cachedCurrencies[currency].isWhitelisted = _getCollateralWhitelist().isOnWhitelist(currency); - cachedCurrencies[currency].finalFee = _getStore().computeFinalFee(currency).rawValue; - return cachedCurrencies[currency].isWhitelisted; + bool isWhitelisted = _getCollateralWhitelist().isOnWhitelist(currency); + if (!isWhitelisted) { + cachedCurrencies[currency].isWhitelisted = false; + return false; + } + if (!cachedCurrencies[currency].isWhitelisted) { + cachedCurrencies[currency].isWhitelisted = true; + cachedCurrencies[currency].finalFee = _getStore().computeFinalFee(currency).rawValue; + } + return true; } // Sends assertion resolved callback to the callback recipient and escalation manager (if set). diff --git a/packages/core/test/foundry/optimistic-oracle-v3/OptimisticOracleV3.StaleWhitelistCache.t.sol b/packages/core/test/foundry/optimistic-oracle-v3/OptimisticOracleV3.StaleWhitelistCache.t.sol new file mode 100644 index 0000000000..ea08d81ccc --- /dev/null +++ b/packages/core/test/foundry/optimistic-oracle-v3/OptimisticOracleV3.StaleWhitelistCache.t.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity ^0.8.0; + +import "./CommonOptimisticOracleV3Test.sol"; + +contract OptimisticOracleV3StaleWhitelistCacheTest is CommonOptimisticOracleV3Test { + AddressWhitelist private collateralWhitelist; + IdentifierWhitelist private identifierWhitelist; + + function setUp() public { + _commonSetup(); + collateralWhitelist = AddressWhitelist(finder.getImplementationAddress(OracleInterfaces.CollateralWhitelist)); + identifierWhitelist = IdentifierWhitelist( + finder.getImplementationAddress(OracleInterfaces.IdentifierWhitelist) + ); + } + + function test_RemovedIdentifierIsRejectedWithoutManualSync() public { + vm.prank(TestAddress.owner); + identifierWhitelist.removeSupportedIdentifier(defaultIdentifier); + assertFalse(identifierWhitelist.isIdentifierSupported(defaultIdentifier)); + + vm.startPrank(TestAddress.account1); + defaultCurrency.allocateTo(TestAddress.account1, defaultBond); + defaultCurrency.approve(address(optimisticOracleV3), defaultBond); + vm.expectRevert("Unsupported identifier"); + optimisticOracleV3.assertTruthWithDefaults(falseClaimAssertion, TestAddress.account1); + vm.stopPrank(); + } + + function test_RemovedCurrencyIsRejectedWithoutManualSync() public { + vm.prank(TestAddress.owner); + collateralWhitelist.removeFromWhitelist(address(defaultCurrency)); + assertFalse(collateralWhitelist.isOnWhitelist(address(defaultCurrency))); + + vm.startPrank(TestAddress.account1); + defaultCurrency.allocateTo(TestAddress.account1, defaultBond); + defaultCurrency.approve(address(optimisticOracleV3), defaultBond); + vm.expectRevert("Unsupported currency"); + optimisticOracleV3.assertTruthWithDefaults(falseClaimAssertion, TestAddress.account1); + vm.stopPrank(); + } +} From 62bef8b528821e71b77cc8079021f015ddadd07a Mon Sep 17 00:00:00 2001 From: Tyler Coatsworth Date: Tue, 9 Jun 2026 20:03:36 -0400 Subject: [PATCH 2/2] fix(chainbridge): reject duplicate initial relayers --- .../contracts/external/chainbridge/Bridge.sol | 1 + .../foundry/external/chainbridge/Bridge.t.sol | 28 +++++++++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 packages/core/test/foundry/external/chainbridge/Bridge.t.sol diff --git a/packages/core/contracts/external/chainbridge/Bridge.sol b/packages/core/contracts/external/chainbridge/Bridge.sol index cf1e876024..aa37a794cf 100644 --- a/packages/core/contracts/external/chainbridge/Bridge.sol +++ b/packages/core/contracts/external/chainbridge/Bridge.sol @@ -121,6 +121,7 @@ contract Bridge is Pausable, AccessControl { _setRoleAdmin(RELAYER_ROLE, DEFAULT_ADMIN_ROLE); for (uint256 i; i < initialRelayers.length; i++) { + require(!hasRole(RELAYER_ROLE, initialRelayers[i]), "duplicate initial relayer"); grantRole(RELAYER_ROLE, initialRelayers[i]); _totalRelayers++; } diff --git a/packages/core/test/foundry/external/chainbridge/Bridge.t.sol b/packages/core/test/foundry/external/chainbridge/Bridge.t.sol new file mode 100644 index 0000000000..2ebd2a29c3 --- /dev/null +++ b/packages/core/test/foundry/external/chainbridge/Bridge.t.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity ^0.8.0; + +import "forge-std/Test.sol"; +import "../../../../contracts/external/chainbridge/Bridge.sol"; + +contract BridgeTest is Test { + function test_TracksUniqueInitialRelayers() public { + address[] memory relayers = new address[](2); + relayers[0] = address(0x1); + relayers[1] = address(0x2); + + Bridge bridge = new Bridge(1, relayers, 2, 0, 100); + + assertEq(bridge._totalRelayers(), 2); + assertTrue(bridge.isRelayer(relayers[0])); + assertTrue(bridge.isRelayer(relayers[1])); + } + + function test_RevertIf_DuplicateInitialRelayer() public { + address[] memory relayers = new address[](2); + relayers[0] = address(0x1); + relayers[1] = address(0x1); + + vm.expectRevert("duplicate initial relayer"); + new Bridge(1, relayers, 2, 0, 100); + } +}