From 5381334945d863b61ebd6498d7f1f7da30bc4a88 Mon Sep 17 00:00:00 2001 From: redhdx Date: Mon, 13 Jan 2025 17:07:55 +0800 Subject: [PATCH] feature: refine zk contracts --- packages/contracts-bedrock/scripts/Config.sol | 2 +- .../contracts-bedrock/scripts/Deploy.s.sol | 1 + .../src/dispute/AnchorStateRegistry.sol | 5 - .../src/dispute/DisputeGameFactory.sol | 16 +-- .../src/dispute/ZkFaultDisputeGame.sol | 109 ++++++++++++------ .../src/dispute/ZkFaultProofConfig.sol | 41 +++---- .../interfaces/IDisputeGameFactory.sol | 4 + 7 files changed, 104 insertions(+), 74 deletions(-) diff --git a/packages/contracts-bedrock/scripts/Config.sol b/packages/contracts-bedrock/scripts/Config.sol index cfb67290d..1a7692d49 100644 --- a/packages/contracts-bedrock/scripts/Config.sol +++ b/packages/contracts-bedrock/scripts/Config.sol @@ -44,7 +44,7 @@ library Config { /// @notice The CREATE2 salt to be used when deploying the implementations. function implSalt() internal view returns (string memory _env) { - _env = vm.envOr("IMPL_SALT", string("ethers phoenix2")); + _env = vm.envOr("IMPL_SALT", string("ethers phoenix")); } /// @notice Returns the path that the state dump file should be written to or read from diff --git a/packages/contracts-bedrock/scripts/Deploy.s.sol b/packages/contracts-bedrock/scripts/Deploy.s.sol index fd084bc1f..95fc23e22 100644 --- a/packages/contracts-bedrock/scripts/Deploy.s.sol +++ b/packages/contracts-bedrock/scripts/Deploy.s.sol @@ -1054,6 +1054,7 @@ contract Deploy is Deployer { _proxy: payable(zkFaultProofConfigProxy), _implementation: zkFaultProofConfig, _innerCallData: abi.encodeCall(ZkFaultProofConfig.initialize, ( + cfg.finalSystemOwner(), cfg.blockDistance(), cfg.l2ChainID(), cfg.aggregationVkey(), diff --git a/packages/contracts-bedrock/src/dispute/AnchorStateRegistry.sol b/packages/contracts-bedrock/src/dispute/AnchorStateRegistry.sol index 5894a4d2b..c1f95f3df 100644 --- a/packages/contracts-bedrock/src/dispute/AnchorStateRegistry.sol +++ b/packages/contracts-bedrock/src/dispute/AnchorStateRegistry.sol @@ -85,9 +85,4 @@ contract AnchorStateRegistry is Initializable, IAnchorStateRegistry, ISemver { // Actually update the anchor state. anchors[gameType] = OutputRoot({ l2BlockNumber: game.l2BlockNumber(), root: Hash.wrap(game.rootClaim().raw()) }); } - - // TODO remove this - function setAnchorState(GameType _gameType, OutputRoot memory _outputRoot) external { - anchors[_gameType] = _outputRoot; - } } diff --git a/packages/contracts-bedrock/src/dispute/DisputeGameFactory.sol b/packages/contracts-bedrock/src/dispute/DisputeGameFactory.sol index 5ef7b6f16..4ea96c84a 100644 --- a/packages/contracts-bedrock/src/dispute/DisputeGameFactory.sol +++ b/packages/contracts-bedrock/src/dispute/DisputeGameFactory.sol @@ -1,6 +1,5 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -import { console2 as console } from "forge-std/console2.sol"; import { LibClone } from "@solady/utils/LibClone.sol"; import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; @@ -133,18 +132,18 @@ contract DisputeGameFactory is OwnableUpgradeable, IDisputeGameFactory, ISemver } function _prepareExtraData( + uint256 l2BlockNumber, Claim[] memory claims, address parentProxy, - uint64 l2BlockNumber, bytes memory extraData ) private pure returns (bytes memory) { // calculate the hash of all _claims bytes32 claimsHash = keccak256(abi.encodePacked(claims)); return abi.encodePacked( + l2BlockNumber, claimsHash, uint32(claims.length), parentProxy, - l2BlockNumber, extraData ); } @@ -201,16 +200,16 @@ contract DisputeGameFactory is OwnableUpgradeable, IDisputeGameFactory, ISemver // | [20, 52) | Root claim | // │ [52, 84) │ Parent block hash at creation time │ // │ [84, 84+n) │ Extra data │ - // | [84, 116) | Hash of all claims | - // | [116, 120) | The length of claims | - // | [120, 140) | Parent game address | - // | [140, 148) | L2 block number | + // | [84, 116) | L2 block number | + // | [116, 148) | Hash of all claims | + // | [148, 152) | The length of claims | + // | [152, 172) | Parent game address | // └───────────────────────────┴────────────────────────────────────┘ Claim rootClaim = _claims[_claims.length-1]; bytes memory extraData = _prepareExtraData( + _l2BlockNumber, _claims, address(parentProxy), - _l2BlockNumber, _extraData ); proxy_ = IDisputeGame(address(impl).clone(abi.encodePacked(msg.sender, rootClaim, blockhash(block.number - 1), extraData))); @@ -228,6 +227,7 @@ contract DisputeGameFactory is OwnableUpgradeable, IDisputeGameFactory, ISemver _disputeGames[uuid] = id; _disputeGameList.push(id); emit DisputeGameCreated(address(proxy_), _gameType, rootClaim); + emit ZkDisputeGameIndexUpdated(_disputeGameList.length - 1); } /// @inheritdoc IDisputeGameFactory diff --git a/packages/contracts-bedrock/src/dispute/ZkFaultDisputeGame.sol b/packages/contracts-bedrock/src/dispute/ZkFaultDisputeGame.sol index 180ef75b4..ba315bd15 100644 --- a/packages/contracts-bedrock/src/dispute/ZkFaultDisputeGame.sol +++ b/packages/contracts-bedrock/src/dispute/ZkFaultDisputeGame.sol @@ -16,9 +16,6 @@ import { ISemver } from "src/universal/ISemver.sol"; import "src/dispute/lib/Types.sol"; import "src/dispute/lib/Errors.sol"; -// TODO remove -import { console2 as console } from "forge-std/console2.sol"; - /// @title FaultDisputeGame /// @notice An implementation of the `IFaultDisputeGame` interface. contract ZkFaultDisputeGame is IZkFaultDisputeGame, Clone, ISemver { @@ -48,8 +45,8 @@ contract ZkFaultDisputeGame is IZkFaultDisputeGame, Clone, ISemver { uint256 internal immutable L2_CHAIN_ID; /// @notice Semantic version. - /// @custom:semver 1.2.0 - string public constant version = "1.2.0"; + /// @custom:semver 1.0.0 + string public constant version = "1.0.0"; /// @notice The starting timestamp of the game Timestamp public createdAt; @@ -63,6 +60,9 @@ contract ZkFaultDisputeGame is IZkFaultDisputeGame, Clone, ISemver { /// @notice Flag for the `initialize` function to prevent re-initialization. bool internal initialized; + /// @notice Credited balances for winning participants. + mapping(address => uint256) public credit; + /// @notice The mapping of claims which have been challenged by signal. mapping(uint256 => bool) public challengedClaims; /// @notice The indexes list of the challenged claims. @@ -89,10 +89,10 @@ contract ZkFaultDisputeGame is IZkFaultDisputeGame, Clone, ISemver { /// @notice The latest finalized output root, serving as the anchor for output bisection. OutputRoot public startingOutputRoot; - uint256 public constant PROPOSER_BOND = 1 ether; - uint256 public constant CHALLENGER_BOND = 1 ether; - address payable public constant BURN_ADDRESS = payable(address(0)); - uint256 public constant PERCENTAGE_DIVISOR = 10000; + uint256 public constant PERCENTAGE_DIVISOR = 1000; + uint256 public immutable PROPOSER_BOND; + uint256 public immutable CHALLENGER_BOND; + address payable public immutable FEE_VAULT_ADDRESS; uint256 public immutable CHALLENGER_REWARD_PERCENTAGE; uint256 public immutable PROVER_REWARD_PERCENTAGE; @@ -107,6 +107,9 @@ contract ZkFaultDisputeGame is IZkFaultDisputeGame, Clone, ISemver { GameType _gameType, Duration _maxGenerateProofDuration, Duration _maxDetectFaultDuration, + uint256 _PROPOSER_BOND, + uint256 _CHALLENGER_BOND, + address _FEE_VAULT_ADDRESS, uint256 _CHALLENGER_REWARD_PERCENTAGE, uint256 _PROVER_REWARD_PERCENTAGE, IDelayedWETH _weth, @@ -118,6 +121,9 @@ contract ZkFaultDisputeGame is IZkFaultDisputeGame, Clone, ISemver { GAME_TYPE = _gameType; MAX_GENERATE_PROOF_DURATION = _maxGenerateProofDuration; MAX_DETECT_FAULT_DURATION = _maxDetectFaultDuration; + PROPOSER_BOND = _PROPOSER_BOND; + CHALLENGER_BOND = _CHALLENGER_BOND; + _FEE_VAULT_ADDRESS = FEE_VAULT_ADDRESS; CHALLENGER_REWARD_PERCENTAGE = _CHALLENGER_REWARD_PERCENTAGE; PROVER_REWARD_PERCENTAGE = _PROVER_REWARD_PERCENTAGE; if (CHALLENGER_REWARD_PERCENTAGE + PROVER_REWARD_PERCENTAGE > PERCENTAGE_DIVISOR) { @@ -170,19 +176,19 @@ contract ZkFaultDisputeGame is IZkFaultDisputeGame, Clone, ISemver { // in the factory, but are not used by the game, which would allow for multiple dispute games for the same // output proposal to be created. // - // Expected length: 0x96 + // Expected length: 0xb2 // - 0x04 selector // - 0x14 creator address // - 0x20 root claim // - 0x20 l1 head - // - 0x40 extraData + // - 0x58 extraData + // - 0x20 l2 block number // - 0x20 claims hash // - 0x04 claims length // - 0x14 parent game contract address - // - 0x08 l2 block number // - 0x02 CWIA bytes assembly { - if iszero(eq(calldatasize(), 0x9a)) { + if iszero(eq(calldatasize(), 0xb2)) { // Store the selector for `BadExtraData()` & revert mstore(0x00, 0x9824bdab) revert(0x1C, 0x04) @@ -206,8 +212,7 @@ contract ZkFaultDisputeGame is IZkFaultDisputeGame, Clone, ISemver { initialized = true; // Deposit the bond. - // TODO: Uncomment this line when the WETH contract is ready. - // WETH.deposit{ value: msg.value }(); + WETH.deposit{ value: msg.value }(); // Set the game's starting timestamp createdAt = Timestamp.wrap(uint64(block.timestamp)); @@ -405,6 +410,7 @@ contract ZkFaultDisputeGame is IZkFaultDisputeGame, Clone, ISemver { status_ = GameStatus.DEFENDER_WINS; } + uint256 currentContractBalance = WETH.balanceOf(address(this)); if (status_ == GameStatus.CHALLENGER_WINS) { // refund valid challengers if there is any for (uint256 i = 0; i < challengedClaimIndexes.length; i++) { @@ -413,8 +419,7 @@ contract ZkFaultDisputeGame is IZkFaultDisputeGame, Clone, ISemver { challengers[challengedClaimIndexes[i]].transfer(CHALLENGER_BOND); } } - // TODO reward part of challengers bond to valdity provers, current reward is zero - uint256 currentContractBalance = address(this).balance; + // TODO reward part of challengers bond to validity provers, current reward is zero // reward the special challenger who submitted the signal which is proven to be valid // 1. someone submitted a valid fault proof corresponding to the challenge index; or // 2. the generate proof window is expired and no one submitted a validity proof @@ -423,32 +428,35 @@ contract ZkFaultDisputeGame is IZkFaultDisputeGame, Clone, ISemver { // there is no successful challenge in the current game. if (isChallengeSuccess) { // there is a challenger who submmitted the dispute claim index by `challengeBySignal` + uint256 challengerBond = (currentContractBalance * CHALLENGER_REWARD_PERCENTAGE) / PERCENTAGE_DIVISOR; if (challengedClaims[successfulChallengeIndex]) { - challengers[successfulChallengeIndex].transfer((currentContractBalance * CHALLENGER_REWARD_PERCENTAGE) / PERCENTAGE_DIVISOR); + _distributeBond(challengers[successfulChallengeIndex], challengerBond); } else { // if there is no challenger, then the challenger is the fault proof prover self - faultProofProver.transfer((currentContractBalance * CHALLENGER_REWARD_PERCENTAGE) / PERCENTAGE_DIVISOR); + _distributeBond(faultProofProver, challengerBond); } + currentContractBalance = currentContractBalance - challengerBond; } // reward the fault proof prover - faultProofProver.transfer((currentContractBalance * PROVER_REWARD_PERCENTAGE) / PERCENTAGE_DIVISOR); - // burn the rest - currentContractBalance = address(this).balance; - BURN_ADDRESS.transfer(currentContractBalance); + uint256 proverBond = (currentContractBalance * PROVER_REWARD_PERCENTAGE) / PERCENTAGE_DIVISOR; + _distributeBond(faultProofProver, proverBond); + currentContractBalance = currentContractBalance - proverBond; } else if (status_ == GameStatus.DEFENDER_WINS) { - // reward part of challengers bond to valdity provers + // reward part of challengers bond to validity provers for (uint256 i = 0; i < invalidChallengeClaimIndexes.length; i++) { - validityProofProvers[invalidChallengeClaimIndexes[i]].transfer((CHALLENGER_BOND * PROVER_REWARD_PERCENTAGE) / PERCENTAGE_DIVISOR); + uint256 proverBond = (CHALLENGER_BOND * PROVER_REWARD_PERCENTAGE) / PERCENTAGE_DIVISOR; + _distributeBond(validityProofProvers[invalidChallengeClaimIndexes[i]], proverBond); + currentContractBalance = currentContractBalance - proverBond; } // refund the bond to proposer - payable(gameCreator()).transfer(PROPOSER_BOND); - // burn the rest - uint256 currentContractBalance = address(this).balance; - BURN_ADDRESS.transfer(currentContractBalance); + distributeBond(gameCreator(), PROPOSER_BOND); + currentContractBalance = currentContractBalance - PROPOSER_BOND; } else { // sanity check revert InvalidGameStatus(); } + // transfer the rest + _distributeBond(FEE_VAULT_ADDRESS, currentContractBalance); resolvedAt = Timestamp.wrap(uint64(block.timestamp)); @@ -480,27 +488,27 @@ contract ZkFaultDisputeGame is IZkFaultDisputeGame, Clone, ISemver { } function claimsHash() public pure returns (Hash claimsHash_) { - claimsHash_ = Hash.wrap(_getArgBytes32(0x54)); + claimsHash_ = Hash.wrap(_getArgBytes32(0x74)); } function claimsLength() public pure returns (uint256 claimsLength_) { - claimsLength_ = uint256(_getArgUint32(0x74)); + claimsLength_ = uint256(_getArgUint32(0x94)); } function parentGameProxy() public pure returns (IZkFaultDisputeGame parentGameProxy_) { - parentGameProxy_ = IZkFaultDisputeGame(_getArgAddress(0x78)); + parentGameProxy_ = IZkFaultDisputeGame(_getArgAddress(0x98)); } /// @inheritdoc IDisputeGame function l2BlockNumber() public pure returns (uint256 l2BlockNumber_) { - l2BlockNumber_ = uint256(_getArgUint64(0x8c)); + l2BlockNumber_ = uint256(_getArgUint256(0x54)); } /// @inheritdoc IDisputeGame function extraData() public pure returns (bytes memory extraData_) { // The extra data starts at the second word within the cwia calldata and // is 60 bytes long. - extraData_ = _getArgBytes(0x54, 0x40); + extraData_ = _getArgBytes(0x54, 0x58); } /// @inheritdoc IDisputeGame @@ -510,10 +518,43 @@ contract ZkFaultDisputeGame is IZkFaultDisputeGame, Clone, ISemver { extraData_ = extraData(); } + //////////////////////////////////////////////////////////////// + // HELPERS // + //////////////////////////////////////////////////////////////// + + /// @notice Pays out the bond of a claim to a given recipient. + /// @param _recipient The recipient of the bond. + /// @param _bond The bond to pay out. + function _distributeBond(address _recipient, uint256 _bond) internal { + // Increase the recipient's credit. + credit[_recipient] += bond; + + // Unlock the bond. + WETH.unlock(_recipient, bond); + } + //////////////////////////////////////////////////////////////// // MISC EXTERNAL // //////////////////////////////////////////////////////////////// + /// @notice Claim the credit belonging to the recipient address. + /// @param _recipient The owner and recipient of the credit. + function claimCredit(address _recipient) external { + // Remove the credit from the recipient prior to performing the external call. + uint256 recipientCredit = credit[_recipient]; + credit[_recipient] = 0; + + // Revert if the recipient has no credit to claim. + if (recipientCredit == 0) revert NoCreditToClaim(); + + // Try to withdraw the WETH amount so it can be used here. + WETH.withdraw(_recipient, recipientCredit); + + // Transfer the credit to the recipient. + (bool success,) = _recipient.call{ value: recipientCredit }(hex""); + if (!success) revert BondTransferFailed(); + } + /// @notice Returns the max clock duration. function maxClockDuration() external view returns (Duration maxClockDuration_) { maxClockDuration_ = MAX_CLOCK_DURATION; diff --git a/packages/contracts-bedrock/src/dispute/ZkFaultProofConfig.sol b/packages/contracts-bedrock/src/dispute/ZkFaultProofConfig.sol index 2c146db48..88d1182ef 100644 --- a/packages/contracts-bedrock/src/dispute/ZkFaultProofConfig.sol +++ b/packages/contracts-bedrock/src/dispute/ZkFaultProofConfig.sol @@ -1,11 +1,12 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; +import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; import { SP1VerifierGateway } from "@sp1-contracts/src/SP1VerifierGateway.sol"; import { Initializable } from "@openzeppelin/contracts/proxy/utils/Initializable.sol"; import { ISemver } from "src/universal/ISemver.sol"; -contract ZkFaultProofConfig is Initializable, ISemver { +contract ZkFaultProofConfig is OwnableUpgradeable, Initializable, ISemver { /// @notice Semantic version. /// @custom:semver 1.0.0 @@ -53,19 +54,10 @@ contract ZkFaultProofConfig is Initializable, ISemver { /// @param newRollupConfigHash The new rollup config hash. event UpdatedRollupConfigHash(bytes32 indexed oldRollupConfigHash, bytes32 indexed newRollupConfigHash); - /// @notice Checkpoints a block hash at a given block number. - /// @param _blockNumber Block number to checkpoint the hash at. - /// @param _blockHash Hash of the block at the given block number. - /// @dev Block number must be in the past 256 blocks or this will revert. - /// @dev Passing both inputs as zero will automatically checkpoint the most recent blockhash. - function checkpointBlockHash(uint256 _blockNumber, bytes32 _blockHash) external { - require(blockhash(_blockNumber) == _blockHash, "ZkFaultProofConfig: block hash and number cannot be checkpointed"); - historicBlockHashes[_blockNumber] = _blockHash; - } - constructor() {} function initialize ( + address _owner, uint256 _blockDistance, uint256 _chainId, bytes32 _aggregationVkey, @@ -73,6 +65,9 @@ contract ZkFaultProofConfig is Initializable, ISemver { address _verifierGateway, bytes32 _rollupConfigHash ) public initializer { + __Ownable_init(); + transferOwnership(_owner); + blockDistance = _blockDistance; chainId = _chainId; aggregationVkey = _aggregationVkey; @@ -81,30 +76,24 @@ contract ZkFaultProofConfig is Initializable, ISemver { rollupConfigHash = _rollupConfigHash; } - function verifyZkFaultProof() external {} - - /// TODO: add permission - function updateAggregationVKey(bytes32 _aggregationVKey) external { - emit UpdatedAggregationVKey(aggregationVkey, _aggregationVKey); + function updateAggregationVKey(bytes32 _aggregationVKey) external onlyOwner { aggregationVkey = _aggregationVKey; + emit UpdatedAggregationVKey(aggregationVkey, _aggregationVKey); } - /// TODO: add permission - function updateRangeVkeyCommitment(bytes32 _rangeVkeyCommitment) external { - emit UpdatedRangeVkeyCommitment(rangeVkeyCommitment, _rangeVkeyCommitment); + function updateRangeVkeyCommitment(bytes32 _rangeVkeyCommitment) external onlyOwner { rangeVkeyCommitment = _rangeVkeyCommitment; + emit UpdatedRangeVkeyCommitment(rangeVkeyCommitment, _rangeVkeyCommitment); } - /// TODO: add permission - function updateVerifierGateway(address _verifierGateway) external { - emit UpdatedVerifierGateway(address(verifierGateway), _verifierGateway); + function updateVerifierGateway(address _verifierGateway) external onlyOwner { verifierGateway = SP1VerifierGateway(_verifierGateway); + emit UpdatedVerifierGateway(address(verifierGateway), _verifierGateway); } - /// TODO: add permission - function updateRollupConfigHash(bytes32 _rollupConfigHash) external { - emit UpdatedRollupConfigHash(rollupConfigHash, _rollupConfigHash); + function updateRollupConfigHash(bytes32 _rollupConfigHash) external onlyOwner { rollupConfigHash = _rollupConfigHash; + emit UpdatedRollupConfigHash(rollupConfigHash, _rollupConfigHash); } -} \ No newline at end of file +} diff --git a/packages/contracts-bedrock/src/dispute/interfaces/IDisputeGameFactory.sol b/packages/contracts-bedrock/src/dispute/interfaces/IDisputeGameFactory.sol index 9e5ed0db8..3d555e745 100644 --- a/packages/contracts-bedrock/src/dispute/interfaces/IDisputeGameFactory.sol +++ b/packages/contracts-bedrock/src/dispute/interfaces/IDisputeGameFactory.sol @@ -24,6 +24,10 @@ interface IDisputeGameFactory { /// @param newBond The new bond (in wei) for initializing the game type. event InitBondUpdated(GameType indexed gameType, uint256 indexed newBond); + /// @notice Emitted when a new dispute game is created, index updated + /// @param gameIndex The index of the DisputeGame. + event ZkDisputeGameIndexUpdated(uint256 indexed gameIndex); + /// @notice Information about a dispute game found in a `findLatestGames` search. struct GameSearchResult { uint256 index;