Skip to content
Merged
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
73 changes: 61 additions & 12 deletions packages/contracts-rootstock/script/RSKDeployOPChain.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,23 @@ pragma solidity 0.8.15;
// deployment + 5 stage calls — each guaranteed to fit under RSKj's RSKIP144
// 6.8M per-tx sublist gas cap.
//
// Input/output shapes are byte-identical to upstream's `DeployOPChain.run(...)`,
// The `Output` shape is byte-identical to upstream's `DeployOPChain.run(...)`,
// so the existing `cmd/deploy-rollup` Go side parses the broadcast JSON
// without modification.
// without modification. Upstream's `Types.DeployOPChainInput` is likewise
// untouched: the one RSK-owned entry point that carries an extra element (the
// dispute-game init bond) keeps it in its own tuple rather than in that struct.
//
// Sigs exposed:
// * runSplit((address,...)) → (address[15] tuple matching DeployOPChain.Output)
// * runSplit((address,...)) → upstream parity, applies
// DEFAULT_INIT_BOND
// * runSplitWithBond((address,...),uint256) → explicit init bond
// * runWithBytes(bytes) → upstream-shaped ABI-bytes
// wrapper over runSplit
// * runSplitWithBytes(bytes) → (DeployOPChainInput, uint256);
// the one oprsk-deployer calls
//
// All four return an ABI-encoded `Output` (address[15] tuple matching
// DeployOPChain.Output).
//
// Other entry points (upgradeSplit, migrateSplit) are deferred — they share
// the same splitter contract but aren't on the critical path for the F1
Expand Down Expand Up @@ -83,23 +94,57 @@ contract RSKDeployOPChain is Script {
}

/// @notice ABI-bytes entry point mirroring upstream `DeployOPChain.runWithBytes`.
/// Lets op-deployer's `forge.NewScriptCaller` invoke `runSplit`
/// through the same `BytesScriptEncoder`/`BytesScriptDecoder`
/// pair it uses for the upstream script — no parallel broadcast
/// parser on the Go side.
/// Byte-for-byte the upstream shape: one ABI-encoded
/// `Types.DeployOPChainInput` in, one encoded `Output` out. Kept
/// so the upstream-parity path stays available and testable;
/// `oprsk-deployer` calls `runSplitWithBytes` instead, which also
/// carries the init bond.
function runWithBytes(bytes memory _input) public returns (bytes memory) {
require(_input.length > 0, "RSKDeployOPChain: input cannot be empty");
Types.DeployOPChainInput memory input = abi.decode(_input, (Types.DeployOPChainInput));
Output memory output_ = runSplit(input);
return abi.encode(output_);
}

/// @notice ABI-bytes entry point used by rskdeployer.DeployOPChainSplit.
/// Same as `runWithBytes` plus an explicit init bond for the
/// enabled permissioned game, so the value travels as a typed
/// argument instead of out-of-band. Upstream's
/// `Types.DeployOPChainInput` is untouched — the extra element
/// lives in this RSK-owned entry point's tuple, which is why
/// upstream compatibility is unaffected.
/// @param _input `abi.encode(Types.DeployOPChainInput, uint256 initBond)`.
function runSplitWithBytes(bytes memory _input) public returns (bytes memory) {
require(_input.length > 0, "RSKDeployOPChain: input cannot be empty");
(Types.DeployOPChainInput memory input, uint256 bond) =
abi.decode(_input, (Types.DeployOPChainInput, uint256));
Output memory output_ = runSplitWithBond(input, bond);
return abi.encode(output_);
}

/// @notice Drop-in replacement for upstream `DeployOPChain.run(input)`,
/// except the single `OPContractsManagerV2.deploy(config)` call
/// is replaced by a splitter deploy + 5 stage broadcasts.
/// is replaced by a splitter deploy + 5 stage broadcasts. Uses the
/// upstream default init bond; `runSplitWithBond` takes an
/// explicit one.
/// @param _input Same `Types.DeployOPChainInput` upstream uses.
/// @return output_ Same `Output` shape upstream emits.
function runSplit(Types.DeployOPChainInput memory _input) public returns (Output memory output_) {
output_ = runSplitWithBond(_input, DEFAULT_INIT_BOND);
}

/// @notice `runSplit` with the dispute-game init bond supplied explicitly.
/// Callers that read the bond from configuration (oprsk-deployer)
/// resolve the effective value on the Go side and pass it here, so
/// this script has no notion of "unset".
/// @param _input Same `Types.DeployOPChainInput` upstream uses.
/// @param _initBond Init bond in wei for the enabled permissioned game
/// type. Disabled game types always get 0 regardless.
/// @return output_ Same `Output` shape upstream emits.
function runSplitWithBond(Types.DeployOPChainInput memory _input, uint256 _initBond)
public
returns (Output memory output_)
{
checkInput(_input);

require(address(_input.opcm).code.length > 0, "RSKDeployOPChain: OPCM address has no code");
Expand All @@ -117,7 +162,7 @@ contract RSKDeployOPChain is Script {
IOPContractsManagerMigrator migrator = opcmV2.opcmMigrator();

// Build the FullConfig identically to upstream `_toOPCMV2DeployInput`.
RSKOPCMSplitter.FullConfig memory cfg = _toFullConfig(_input);
RSKOPCMSplitter.FullConfig memory cfg = _toFullConfig(_input, _initBond);

// ----------- BROADCAST 1 — deploy the splitter -----------
vm.broadcast(msg.sender);
Expand Down Expand Up @@ -177,7 +222,7 @@ contract RSKDeployOPChain is Script {
// — the field shapes are identical by construction.
// ---------------------------------------------------------------------

function _toFullConfig(Types.DeployOPChainInput memory _input)
function _toFullConfig(Types.DeployOPChainInput memory _input, uint256 _initBond)
internal
view
returns (RSKOPCMSplitter.FullConfig memory cfg_)
Expand All @@ -194,6 +239,10 @@ contract RSKDeployOPChain is Script {
challenger: _input.challenger
});

// Only the enabled permissioned game takes `_initBond`; every disabled
// entry below keeps a literal 0, which the splitter's config
// validation requires (RSKOPCMSplitter reverts
// `RSKOPCMSplitter_InvalidGameConfigs` on disabled + non-zero).
IOPContractsManagerUtils.DisputeGameConfig[] memory disputeGameConfigs =
new IOPContractsManagerUtils.DisputeGameConfig[](6);

Expand All @@ -213,7 +262,7 @@ contract RSKDeployOPChain is Script {
})
: IOPContractsManagerUtils.DisputeGameConfig({
enabled: true,
initBond: DEFAULT_INIT_BOND,
initBond: _initBond,
gameType: GameTypes.PERMISSIONED_CANNON,
gameArgs: abi.encode(pdgConfig)
});
Expand All @@ -235,7 +284,7 @@ contract RSKDeployOPChain is Script {
disputeGameConfigs[4] = isSuperRoot
? IOPContractsManagerUtils.DisputeGameConfig({
enabled: true,
initBond: DEFAULT_INIT_BOND,
initBond: _initBond,
Comment thread
asoto-iov marked this conversation as resolved.
gameType: GameTypes.SUPER_PERMISSIONED_CANNON,
gameArgs: abi.encode(pdgConfig)
})
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.15;

import {IDisputeGameFactory} from "interfaces/dispute/IDisputeGameFactory.sol";
import {GameTypes} from "src/dispute/lib/Types.sol";

import {RSKDeployOPChain} from "../script/RSKDeployOPChain.s.sol";
import {RSKOPCTestBase} from "./helpers/RSKOPCTestBase.sol";

/// @notice Tests for the configurable dispute-game init bond that
/// rskdeployer.DeployOPChainSplit supplies from
/// `[deployer].dispute_game_init_bond_wei`.
/// @dev The bond is an ordinary argument, so these are ordinary tests: no
/// process-global state, no harness, no quarantined invocation. The Go
/// side resolves the effective value (config or default) and passes it,
/// which is why the script itself has no notion of "unset".
contract RSKDeployOPChainInitBond_Test is RSKOPCTestBase {
function test_runSplitWithBond_zero_deploysZeroBond() public {
RSKDeployOPChain script = new RSKDeployOPChain();
RSKDeployOPChain.Output memory output_ = script.runSplitWithBond(deployInput, 0);

IDisputeGameFactory dgf = IDisputeGameFactory(address(output_.disputeGameFactoryProxy));
assertEq(dgf.initBonds(GameTypes.PERMISSIONED_CANNON), 0);
assertEq(dgf.initBonds(GameTypes.SUPER_PERMISSIONED_CANNON), 0);
}

function test_runSplitWithBond_custom_deploysCustomBond() public {
RSKDeployOPChain script = new RSKDeployOPChain();
RSKDeployOPChain.Output memory output_ = script.runSplitWithBond(deployInput, 12345);

IDisputeGameFactory dgf = IDisputeGameFactory(address(output_.disputeGameFactoryProxy));
assertEq(dgf.initBonds(GameTypes.PERMISSIONED_CANNON), 12345);
// Disabled game types keep a zero bond regardless of the argument.
assertEq(dgf.initBonds(GameTypes.SUPER_PERMISSIONED_CANNON), 0);
}

/// @notice `runSplit` is the upstream-parity path: no bond argument, so it
/// must apply the upstream default. This is what an absent
/// dispute_game_init_bond_wei has to preserve.
function test_runSplit_appliesUpstreamDefaultBond() public {
RSKDeployOPChain script = new RSKDeployOPChain();
RSKDeployOPChain.Output memory output_ = script.runSplit(deployInput);

IDisputeGameFactory dgf = IDisputeGameFactory(address(output_.disputeGameFactoryProxy));
assertEq(dgf.initBonds(GameTypes.PERMISSIONED_CANNON), DEFAULT_INIT_BOND);
}

/// @notice The RSK entry point carries (input, bond) in one ABI blob;
/// rskdeployer packs exactly this shape. A mismatch here is the
/// failure mode that would break a real deploy at decode time.
function test_runSplitWithBytes_decodesInputAndBond() public {
RSKDeployOPChain script = new RSKDeployOPChain();
bytes memory encoded = script.runSplitWithBytes(abi.encode(deployInput, uint256(777)));
RSKDeployOPChain.Output memory output_ = abi.decode(encoded, (RSKDeployOPChain.Output));

IDisputeGameFactory dgf = IDisputeGameFactory(address(output_.disputeGameFactoryProxy));
assertEq(dgf.initBonds(GameTypes.PERMISSIONED_CANNON), 777);
// Output must round-trip through the same 15-field shape as runWithBytes.
assertEq(address(output_.disputeGameFactoryProxy), address(dgf));
assertGt(address(output_.systemConfigProxy).code.length, 0);
}

function test_runSplitWithBytes_emptyInput_reverts() public {
RSKDeployOPChain script = new RSKDeployOPChain();
vm.expectRevert("RSKDeployOPChain: input cannot be empty");
script.runSplitWithBytes(bytes(""));
}

/// @notice Guards the Go-side default against an upstream bump: rskdeployer
/// hard-codes 0.08 ether as DefaultDisputeGameInitBondWei, so if
/// upstream ever changes this constant the two must be reconciled
/// deliberately rather than drifting apart on a merge.
function test_defaultInitBond_matchesGoSideConstant() public {
RSKDeployOPChain script = new RSKDeployOPChain();
assertEq(script.DEFAULT_INIT_BOND(), 80_000_000_000_000_000);
}
}
26 changes: 25 additions & 1 deletion packages/contracts-rootstock/test/RSKOPCMSplitter.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@
pragma solidity 0.8.15;

import {IProxyAdmin} from "interfaces/universal/IProxyAdmin.sol";
import {IDisputeGame} from "interfaces/dispute/IDisputeGame.sol";
import {IDisputeGameFactory} from "interfaces/dispute/IDisputeGameFactory.sol";
import {IFaultDisputeGame} from "interfaces/dispute/IFaultDisputeGame.sol";
import {Features} from "src/libraries/Features.sol";
import {GameTypes} from "src/dispute/lib/Types.sol";
import {Claim, GameStatus, GameTypes} from "src/dispute/lib/Types.sol";

import {RSKOPCMSplitter} from "../contracts/RSKOPCMSplitter.sol";
import {RSKOPCTestBase} from "./helpers/RSKOPCTestBase.sol";
Expand Down Expand Up @@ -174,6 +176,28 @@ contract RSKOPCMSplitter_Test is RSKOPCTestBase {
assertEq(splitter.phase(), 2);
}

function test_bondZero_gameCreatesAndResolves() public {
fullConfig.disputeGameConfigs[1].initBond = 0;
RSKOPCMSplitter.ChainContracts memory cts = _runSplitter();

IDisputeGameFactory dgf = IDisputeGameFactory(cts.disputeGameFactory);
assertEq(dgf.initBonds(GameTypes.PERMISSIONED_CANNON), 0);

// With a non-zero bond this zero-value create would revert with
// IncorrectBondAmount; PermissionedDisputeGame requires tx.origin to
// be the proposer, hence the two-argument prank.
vm.prank(proposer, proposer);
IDisputeGame game =
dgf.create{value: 0}(GameTypes.PERMISSIONED_CANNON, Claim.wrap(bytes32(uint256(1))), abi.encode(uint256(1)));

vm.warp(block.timestamp + maxClockDuration.raw() + 1);
IFaultDisputeGame(address(game)).resolveClaim(0, 0);
game.resolve();

assertEq(uint8(game.status()), uint8(GameStatus.DEFENDER_WINS));
assertEq(IFaultDisputeGame(address(game)).credit(proposer), 0);
}

function test_lateCollaboratorFailure_isAtomicAndRetryable() public {
_startAtStep2();

Expand Down
Loading