Skip to content

Commit 56ee47e

Browse files
contracts: implement onlyDelegateCall and add tests for audit fixes (ethereum-optimism#19272)
* contracts: implement audit code fixes and add tests Add onlyDelegateCall enforcement to upgradeSuperchain, upgrade, and migrate functions (#17). Include msg.sender in deploy salt to prevent cross-caller CREATE2 collisions (#17). Add duplicate instruction key detection in upgrade validation (#9). Validate startingRespectedGameType against enabled game configs (#10). Add code-existence check in loadBytes (#18). Add setUp guard to VerifyOPCM.runSingle (#4). Remove unused _findChar function (#5). Pass real AddressManager in migrator proxy deploy args (#11). Add tests covering all audit fix behaviors. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * contracts: regenerate semver-lock.json for OPContractsManagerV2 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * contracts: bump OPContractsManagerV2 version to 7.0.10 Semver-diff requires a patch version bump when bytecode changes. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 88d42e5 commit 56ee47e

9 files changed

Lines changed: 210 additions & 28 deletions

File tree

packages/contracts-bedrock/interfaces/L1/opcm/IOPContractsManagerV2.sol

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,8 @@ interface IOPContractsManagerV2 {
8080
error OPContractsManagerV2_InvalidUpgradeInput();
8181
error OPContractsManagerV2_SuperchainConfigNeedsUpgrade();
8282
error OPContractsManagerV2_InvalidUpgradeInstruction(string _key);
83+
error OPContractsManagerV2_DuplicateUpgradeInstruction(string _key);
84+
error OPContractsManagerV2_OnlyDelegateCall();
8385
error OPContractsManagerV2_CannotUpgradeToCustomGasToken();
8486
error OPContractsManagerV2_InvalidUpgradeSequence(string _lastVersion, string _thisVersion);
8587
error IdentityPrecompileCallFailed();

packages/contracts-bedrock/scripts/deploy/VerifyOPCM.s.sol

Lines changed: 5 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -98,9 +98,6 @@ contract VerifyOPCM is Script {
9898
/// @notice Thrown when a staticcall to a validator getter fails.
9999
error VerifyOPCM_ValidatorCallFailed(string sig);
100100

101-
/// @notice Thrown when _findChar is called with a multi-character string.
102-
error VerifyOPCM_MustBeSingleChar();
103-
104101
/// @notice Preamble used for blueprint contracts.
105102
bytes constant BLUEPRINT_PREAMBLE = hex"FE7100";
106103

@@ -290,6 +287,11 @@ contract VerifyOPCM is Script {
290287
/// @param _addr Address of the contract to verify.
291288
/// @param _skipConstructorVerification Whether to skip constructor verification.
292289
function runSingle(string memory _name, address _addr, bool _skipConstructorVerification) public {
290+
// Make sure the setup function has been called.
291+
if (!ready) {
292+
setUp();
293+
}
294+
293295
// This function is used as part of the release checklist to verify new contracts.
294296
// Rather than requiring an opcm input parameter, just pass in an empty reference
295297
// as we really only need this for features that are in development.
@@ -1604,21 +1606,4 @@ contract VerifyOPCM is Script {
16041606
if (!ok) revert VerifyOPCM_ValidatorCallFailed(_sig);
16051607
return abi.decode(data, (bytes32));
16061608
}
1607-
1608-
/// @notice Finds the position of a character in a string.
1609-
/// @param _str The string to search.
1610-
/// @param _char The character to find (as a single-char string).
1611-
/// @return The index of the first occurrence, or string length if not found.
1612-
function _findChar(string memory _str, string memory _char) internal pure returns (uint256) {
1613-
bytes memory strBytes = bytes(_str);
1614-
bytes memory charBytes = bytes(_char);
1615-
if (charBytes.length != 1) revert VerifyOPCM_MustBeSingleChar();
1616-
bytes1 target = charBytes[0];
1617-
for (uint256 i = 0; i < strBytes.length; i++) {
1618-
if (strBytes[i] == target) {
1619-
return i;
1620-
}
1621-
}
1622-
return strBytes.length;
1623-
}
16241609
}

packages/contracts-bedrock/snapshots/abi/OPContractsManagerV2.json

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -804,6 +804,17 @@
804804
"name": "OPContractsManagerV2_CannotUpgradeToCustomGasToken",
805805
"type": "error"
806806
},
807+
{
808+
"inputs": [
809+
{
810+
"internalType": "string",
811+
"name": "_key",
812+
"type": "string"
813+
}
814+
],
815+
"name": "OPContractsManagerV2_DuplicateUpgradeInstruction",
816+
"type": "error"
817+
},
807818
{
808819
"inputs": [],
809820
"name": "OPContractsManagerV2_InvalidGameConfigs",
@@ -841,6 +852,11 @@
841852
"name": "OPContractsManagerV2_InvalidUpgradeSequence",
842853
"type": "error"
843854
},
855+
{
856+
"inputs": [],
857+
"name": "OPContractsManagerV2_OnlyDelegateCall",
858+
"type": "error"
859+
},
844860
{
845861
"inputs": [],
846862
"name": "OPContractsManagerV2_SuperchainConfigNeedsUpgrade",

packages/contracts-bedrock/snapshots/semver-lock.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,8 @@
5252
"sourceCodeHash": "0xb3184aa5d95a82109e7134d1f61941b30e25f655b9849a0e303d04bbce0cde0b"
5353
},
5454
"src/L1/opcm/OPContractsManagerV2.sol:OPContractsManagerV2": {
55-
"initCodeHash": "0x88ada0dfefb77eea33baaf11d9b5a5ad51cb8c6476611d0f2376897413074619",
56-
"sourceCodeHash": "0x1cc9dbcd4c7652f482c43e2630b324d088e825d12532711a41c636e8392636b3"
55+
"initCodeHash": "0xca9edfa050a5583f063194fd8d098124d6f3c1367eec8875c0c8acf5d971657f",
56+
"sourceCodeHash": "0x0238b990636aab82f93450b1ee2ff7a1f69d55a0b197265e696b70d285c85992"
5757
},
5858
"src/L2/BaseFeeVault.sol:BaseFeeVault": {
5959
"initCodeHash": "0x838bbd7f381e84e21887f72bd1da605bfc4588b3c39aed96cbce67c09335b3ee",

packages/contracts-bedrock/src/L1/opcm/OPContractsManagerMigrator.sol

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import { Constants } from "src/libraries/Constants.sol";
1111
import { Features } from "src/libraries/Features.sol";
1212

1313
// Interfaces
14-
import { IAddressManager } from "interfaces/legacy/IAddressManager.sol";
1514
import { IDelayedWETH } from "interfaces/dispute/IDelayedWETH.sol";
1615
import { IAnchorStateRegistry } from "interfaces/dispute/IAnchorStateRegistry.sol";
1716
import { IDisputeGame } from "interfaces/dispute/IDisputeGame.sol";
@@ -107,7 +106,7 @@ contract OPContractsManagerMigrator is OPContractsManagerUtilsCaller {
107106
// what we use here.
108107
IOPContractsManagerUtils.ProxyDeployArgs memory proxyDeployArgs = IOPContractsManagerUtils.ProxyDeployArgs({
109108
proxyAdmin: _input.chainSystemConfigs[0].proxyAdmin(),
110-
addressManager: IAddressManager(address(0)), // AddressManager NOT needed for these proxies.
109+
addressManager: _input.chainSystemConfigs[0].proxyAdmin().addressManager(),
111110
l2ChainId: block.timestamp,
112111
saltMixer: "interop salt mixer"
113112
});

packages/contracts-bedrock/src/L1/opcm/OPContractsManagerUtils.sol

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,12 @@ contract OPContractsManagerUtils {
195195
return overrideInstruction.data;
196196
}
197197

198+
// Check that the source contract has code. Calling an EOA returns success with empty
199+
// data, which would cause issues when the caller tries to decode the result.
200+
if (_source.code.length == 0) {
201+
revert OPContractsManagerUtils_ConfigLoadFailed(_name);
202+
}
203+
198204
// Otherwise, load the data from the source contract.
199205
(bool success, bytes memory result) = address(_source).staticcall(abi.encodePacked(_selector));
200206
if (!success) {

packages/contracts-bedrock/src/L1/opcm/OPContractsManagerV2.sol

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,12 @@ contract OPContractsManagerV2 is ISemver, OPContractsManagerUtilsCaller {
126126
/// @notice Thrown when an invalid upgrade instruction is provided.
127127
error OPContractsManagerV2_InvalidUpgradeInstruction(string _key);
128128

129+
/// @notice Thrown when duplicate upgrade instruction keys are provided.
130+
error OPContractsManagerV2_DuplicateUpgradeInstruction(string _key);
131+
132+
/// @notice Thrown when a function that must be delegatecalled is called directly.
133+
error OPContractsManagerV2_OnlyDelegateCall();
134+
129135
/// @notice Thrown when a chain attempts to upgrade to custom gas token after initial deployment.
130136
error OPContractsManagerV2_CannotUpgradeToCustomGasToken();
131137

@@ -147,9 +153,9 @@ contract OPContractsManagerV2 is ISemver, OPContractsManagerUtilsCaller {
147153
/// - Major bump: New required sequential upgrade
148154
/// - Minor bump: Replacement OPCM for same upgrade
149155
/// - Patch bump: Development changes (expected for normal dev work)
150-
/// @custom:semver 7.0.9
156+
/// @custom:semver 7.0.10
151157
function version() public pure returns (string memory) {
152-
return "7.0.9";
158+
return "7.0.10";
153159
}
154160

155161
/// @param _standardValidator The standard validator for this OPCM release.
@@ -176,6 +182,8 @@ contract OPContractsManagerV2 is ISemver, OPContractsManagerUtilsCaller {
176182
/// Superchain-wide contracts.
177183
/// @param _inp The input for the Superchain upgrade.
178184
function upgradeSuperchain(SuperchainUpgradeInput memory _inp) external returns (SuperchainContracts memory) {
185+
_onlyDelegateCall();
186+
179187
// NOTE: Since this function is very minimal and only upgrades the SuperchainConfig
180188
// contract, not bothering to fully follow the pattern of the normal chain upgrade flow.
181189
// If we expand the scope of this function to add other Superchain-wide contracts, we'll
@@ -197,6 +205,9 @@ contract OPContractsManagerV2 is ISemver, OPContractsManagerUtilsCaller {
197205
/// @param _cfg The full chain deployment configuration.
198206
/// @return The chain contracts.
199207
function deploy(FullConfig memory _cfg) external returns (ChainContracts memory) {
208+
// Include msg.sender in the salt mixer to prevent cross-caller CREATE2 collisions.
209+
string memory saltMixer = string(bytes.concat(bytes20(msg.sender), bytes(_cfg.saltMixer)));
210+
200211
// Deploy is the ONLY place where we allow the "ALL" permission for proxy deployment.
201212
IOPContractsManagerUtils.ExtraInstruction[] memory instructions =
202213
new IOPContractsManagerUtils.ExtraInstruction[](1);
@@ -207,7 +218,7 @@ contract OPContractsManagerV2 is ISemver, OPContractsManagerUtilsCaller {
207218

208219
// Load the chain contracts.
209220
ChainContracts memory cts =
210-
_loadChainContracts(ISystemConfig(address(0)), _cfg.l2ChainId, _cfg.saltMixer, instructions);
221+
_loadChainContracts(ISystemConfig(address(0)), _cfg.l2ChainId, saltMixer, instructions);
211222

212223
// Execute the deployment.
213224
return _apply(_cfg, cts, true);
@@ -217,6 +228,8 @@ contract OPContractsManagerV2 is ISemver, OPContractsManagerUtilsCaller {
217228
/// @param _inp The chain upgrade input.
218229
/// @return The upgraded chain contracts.
219230
function upgrade(UpgradeInput memory _inp) external returns (ChainContracts memory) {
231+
_onlyDelegateCall();
232+
220233
// Sanity check that the SystemConfig isn't address(0). We use address(0) as a special
221234
// value to indicate that this is an initial deployment, so we definitely don't want to
222235
// allow it here.
@@ -264,6 +277,8 @@ contract OPContractsManagerV2 is ISemver, OPContractsManagerUtilsCaller {
264277
/// look or function like all of the other functions in OPCMv2.
265278
/// @param _input The input parameters for the migration.
266279
function migrate(IOPContractsManagerMigrator.MigrateInput calldata _input) public {
280+
_onlyDelegateCall();
281+
267282
// Delegatecall to the migrator contract.
268283
(bool success, bytes memory result) =
269284
address(opcmMigrator).delegatecall(abi.encodeCall(IOPContractsManagerMigrator.migrate, (_input)));
@@ -286,6 +301,17 @@ contract OPContractsManagerV2 is ISemver, OPContractsManagerUtilsCaller {
286301
view
287302
{
288303
for (uint256 i = 0; i < _extraInstructions.length; i++) {
304+
// Check for duplicate instruction keys. PermittedProxyDeployment is exempt because
305+
// multiple proxy deployments may need to be permitted in a single upgrade.
306+
if (!_isMatchingInstructionByKey(_extraInstructions[i], Constants.PERMITTED_PROXY_DEPLOYMENT_KEY)) {
307+
for (uint256 j = i + 1; j < _extraInstructions.length; j++) {
308+
if (keccak256(bytes(_extraInstructions[i].key)) == keccak256(bytes(_extraInstructions[j].key))) {
309+
revert OPContractsManagerV2_DuplicateUpgradeInstruction(_extraInstructions[i].key);
310+
}
311+
}
312+
}
313+
314+
// Check that the instruction is permitted.
289315
if (!_isPermittedInstruction(_extraInstructions[i])) {
290316
revert OPContractsManagerV2_InvalidUpgradeInstruction(_extraInstructions[i].key);
291317
}
@@ -316,6 +342,13 @@ contract OPContractsManagerV2 is ISemver, OPContractsManagerUtilsCaller {
316342
}
317343
}
318344

345+
// Allow overriding the starting respected game type during upgrades. This is needed when
346+
// disabling the currently-respected game type, since the validation requires the starting
347+
// respected game type to correspond to an enabled game config.
348+
if (_isMatchingInstructionByKey(_instruction, "overrides.cfg.startingRespectedGameType")) {
349+
return true;
350+
}
351+
319352
// Always return false by default.
320353
return false;
321354
}
@@ -684,6 +717,21 @@ contract OPContractsManagerV2 is ISemver, OPContractsManagerUtilsCaller {
684717
if (!_cfg.disputeGameConfigs[1].enabled) {
685718
revert OPContractsManagerV2_InvalidGameConfigs();
686719
}
720+
721+
// Validate that the starting respected game type corresponds to an enabled game config.
722+
bool startingGameTypeFound = false;
723+
for (uint256 i = 0; i < _cfg.disputeGameConfigs.length; i++) {
724+
if (
725+
_cfg.disputeGameConfigs[i].gameType.raw() == _cfg.startingRespectedGameType.raw()
726+
&& _cfg.disputeGameConfigs[i].enabled
727+
) {
728+
startingGameTypeFound = true;
729+
break;
730+
}
731+
}
732+
if (!startingGameTypeFound) {
733+
revert OPContractsManagerV2_InvalidGameConfigs();
734+
}
687735
}
688736

689737
/// @notice Executes the deployment/upgrade action.
@@ -1003,6 +1051,13 @@ contract OPContractsManagerV2 is ISemver, OPContractsManagerUtilsCaller {
10031051
// INTERNAL UTILITY FUNCTIONS //
10041052
///////////////////////////////////////////////////////////////////////////
10051053

1054+
/// @notice Reverts if the function is being called directly rather than via delegatecall.
1055+
function _onlyDelegateCall() internal view {
1056+
if (address(this) == address(opcmV2)) {
1057+
revert OPContractsManagerV2_OnlyDelegateCall();
1058+
}
1059+
}
1060+
10061061
/// @notice Helper for retrieving the version of the OPCM contract.
10071062
/// @dev We use opcmV2.version() because it allows us to properly mock the version function
10081063
/// in tests without running into issues because this contract is being DELEGATECALLed.

packages/contracts-bedrock/test/L1/opcm/OPContractsManagerUtils.t.sol

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,18 @@ contract OPContractsManagerUtils_LoadBytes_Test is OPContractsManagerUtils_TestI
331331
assertEq(result, _overrideData, "Should return override data");
332332
}
333333

334+
/// @notice Tests that loadBytes reverts when the source address has no code.
335+
function test_loadBytes_sourceNoCode_reverts() public {
336+
address eoa = makeAddr("eoa");
337+
338+
vm.expectRevert(
339+
abi.encodeWithSelector(
340+
IOPContractsManagerUtils.OPContractsManagerUtils_ConfigLoadFailed.selector, "testField"
341+
)
342+
);
343+
utils.loadBytes(eoa, MOCK_SELECTOR, "testField", _emptyInstructions());
344+
}
345+
334346
/// @notice Tests that loadBytes reverts when the source call fails.
335347
function test_loadBytes_sourceCallFails_reverts() public {
336348
// Mock the source to revert.

0 commit comments

Comments
 (0)