From 66eb3102b1847dda3fc06ab6687d3c96b270dd11 Mon Sep 17 00:00:00 2001 From: Li-Qing Wang Date: Tue, 16 Dec 2025 15:36:44 +0800 Subject: [PATCH 01/11] refactor: use upgradeable pattern --- foundry.lock | 8 +++ script/DeployContractDebug.sol | 32 ++++----- script/DeployGateway.sol | 36 +++++----- src/CommitteeManagement.sol | 126 +++++++++++++++++++++++++-------- src/GatewayDebug.sol | 6 -- src/MultiSigVerifier.sol | 84 ++++++++++++++++++---- src/SequencerSetPublisher.sol | 3 +- src/StakeManagement.sol | 75 +++++++++++++++----- test/MultiSigVerifier.t.sol | 3 +- 9 files changed, 272 insertions(+), 101 deletions(-) create mode 100644 foundry.lock diff --git a/foundry.lock b/foundry.lock new file mode 100644 index 0000000..7646659 --- /dev/null +++ b/foundry.lock @@ -0,0 +1,8 @@ +{ + "lib/openzeppelin-contracts-upgradeable": { + "rev": "3d5fa5c24c411112bab47bec25cfa9ad0af0e6e8" + }, + "lib/forge-std": { + "rev": "3b20d60d14b343ee4f908cb8079495c07f5e8981" + } +} \ No newline at end of file diff --git a/script/DeployContractDebug.sol b/script/DeployContractDebug.sol index f4a0a59..5a74cce 100644 --- a/script/DeployContractDebug.sol +++ b/script/DeployContractDebug.sol @@ -30,31 +30,35 @@ contract DeployGateway is Script { } function deploy() public { - // deploy contracts GatewayDebug gatewayImpl = new GatewayDebug(); console.log("Gateway implementation contract address: ", address(gatewayImpl)); - UpgradeableProxy proxy = new UpgradeableProxy(address(gatewayImpl), deployer, ""); - console.log("Gateway proxy contract contract address: ", address(proxy)); - GatewayDebug gateway = GatewayDebug(payable(proxy)); + UpgradeableProxy gatewayProxy = new UpgradeableProxy(address(gatewayImpl), deployer, ""); + console.log("Gateway proxy contract address: ", address(gatewayProxy)); + GatewayDebug gateway = GatewayDebug(payable(gatewayProxy)); PegBTC pegBTC = new PegBTC(address(gateway)); console.log("PegBTC contract address: ", address(pegBTC)); - // Read committee config from env - // - COMMITTEE_0, COMMITTEE_1, ... (addresses) - // - WATCHTOWER_0, WATCHTOWER_1, ... (bytes32) address[] memory initialMembers = _readSequentialAddresses("COMMITTEE"); uint256 initialRequired = (initialMembers.length * 2 + 2) / 3; bytes32[] memory initialWatchtowers = _readSequentialBytes32("WATCHTOWER"); address[] memory initialAuthorizedCallers = new address[](1); initialAuthorizedCallers[0] = address(gateway); - CommitteeManagementDebug committeeManagement = - new CommitteeManagementDebug(initialMembers, initialRequired, initialAuthorizedCallers, initialWatchtowers); - console.log("CommitteeManagement contract address: ", address(committeeManagement)); - StakeManagement stakeManagement = new StakeManagement(IERC20(address(pegBTC)), address(gateway)); - console.log("StakeManagement contract address: ", address(stakeManagement)); + CommitteeManagementDebug committeeImpl = new CommitteeManagementDebug(); + console.log("CommitteeManagement implementation contract address: ", address(committeeImpl)); + UpgradeableProxy committeeProxy = new UpgradeableProxy(address(committeeImpl), deployer, ""); + console.log("CommitteeManagement proxy contract address: ", address(committeeProxy)); + CommitteeManagement committeeManagement = CommitteeManagement(address(committeeProxy)); + committeeManagement.initialize(initialMembers, initialRequired, initialAuthorizedCallers, initialWatchtowers); + + StakeManagement stakeImpl = new StakeManagement(); + console.log("StakeManagement implementation contract address: ", address(stakeImpl)); + UpgradeableProxy stakeProxy = new UpgradeableProxy(address(stakeImpl), deployer, ""); + console.log("StakeManagement proxy contract address: ", address(stakeProxy)); + StakeManagement stakeManagement = StakeManagement(address(stakeProxy)); + stakeManagement.initialize(IERC20(address(pegBTC)), address(gateway)); gateway.initialize( IPegBTC(address(pegBTC)), @@ -64,10 +68,7 @@ contract DeployGateway is Script { ); } - // Helpers: read env arrays as sequential variables with numeric suffixes - // Example: BASE=PREFIX, reads PREFIX_0, PREFIX_1, ... until a default is hit. function _readSequentialAddresses(string memory baseKey) internal view returns (address[] memory out) { - // first pass: count uint256 count = 0; while (true) { string memory key = string(abi.encodePacked(baseKey, "_", vm.toString(count))); @@ -85,7 +86,6 @@ contract DeployGateway is Script { } function _readSequentialBytes32(string memory baseKey) internal view returns (bytes32[] memory out) { - // first pass: count uint256 count = 0; while (true) { string memory key = string(abi.encodePacked(baseKey, "_", vm.toString(count))); diff --git a/script/DeployGateway.sol b/script/DeployGateway.sol index aa7c001..d8e6520 100644 --- a/script/DeployGateway.sol +++ b/script/DeployGateway.sol @@ -10,8 +10,6 @@ import {StakeManagement} from "../src/StakeManagement.sol"; import {GatewayUpgradeable} from "../src/Gateway.sol"; import {PegBTC} from "../src/PegBTC.sol"; import {UpgradeableProxy} from "../src/UpgradeableProxy.sol"; -import {CommitteeManagement} from "../src/CommitteeManagement.sol"; -import {StakeManagement} from "../src/StakeManagement.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; contract DeployGateway is Script { @@ -32,31 +30,39 @@ contract DeployGateway is Script { } function deploy() public { - // deploy contracts + // deploy gateway implementation + proxy GatewayUpgradeable gatewayImpl = new GatewayUpgradeable(); console.log("Gateway implementation contract address: ", address(gatewayImpl)); - UpgradeableProxy proxy = new UpgradeableProxy(address(gatewayImpl), deployer, ""); - console.log("Gateway proxy contract contract address: ", address(proxy)); - GatewayUpgradeable gateway = GatewayUpgradeable(payable(proxy)); + UpgradeableProxy gatewayProxy = new UpgradeableProxy(address(gatewayImpl), deployer, ""); + console.log("Gateway proxy contract address: ", address(gatewayProxy)); + GatewayUpgradeable gateway = GatewayUpgradeable(payable(gatewayProxy)); PegBTC pegBTC = new PegBTC(address(gateway)); console.log("PegBTC contract address: ", address(pegBTC)); // Read committee config from env - // - COMMITTEE_0, COMMITTEE_1, ... (addresses) - // - WATCHTOWER_0, WATCHTOWER_1, ... (bytes32) address[] memory initialMembers = _readSequentialAddresses("COMMITTEE"); uint256 initialRequired = (initialMembers.length * 2 + 2) / 3; bytes32[] memory initialWatchtowers = _readSequentialBytes32("WATCHTOWER"); address[] memory initialAuthorizedCallers = new address[](1); initialAuthorizedCallers[0] = address(gateway); - CommitteeManagement committeeManagement = - new CommitteeManagement(initialMembers, initialRequired, initialAuthorizedCallers, initialWatchtowers); - console.log("CommitteeManagement contract address: ", address(committeeManagement)); - StakeManagement stakeManagement = new StakeManagement(IERC20(address(pegBTC)), address(gateway)); - console.log("StakeManagement contract address: ", address(stakeManagement)); + // Deploy CommitteeManagement implementation + proxy + CommitteeManagement committeeImpl = new CommitteeManagement(); + console.log("CommitteeManagement implementation contract address: ", address(committeeImpl)); + UpgradeableProxy committeeProxy = new UpgradeableProxy(address(committeeImpl), deployer, ""); + console.log("CommitteeManagement proxy contract address: ", address(committeeProxy)); + CommitteeManagement committeeManagement = CommitteeManagement(address(committeeProxy)); + committeeManagement.initialize(initialMembers, initialRequired, initialAuthorizedCallers, initialWatchtowers); + + // Deploy StakeManagement implementation + proxy + StakeManagement stakeImpl = new StakeManagement(); + console.log("StakeManagement implementation contract address: ", address(stakeImpl)); + UpgradeableProxy stakeProxy = new UpgradeableProxy(address(stakeImpl), deployer, ""); + console.log("StakeManagement proxy contract address: ", address(stakeProxy)); + StakeManagement stakeManagement = StakeManagement(address(stakeProxy)); + stakeManagement.initialize(IERC20(address(pegBTC)), address(gateway)); gateway.initialize( IPegBTC(address(pegBTC)), @@ -66,10 +72,7 @@ contract DeployGateway is Script { ); } - // Helpers: read env arrays as sequential variables with numeric suffixes - // Example: BASE=PREFIX, reads PREFIX_0, PREFIX_1, ... until a default is hit. function _readSequentialAddresses(string memory baseKey) internal view returns (address[] memory out) { - // first pass: count uint256 count = 0; while (true) { string memory key = string(abi.encodePacked(baseKey, "_", vm.toString(count))); @@ -87,7 +90,6 @@ contract DeployGateway is Script { } function _readSequentialBytes32(string memory baseKey) internal view returns (bytes32[] memory out) { - // first pass: count uint256 count = 0; while (true) { string memory key = string(abi.encodePacked(baseKey, "_", vm.toString(count))); diff --git a/src/CommitteeManagement.sol b/src/CommitteeManagement.sol index 489e316..195f653 100644 --- a/src/CommitteeManagement.sol +++ b/src/CommitteeManagement.sol @@ -2,7 +2,9 @@ pragma solidity ^0.8.28; import {MultiSigVerifier} from "./MultiSigVerifier.sol"; -import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; +import { + EnumerableSet +} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; /// @title CommitteeManagement /// @notice Manages committee membership verification, authorized message execution with anti-replay, @@ -28,17 +30,23 @@ contract CommitteeManagement is MultiSigVerifier { /// @notice Tracks whether a nonced message hash has been consumed on-chain to prevent replay. mapping(bytes32 => bool) public executed; - // ========== Constructor ========== + // ========== Initialization ========== + constructor() { + _disableInitializers(); + } + + /// @notice Initializes committee membership, watchtowers, and authorized callers. /// @param initialMembers Initial committee owner addresses /// @param requiredSignatures Threshold for a message to be considered authorized /// @param initialAuthorizedCallers Initial authorized caller addresses (Gateway, etc.) /// @param initialWatchtowers Initial watchtower addresses - constructor( + function initialize( address[] memory initialMembers, uint256 requiredSignatures, address[] memory initialAuthorizedCallers, bytes32[] memory initialWatchtowers - ) MultiSigVerifier(initialMembers, requiredSignatures) { + ) public initializer { + __MultiSigVerifier_init(initialMembers, requiredSignatures); for (uint256 i = 0; i < initialAuthorizedCallers.length; i++) { authorizedCallers.add(initialAuthorizedCallers[i]); } @@ -66,7 +74,10 @@ contract CommitteeManagement is MultiSigVerifier { /// @notice Helper to verify signatures using the inherited MultiSigVerifier /// @param msgHash The message hash to verify (already domain- and nonce-bound if applicable) /// @param signatures Committee signatures - function verifySignatures(bytes32 msgHash, bytes[] memory signatures) external view returns (bool) { + function verifySignatures( + bytes32 msgHash, + bytes[] memory signatures + ) external view returns (bool) { return verify(msgHash, signatures); } @@ -90,7 +101,10 @@ contract CommitteeManagement is MultiSigVerifier { // Enforce uniqueness of peerId across members (by hash) bytes32 h = keccak256(peerId); address current = peerIdOwnerByHash[h]; - require(current == address(0) || current == msg.sender, "peerId already registered by another member"); + require( + current == address(0) || current == msg.sender, + "peerId already registered by another member" + ); committeePeerId[msg.sender] = peerId; peerIdOwnerByHash[h] = msg.sender; @@ -99,7 +113,9 @@ contract CommitteeManagement is MultiSigVerifier { } /// @notice Get the stored PeerId of a committee member - function getCommitteePeerId(address member) external view returns (bytes memory) { + function getCommitteePeerId( + address member + ) external view returns (bytes memory) { require(isOwner[member], "Not a committee member"); bytes memory id = committeePeerId[member]; require(id.length != 0, "Member has no registered PeerId"); @@ -121,7 +137,10 @@ contract CommitteeManagement is MultiSigVerifier { // ========== Modifiers ========== /// @dev Restricts external execution of nonced signatures to whitelisted callers. modifier onlyAuthorizedCaller() { - require(authorizedCallers.contains(msg.sender), "caller not authorized"); + require( + authorizedCallers.contains(msg.sender), + "caller not authorized" + ); _; } @@ -130,10 +149,17 @@ contract CommitteeManagement is MultiSigVerifier { /// @param msgHash Preimage message hash (without nonce). Must be domain-bound by this contract in its encoder /// @param nonce Per-usage nonce agreed off-chain and included in signatures /// @param signatures Committee signatures authorizing the action - function _executeNoncedSignatures(bytes32 msgHash, uint256 nonce, bytes[] memory signatures) internal { + function _executeNoncedSignatures( + bytes32 msgHash, + uint256 nonce, + bytes[] memory signatures + ) internal { bytes32 noncedHash = getNoncedDigest(msgHash, nonce); require(!executed[noncedHash], "Already executed"); - require(verify(noncedHash, signatures), "Not enough valid committee signatures"); + require( + verify(noncedHash, signatures), + "Not enough valid committee signatures" + ); executed[noncedHash] = true; } @@ -142,23 +168,32 @@ contract CommitteeManagement is MultiSigVerifier { /// @param msgHash Preimage message hash (without nonce). Must be domain-bound by this contract in its encoder /// @param nonce Per-usage nonce agreed off-chain and included in signatures /// @param signatures Committee signatures authorizing the action - function executeNoncedSignatures(bytes32 msgHash, uint256 nonce, bytes[] memory signatures) - external - onlyAuthorizedCaller - { + function executeNoncedSignatures( + bytes32 msgHash, + uint256 nonce, + bytes[] memory signatures + ) external onlyAuthorizedCaller { _executeNoncedSignatures(msgHash, nonce, signatures); } // ========== Watchtower Management ========== /// @notice Add a watchtower address via committee authorization - function addWatchtower(bytes32 watchtower, uint256 nonce, bytes[] memory authSignatures) external { + function addWatchtower( + bytes32 watchtower, + uint256 nonce, + bytes[] memory authSignatures + ) external { bytes32 msgHash = getAddWatchtowerDigest(watchtower); _executeNoncedSignatures(msgHash, nonce, authSignatures); watchtowerList.add(watchtower); } /// @notice Remove a watchtower address via committee authorization - function removeWatchtower(bytes32 watchtower, uint256 nonce, bytes[] memory authSignatures) external { + function removeWatchtower( + bytes32 watchtower, + uint256 nonce, + bytes[] memory authSignatures + ) external { bytes32 msgHash = getRemoveWatchtowerDigest(watchtower); _executeNoncedSignatures(msgHash, nonce, authSignatures); watchtowerList.remove(watchtower); @@ -166,33 +201,48 @@ contract CommitteeManagement is MultiSigVerifier { // ========== Digest Helpers (Watchtower) ========== /// @dev Returns the domain-bound message hash for adding a watchtower (without nonce) - function getAddWatchtowerDigest(bytes32 watchtower) internal view returns (bytes32) { + function getAddWatchtowerDigest( + bytes32 watchtower + ) internal view returns (bytes32) { bytes32 typeHash = keccak256("ADD_WATCHTOWER(bytes32 watchtower)"); return keccak256(abi.encode(typeHash, address(this), watchtower)); } /// @notice Returns the fully nonced digest for adding a watchtower - function getAddWatchtowerDigestNonced(bytes32 watchtower, uint256 nonce) public view returns (bytes32) { + function getAddWatchtowerDigestNonced( + bytes32 watchtower, + uint256 nonce + ) public view returns (bytes32) { bytes32 msgHash = getAddWatchtowerDigest(watchtower); return getNoncedDigest(msgHash, nonce); } /// @dev Returns the domain-bound message hash for removing a watchtower (without nonce) - function getRemoveWatchtowerDigest(bytes32 watchtower) internal view returns (bytes32) { + function getRemoveWatchtowerDigest( + bytes32 watchtower + ) internal view returns (bytes32) { bytes32 typeHash = keccak256("REMOVE_WATCHTOWER(bytes32 watchtower)"); return keccak256(abi.encode(typeHash, address(this), watchtower)); } /// @notice Returns the fully nonced digest for removing a watchtower - function getRemoveWatchtowerDigestNonced(bytes32 watchtower, uint256 nonce) public view returns (bytes32) { + function getRemoveWatchtowerDigestNonced( + bytes32 watchtower, + uint256 nonce + ) public view returns (bytes32) { bytes32 msgHash = getRemoveWatchtowerDigest(watchtower); return getNoncedDigest(msgHash, nonce); } /// @notice Returns the fully nonced digest for an action-specific preimage hash /// @dev Domain-bound by this contract address and includes the provided nonce. - function getNoncedDigest(bytes32 msgHash, uint256 nonce) public view returns (bytes32) { - bytes32 typeHash = keccak256("NONCED_MESSAGE(bytes32 msgHash,uint256 nonce)"); + function getNoncedDigest( + bytes32 msgHash, + uint256 nonce + ) public view returns (bytes32) { + bytes32 typeHash = keccak256( + "NONCED_MESSAGE(bytes32 msgHash,uint256 nonce)" + ); return keccak256(abi.encode(typeHash, address(this), msgHash, nonce)); } @@ -209,7 +259,11 @@ contract CommitteeManagement is MultiSigVerifier { } /// @notice Add an authorized external caller via committee authorization - function addAuthorizedCaller(address caller, uint256 nonce, bytes[] memory authSignatures) external { + function addAuthorizedCaller( + address caller, + uint256 nonce, + bytes[] memory authSignatures + ) external { bytes32 msgHash = getAddAuthorizedCallerDigest(caller); _executeNoncedSignatures(msgHash, nonce, authSignatures); authorizedCallers.add(caller); @@ -217,7 +271,11 @@ contract CommitteeManagement is MultiSigVerifier { } /// @notice Remove an authorized external caller via committee authorization - function removeAuthorizedCaller(address caller, uint256 nonce, bytes[] memory authSignatures) external { + function removeAuthorizedCaller( + address caller, + uint256 nonce, + bytes[] memory authSignatures + ) external { bytes32 msgHash = getRemoveAuthorizedCallerDigest(caller); _executeNoncedSignatures(msgHash, nonce, authSignatures); authorizedCallers.remove(caller); @@ -226,26 +284,38 @@ contract CommitteeManagement is MultiSigVerifier { // ========== Digest Helpers (Authorized Callers) ========== /// @dev Returns the domain-bound message hash for adding an authorized external caller (without nonce) - function getAddAuthorizedCallerDigest(address caller) internal view returns (bytes32) { + function getAddAuthorizedCallerDigest( + address caller + ) internal view returns (bytes32) { bytes32 typeHash = keccak256("ADD_AUTH_CALLER(address caller)"); return keccak256(abi.encode(typeHash, address(this), caller)); } /// @notice Returns the fully nonced digest for adding an authorized external caller - function getAddAuthorizedCallerDigestNonced(address caller, uint256 nonce) public view returns (bytes32) { + function getAddAuthorizedCallerDigestNonced( + address caller, + uint256 nonce + ) public view returns (bytes32) { bytes32 msgHash = getAddAuthorizedCallerDigest(caller); return getNoncedDigest(msgHash, nonce); } /// @dev Returns the domain-bound message hash for removing an authorized external caller (without nonce) - function getRemoveAuthorizedCallerDigest(address caller) internal view returns (bytes32) { + function getRemoveAuthorizedCallerDigest( + address caller + ) internal view returns (bytes32) { bytes32 typeHash = keccak256("REMOVE_AUTH_CALLER(address caller)"); return keccak256(abi.encode(typeHash, address(this), caller)); } /// @notice Returns the fully nonced digest for removing an authorized external caller - function getRemoveAuthorizedCallerDigestNonced(address caller, uint256 nonce) public view returns (bytes32) { + function getRemoveAuthorizedCallerDigestNonced( + address caller, + uint256 nonce + ) public view returns (bytes32) { bytes32 msgHash = getRemoveAuthorizedCallerDigest(caller); return getNoncedDigest(msgHash, nonce); } + + uint256[50] private __gap; } diff --git a/src/GatewayDebug.sol b/src/GatewayDebug.sol index 3543439..5d9f09c 100644 --- a/src/GatewayDebug.sol +++ b/src/GatewayDebug.sol @@ -84,12 +84,6 @@ contract CommitteeManagementDebug is CommitteeManagement { using EnumerableSet for EnumerableSet.AddressSet; using EnumerableSet for EnumerableSet.Bytes32Set; - constructor( - address[] memory initialMembers, - uint256 initialRequired, - address[] memory initialAuthorizedCallers, - bytes32[] memory initialWatchtowers - ) CommitteeManagement(initialMembers, initialRequired, initialAuthorizedCallers, initialWatchtowers) {} modifier onlyCommittee() { require(isOwner[msg.sender], "only committee member can call"); diff --git a/src/MultiSigVerifier.sol b/src/MultiSigVerifier.sol index 9e055c2..bb17c14 100644 --- a/src/MultiSigVerifier.sol +++ b/src/MultiSigVerifier.sol @@ -1,11 +1,16 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.28; -import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; -import "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; +import { + Initializable +} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import { + MessageHashUtils +} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; /// @title Simple Multi-Signature Verifier with Owner Rotation and Anti-Replay -contract MultiSigVerifier { +contract MultiSigVerifier is Initializable { using ECDSA for bytes32; using MessageHashUtils for bytes32; @@ -16,21 +21,48 @@ contract MultiSigVerifier { uint256 public requiredSignatures; uint256 public nonce; - event OwnersUpdated(address[] newOwners, uint256 newRequired, uint256 newNonce); + event OwnersUpdated( + address[] newOwners, + uint256 newRequired, + uint256 newNonce + ); + + function initialize( + address[] calldata owners, + uint256 _requiredSignatures + ) external initializer { + __MultiSigVerifier_init(owners, _requiredSignatures); + } + + function __MultiSigVerifier_init( + address[] memory owners, + uint256 _requiredSignatures + ) internal onlyInitializing { + __MultiSigVerifier_init_unchained(owners, _requiredSignatures); + } - constructor(address[] memory owners, uint256 _requiredSignatures) { + function __MultiSigVerifier_init_unchained( + address[] memory owners, + uint256 _requiredSignatures + ) internal onlyInitializing { _setOwners(owners, _requiredSignatures); nonce = 0; } /// @notice Verify if a message has enough valid signatures from the current owner set. - function verify(bytes32 messageHash, bytes[] memory signatures) public view returns (bool) { + function verify( + bytes32 messageHash, + bytes[] memory signatures + ) public view returns (bool) { uint256 validSignatures = 0; address[] memory seen = new address[](signatures.length); for (uint256 i = 0; i < signatures.length; i++) { address signer = messageHash.recover(signatures[i]); - if (isOwner[signer] && !_alreadySigned(seen, signer, validSignatures)) { + if ( + isOwner[signer] && + !_alreadySigned(seen, signer, validSignatures) + ) { seen[validSignatures] = signer; validSignatures++; } @@ -39,11 +71,20 @@ contract MultiSigVerifier { } /// @notice Update the owner set and threshold, authorized by the CURRENT owners. - function updateOwners(address[] calldata newOwners, uint256 newRequired, bytes[] calldata signatures) external { + function updateOwners( + address[] calldata newOwners, + uint256 newRequired, + bytes[] calldata signatures + ) external { require(newOwners.length > 0, "Owners required"); - require(newRequired > 0 && newRequired <= newOwners.length, "Invalid threshold"); + require( + newRequired > 0 && newRequired <= newOwners.length, + "Invalid threshold" + ); - bytes32 digest = keccak256(abi.encodePacked(nonce, newOwners, newRequired)); + bytes32 digest = keccak256( + abi.encodePacked(nonce, newOwners, newRequired) + ); require(verify(digest, signatures), "No enough valid owner sigs"); @@ -56,9 +97,15 @@ contract MultiSigVerifier { /// ---------------------------------------------------------------- /// internal helpers /// ---------------------------------------------------------------- - function _setOwners(address[] memory owners, uint256 _requiredSignatures) internal { + function _setOwners( + address[] memory owners, + uint256 _requiredSignatures + ) internal { require(owners.length > 0, "Owners required"); - require(_requiredSignatures > 0 && _requiredSignatures <= owners.length, "Invalid threshold"); + require( + _requiredSignatures > 0 && _requiredSignatures <= owners.length, + "Invalid threshold" + ); for (uint256 i = 0; i < owners.length; i++) { address o = owners[i]; @@ -71,7 +118,10 @@ contract MultiSigVerifier { requiredSignatures = _requiredSignatures; } - function _applyOwners(address[] calldata newOwners, uint256 _requiredSignatures) internal { + function _applyOwners( + address[] calldata newOwners, + uint256 _requiredSignatures + ) internal { // clear old owners for (uint256 i = 0; i < ownerList.length; i++) { isOwner[ownerList[i]] = false; @@ -93,7 +143,11 @@ contract MultiSigVerifier { requiredSignatures = _requiredSignatures; } - function _alreadySigned(address[] memory signers, address signer, uint256 count) internal pure returns (bool) { + function _alreadySigned( + address[] memory signers, + address signer, + uint256 count + ) internal pure returns (bool) { for (uint256 i = 0; i < count; i++) { if (signers[i] == signer) return true; } @@ -103,4 +157,6 @@ contract MultiSigVerifier { function getOwners() external view returns (address[] memory) { return ownerList; } + + uint256[50] private __gap; } diff --git a/src/SequencerSetPublisher.sol b/src/SequencerSetPublisher.sol index 13017d8..11ceb41 100644 --- a/src/SequencerSetPublisher.sol +++ b/src/SequencerSetPublisher.sol @@ -39,7 +39,8 @@ contract SequencerSetPublisher is Initializable, OwnableUpgradeable, ISequencerS assert(initPublisherBTCPubkeys[i].length == 33); publisherBTCPubkeys[initPublishers[i]] = initPublisherBTCPubkeys[i]; } - multiSigVerifier = new MultiSigVerifier(initPublishers, quorum); + multiSigVerifier = new MultiSigVerifier(); + multiSigVerifier.initialize(initPublishers, quorum); heightPublishers[0] = initPublishers; } diff --git a/src/StakeManagement.sol b/src/StakeManagement.sol index a9c0242..0de4528 100644 --- a/src/StakeManagement.sol +++ b/src/StakeManagement.sol @@ -2,12 +2,14 @@ pragma solidity ^0.8.28; import "./interfaces/IStakeManagement.sol"; - +import { + Initializable +} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -contract StakeManagement is IStakeManagement { - IERC20 public immutable stakeToken; - address public immutable gatewayAddress; +contract StakeManagement is IStakeManagement, Initializable { + IERC20 public stakeToken; + address public gatewayAddress; mapping(address => uint256) private stakes; mapping(address => uint256) private lockedStakes; @@ -15,7 +17,16 @@ contract StakeManagement is IStakeManagement { mapping(address => bytes32) public addressToPubkey; // XOnlyPubkey mapping(bytes32 => address) public pubkeyToAddress; - constructor(IERC20 _stakeToken, address _gatewayAddress) { + constructor() { + _disableInitializers(); + } + + function initialize( + IERC20 _stakeToken, + address _gatewayAddress + ) public initializer { + require(address(_stakeToken) != address(0), "stake token zero"); + require(_gatewayAddress != address(0), "gateway zero"); stakeToken = _stakeToken; gatewayAddress = _gatewayAddress; } @@ -24,11 +35,15 @@ contract StakeManagement is IStakeManagement { return address(stakeToken); } - function stakeOf(address operator) external view override returns (uint256) { + function stakeOf( + address operator + ) external view override returns (uint256) { return stakes[operator]; } - function lockedStakeOf(address operator) external view override returns (uint256) { + function lockedStakeOf( + address operator + ) external view override returns (uint256) { return lockedStakes[operator]; } @@ -40,19 +55,27 @@ contract StakeManagement is IStakeManagement { lockedStakes[operator] = 0; } stakes[operator] -= amount; - // Transfer the slashed tokens to the gateway contract, which will handle distribution - require(stakeToken.transfer(gatewayAddress, amount), "stake transfer failed"); + // Transfer the slashed tokens to the gateway which redistributes rewards + require( + stakeToken.transfer(gatewayAddress, amount), + "stake transfer failed" + ); } function lockStake(address operator, uint256 amount) external override { - require(msg.sender == operator || msg.sender == gatewayAddress, "only operator or gateway can lock stake"); - require(stakes[operator] - lockedStakes[operator] >= amount, "insufficient available stake to lock"); + require( + msg.sender == operator || msg.sender == gatewayAddress, + "only operator or gateway can lock stake" + ); + require( + stakes[operator] - lockedStakes[operator] >= amount, + "insufficient available stake to lock" + ); lockedStakes[operator] += amount; } function unlockStake(address operator, uint256 amount) external override { require(msg.sender == gatewayAddress, "only gateway can unlock stake"); - // unlock up to the amount, if less locked, unlock all if (lockedStakes[operator] >= amount) { lockedStakes[operator] -= amount; } else { @@ -61,21 +84,37 @@ contract StakeManagement is IStakeManagement { } function stake(uint256 amount) external { - require(stakeToken.transferFrom(msg.sender, address(this), amount), "stake transfer failed"); + require( + stakeToken.transferFrom(msg.sender, address(this), amount), + "stake transfer failed" + ); stakes[msg.sender] += amount; } function unstake(uint256 amount) external { - require(stakes[msg.sender] - lockedStakes[msg.sender] >= amount, "insufficient available stake to unstake"); + require( + stakes[msg.sender] - lockedStakes[msg.sender] >= amount, + "insufficient available stake to unstake" + ); stakes[msg.sender] -= amount; - require(stakeToken.transfer(msg.sender, amount), "stake transfer failed"); + require( + stakeToken.transfer(msg.sender, amount), + "stake transfer failed" + ); } - // can only register pubkey once, and cannot update function registerPubkey(bytes32 pubkey) external { - require(addressToPubkey[msg.sender] == bytes32(0), "already registered a pubkey"); - require(pubkeyToAddress[pubkey] == address(0), "pubkey already registered by another address"); + require( + addressToPubkey[msg.sender] == bytes32(0), + "already registered a pubkey" + ); + require( + pubkeyToAddress[pubkey] == address(0), + "pubkey already registered by another address" + ); addressToPubkey[msg.sender] = pubkey; pubkeyToAddress[pubkey] = msg.sender; } + + uint256[50] private __gap; } diff --git a/test/MultiSigVerifier.t.sol b/test/MultiSigVerifier.t.sol index 63c74b1..64c29ba 100644 --- a/test/MultiSigVerifier.t.sol +++ b/test/MultiSigVerifier.t.sol @@ -32,7 +32,8 @@ contract MultiSigVerifierTest is Test { message = keccak256("hello world").toEthSignedMessageHash(); // Require at least 2 signatures - verifier = new MultiSigVerifier(owners, 2); + verifier = new MultiSigVerifier(); + verifier.initialize(owners, 2); } function testVerifyWithEnoughSignatures() public view { From 15b8255c8ad2988172a2518de0679874304d9d25 Mon Sep 17 00:00:00 2001 From: Li-Qing Wang Date: Tue, 16 Dec 2025 15:50:03 +0800 Subject: [PATCH 02/11] fix: pass in contract address instead of deploying a implementation --- script/SSPDeploy.s.sol | 4 +++- src/SequencerSetPublisher.sol | 6 +++++- test/SequencerSetPublisher.t.sol | 10 ++++++++-- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/script/SSPDeploy.s.sol b/script/SSPDeploy.s.sol index 2001fb7..ec33db7 100644 --- a/script/SSPDeploy.s.sol +++ b/script/SSPDeploy.s.sol @@ -3,6 +3,7 @@ pragma solidity ^0.8.28; import "forge-std/Script.sol"; import "../src/SequencerSetPublisher.sol"; +import "../src/MultiSigVerifier.sol"; contract Deploy is Script { function run() external { @@ -26,8 +27,9 @@ contract Deploy is Script { vm.startBroadcast(); SequencerSetPublisher publisher = new SequencerSetPublisher(); + MultiSigVerifier multiSigVerifier = new MultiSigVerifier(); - publisher.initialize(initialOwner, initPublishers, initPublisherBTCPubkeys); + publisher.initialize(initialOwner, address(multiSigVerifier), initPublishers, initPublisherBTCPubkeys); vm.stopBroadcast(); diff --git a/src/SequencerSetPublisher.sol b/src/SequencerSetPublisher.sol index 11ceb41..dd5c510 100644 --- a/src/SequencerSetPublisher.sol +++ b/src/SequencerSetPublisher.sol @@ -27,11 +27,16 @@ contract SequencerSetPublisher is Initializable, OwnableUpgradeable, ISequencerS function initialize( address initialOwner, + address multiSigVerifierAddress, address[] calldata initPublishers, bytes[] calldata initPublisherBTCPubkeys ) public initializer { + require(multiSigVerifierAddress != address(0), "Invalid multisig address"); require(initPublisherBTCPubkeys.length == initPublishers.length, "Invalid Publishers"); __Ownable_init(initialOwner); + multiSigVerifier = MultiSigVerifier(multiSigVerifierAddress); + require(multiSigVerifier.ownerCount() == 0, "Verifier already initialized"); + // TODO: deploy a dedicated MultiSigVerifier proxy here once we stop injecting the address externally. // ensure valid sigs >= 2/3 uint256 quorum = (initPublishers.length * 2 + 2) / 3; latestConfirmedHeight = 0; @@ -39,7 +44,6 @@ contract SequencerSetPublisher is Initializable, OwnableUpgradeable, ISequencerS assert(initPublisherBTCPubkeys[i].length == 33); publisherBTCPubkeys[initPublishers[i]] = initPublisherBTCPubkeys[i]; } - multiSigVerifier = new MultiSigVerifier(); multiSigVerifier.initialize(initPublishers, quorum); heightPublishers[0] = initPublishers; } diff --git a/test/SequencerSetPublisher.t.sol b/test/SequencerSetPublisher.t.sol index f46eeae..a9ba3a9 100644 --- a/test/SequencerSetPublisher.t.sol +++ b/test/SequencerSetPublisher.t.sol @@ -66,8 +66,14 @@ contract SequencerSetPublisherTest is Test { initPublishers[2] = vm.addr(batch[2]); sspublisher = new SequencerSetPublisher(); - - sspublisher.initialize(owner, initPublishers, _get_pubkey_from_prvkey(initPublishers.length)); + MultiSigVerifier verifier = new MultiSigVerifier(); + + sspublisher.initialize( + owner, + address(verifier), + initPublishers, + _get_pubkey_from_prvkey(initPublishers.length) + ); } function testInitialize() public view { From 1d1ee96b53547f4f145726fd38fa59442f9ce11d Mon Sep 17 00:00:00 2001 From: Li-Qing Wang Date: Tue, 16 Dec 2025 16:21:32 +0800 Subject: [PATCH 03/11] feat: add interface for Gateway, format code --- script/DeployContractDebug.sol | 16 +- script/DeployGateway.sol | 16 +- script/SSPDeploy.s.sol | 34 +- src/Gateway.sol | 748 ++++++++++++++---------- src/PegBTC.sol | 4 +- src/SequencerSetPublisher.sol | 104 +++- src/StakeManagement.sol | 2 +- src/interfaces/ICommitteeManagement.sol | 14 +- src/interfaces/IGateway.sol | 193 ++++++ test/MultiSigVerifier.t.sol | 25 +- test/SequencerSetPublisher.t.sol | 176 ++++-- 11 files changed, 912 insertions(+), 420 deletions(-) create mode 100644 src/interfaces/IGateway.sol diff --git a/script/DeployContractDebug.sol b/script/DeployContractDebug.sol index 5a74cce..b03608e 100644 --- a/script/DeployContractDebug.sol +++ b/script/DeployContractDebug.sol @@ -6,6 +6,8 @@ import {IPegBTC} from "../src/interfaces/IPegBTC.sol"; import {IBitcoinSPV} from "../src/interfaces/IBitcoinSPV.sol"; import {CommitteeManagement} from "../src/CommitteeManagement.sol"; import {StakeManagement} from "../src/StakeManagement.sol"; +import {ICommitteeManagement} from "../src/interfaces/ICommitteeManagement.sol"; +import {IStakeManagement} from "../src/interfaces/IStakeManagement.sol"; import {GatewayDebug, CommitteeManagementDebug} from "../src/GatewayDebug.sol"; import {PegBTC} from "../src/PegBTC.sol"; @@ -50,21 +52,23 @@ contract DeployGateway is Script { console.log("CommitteeManagement implementation contract address: ", address(committeeImpl)); UpgradeableProxy committeeProxy = new UpgradeableProxy(address(committeeImpl), deployer, ""); console.log("CommitteeManagement proxy contract address: ", address(committeeProxy)); - CommitteeManagement committeeManagement = CommitteeManagement(address(committeeProxy)); - committeeManagement.initialize(initialMembers, initialRequired, initialAuthorizedCallers, initialWatchtowers); + CommitteeManagement committeeManagementImpl = CommitteeManagement(address(committeeProxy)); + committeeManagementImpl.initialize(initialMembers, initialRequired, initialAuthorizedCallers, initialWatchtowers); + ICommitteeManagement committeeManagement = ICommitteeManagement(address(committeeProxy)); StakeManagement stakeImpl = new StakeManagement(); console.log("StakeManagement implementation contract address: ", address(stakeImpl)); UpgradeableProxy stakeProxy = new UpgradeableProxy(address(stakeImpl), deployer, ""); console.log("StakeManagement proxy contract address: ", address(stakeProxy)); - StakeManagement stakeManagement = StakeManagement(address(stakeProxy)); - stakeManagement.initialize(IERC20(address(pegBTC)), address(gateway)); + StakeManagement stakeManagementImpl = StakeManagement(address(stakeProxy)); + stakeManagementImpl.initialize(IERC20(address(pegBTC)), address(gateway)); + IStakeManagement stakeManagement = IStakeManagement(address(stakeProxy)); gateway.initialize( IPegBTC(address(pegBTC)), IBitcoinSPV(bitcoinSPV), - CommitteeManagement(address(committeeManagement)), - StakeManagement(address(stakeManagement)) + committeeManagement, + stakeManagement ); } diff --git a/script/DeployGateway.sol b/script/DeployGateway.sol index d8e6520..259171b 100644 --- a/script/DeployGateway.sol +++ b/script/DeployGateway.sol @@ -6,6 +6,8 @@ import {IPegBTC} from "../src/interfaces/IPegBTC.sol"; import {IBitcoinSPV} from "../src/interfaces/IBitcoinSPV.sol"; import {CommitteeManagement} from "../src/CommitteeManagement.sol"; import {StakeManagement} from "../src/StakeManagement.sol"; +import {ICommitteeManagement} from "../src/interfaces/ICommitteeManagement.sol"; +import {IStakeManagement} from "../src/interfaces/IStakeManagement.sol"; import {GatewayUpgradeable} from "../src/Gateway.sol"; import {PegBTC} from "../src/PegBTC.sol"; @@ -53,22 +55,24 @@ contract DeployGateway is Script { console.log("CommitteeManagement implementation contract address: ", address(committeeImpl)); UpgradeableProxy committeeProxy = new UpgradeableProxy(address(committeeImpl), deployer, ""); console.log("CommitteeManagement proxy contract address: ", address(committeeProxy)); - CommitteeManagement committeeManagement = CommitteeManagement(address(committeeProxy)); - committeeManagement.initialize(initialMembers, initialRequired, initialAuthorizedCallers, initialWatchtowers); + CommitteeManagement committeeManagementImpl = CommitteeManagement(address(committeeProxy)); + committeeManagementImpl.initialize(initialMembers, initialRequired, initialAuthorizedCallers, initialWatchtowers); + ICommitteeManagement committeeManagement = ICommitteeManagement(address(committeeProxy)); // Deploy StakeManagement implementation + proxy StakeManagement stakeImpl = new StakeManagement(); console.log("StakeManagement implementation contract address: ", address(stakeImpl)); UpgradeableProxy stakeProxy = new UpgradeableProxy(address(stakeImpl), deployer, ""); console.log("StakeManagement proxy contract address: ", address(stakeProxy)); - StakeManagement stakeManagement = StakeManagement(address(stakeProxy)); - stakeManagement.initialize(IERC20(address(pegBTC)), address(gateway)); + StakeManagement stakeManagementImpl = StakeManagement(address(stakeProxy)); + stakeManagementImpl.initialize(IERC20(address(pegBTC)), address(gateway)); + IStakeManagement stakeManagement = IStakeManagement(address(stakeProxy)); gateway.initialize( IPegBTC(address(pegBTC)), IBitcoinSPV(bitcoinSPV), - CommitteeManagement(address(committeeManagement)), - StakeManagement(address(stakeManagement)) + committeeManagement, + stakeManagement ); } diff --git a/script/SSPDeploy.s.sol b/script/SSPDeploy.s.sol index ec33db7..d4fbde3 100644 --- a/script/SSPDeploy.s.sol +++ b/script/SSPDeploy.s.sol @@ -1,9 +1,10 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.28; -import "forge-std/Script.sol"; -import "../src/SequencerSetPublisher.sol"; -import "../src/MultiSigVerifier.sol"; +import {Script} from "forge-std/Script.sol"; +import {console} from "forge-std/console.sol"; +import {SequencerSetPublisher} from "../src/SequencerSetPublisher.sol"; +import {MultiSigVerifier} from "../src/MultiSigVerifier.sol"; contract Deploy is Script { function run() external { @@ -18,18 +19,33 @@ contract Deploy is Script { initPublishers[4] = 0xa0F88c27B535615A8D8808c6023986a540161021; bytes[] memory initPublisherBTCPubkeys = new bytes[](5); - initPublisherBTCPubkeys[0] = hex"031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f"; - initPublisherBTCPubkeys[1] = hex"024d4b6cd1361032ca9bd2aeb9d900aa4d45d9ead80ac9423374c451a7254d0766"; - initPublisherBTCPubkeys[2] = hex"02531fe6068134503d2723133227c867ac8fa6c83c537e9a44c3c5bdbdcb1fe337"; - initPublisherBTCPubkeys[3] = hex"03462779ad4aad39514614751a71085f2f10e1c7a593e4e030efb5b8721ce55b0b"; - initPublisherBTCPubkeys[4] = hex"0362c0a046dacce86ddd0343c6d3c7c79c2208ba0d9c9cf24a6d046d21d21f90f7"; + initPublisherBTCPubkeys[ + 0 + ] = hex"031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f"; + initPublisherBTCPubkeys[ + 1 + ] = hex"024d4b6cd1361032ca9bd2aeb9d900aa4d45d9ead80ac9423374c451a7254d0766"; + initPublisherBTCPubkeys[ + 2 + ] = hex"02531fe6068134503d2723133227c867ac8fa6c83c537e9a44c3c5bdbdcb1fe337"; + initPublisherBTCPubkeys[ + 3 + ] = hex"03462779ad4aad39514614751a71085f2f10e1c7a593e4e030efb5b8721ce55b0b"; + initPublisherBTCPubkeys[ + 4 + ] = hex"0362c0a046dacce86ddd0343c6d3c7c79c2208ba0d9c9cf24a6d046d21d21f90f7"; vm.startBroadcast(); SequencerSetPublisher publisher = new SequencerSetPublisher(); MultiSigVerifier multiSigVerifier = new MultiSigVerifier(); - publisher.initialize(initialOwner, address(multiSigVerifier), initPublishers, initPublisherBTCPubkeys); + publisher.initialize( + initialOwner, + address(multiSigVerifier), + initPublishers, + initPublisherBTCPubkeys + ); vm.stopBroadcast(); diff --git a/src/Gateway.sol b/src/Gateway.sol index 7951868..f4d9a92 100644 --- a/src/Gateway.sol +++ b/src/Gateway.sol @@ -1,48 +1,21 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.28; -import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; -import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import { + Initializable +} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; import {IBitcoinSPV} from "./interfaces/IBitcoinSPV.sol"; import {IPegBTC} from "./interfaces/IPegBTC.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import {CommitteeManagement} from "./CommitteeManagement.sol"; -import {StakeManagement} from "./StakeManagement.sol"; +import {ICommitteeManagement} from "./interfaces/ICommitteeManagement.sol"; +import {IStakeManagement} from "./interfaces/IStakeManagement.sol"; +import {IGateway} from "./interfaces/IGateway.sol"; import {Converter} from "./libraries/Converter.sol"; import {BitvmTxParser} from "./libraries/BitvmTxParser.sol"; import {MerkleProof} from "./libraries/MerkleProof.sol"; -// Custom errors to reduce bytecode size (replace long revert strings) -error NotCommittee(); -error NotOperator(); -error InstanceUsed(); -error NotPending(); -error WindowExpired(); -error WindowNotExpired(); -error NotEnoughCommittee(); -error InvalidPubkeyLen(); -error InvalidPubkeyParity(); -error InstanceMismatch(); -error PeginAmountMismatch(); -error InvalidHeader(); -error MerkleVerifyFail(); -error InvalidSignatures(); -error FeeTooHigh(); -error OperatorNotRegistered(); -error StakeInsufficient(); -error GraphAlreadyPosted(); -error GraphPeginTxidMismatch(); -error WithdrawStatusInvalid(); -error NotWithdrawable(); -error TimelockNotExpired(); -error KickoffHeightLow(); -error TxidMismatch(); -error AlreadyDisproved(); -error IndexOutOfRange(); -error UnknownDisproveType(); -error DisproveInvalidHeader(); - contract BitvmPolicy { uint64 constant rateMultiplier = 10000; @@ -60,167 +33,37 @@ contract BitvmPolicy { // TODO Initializer & setters } -contract GatewayUpgradeable is BitvmPolicy, Initializable { +contract GatewayUpgradeable is BitvmPolicy, Initializable, IGateway { using ECDSA for bytes32; // EIP-712-like typehash constants to avoid recomputing literals bytes32 private constant POST_PEGIN_TYPEHASH = - keccak256("POST_PEGIN_DATA(address contract,bytes16 instanceId,bytes32 peginTxid)"); + keccak256( + "POST_PEGIN_DATA(address contract,bytes16 instanceId,bytes32 peginTxid)" + ); bytes32 private constant POST_GRAPH_TYPEHASH = - keccak256("POST_GRAPH_DATA(address contract,bytes16 instanceId,bytes16 graphId,bytes32 graphDataHash)"); - bytes32 private constant CANCEL_WITHDRAW_TYPEHASH = keccak256("CANCEL_WITHDRAW(address contract,bytes16 graphId)"); + keccak256( + "POST_GRAPH_DATA(address contract,bytes16 instanceId,bytes16 graphId,bytes32 graphDataHash)" + ); + bytes32 private constant CANCEL_WITHDRAW_TYPEHASH = + keccak256("CANCEL_WITHDRAW(address contract,bytes16 graphId)"); bytes32 private constant UNLOCK_STAKE_TYPEHASH = - keccak256("UNLOCK_OPERATOR_STAKE(address contract,address operator,uint256 amount)"); - - event BridgeInRequest( - bytes16 indexed instanceId, - address indexed depositorAddress, - uint64 peginAmountSats, - uint64[3] txnFees, - Utxo[] userInputs, - bytes32 userXonlyPubkey, - string userChangeAddress, - string userRefundAddress - ); - event CommitteeResponse(bytes16 indexed instanceId, address indexed committeeAddress, bytes committeePubkey); - event BridgeIn( - address indexed depositorAddress, - bytes16 indexed instanceId, - uint64 indexed peginAmountSats, - uint64 feeAmountSats - ); - event PostGraphData(bytes16 indexed instanceId, bytes16 indexed graphId); - event InitWithdraw( - bytes16 indexed instanceId, bytes16 indexed graphId, address indexed operatorAddress, uint64 withdrawAmountSats - ); - event CancelWithdraw(bytes16 indexed instanceId, bytes16 indexed graphId, address indexed triggerAddress); - event ProceedWithdraw(bytes16 indexed instanceId, bytes16 indexed graphId, bytes32 kickoffTxid); - event WithdrawHappyPath( - bytes16 indexed instanceId, - bytes16 indexed graphId, - bytes32 take1Txid, - address indexed operatorAddress, - uint64 rewardAmountSats - ); - event WithdrawUnhappyPath( - bytes16 indexed instanceId, - bytes16 indexed graphId, - bytes32 take2Txid, - address indexed operatorAddress, - uint64 rewardAmountSats - ); - event WithdrawDisproved( - bytes16 indexed instanceId, - bytes16 indexed graphId, - DisproveTxType disproveTxType, - uint256 txnIndex, - bytes32 challengeStartTxid, - bytes32 challengeFinishTxid, - address challengerAddress, - address disproverAddress, - uint64 challengerRewardAmount, - uint64 disproverRewardAmount - ); - - enum DisproveTxType { - AssertTimeout, - OperatorCommitTimeout, - OperatorNack, - Disprove, - QuickChallenge, - ChallengeIncompeleteKickoff - } - enum PeginStatus { - None, - Pending, - Withdrawbale, - Processing, - Locked, - Claimed, - Discarded - } - enum WithdrawStatus { - None, - Processing, - Initialized, - Canceled, - Complete, - Disproved - } - - struct Utxo { - bytes32 txid; - uint32 vout; - uint64 amountSats; - } - - struct PeginDataInner { - PeginStatus status; - bytes16 instanceId; - address depositorAddress; - uint64 peginAmountSats; - uint64[3] txnFees; - Utxo[] userInputs; - bytes32 userXonlyPubkey; - string userChangeAddress; - string userRefundAddress; - bytes32 peginTxid; - uint256 createdAt; - // EnumerableMap - address[] committeeAddresses; - mapping(address value => uint256) committeeAddressPositions; - mapping(address => bytes1) committeePubkeyParitys; // even (0x02), odd (0x03) - mapping(address => bytes32) committeeXonlyPubkeys; - } - - struct PeginData { - PeginStatus status; - bytes16 instanceId; - address depositorAddress; - uint64 peginAmountSats; - uint64[3] txnFees; // [ peginPrepare , peginConfirm peginCancel ] - Utxo[] userInputs; - bytes32 userXonlyPubkey; - string userChangeAddress; - string userRefundAddress; - bytes32 peginTxid; - uint256 createdAt; - address[] committeeAddresses; - bytes[] committeePubkeys; - } - - struct WithdrawData { - WithdrawStatus status; - bytes32 peginTxid; - address operatorAddress; - bytes16 instanceId; - uint256 lockAmount; - uint256 btcBlockHeightAtWithdraw; - } - - struct GraphData { - bytes1 operatorPubkeyPrefix; - bytes32 operatorPubkey; - bytes32 peginTxid; - bytes32 kickoffTxid; - bytes32 take1Txid; - bytes32 take2Txid; - bytes32 commitTimoutTxid; - bytes32[] assertTimoutTxids; - bytes32[] NackTxids; - } + keccak256( + "UNLOCK_OPERATOR_STAKE(address contract,address operator,uint256 amount)" + ); IPegBTC public pegBTC; IBitcoinSPV public bitcoinSPV; - CommitteeManagement public committeeManagement; - StakeManagement public stakeManagement; + ICommitteeManagement public committeeManagement; + IStakeManagement public stakeManagement; uint256 public responseWindowBlocks = 40; // 40 goat blocks ~ 2 minutes uint256 public cancelWithdrawTimelock = 144; // 144 btc blocks ~ 24 hours bytes16[] public instanceIds; - mapping(bytes16 instanceId => bytes16[] graphIds) public instanceIdToGraphIds; + mapping(bytes16 instanceId => bytes16[] graphIds) + public instanceIdToGraphIds; mapping(bytes16 instanceId => PeginDataInner) public peginDataMap; mapping(bytes16 graphId => GraphData) public graphDataMap; mapping(bytes16 graphId => WithdrawData) public withdrawDataMap; @@ -229,8 +72,8 @@ contract GatewayUpgradeable is BitvmPolicy, Initializable { function initialize( IPegBTC _pegBTC, IBitcoinSPV _bitcoinSPV, - CommitteeManagement _committeeManagement, - StakeManagement _stakeManagement + ICommitteeManagement _committeeManagement, + IStakeManagement _stakeManagement ) external initializer { // set initial parameters minChallengeAmountSats = 1000000; // 0.01 BTC @@ -254,39 +97,46 @@ contract GatewayUpgradeable is BitvmPolicy, Initializable { } // getters - function getGraphIdsByInstanceId(bytes16 instanceId) external view returns (bytes16[] memory) { + function getGraphIdsByInstanceId( + bytes16 instanceId + ) external view returns (bytes16[] memory) { return instanceIdToGraphIds[instanceId]; } - function getPeginData(bytes16 instanceId) external view returns (PeginData memory) { + function getPeginData( + bytes16 instanceId + ) external view returns (PeginData memory) { PeginDataInner storage data = peginDataMap[instanceId]; - return PeginData({ - status: data.status, - instanceId: data.instanceId, - depositorAddress: data.depositorAddress, - peginAmountSats: data.peginAmountSats, - txnFees: data.txnFees, - userInputs: data.userInputs, - userXonlyPubkey: data.userXonlyPubkey, - userChangeAddress: data.userChangeAddress, - userRefundAddress: data.userRefundAddress, - peginTxid: data.peginTxid, - createdAt: data.createdAt, - committeeAddresses: data.committeeAddresses, - committeePubkeys: getCommitteePubkeysUnsafe(instanceId) - }); - } - - function getGraphData(bytes16 graphId) external view returns (GraphData memory) { + return + PeginData({ + status: data.status, + instanceId: data.instanceId, + depositorAddress: data.depositorAddress, + peginAmountSats: data.peginAmountSats, + txnFees: data.txnFees, + userInputs: data.userInputs, + userXonlyPubkey: data.userXonlyPubkey, + userChangeAddress: data.userChangeAddress, + userRefundAddress: data.userRefundAddress, + peginTxid: data.peginTxid, + createdAt: data.createdAt, + committeeAddresses: data.committeeAddresses, + committeePubkeys: getCommitteePubkeysUnsafe(instanceId) + }); + } + + function getGraphData( + bytes16 graphId + ) external view returns (GraphData memory) { return graphDataMap[graphId]; } // helpers - function verifyCommitteeSignatures(bytes32 msgHash, bytes[] memory signatures, address[] memory members) - public - pure - returns (bool) - { + function verifyCommitteeSignatures( + bytes32 msgHash, + bytes[] memory signatures, + address[] memory members + ) public pure returns (bool) { address[] memory signers = new address[](signatures.length); for (uint256 i = 0; i < signatures.length; i++) { address signer = msgHash.recover(signatures[i]); @@ -308,48 +158,89 @@ contract GatewayUpgradeable is BitvmPolicy, Initializable { return true; } - function getPostPeginDigest(bytes16 instanceId, bytes32 peginTxid) public view returns (bytes32) { - return keccak256(abi.encode(POST_PEGIN_TYPEHASH, address(this), instanceId, peginTxid)); + function getPostPeginDigest( + bytes16 instanceId, + bytes32 peginTxid + ) public view returns (bytes32) { + return + keccak256( + abi.encode( + POST_PEGIN_TYPEHASH, + address(this), + instanceId, + peginTxid + ) + ); } - function getPostGraphDigest(bytes16 instanceId, bytes16 graphId, GraphData calldata graphData) - public - view - returns (bytes32) - { + function getPostGraphDigest( + bytes16 instanceId, + bytes16 graphId, + GraphData calldata graphData + ) public view returns (bytes32) { bytes32 graphDataHash = keccak256(abi.encode(graphData)); - return keccak256(abi.encode(POST_GRAPH_TYPEHASH, address(this), instanceId, graphId, graphDataHash)); + return + keccak256( + abi.encode( + POST_GRAPH_TYPEHASH, + address(this), + instanceId, + graphId, + graphDataHash + ) + ); } - function getCancelWithdrawDigest(bytes16 graphId) internal view returns (bytes32) { - return keccak256(abi.encode(CANCEL_WITHDRAW_TYPEHASH, address(this), graphId)); + function getCancelWithdrawDigest( + bytes16 graphId + ) internal view returns (bytes32) { + return + keccak256( + abi.encode(CANCEL_WITHDRAW_TYPEHASH, address(this), graphId) + ); } - function getCancelWithdrawDigestNonced(bytes16 graphId, uint256 nonce) public view returns (bytes32) { + function getCancelWithdrawDigestNonced( + bytes16 graphId, + uint256 nonce + ) public view returns (bytes32) { bytes32 msgHash = getCancelWithdrawDigest(graphId); return committeeManagement.getNoncedDigest(msgHash, nonce); } - function getUnlockStakeDigest(address operator, uint256 amount) internal view returns (bytes32) { - return keccak256(abi.encode(UNLOCK_STAKE_TYPEHASH, address(this), operator, amount)); + function getUnlockStakeDigest( + address operator, + uint256 amount + ) internal view returns (bytes32) { + return + keccak256( + abi.encode( + UNLOCK_STAKE_TYPEHASH, + address(this), + operator, + amount + ) + ); } - function getUnlockStakeDigestNonced(address operator, uint256 amount, uint256 nonce) - public - view - returns (bytes32) - { + function getUnlockStakeDigestNonced( + address operator, + uint256 amount, + uint256 nonce + ) public view returns (bytes32) { bytes32 msgHash = getUnlockStakeDigest(operator, amount); return committeeManagement.getNoncedDigest(msgHash, nonce); } modifier onlyCommittee() { - if (!committeeManagement.isCommitteeMember(msg.sender)) revert NotCommittee(); + if (!committeeManagement.isCommitteeMember(msg.sender)) + revert NotCommittee(); _; } modifier onlyOperator(bytes16 graphId) { - if (withdrawDataMap[graphId].operatorAddress != msg.sender) revert NotOperator(); + if (withdrawDataMap[graphId].operatorAddress != msg.sender) + revert NotOperator(); _; } @@ -392,13 +283,18 @@ contract GatewayUpgradeable is BitvmPolicy, Initializable { ); } - function answerPeginRequest(bytes16 instanceId, bytes memory committeePubkey) external onlyCommittee { + function answerPeginRequest( + bytes16 instanceId, + bytes memory committeePubkey + ) external onlyCommittee { PeginDataInner storage peginData = peginDataMap[instanceId]; if (peginData.status != PeginStatus.Pending) revert NotPending(); - if (peginData.createdAt + responseWindowBlocks < block.number) revert WindowExpired(); + if (peginData.createdAt + responseWindowBlocks < block.number) + revert WindowExpired(); if (committeePubkey.length != 33) revert InvalidPubkeyLen(); bytes1 committeePubkeyParity = committeePubkey[0]; - if (!(committeePubkeyParity == 0x02 || committeePubkeyParity == 0x03)) revert InvalidPubkeyParity(); + if (!(committeePubkeyParity == 0x02 || committeePubkeyParity == 0x03)) + revert InvalidPubkeyParity(); bytes32 committeeXonlyPubkey; assembly { committeeXonlyPubkey := mload(add(committeePubkey, 0x21)) @@ -408,33 +304,55 @@ contract GatewayUpgradeable is BitvmPolicy, Initializable { if (peginData.committeeAddressPositions[committeeAddress] == 0) { peginData.committeeAddresses.push(committeeAddress); // The value is stored at length-1, but we add 1 to all indexes and use 0 as a sentinel value - peginData.committeeAddressPositions[committeeAddress] = peginData.committeeAddresses.length; + peginData.committeeAddressPositions[committeeAddress] = peginData + .committeeAddresses + .length; } - peginData.committeePubkeyParitys[committeeAddress] = committeePubkeyParity; - peginData.committeeXonlyPubkeys[committeeAddress] = committeeXonlyPubkey; + peginData.committeePubkeyParitys[ + committeeAddress + ] = committeePubkeyParity; + peginData.committeeXonlyPubkeys[ + committeeAddress + ] = committeeXonlyPubkey; emit CommitteeResponse(instanceId, committeeAddress, committeePubkey); } - function getCommitteePubkeys(bytes16 instanceId) public view returns (bytes[] memory committeePubkeys) { - if (peginDataMap[instanceId].createdAt + responseWindowBlocks >= block.number) revert WindowNotExpired(); + function getCommitteePubkeys( + bytes16 instanceId + ) public view returns (bytes[] memory committeePubkeys) { + if ( + peginDataMap[instanceId].createdAt + responseWindowBlocks >= + block.number + ) revert WindowNotExpired(); committeePubkeys = getCommitteePubkeysUnsafe(instanceId); - if (committeePubkeys.length < committeeManagement.quorumSize()) revert NotEnoughCommittee(); + if (committeePubkeys.length < committeeManagement.quorumSize()) + revert NotEnoughCommittee(); } - function getCommitteeAddresses(bytes16 instanceId) public view returns (address[] memory committeeAddresses) { - if (peginDataMap[instanceId].createdAt + responseWindowBlocks >= block.number) revert WindowNotExpired(); + function getCommitteeAddresses( + bytes16 instanceId + ) public view returns (address[] memory committeeAddresses) { + if ( + peginDataMap[instanceId].createdAt + responseWindowBlocks >= + block.number + ) revert WindowNotExpired(); committeeAddresses = peginDataMap[instanceId].committeeAddresses; - if (committeeAddresses.length < committeeManagement.quorumSize()) revert NotEnoughCommittee(); + if (committeeAddresses.length < committeeManagement.quorumSize()) + revert NotEnoughCommittee(); } - function getCommitteePubkeysUnsafe(bytes16 instanceId) public view returns (bytes[] memory committeePubkeys) { + function getCommitteePubkeysUnsafe( + bytes16 instanceId + ) public view returns (bytes[] memory committeePubkeys) { PeginDataInner storage peginData = peginDataMap[instanceId]; committeePubkeys = new bytes[](peginData.committeeAddresses.length); for (uint256 i = 0; i < peginData.committeeAddresses.length; ++i) { address committeeAddress = peginData.committeeAddresses[i]; bytes1 parity = peginData.committeePubkeyParitys[committeeAddress]; - bytes32 XonlyPubkeys = peginData.committeeXonlyPubkeys[committeeAddress]; + bytes32 XonlyPubkeys = peginData.committeeXonlyPubkeys[ + committeeAddress + ]; committeePubkeys[i] = abi.encodePacked(parity, XonlyPubkeys); } } @@ -449,21 +367,41 @@ contract GatewayUpgradeable is BitvmPolicy, Initializable { ) external onlyCommittee { PeginDataInner storage peginData = peginDataMap[instanceId]; if (peginData.status != PeginStatus.Pending) revert NotPending(); - (bytes32 peginTxid, uint64 peginAmountSats, address depositorAddress, bytes16 parsedInstanceId) = - BitvmTxParser.parsePegin(rawPeginTx); + ( + bytes32 peginTxid, + uint64 peginAmountSats, + address depositorAddress, + bytes16 parsedInstanceId + ) = BitvmTxParser.parsePegin(rawPeginTx); if (parsedInstanceId != instanceId) revert InstanceMismatch(); - if (peginAmountSats != peginData.peginAmountSats) revert PeginAmountMismatch(); + if (peginAmountSats != peginData.peginAmountSats) + revert PeginAmountMismatch(); // validate pegin tx - (bytes32 blockHash, bytes32 merkleRoot) = MerkleProof.parseBtcBlockHeader(peginProof.rawHeader); - if (bitcoinSPV.blockHash(peginProof.height) != blockHash) revert InvalidHeader(); - if (!MerkleProof.verifyMerkleProof(merkleRoot, peginProof.proof, peginTxid, peginProof.index)) { + (bytes32 blockHash, bytes32 merkleRoot) = MerkleProof + .parseBtcBlockHeader(peginProof.rawHeader); + if (bitcoinSPV.blockHash(peginProof.height) != blockHash) + revert InvalidHeader(); + if ( + !MerkleProof.verifyMerkleProof( + merkleRoot, + peginProof.proof, + peginTxid, + peginProof.index + ) + ) { revert MerkleVerifyFail(); } // validate committeeSigs bytes32 pegin_digest = getPostPeginDigest(instanceId, peginTxid); - if (!verifyCommitteeSignatures(pegin_digest, committeeSigs, getCommitteeAddresses(instanceId))) { + if ( + !verifyCommitteeSignatures( + pegin_digest, + committeeSigs, + getCommitteeAddresses(instanceId) + ) + ) { revert InvalidSignatures(); } @@ -473,12 +411,22 @@ contract GatewayUpgradeable is BitvmPolicy, Initializable { // mint pegBTC to user // deduct a fee from the User to cover the Operator's peg-out reward - uint64 feeAmountSats = minPeginFeeSats + peginAmountSats * peginFeeRate / rateMultiplier; + uint64 feeAmountSats = minPeginFeeSats + + (peginAmountSats * peginFeeRate) / + rateMultiplier; if (feeAmountSats >= peginAmountSats) revert FeeTooHigh(); - pegBTC.mint(depositorAddress, Converter.amountFromSats(peginAmountSats - feeAmountSats)); + pegBTC.mint( + depositorAddress, + Converter.amountFromSats(peginAmountSats - feeAmountSats) + ); pegBTC.mint(address(this), Converter.amountFromSats(feeAmountSats)); - emit BridgeIn(depositorAddress, instanceId, peginAmountSats, feeAmountSats); + emit BridgeIn( + depositorAddress, + instanceId, + peginAmountSats, + feeAmountSats + ); } function postGraphData( @@ -489,20 +437,35 @@ contract GatewayUpgradeable is BitvmPolicy, Initializable { ) public onlyCommittee { // check operator stake // Note:committee should check operator's locked stake before pre-signed any graph txns - address operatorStakeAddress = stakeManagement.pubkeyToAddress(graphData.operatorPubkey); + address operatorStakeAddress = stakeManagement.pubkeyToAddress( + graphData.operatorPubkey + ); if (operatorStakeAddress == address(0)) revert OperatorNotRegistered(); - if (stakeManagement.lockedStakeOf(operatorStakeAddress) < minStakeAmount) revert StakeInsufficient(); + if ( + stakeManagement.lockedStakeOf(operatorStakeAddress) < minStakeAmount + ) revert StakeInsufficient(); // check committeeSigs - bytes32 graph_digest = getPostGraphDigest(instanceId, graphId, graphData); - if (!verifyCommitteeSignatures(graph_digest, committeeSigs, getCommitteeAddresses(instanceId))) { + bytes32 graph_digest = getPostGraphDigest( + instanceId, + graphId, + graphData + ); + if ( + !verifyCommitteeSignatures( + graph_digest, + committeeSigs, + getCommitteeAddresses(instanceId) + ) + ) { revert InvalidSignatures(); } // check graph data if (graphDataMap[graphId].peginTxid != 0) revert GraphAlreadyPosted(); PeginDataInner storage peginData = peginDataMap[instanceId]; - if (graphData.peginTxid != peginData.peginTxid) revert GraphPeginTxidMismatch(); + if (graphData.peginTxid != peginData.peginTxid) + revert GraphPeginTxidMismatch(); // store graph data graphDataMap[graphId] = graphData; @@ -513,17 +476,23 @@ contract GatewayUpgradeable is BitvmPolicy, Initializable { function initWithdraw(bytes16 instanceId, bytes16 graphId) external { WithdrawData storage withdrawData = withdrawDataMap[graphId]; - if (!(withdrawData.status == WithdrawStatus.None || withdrawData.status == WithdrawStatus.Canceled)) { + if ( + !(withdrawData.status == WithdrawStatus.None || + withdrawData.status == WithdrawStatus.Canceled) + ) { revert WithdrawStatusInvalid(); } PeginDataInner storage peginData = peginDataMap[instanceId]; - if (peginData.status != PeginStatus.Withdrawbale) revert NotWithdrawable(); + if (peginData.status != PeginStatus.Withdrawbale) + revert NotWithdrawable(); // lock the pegin utxo so others can not withdraw it peginData.status = PeginStatus.Locked; // lock operator's pegBTC - uint256 lockAmount = Converter.amountFromSats(peginData.peginAmountSats); + uint256 lockAmount = Converter.amountFromSats( + peginData.peginAmountSats + ); pegBTC.transferFrom(msg.sender, address(this), lockAmount); withdrawData.peginTxid = peginData.peginTxid; @@ -533,14 +502,25 @@ contract GatewayUpgradeable is BitvmPolicy, Initializable { withdrawData.lockAmount = lockAmount; withdrawData.btcBlockHeightAtWithdraw = bitcoinSPV.latestHeight(); - emit InitWithdraw(instanceId, graphId, withdrawData.operatorAddress, peginData.peginAmountSats); + emit InitWithdraw( + instanceId, + graphId, + withdrawData.operatorAddress, + peginData.peginAmountSats + ); } function cancelWithdraw(bytes16 graphId) external onlyOperator(graphId) { WithdrawData storage withdrawData = withdrawDataMap[graphId]; - PeginDataInner storage peginData = peginDataMap[withdrawData.instanceId]; - if (withdrawData.status != WithdrawStatus.Initialized) revert WithdrawStatusInvalid(); - if (withdrawData.btcBlockHeightAtWithdraw + cancelWithdrawTimelock >= bitcoinSPV.latestHeight()) { + PeginDataInner storage peginData = peginDataMap[ + withdrawData.instanceId + ]; + if (withdrawData.status != WithdrawStatus.Initialized) + revert WithdrawStatusInvalid(); + if ( + withdrawData.btcBlockHeightAtWithdraw + cancelWithdrawTimelock >= + bitcoinSPV.latestHeight() + ) { revert TimelockNotExpired(); } withdrawData.status = WithdrawStatus.Canceled; @@ -550,14 +530,25 @@ contract GatewayUpgradeable is BitvmPolicy, Initializable { emit CancelWithdraw(withdrawData.instanceId, graphId, msg.sender); } - function committeeCancelWithdraw(bytes16 graphId, uint256 nonce, bytes[] calldata committeeSigs) external { + function committeeCancelWithdraw( + bytes16 graphId, + uint256 nonce, + bytes[] calldata committeeSigs + ) external { // validate committeeSigs WithdrawData storage withdrawData = withdrawDataMap[graphId]; bytes32 cancel_digest = getCancelWithdrawDigest(graphId); - committeeManagement.executeNoncedSignatures(cancel_digest, nonce, committeeSigs); + committeeManagement.executeNoncedSignatures( + cancel_digest, + nonce, + committeeSigs + ); // update storage - PeginDataInner storage peginData = peginDataMap[withdrawData.instanceId]; - if (withdrawData.status != WithdrawStatus.Initialized) revert WithdrawStatusInvalid(); + PeginDataInner storage peginData = peginDataMap[ + withdrawData.instanceId + ]; + if (withdrawData.status != WithdrawStatus.Initialized) + revert WithdrawStatusInvalid(); withdrawData.status = WithdrawStatus.Canceled; pegBTC.transfer(msg.sender, withdrawData.lockAmount); peginData.status = PeginStatus.Withdrawbale; @@ -572,15 +563,26 @@ contract GatewayUpgradeable is BitvmPolicy, Initializable { ) external onlyCommittee { WithdrawData storage withdrawData = withdrawDataMap[graphId]; bytes16 instanceId = withdrawData.instanceId; - if (withdrawData.status != WithdrawStatus.Initialized) revert WithdrawStatusInvalid(); - if (withdrawData.btcBlockHeightAtWithdraw >= kickoffProof.height) revert KickoffHeightLow(); + if (withdrawData.status != WithdrawStatus.Initialized) + revert WithdrawStatusInvalid(); + if (withdrawData.btcBlockHeightAtWithdraw >= kickoffProof.height) + revert KickoffHeightLow(); GraphData storage graphData = graphDataMap[graphId]; bytes32 kickoffTxid = BitvmTxParser.computeTxid(rawKickoffTx); if (kickoffTxid != graphData.kickoffTxid) revert TxidMismatch(); - (bytes32 blockHash, bytes32 merkleRoot) = MerkleProof.parseBtcBlockHeader(kickoffProof.rawHeader); - if (bitcoinSPV.blockHash(kickoffProof.height) != blockHash) revert InvalidHeader(); - if (!MerkleProof.verifyMerkleProof(merkleRoot, kickoffProof.proof, kickoffTxid, kickoffProof.index)) { + (bytes32 blockHash, bytes32 merkleRoot) = MerkleProof + .parseBtcBlockHeader(kickoffProof.rawHeader); + if (bitcoinSPV.blockHash(kickoffProof.height) != blockHash) + revert InvalidHeader(); + if ( + !MerkleProof.verifyMerkleProof( + merkleRoot, + kickoffProof.proof, + kickoffTxid, + kickoffProof.index + ) + ) { revert MerkleVerifyFail(); } @@ -601,14 +603,24 @@ contract GatewayUpgradeable is BitvmPolicy, Initializable { WithdrawData storage withdrawData = withdrawDataMap[graphId]; bytes16 instanceId = withdrawData.instanceId; PeginDataInner storage peginData = peginDataMap[instanceId]; - if (withdrawData.status != WithdrawStatus.Processing) revert WithdrawStatusInvalid(); + if (withdrawData.status != WithdrawStatus.Processing) + revert WithdrawStatusInvalid(); GraphData storage graphData = graphDataMap[graphId]; bytes32 take1Txid = BitvmTxParser.computeTxid(rawTake1Tx); if (take1Txid != graphData.take1Txid) revert TxidMismatch(); - (bytes32 blockHash, bytes32 merkleRoot) = MerkleProof.parseBtcBlockHeader(take1Proof.rawHeader); - if (bitcoinSPV.blockHash(take1Proof.height) != blockHash) revert InvalidHeader(); - if (!MerkleProof.verifyMerkleProof(merkleRoot, take1Proof.proof, take1Txid, take1Proof.index)) { + (bytes32 blockHash, bytes32 merkleRoot) = MerkleProof + .parseBtcBlockHeader(take1Proof.rawHeader); + if (bitcoinSPV.blockHash(take1Proof.height) != blockHash) + revert InvalidHeader(); + if ( + !MerkleProof.verifyMerkleProof( + merkleRoot, + take1Proof.proof, + take1Txid, + take1Proof.index + ) + ) { revert MerkleVerifyFail(); } @@ -616,11 +628,21 @@ contract GatewayUpgradeable is BitvmPolicy, Initializable { withdrawData.status = WithdrawStatus.Complete; // incentive mechanism for honest Operators - uint64 rewardAmountSats = - minOperatorRewardSats + peginData.peginAmountSats * operatorRewardRate / rateMultiplier; - pegBTC.transfer(withdrawData.operatorAddress, Converter.amountFromSats(rewardAmountSats)); + uint64 rewardAmountSats = minOperatorRewardSats + + (peginData.peginAmountSats * operatorRewardRate) / + rateMultiplier; + pegBTC.transfer( + withdrawData.operatorAddress, + Converter.amountFromSats(rewardAmountSats) + ); - emit WithdrawHappyPath(instanceId, graphId, take1Txid, withdrawData.operatorAddress, rewardAmountSats); + emit WithdrawHappyPath( + instanceId, + graphId, + take1Txid, + withdrawData.operatorAddress, + rewardAmountSats + ); } function finishWithdrawUnhappyPath( @@ -631,14 +653,24 @@ contract GatewayUpgradeable is BitvmPolicy, Initializable { WithdrawData storage withdrawData = withdrawDataMap[graphId]; bytes16 instanceId = withdrawData.instanceId; PeginDataInner storage peginData = peginDataMap[instanceId]; - if (withdrawData.status != WithdrawStatus.Processing) revert WithdrawStatusInvalid(); + if (withdrawData.status != WithdrawStatus.Processing) + revert WithdrawStatusInvalid(); GraphData storage graphData = graphDataMap[graphId]; bytes32 take2Txid = BitvmTxParser.computeTxid(rawTake2Tx); if (take2Txid != graphData.take2Txid) revert TxidMismatch(); - (bytes32 blockHash, bytes32 merkleRoot) = MerkleProof.parseBtcBlockHeader(take2Proof.rawHeader); - if (bitcoinSPV.blockHash(take2Proof.height) != blockHash) revert InvalidHeader(); - if (!MerkleProof.verifyMerkleProof(merkleRoot, take2Proof.proof, take2Txid, take2Proof.index)) { + (bytes32 blockHash, bytes32 merkleRoot) = MerkleProof + .parseBtcBlockHeader(take2Proof.rawHeader); + if (bitcoinSPV.blockHash(take2Proof.height) != blockHash) + revert InvalidHeader(); + if ( + !MerkleProof.verifyMerkleProof( + merkleRoot, + take2Proof.proof, + take2Txid, + take2Proof.index + ) + ) { revert MerkleVerifyFail(); } @@ -646,11 +678,21 @@ contract GatewayUpgradeable is BitvmPolicy, Initializable { withdrawData.status = WithdrawStatus.Complete; // incentive mechanism for honest Operators - uint64 rewardAmountSats = - minOperatorRewardSats + peginData.peginAmountSats * operatorRewardRate / rateMultiplier; - pegBTC.transfer(withdrawData.operatorAddress, Converter.amountFromSats(rewardAmountSats)); + uint64 rewardAmountSats = minOperatorRewardSats + + (peginData.peginAmountSats * operatorRewardRate) / + rateMultiplier; + pegBTC.transfer( + withdrawData.operatorAddress, + Converter.amountFromSats(rewardAmountSats) + ); - emit WithdrawUnhappyPath(instanceId, graphId, take2Txid, withdrawData.operatorAddress, rewardAmountSats); + emit WithdrawUnhappyPath( + instanceId, + graphId, + take2Txid, + withdrawData.operatorAddress, + rewardAmountSats + ); } // if no challengeStartTx happens (for QuickChallenge & ChallengeIncompeleteKickoff), set rawChallengeStartTx.inputVector to empty @@ -667,7 +709,8 @@ contract GatewayUpgradeable is BitvmPolicy, Initializable { GraphData storage graphData = graphDataMap[graphId]; bytes16 instanceId = withdrawData.instanceId; // Malicious operator may skip initWithdraw & procceedWithdraw - if (withdrawData.status == WithdrawStatus.Disproved) revert AlreadyDisproved(); + if (withdrawData.status == WithdrawStatus.Disproved) + revert AlreadyDisproved(); // verify ChallengeStart tx bytes32 challengeStartTxid; @@ -677,22 +720,32 @@ contract GatewayUpgradeable is BitvmPolicy, Initializable { bytes32 blockHash; bytes32 merkleRoot; if ( - ( - disproveTxType == DisproveTxType.QuickChallenge - || disproveTxType == DisproveTxType.ChallengeIncompeleteKickoff - ) && (rawChallengeStartTx.inputVector.length == 0) + (disproveTxType == DisproveTxType.QuickChallenge || + disproveTxType == DisproveTxType.ChallengeIncompeleteKickoff) && + (rawChallengeStartTx.inputVector.length == 0) ) { // no challenge start tx } else { - (challengeStartTxid, kickoffTxid, kickoffVout, challengerAddress) = - BitvmTxParser.parseChallengeTx(rawChallengeStartTx); + ( + challengeStartTxid, + kickoffTxid, + kickoffVout, + challengerAddress + ) = BitvmTxParser.parseChallengeTx(rawChallengeStartTx); if (kickoffTxid != graphData.kickoffTxid) revert TxidMismatch(); - if (kickoffVout != BitvmTxParser.CHALLENGE_CONNECTOR_VOUT) revert TxidMismatch(); - (blockHash, merkleRoot) = MerkleProof.parseBtcBlockHeader(challengeStartTxProof.rawHeader); - if (bitcoinSPV.blockHash(challengeStartTxProof.height) != blockHash) revert DisproveInvalidHeader(); + if (kickoffVout != BitvmTxParser.CHALLENGE_CONNECTOR_VOUT) + revert TxidMismatch(); + (blockHash, merkleRoot) = MerkleProof.parseBtcBlockHeader( + challengeStartTxProof.rawHeader + ); + if (bitcoinSPV.blockHash(challengeStartTxProof.height) != blockHash) + revert DisproveInvalidHeader(); if ( !MerkleProof.verifyMerkleProof( - merkleRoot, challengeStartTxProof.proof, challengeStartTxid, challengeStartTxProof.index + merkleRoot, + challengeStartTxProof.proof, + challengeStartTxid, + challengeStartTxProof.index ) ) revert MerkleVerifyFail(); } @@ -701,46 +754,84 @@ contract GatewayUpgradeable is BitvmPolicy, Initializable { bytes32 challengeFinishTxid; address disproverAddress; if (disproveTxType == DisproveTxType.AssertTimeout) { - (challengeFinishTxid) = BitvmTxParser.computeTxid(rawChallengeFinishTx); - if (graphData.assertTimoutTxids.length <= txnIndex) revert IndexOutOfRange(); - if (challengeFinishTxid != graphData.assertTimoutTxids[txnIndex]) revert TxidMismatch(); + (challengeFinishTxid) = BitvmTxParser.computeTxid( + rawChallengeFinishTx + ); + if (graphData.assertTimoutTxids.length <= txnIndex) + revert IndexOutOfRange(); + if (challengeFinishTxid != graphData.assertTimoutTxids[txnIndex]) + revert TxidMismatch(); } else if (disproveTxType == DisproveTxType.OperatorCommitTimeout) { - (challengeFinishTxid) = BitvmTxParser.computeTxid(rawChallengeFinishTx); - if (challengeFinishTxid != graphData.commitTimoutTxid) revert TxidMismatch(); + (challengeFinishTxid) = BitvmTxParser.computeTxid( + rawChallengeFinishTx + ); + if (challengeFinishTxid != graphData.commitTimoutTxid) + revert TxidMismatch(); } else if (disproveTxType == DisproveTxType.OperatorNack) { - (challengeFinishTxid) = BitvmTxParser.computeTxid(rawChallengeFinishTx); - if (graphData.NackTxids.length <= txnIndex) revert IndexOutOfRange(); - if (challengeFinishTxid != graphData.NackTxids[txnIndex]) revert TxidMismatch(); + (challengeFinishTxid) = BitvmTxParser.computeTxid( + rawChallengeFinishTx + ); + if (graphData.NackTxids.length <= txnIndex) + revert IndexOutOfRange(); + if (challengeFinishTxid != graphData.NackTxids[txnIndex]) + revert TxidMismatch(); } else if (disproveTxType == DisproveTxType.Disprove) { - (challengeFinishTxid, kickoffTxid, kickoffVout, disproverAddress) = - BitvmTxParser.parseDisproveTx(rawChallengeFinishTx); + ( + challengeFinishTxid, + kickoffTxid, + kickoffVout, + disproverAddress + ) = BitvmTxParser.parseDisproveTx(rawChallengeFinishTx); if (kickoffTxid != graphData.kickoffTxid) revert TxidMismatch(); - if (kickoffVout != BitvmTxParser.DISPROVE_CONNECTOR_VOUT) revert TxidMismatch(); + if (kickoffVout != BitvmTxParser.DISPROVE_CONNECTOR_VOUT) + revert TxidMismatch(); } else if (disproveTxType == DisproveTxType.QuickChallenge) { - (challengeFinishTxid, kickoffTxid, kickoffVout, disproverAddress) = - BitvmTxParser.parseQuickChallengeTx(rawChallengeFinishTx); + ( + challengeFinishTxid, + kickoffTxid, + kickoffVout, + disproverAddress + ) = BitvmTxParser.parseQuickChallengeTx(rawChallengeFinishTx); if (kickoffTxid != graphData.kickoffTxid) revert TxidMismatch(); - if (kickoffVout != BitvmTxParser.GUARDIAN_CONNECTOR_VOUT) revert TxidMismatch(); - } else if (disproveTxType == DisproveTxType.ChallengeIncompeleteKickoff) { - (challengeFinishTxid, kickoffTxid, kickoffVout, disproverAddress) = - BitvmTxParser.parseChallengeIncompleteKickoffTx(rawChallengeFinishTx); + if (kickoffVout != BitvmTxParser.GUARDIAN_CONNECTOR_VOUT) + revert TxidMismatch(); + } else if ( + disproveTxType == DisproveTxType.ChallengeIncompeleteKickoff + ) { + ( + challengeFinishTxid, + kickoffTxid, + kickoffVout, + disproverAddress + ) = BitvmTxParser.parseChallengeIncompleteKickoffTx( + rawChallengeFinishTx + ); if (kickoffTxid != graphData.kickoffTxid) revert TxidMismatch(); - if (kickoffVout != BitvmTxParser.GUARDIAN_CONNECTOR_VOUT) revert TxidMismatch(); + if (kickoffVout != BitvmTxParser.GUARDIAN_CONNECTOR_VOUT) + revert TxidMismatch(); } else { revert UnknownDisproveType(); } - (blockHash, merkleRoot) = MerkleProof.parseBtcBlockHeader(challengeFinishTxProof.rawHeader); - if (bitcoinSPV.blockHash(challengeFinishTxProof.height) != blockHash) revert DisproveInvalidHeader(); + (blockHash, merkleRoot) = MerkleProof.parseBtcBlockHeader( + challengeFinishTxProof.rawHeader + ); + if (bitcoinSPV.blockHash(challengeFinishTxProof.height) != blockHash) + revert DisproveInvalidHeader(); if ( !MerkleProof.verifyMerkleProof( - merkleRoot, challengeFinishTxProof.proof, challengeFinishTxid, challengeFinishTxProof.index + merkleRoot, + challengeFinishTxProof.proof, + challengeFinishTxid, + challengeFinishTxProof.index ) ) revert MerkleVerifyFail(); withdrawData.status = WithdrawStatus.Disproved; // slash Operator & reward Challenger and Disprover IERC20 stakeToken = IERC20(stakeManagement.stakeTokenAddress()); - address operatorStakeAddress = stakeManagement.pubkeyToAddress(graphData.operatorPubkey); + address operatorStakeAddress = stakeManagement.pubkeyToAddress( + graphData.operatorPubkey + ); uint256 slashAmount = minSlashAmount; uint256 operatorStake = stakeManagement.stakeOf(operatorStakeAddress); if (operatorStake < slashAmount) slashAmount = operatorStake; @@ -775,11 +866,18 @@ contract GatewayUpgradeable is BitvmPolicy, Initializable { prekickoff-connector through another path). Once the committee members have verified this, they provide their signatures. */ - function unlockOperatorStake(address operator, uint256 amount, uint256 nonce, bytes[] calldata committeeSigs) - external - { + function unlockOperatorStake( + address operator, + uint256 amount, + uint256 nonce, + bytes[] calldata committeeSigs + ) external { bytes32 msgHash = getUnlockStakeDigest(operator, amount); - committeeManagement.executeNoncedSignatures(msgHash, nonce, committeeSigs); + committeeManagement.executeNoncedSignatures( + msgHash, + nonce, + committeeSigs + ); stakeManagement.unlockStake(operator, amount); } } diff --git a/src/PegBTC.sol b/src/PegBTC.sol index eee1dca..c53de1e 100644 --- a/src/PegBTC.sol +++ b/src/PegBTC.sol @@ -1,8 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.28; -import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; -import "@openzeppelin/contracts/access/Ownable.sol"; +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; contract PegBTC is ERC20, Ownable { constructor(address owner) Ownable(owner) ERC20("PegBTC", "PBTC") {} diff --git a/src/SequencerSetPublisher.sol b/src/SequencerSetPublisher.sol index dd5c510..37979f6 100644 --- a/src/SequencerSetPublisher.sol +++ b/src/SequencerSetPublisher.sol @@ -1,21 +1,32 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.28; -import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; -import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import { + Initializable +} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import { + OwnableUpgradeable +} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; -import "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; +import { + MessageHashUtils +} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; -import "./MultiSigVerifier.sol"; -import "./interfaces/ISequencerSetPublisher.sol"; -import "./Constants.sol"; +import {MultiSigVerifier} from "./MultiSigVerifier.sol"; +import {ISequencerSetPublisher} from "./interfaces/ISequencerSetPublisher.sol"; +import {Constants} from "./Constants.sol"; // Sequencer Set Publisher -contract SequencerSetPublisher is Initializable, OwnableUpgradeable, ISequencerSetPublisher { +contract SequencerSetPublisher is + Initializable, + OwnableUpgradeable, + ISequencerSetPublisher +{ using ECDSA for bytes32; using MessageHashUtils for bytes32; - mapping(uint256 height => mapping(address publisher => bytes32 cmt)) public heightSequencerCmt; + mapping(uint256 height => mapping(address publisher => bytes32 cmt)) + public heightSequencerCmt; mapping(bytes32 cmt => uint256 cnt) public sequencerCmtCnt; mapping(uint256 height => address[]) public heightPublishers; @@ -31,11 +42,20 @@ contract SequencerSetPublisher is Initializable, OwnableUpgradeable, ISequencerS address[] calldata initPublishers, bytes[] calldata initPublisherBTCPubkeys ) public initializer { - require(multiSigVerifierAddress != address(0), "Invalid multisig address"); - require(initPublisherBTCPubkeys.length == initPublishers.length, "Invalid Publishers"); + require( + multiSigVerifierAddress != address(0), + "Invalid multisig address" + ); + require( + initPublisherBTCPubkeys.length == initPublishers.length, + "Invalid Publishers" + ); __Ownable_init(initialOwner); multiSigVerifier = MultiSigVerifier(multiSigVerifierAddress); - require(multiSigVerifier.ownerCount() == 0, "Verifier already initialized"); + require( + multiSigVerifier.ownerCount() == 0, + "Verifier already initialized" + ); // TODO: deploy a dedicated MultiSigVerifier proxy here once we stop injecting the address externally. // ensure valid sigs >= 2/3 uint256 quorum = (initPublishers.length * 2 + 2) / 3; @@ -51,13 +71,27 @@ contract SequencerSetPublisher is Initializable, OwnableUpgradeable, ISequencerS /// @notice Publish a new sequencer set, which should be signed by the older publishers. /// @param ss The Sequencer Set /// @param signature The P2WSH signature - function updateSequencerSet(SequencerSet calldata ss, bytes calldata signature) external override { - require(ss.goatBlockNumber >= latestConfirmedHeight, InvalidGOATHeight()); + function updateSequencerSet( + SequencerSet calldata ss, + bytes calldata signature + ) external override { + require( + ss.goatBlockNumber >= latestConfirmedHeight, + InvalidGOATHeight() + ); require(multiSigVerifier.isOwner(msg.sender), InvalidPublisherSet()); - require(msg.sender == ss.p2wshSigHash.recover(signature), P2WSHSignatureMismatch()); + require( + msg.sender == ss.p2wshSigHash.recover(signature), + P2WSHSignatureMismatch() + ); // Ensure the publisher set is not changed. - bytes32 expectedPublishersHash = keccak256(abi.encodePacked(multiSigVerifier.getOwners())); - require(ss.publishersHash == expectedPublishersHash, MismatchPublisher()); + bytes32 expectedPublishersHash = keccak256( + abi.encodePacked(multiSigVerifier.getOwners()) + ); + require( + ss.publishersHash == expectedPublishersHash, + MismatchPublisher() + ); bytes32 cmt = keccak256( abi.encodePacked( @@ -69,7 +103,10 @@ contract SequencerSetPublisher is Initializable, OwnableUpgradeable, ISequencerS ) ); // Avoid double commit - require(heightSequencerCmt[ss.goatBlockNumber][msg.sender] == bytes32(0), DoubleCommit()); + require( + heightSequencerCmt[ss.goatBlockNumber][msg.sender] == bytes32(0), + DoubleCommit() + ); heightSequencerCmt[ss.goatBlockNumber][msg.sender] = cmt; sequencerCmtCnt[cmt] += 1; @@ -93,19 +130,33 @@ contract SequencerSetPublisher is Initializable, OwnableUpgradeable, ISequencerS require(height == ss.goatBlockNumber, InvalidGOATHeight()); address[] memory publishers = multiSigVerifier.getOwners(); - bytes32 expectedPublishersHash = keccak256(abi.encodePacked(publishers)); - require(ss.publishersHash == expectedPublishersHash, MismatchPublisher()); + bytes32 expectedPublishersHash = keccak256( + abi.encodePacked(publishers) + ); + require( + ss.publishersHash == expectedPublishersHash, + MismatchPublisher() + ); if (latestConfirmedHeight > 0) { // check the continuality of the update chain - bytes32 prevCmt = calcMajoritySequencerSetCmtAtHeightOrLatest(latestConfirmedHeight); + bytes32 prevCmt = calcMajoritySequencerSetCmtAtHeightOrLatest( + latestConfirmedHeight + ); SequencerSet storage prevSs = cmtSequencerSet[prevCmt]; - require(prevSs.nextPublishersHash == ss.publishersHash, InvalidPublisherSet()); + require( + prevSs.nextPublishersHash == ss.publishersHash, + InvalidPublisherSet() + ); } // ensure valid sigs >= 2/3 uint256 quorum = (newPublishers.length * 2 + 2) / 3; - multiSigVerifier.updateOwners(newPublishers, quorum, changePublisherSigs); + multiSigVerifier.updateOwners( + newPublishers, + quorum, + changePublisherSigs + ); for (uint256 i = 0; i < newPublisherBTCPubkeys.length; i++) { assert(newPublisherBTCPubkeys[i].length == 33); @@ -115,7 +166,9 @@ contract SequencerSetPublisher is Initializable, OwnableUpgradeable, ISequencerS } /// @notice Check if we have an aggrement on the cmt of the latest height. - function calcMajoritySequencerSetCmtAtHeightOrLatest(uint256 height) public view returns (bytes32) { + function calcMajoritySequencerSetCmtAtHeightOrLatest( + uint256 height + ) public view returns (bytes32) { require(height > 0, InvalidGOATHeight()); address[] memory publishers = heightPublishers[height]; @@ -130,7 +183,10 @@ contract SequencerSetPublisher is Initializable, OwnableUpgradeable, ISequencerS } } - require(quorum * 3 >= 2 * publishers.length, InvalidQuorumSequencerSet()); + require( + quorum * 3 >= 2 * publishers.length, + InvalidQuorumSequencerSet() + ); return agreement; } } diff --git a/src/StakeManagement.sol b/src/StakeManagement.sol index 0de4528..1fa585d 100644 --- a/src/StakeManagement.sol +++ b/src/StakeManagement.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.28; -import "./interfaces/IStakeManagement.sol"; +import {IStakeManagement} from "./interfaces/IStakeManagement.sol"; import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; diff --git a/src/interfaces/ICommitteeManagement.sol b/src/interfaces/ICommitteeManagement.sol index 2e5325b..a04b8b1 100644 --- a/src/interfaces/ICommitteeManagement.sol +++ b/src/interfaces/ICommitteeManagement.sol @@ -5,5 +5,17 @@ interface ICommitteeManagement { function isCommitteeMember(address member) external view returns (bool); function committeeSize() external view returns (uint256); function quorumSize() external view returns (uint256); - function verifySignatures(bytes32 msgHash, bytes[] memory signatures) external view returns (bool); + function verifySignatures( + bytes32 msgHash, + bytes[] memory signatures + ) external view returns (bool); + function getNoncedDigest( + bytes32 msgHash, + uint256 nonce + ) external view returns (bytes32); + function executeNoncedSignatures( + bytes32 msgHash, + uint256 nonce, + bytes[] memory signatures + ) external; } diff --git a/src/interfaces/IGateway.sol b/src/interfaces/IGateway.sol new file mode 100644 index 0000000..1b458a3 --- /dev/null +++ b/src/interfaces/IGateway.sol @@ -0,0 +1,193 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +/// @notice Interface housing shared Gateway declarations. Keeps the main contract lean by +/// centralizing events, errors, enums, and structs that integrators need to reference. +interface IGateway { + // ===== Errors ===== + error NotCommittee(); + error NotOperator(); + error InstanceUsed(); + error NotPending(); + error WindowExpired(); + error WindowNotExpired(); + error NotEnoughCommittee(); + error InvalidPubkeyLen(); + error InvalidPubkeyParity(); + error InstanceMismatch(); + error PeginAmountMismatch(); + error InvalidHeader(); + error MerkleVerifyFail(); + error InvalidSignatures(); + error FeeTooHigh(); + error OperatorNotRegistered(); + error StakeInsufficient(); + error GraphAlreadyPosted(); + error GraphPeginTxidMismatch(); + error WithdrawStatusInvalid(); + error NotWithdrawable(); + error TimelockNotExpired(); + error KickoffHeightLow(); + error TxidMismatch(); + error AlreadyDisproved(); + error IndexOutOfRange(); + error UnknownDisproveType(); + error DisproveInvalidHeader(); + + // ===== Enums ===== + enum DisproveTxType { + AssertTimeout, + OperatorCommitTimeout, + OperatorNack, + Disprove, + QuickChallenge, + ChallengeIncompeleteKickoff + } + + enum PeginStatus { + None, + Pending, + Withdrawbale, + Processing, + Locked, + Claimed, + Discarded + } + + enum WithdrawStatus { + None, + Processing, + Initialized, + Canceled, + Complete, + Disproved + } + + // ===== Structs ===== + struct Utxo { + bytes32 txid; + uint32 vout; + uint64 amountSats; + } + + struct PeginDataInner { + PeginStatus status; + bytes16 instanceId; + address depositorAddress; + uint64 peginAmountSats; + uint64[3] txnFees; + Utxo[] userInputs; + bytes32 userXonlyPubkey; + string userChangeAddress; + string userRefundAddress; + bytes32 peginTxid; + uint256 createdAt; + address[] committeeAddresses; + mapping(address value => uint256) committeeAddressPositions; + mapping(address => bytes1) committeePubkeyParitys; + mapping(address => bytes32) committeeXonlyPubkeys; + } + + struct PeginData { + PeginStatus status; + bytes16 instanceId; + address depositorAddress; + uint64 peginAmountSats; + uint64[3] txnFees; + Utxo[] userInputs; + bytes32 userXonlyPubkey; + string userChangeAddress; + string userRefundAddress; + bytes32 peginTxid; + uint256 createdAt; + address[] committeeAddresses; + bytes[] committeePubkeys; + } + + struct WithdrawData { + WithdrawStatus status; + bytes32 peginTxid; + address operatorAddress; + bytes16 instanceId; + uint256 lockAmount; + uint256 btcBlockHeightAtWithdraw; + } + + struct GraphData { + bytes1 operatorPubkeyPrefix; + bytes32 operatorPubkey; + bytes32 peginTxid; + bytes32 kickoffTxid; + bytes32 take1Txid; + bytes32 take2Txid; + bytes32 commitTimoutTxid; + bytes32[] assertTimoutTxids; + bytes32[] NackTxids; + } + + // ===== Events ===== + event BridgeInRequest( + bytes16 indexed instanceId, + address indexed depositorAddress, + uint64 peginAmountSats, + uint64[3] txnFees, + Utxo[] userInputs, + bytes32 userXonlyPubkey, + string userChangeAddress, + string userRefundAddress + ); + event CommitteeResponse( + bytes16 indexed instanceId, + address indexed committeeAddress, + bytes committeePubkey + ); + event BridgeIn( + address indexed depositorAddress, + bytes16 indexed instanceId, + uint64 indexed peginAmountSats, + uint64 feeAmountSats + ); + event PostGraphData(bytes16 indexed instanceId, bytes16 indexed graphId); + event InitWithdraw( + bytes16 indexed instanceId, + bytes16 indexed graphId, + address indexed operatorAddress, + uint64 withdrawAmountSats + ); + event CancelWithdraw( + bytes16 indexed instanceId, + bytes16 indexed graphId, + address indexed triggerAddress + ); + event ProceedWithdraw( + bytes16 indexed instanceId, + bytes16 indexed graphId, + bytes32 kickoffTxid + ); + event WithdrawHappyPath( + bytes16 indexed instanceId, + bytes16 indexed graphId, + bytes32 take1Txid, + address indexed operatorAddress, + uint64 rewardAmountSats + ); + event WithdrawUnhappyPath( + bytes16 indexed instanceId, + bytes16 indexed graphId, + bytes32 take2Txid, + address indexed operatorAddress, + uint64 rewardAmountSats + ); + event WithdrawDisproved( + bytes16 indexed instanceId, + bytes16 indexed graphId, + DisproveTxType disproveTxType, + uint256 txnIndex, + bytes32 challengeStartTxid, + bytes32 challengeFinishTxid, + address challengerAddress, + address disproverAddress, + uint64 challengerRewardAmount, + uint64 disproverRewardAmount + ); +} diff --git a/test/MultiSigVerifier.t.sol b/test/MultiSigVerifier.t.sol index 64c29ba..2b3aae6 100644 --- a/test/MultiSigVerifier.t.sol +++ b/test/MultiSigVerifier.t.sol @@ -1,10 +1,12 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.28; -import "forge-std/Test.sol"; -import "../src/MultiSigVerifier.sol"; -import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; -import "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; +import {Test} from "forge-std/Test.sol"; +import {MultiSigVerifier} from "../src/MultiSigVerifier.sol"; +import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import { + MessageHashUtils +} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; contract MultiSigVerifierTest is Test { using ECDSA for bytes32; @@ -75,12 +77,15 @@ contract MultiSigVerifierTest is Test { assertFalse(ok, "Non-owner signature should not be valid"); } - function _signUpdate(uint256 privKey, address[] memory newOwners, uint256 newRequired, uint256 nonce) - internal - pure - returns (bytes memory sig) - { - bytes32 digest = keccak256(abi.encodePacked(nonce, newOwners, newRequired)); + function _signUpdate( + uint256 privKey, + address[] memory newOwners, + uint256 newRequired, + uint256 nonce + ) internal pure returns (bytes memory sig) { + bytes32 digest = keccak256( + abi.encodePacked(nonce, newOwners, newRequired) + ); (uint8 v, bytes32 r, bytes32 s) = vm.sign(privKey, digest); sig = abi.encodePacked(r, s, v); diff --git a/test/SequencerSetPublisher.t.sol b/test/SequencerSetPublisher.t.sol index a9ba3a9..78a9183 100644 --- a/test/SequencerSetPublisher.t.sol +++ b/test/SequencerSetPublisher.t.sol @@ -2,13 +2,17 @@ pragma solidity ^0.8.28; import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; -import "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; -import "forge-std/Test.sol"; -import "../src/SequencerSetPublisher.sol"; -import "../src/interfaces/ISequencerSetPublisher.sol"; -import "../src/MultiSigVerifier.sol"; -import "../src/interfaces/ISequencerSetPublisher.sol"; -import "forge-std/console.sol"; +import { + MessageHashUtils +} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; +import {Test} from "forge-std/Test.sol"; +import {StdStorage, stdStorage} from "forge-std/StdStorage.sol"; +import {SequencerSetPublisher} from "../src/SequencerSetPublisher.sol"; +import { + ISequencerSetPublisher +} from "../src/interfaces/ISequencerSetPublisher.sol"; +import {MultiSigVerifier} from "../src/MultiSigVerifier.sol"; +import {console} from "forge-std/console.sol"; contract SequencerSetPublisherTest is Test { using ECDSA for bytes32; @@ -23,13 +27,25 @@ contract SequencerSetPublisherTest is Test { uint256[] batch1; uint256[] batch2; - function _get_pubkey_from_prvkey(uint256 number) internal pure returns (bytes[] memory) { + function _get_pubkey_from_prvkey( + uint256 number + ) internal pure returns (bytes[] memory) { bytes[5] memory newPublisherPubkeysConstant; - newPublisherPubkeysConstant[0] = hex"031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f"; - newPublisherPubkeysConstant[1] = hex"024d4b6cd1361032ca9bd2aeb9d900aa4d45d9ead80ac9423374c451a7254d0766"; - newPublisherPubkeysConstant[2] = hex"02531fe6068134503d2723133227c867ac8fa6c83c537e9a44c3c5bdbdcb1fe337"; - newPublisherPubkeysConstant[3] = hex"03462779ad4aad39514614751a71085f2f10e1c7a593e4e030efb5b8721ce55b0b"; - newPublisherPubkeysConstant[4] = hex"0362c0a046dacce86ddd0343c6d3c7c79c2208ba0d9c9cf24a6d046d21d21f90f7"; + newPublisherPubkeysConstant[ + 0 + ] = hex"031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f"; + newPublisherPubkeysConstant[ + 1 + ] = hex"024d4b6cd1361032ca9bd2aeb9d900aa4d45d9ead80ac9423374c451a7254d0766"; + newPublisherPubkeysConstant[ + 2 + ] = hex"02531fe6068134503d2723133227c867ac8fa6c83c537e9a44c3c5bdbdcb1fe337"; + newPublisherPubkeysConstant[ + 3 + ] = hex"03462779ad4aad39514614751a71085f2f10e1c7a593e4e030efb5b8721ce55b0b"; + newPublisherPubkeysConstant[ + 4 + ] = hex"0362c0a046dacce86ddd0343c6d3c7c79c2208ba0d9c9cf24a6d046d21d21f90f7"; require(number <= newPublisherPubkeysConstant.length); bytes[] memory newPublisherPubkeys = new bytes[](number); @@ -101,19 +117,31 @@ contract SequencerSetPublisherTest is Test { uint256 nonce = sspublisher.multiSigVerifier().nonce(); uint256 newRequired = (newPublishers.length * 2 + 2) / 3; - bytes32 digest = keccak256(abi.encodePacked(nonce, newPublishers, newRequired)); + bytes32 digest = keccak256( + abi.encodePacked(nonce, newPublishers, newRequired) + ); - bytes[] memory newPublisherPubkeys = _get_pubkey_from_prvkey(newPublishers.length); + bytes[] memory newPublisherPubkeys = _get_pubkey_from_prvkey( + newPublishers.length + ); uint256 oldRequired = (oldPublishers.length * 2 + 2) / 3; bytes[] memory sigs = new bytes[](oldRequired); for (uint256 j = 0; j < oldRequired; j++) { - (uint8 v, bytes32 r, bytes32 s) = vm.sign(oldPublisherKeys[j], digest); + (uint8 v, bytes32 r, bytes32 s) = vm.sign( + oldPublisherKeys[j], + digest + ); sigs[j] = abi.encodePacked(r, s, v); } console.log("height: ", height); vm.startPrank(oldPublishers[1]); - sspublisher.updatePublisherSet(newPublishers, newPublisherPubkeys, sigs, height); + sspublisher.updatePublisherSet( + newPublishers, + newPublisherPubkeys, + sigs, + height + ); vm.stopPrank(); MultiSigVerifier verifier = sspublisher.multiSigVerifier(); @@ -123,26 +151,68 @@ contract SequencerSetPublisherTest is Test { function testUpdatePublisherSet() public { // genesis sequencer set commit, publisher is not changed - run_sequencer_update_test(batch, batch, 10, keccak256("commit1"), keccak256("set1"), keccak256("set2")); + run_sequencer_update_test( + batch, + batch, + 10, + keccak256("commit1"), + keccak256("set1"), + keccak256("set2") + ); // publisher commit, sequencer set is not changed - run_sequencer_update_test(batch, batch1, 11, keccak256("commit2"), keccak256("set2"), keccak256("set2")); + run_sequencer_update_test( + batch, + batch1, + 11, + keccak256("commit2"), + keccak256("set2"), + keccak256("set2") + ); // apply publisher update assert(sspublisher.latestConfirmedHeight() == 0); run_publisher_update_test(batch, batch1, 11); assert(sspublisher.latestConfirmedHeight() == 11); // sequencer set commit, publisher is not changed - run_sequencer_update_test(batch1, batch1, 12, keccak256("commit3"), keccak256("set2"), keccak256("set22")); - run_sequencer_update_test(batch1, batch1, 13, keccak256("commit3"), keccak256("set22"), keccak256("set3")); + run_sequencer_update_test( + batch1, + batch1, + 12, + keccak256("commit3"), + keccak256("set2"), + keccak256("set22") + ); + run_sequencer_update_test( + batch1, + batch1, + 13, + keccak256("commit3"), + keccak256("set22"), + keccak256("set3") + ); // publisher commit, sequencer set is not changed - run_sequencer_update_test(batch1, batch2, 17, keccak256("commit4"), keccak256("set3"), keccak256("set3")); + run_sequencer_update_test( + batch1, + batch2, + 17, + keccak256("commit4"), + keccak256("set3"), + keccak256("set3") + ); // apply publisher update assert(sspublisher.latestConfirmedHeight() == 11); run_publisher_update_test(batch1, batch2, 17); assert(sspublisher.latestConfirmedHeight() == 17); // sequencer set commit, publisher is not changed - run_sequencer_update_test(batch2, batch2, 20, keccak256("commit5"), keccak256("set3"), keccak256("set4")); + run_sequencer_update_test( + batch2, + batch2, + 20, + keccak256("commit5"), + keccak256("set3"), + keccak256("set4") + ); } function run_sequencer_update_test( @@ -157,23 +227,29 @@ contract SequencerSetPublisherTest is Test { for (uint256 i = 0; i < publisherKeys.length; i++) { publishers[i] = vm.addr(publisherKeys[i]); } - address[] memory nextPublishers = new address[](nextPublisherKeys.length); + address[] memory nextPublishers = new address[]( + nextPublisherKeys.length + ); for (uint256 i = 0; i < nextPublisherKeys.length; i++) { nextPublishers[i] = vm.addr(nextPublisherKeys[i]); } - ISequencerSetPublisher.SequencerSet memory ss = ISequencerSetPublisher.SequencerSet({ - sequencerSetHash: sequencerSetHash, - publishersHash: keccak256(abi.encodePacked(publishers)), - nextPublishersHash: keccak256(abi.encodePacked(nextPublishers)), - p2wshSigHash: p2wshSigHash.toEthSignedMessageHash(), - goatBlockNumber: height - }); + ISequencerSetPublisher.SequencerSet memory ss = ISequencerSetPublisher + .SequencerSet({ + sequencerSetHash: sequencerSetHash, + publishersHash: keccak256(abi.encodePacked(publishers)), + nextPublishersHash: keccak256(abi.encodePacked(nextPublishers)), + p2wshSigHash: p2wshSigHash.toEthSignedMessageHash(), + goatBlockNumber: height + }); uint256 oldRequired = (publishers.length * 2 + 2) / 3; for (uint256 i = 0; i < oldRequired; i++) { - (uint8 v, bytes32 r, bytes32 s) = vm.sign(publisherKeys[i], ss.p2wshSigHash); + (uint8 v, bytes32 r, bytes32 s) = vm.sign( + publisherKeys[i], + ss.p2wshSigHash + ); bytes memory sig = abi.encodePacked(r, s, v); vm.startPrank(publishers[i]); sspublisher.updateSequencerSet(ss, sig); @@ -183,9 +259,37 @@ contract SequencerSetPublisherTest is Test { function testUpdateSequencerSet() public { // New publishers - run_sequencer_update_test(batch, batch, 10, keccak256("commit1"), keccak256("set1"), keccak256("set2")); - run_sequencer_update_test(batch, batch, 11, keccak256("commit2"), keccak256("set2"), keccak256("set3")); - run_sequencer_update_test(batch, batch, 12, keccak256("commit3"), keccak256("set3"), keccak256("set4")); - run_sequencer_update_test(batch, batch, 13, keccak256("commit4"), keccak256("set4"), keccak256("set5")); + run_sequencer_update_test( + batch, + batch, + 10, + keccak256("commit1"), + keccak256("set1"), + keccak256("set2") + ); + run_sequencer_update_test( + batch, + batch, + 11, + keccak256("commit2"), + keccak256("set2"), + keccak256("set3") + ); + run_sequencer_update_test( + batch, + batch, + 12, + keccak256("commit3"), + keccak256("set3"), + keccak256("set4") + ); + run_sequencer_update_test( + batch, + batch, + 13, + keccak256("commit4"), + keccak256("set4"), + keccak256("set5") + ); } } From baec08afcbc590181c24c7bc9489c14d9bad7a70 Mon Sep 17 00:00:00 2001 From: Li-Qing Wang Date: Tue, 16 Dec 2025 16:27:34 +0800 Subject: [PATCH 04/11] fix: fix compile error in the scripts --- script/DeployContractDebug.sol | 99 ++++++++++++++++++++++++++-------- script/DeployGateway.sol | 99 ++++++++++++++++++++++++++-------- 2 files changed, 156 insertions(+), 42 deletions(-) diff --git a/script/DeployContractDebug.sol b/script/DeployContractDebug.sol index b03608e..ba495e3 100644 --- a/script/DeployContractDebug.sol +++ b/script/DeployContractDebug.sol @@ -33,9 +33,16 @@ contract DeployGateway is Script { function deploy() public { GatewayDebug gatewayImpl = new GatewayDebug(); - console.log("Gateway implementation contract address: ", address(gatewayImpl)); + console.log( + "Gateway implementation contract address: ", + address(gatewayImpl) + ); - UpgradeableProxy gatewayProxy = new UpgradeableProxy(address(gatewayImpl), deployer, ""); + UpgradeableProxy gatewayProxy = new UpgradeableProxy( + address(gatewayImpl), + deployer, + "" + ); console.log("Gateway proxy contract address: ", address(gatewayProxy)); GatewayDebug gateway = GatewayDebug(payable(gatewayProxy)); @@ -44,25 +51,63 @@ contract DeployGateway is Script { address[] memory initialMembers = _readSequentialAddresses("COMMITTEE"); uint256 initialRequired = (initialMembers.length * 2 + 2) / 3; - bytes32[] memory initialWatchtowers = _readSequentialBytes32("WATCHTOWER"); + bytes32[] memory initialWatchtowers = _readSequentialBytes32( + "WATCHTOWER" + ); address[] memory initialAuthorizedCallers = new address[](1); initialAuthorizedCallers[0] = address(gateway); CommitteeManagementDebug committeeImpl = new CommitteeManagementDebug(); - console.log("CommitteeManagement implementation contract address: ", address(committeeImpl)); - UpgradeableProxy committeeProxy = new UpgradeableProxy(address(committeeImpl), deployer, ""); - console.log("CommitteeManagement proxy contract address: ", address(committeeProxy)); - CommitteeManagement committeeManagementImpl = CommitteeManagement(address(committeeProxy)); - committeeManagementImpl.initialize(initialMembers, initialRequired, initialAuthorizedCallers, initialWatchtowers); - ICommitteeManagement committeeManagement = ICommitteeManagement(address(committeeProxy)); + console.log( + "CommitteeManagement implementation contract address: ", + address(committeeImpl) + ); + UpgradeableProxy committeeProxy = new UpgradeableProxy( + address(committeeImpl), + deployer, + "" + ); + console.log( + "CommitteeManagement proxy contract address: ", + address(committeeProxy) + ); + CommitteeManagement committeeManagementImpl = CommitteeManagement( + address(committeeProxy) + ); + committeeManagementImpl.initialize( + initialMembers, + initialRequired, + initialAuthorizedCallers, + initialWatchtowers + ); + ICommitteeManagement committeeManagement = ICommitteeManagement( + address(committeeProxy) + ); StakeManagement stakeImpl = new StakeManagement(); - console.log("StakeManagement implementation contract address: ", address(stakeImpl)); - UpgradeableProxy stakeProxy = new UpgradeableProxy(address(stakeImpl), deployer, ""); - console.log("StakeManagement proxy contract address: ", address(stakeProxy)); - StakeManagement stakeManagementImpl = StakeManagement(address(stakeProxy)); - stakeManagementImpl.initialize(IERC20(address(pegBTC)), address(gateway)); - IStakeManagement stakeManagement = IStakeManagement(address(stakeProxy)); + console.log( + "StakeManagement implementation contract address: ", + address(stakeImpl) + ); + UpgradeableProxy stakeProxy = new UpgradeableProxy( + address(stakeImpl), + deployer, + "" + ); + console.log( + "StakeManagement proxy contract address: ", + address(stakeProxy) + ); + StakeManagement stakeManagementImpl = StakeManagement( + address(stakeProxy) + ); + stakeManagementImpl.initialize( + IERC20(address(pegBTC)), + address(gateway) + ); + IStakeManagement stakeManagement = IStakeManagement( + address(stakeProxy) + ); gateway.initialize( IPegBTC(address(pegBTC)), @@ -72,10 +117,14 @@ contract DeployGateway is Script { ); } - function _readSequentialAddresses(string memory baseKey) internal view returns (address[] memory out) { + function _readSequentialAddresses( + string memory baseKey + ) internal view returns (address[] memory out) { uint256 count = 0; while (true) { - string memory key = string(abi.encodePacked(baseKey, "_", vm.toString(count))); + string memory key = string( + abi.encodePacked(baseKey, "_", vm.toString(count)) + ); address val = vm.envOr(key, address(0)); if (val == address(0)) break; unchecked { @@ -84,15 +133,21 @@ contract DeployGateway is Script { } out = new address[](count); for (uint256 i = 0; i < count; i++) { - string memory key = string(abi.encodePacked(baseKey, "_", vm.toString(i))); + string memory key = string( + abi.encodePacked(baseKey, "_", vm.toString(i)) + ); out[i] = vm.envAddress(key); } } - function _readSequentialBytes32(string memory baseKey) internal view returns (bytes32[] memory out) { + function _readSequentialBytes32( + string memory baseKey + ) internal view returns (bytes32[] memory out) { uint256 count = 0; while (true) { - string memory key = string(abi.encodePacked(baseKey, "_", vm.toString(count))); + string memory key = string( + abi.encodePacked(baseKey, "_", vm.toString(count)) + ); bytes32 val = vm.envOr(key, bytes32(0)); if (val == bytes32(0)) break; unchecked { @@ -101,7 +156,9 @@ contract DeployGateway is Script { } out = new bytes32[](count); for (uint256 i = 0; i < count; i++) { - string memory key = string(abi.encodePacked(baseKey, "_", vm.toString(i))); + string memory key = string( + abi.encodePacked(baseKey, "_", vm.toString(i)) + ); out[i] = vm.envBytes32(key); } } diff --git a/script/DeployGateway.sol b/script/DeployGateway.sol index 259171b..8285ba2 100644 --- a/script/DeployGateway.sol +++ b/script/DeployGateway.sol @@ -34,9 +34,16 @@ contract DeployGateway is Script { function deploy() public { // deploy gateway implementation + proxy GatewayUpgradeable gatewayImpl = new GatewayUpgradeable(); - console.log("Gateway implementation contract address: ", address(gatewayImpl)); + console.log( + "Gateway implementation contract address: ", + address(gatewayImpl) + ); - UpgradeableProxy gatewayProxy = new UpgradeableProxy(address(gatewayImpl), deployer, ""); + UpgradeableProxy gatewayProxy = new UpgradeableProxy( + address(gatewayImpl), + deployer, + "" + ); console.log("Gateway proxy contract address: ", address(gatewayProxy)); GatewayUpgradeable gateway = GatewayUpgradeable(payable(gatewayProxy)); @@ -46,27 +53,65 @@ contract DeployGateway is Script { // Read committee config from env address[] memory initialMembers = _readSequentialAddresses("COMMITTEE"); uint256 initialRequired = (initialMembers.length * 2 + 2) / 3; - bytes32[] memory initialWatchtowers = _readSequentialBytes32("WATCHTOWER"); + bytes32[] memory initialWatchtowers = _readSequentialBytes32( + "WATCHTOWER" + ); address[] memory initialAuthorizedCallers = new address[](1); initialAuthorizedCallers[0] = address(gateway); // Deploy CommitteeManagement implementation + proxy CommitteeManagement committeeImpl = new CommitteeManagement(); - console.log("CommitteeManagement implementation contract address: ", address(committeeImpl)); - UpgradeableProxy committeeProxy = new UpgradeableProxy(address(committeeImpl), deployer, ""); - console.log("CommitteeManagement proxy contract address: ", address(committeeProxy)); - CommitteeManagement committeeManagementImpl = CommitteeManagement(address(committeeProxy)); - committeeManagementImpl.initialize(initialMembers, initialRequired, initialAuthorizedCallers, initialWatchtowers); - ICommitteeManagement committeeManagement = ICommitteeManagement(address(committeeProxy)); + console.log( + "CommitteeManagement implementation contract address: ", + address(committeeImpl) + ); + UpgradeableProxy committeeProxy = new UpgradeableProxy( + address(committeeImpl), + deployer, + "" + ); + console.log( + "CommitteeManagement proxy contract address: ", + address(committeeProxy) + ); + CommitteeManagement committeeManagementImpl = CommitteeManagement( + address(committeeProxy) + ); + committeeManagementImpl.initialize( + initialMembers, + initialRequired, + initialAuthorizedCallers, + initialWatchtowers + ); + ICommitteeManagement committeeManagement = ICommitteeManagement( + address(committeeProxy) + ); // Deploy StakeManagement implementation + proxy StakeManagement stakeImpl = new StakeManagement(); - console.log("StakeManagement implementation contract address: ", address(stakeImpl)); - UpgradeableProxy stakeProxy = new UpgradeableProxy(address(stakeImpl), deployer, ""); - console.log("StakeManagement proxy contract address: ", address(stakeProxy)); - StakeManagement stakeManagementImpl = StakeManagement(address(stakeProxy)); - stakeManagementImpl.initialize(IERC20(address(pegBTC)), address(gateway)); - IStakeManagement stakeManagement = IStakeManagement(address(stakeProxy)); + console.log( + "StakeManagement implementation contract address: ", + address(stakeImpl) + ); + UpgradeableProxy stakeProxy = new UpgradeableProxy( + address(stakeImpl), + deployer, + "" + ); + console.log( + "StakeManagement proxy contract address: ", + address(stakeProxy) + ); + StakeManagement stakeManagementImpl = StakeManagement( + address(stakeProxy) + ); + stakeManagementImpl.initialize( + IERC20(address(pegBTC)), + address(gateway) + ); + IStakeManagement stakeManagement = IStakeManagement( + address(stakeProxy) + ); gateway.initialize( IPegBTC(address(pegBTC)), @@ -76,10 +121,14 @@ contract DeployGateway is Script { ); } - function _readSequentialAddresses(string memory baseKey) internal view returns (address[] memory out) { + function _readSequentialAddresses( + string memory baseKey + ) internal view returns (address[] memory out) { uint256 count = 0; while (true) { - string memory key = string(abi.encodePacked(baseKey, "_", vm.toString(count))); + string memory key = string( + abi.encodePacked(baseKey, "_", vm.toString(count)) + ); address val = vm.envOr(key, address(0)); if (val == address(0)) break; unchecked { @@ -88,15 +137,21 @@ contract DeployGateway is Script { } out = new address[](count); for (uint256 i = 0; i < count; i++) { - string memory key = string(abi.encodePacked(baseKey, "_", vm.toString(i))); + string memory key = string( + abi.encodePacked(baseKey, "_", vm.toString(i)) + ); out[i] = vm.envAddress(key); } } - function _readSequentialBytes32(string memory baseKey) internal view returns (bytes32[] memory out) { + function _readSequentialBytes32( + string memory baseKey + ) internal view returns (bytes32[] memory out) { uint256 count = 0; while (true) { - string memory key = string(abi.encodePacked(baseKey, "_", vm.toString(count))); + string memory key = string( + abi.encodePacked(baseKey, "_", vm.toString(count)) + ); bytes32 val = vm.envOr(key, bytes32(0)); if (val == bytes32(0)) break; unchecked { @@ -105,7 +160,9 @@ contract DeployGateway is Script { } out = new bytes32[](count); for (uint256 i = 0; i < count; i++) { - string memory key = string(abi.encodePacked(baseKey, "_", vm.toString(i))); + string memory key = string( + abi.encodePacked(baseKey, "_", vm.toString(i)) + ); out[i] = vm.envBytes32(key); } } From 2036ec7326ef45f6bcfd96654fd56f2258105391 Mon Sep 17 00:00:00 2001 From: Li-Qing Wang Date: Wed, 17 Dec 2025 15:49:06 +0800 Subject: [PATCH 05/11] fix: optimize gateway contract --- src/Gateway.sol | 231 ++++++++++++++++++++---------------------------- 1 file changed, 97 insertions(+), 134 deletions(-) diff --git a/src/Gateway.sol b/src/Gateway.sol index f4d9a92..44777e1 100644 --- a/src/Gateway.sol +++ b/src/Gateway.sol @@ -232,6 +232,83 @@ contract GatewayUpgradeable is BitvmPolicy, Initializable, IGateway { return committeeManagement.getNoncedDigest(msgHash, nonce); } + function _operatorReward( + uint64 peginAmountSats + ) internal view returns (uint64) { + return + minOperatorRewardSats + + (peginAmountSats * operatorRewardRate) / + rateMultiplier; + } + + function _verifyMerkleInclusion( + MerkleProof.BitcoinTxProof calldata proof, + bytes32 txid, + bool disproveContext + ) internal view { + (bytes32 blockHash, bytes32 merkleRoot) = MerkleProof + .parseBtcBlockHeader(proof.rawHeader); + if (bitcoinSPV.blockHash(proof.height) != blockHash) { + if (disproveContext) revert DisproveInvalidHeader(); + revert InvalidHeader(); + } + if ( + !MerkleProof.verifyMerkleProof( + merkleRoot, + proof.proof, + txid, + proof.index + ) + ) { + revert MerkleVerifyFail(); + } + } + + function _finalizeWithdraw( + bytes16 graphId, + BitvmTxParser.BitcoinTx calldata rawTakeTx, + MerkleProof.BitcoinTxProof calldata takeProof, + bytes32 expectedTxid, + bool happyPath + ) internal { + WithdrawData storage withdrawData = withdrawDataMap[graphId]; + bytes16 instanceId = withdrawData.instanceId; + PeginDataInner storage peginData = peginDataMap[instanceId]; + if (withdrawData.status != WithdrawStatus.Processing) + revert WithdrawStatusInvalid(); + + bytes32 takeTxid = BitvmTxParser.computeTxid(rawTakeTx); + if (takeTxid != expectedTxid) revert TxidMismatch(); + _verifyMerkleInclusion(takeProof, takeTxid, false); + + peginData.status = PeginStatus.Claimed; + withdrawData.status = WithdrawStatus.Complete; + + uint64 rewardAmountSats = _operatorReward(peginData.peginAmountSats); + pegBTC.transfer( + withdrawData.operatorAddress, + Converter.amountFromSats(rewardAmountSats) + ); + + if (happyPath) { + emit WithdrawHappyPath( + instanceId, + graphId, + takeTxid, + withdrawData.operatorAddress, + rewardAmountSats + ); + } else { + emit WithdrawUnhappyPath( + instanceId, + graphId, + takeTxid, + withdrawData.operatorAddress, + rewardAmountSats + ); + } + } + modifier onlyCommittee() { if (!committeeManagement.isCommitteeMember(msg.sender)) revert NotCommittee(); @@ -378,20 +455,7 @@ contract GatewayUpgradeable is BitvmPolicy, Initializable, IGateway { revert PeginAmountMismatch(); // validate pegin tx - (bytes32 blockHash, bytes32 merkleRoot) = MerkleProof - .parseBtcBlockHeader(peginProof.rawHeader); - if (bitcoinSPV.blockHash(peginProof.height) != blockHash) - revert InvalidHeader(); - if ( - !MerkleProof.verifyMerkleProof( - merkleRoot, - peginProof.proof, - peginTxid, - peginProof.index - ) - ) { - revert MerkleVerifyFail(); - } + _verifyMerkleInclusion(peginProof, peginTxid, false); // validate committeeSigs bytes32 pegin_digest = getPostPeginDigest(instanceId, peginTxid); @@ -571,20 +635,7 @@ contract GatewayUpgradeable is BitvmPolicy, Initializable, IGateway { GraphData storage graphData = graphDataMap[graphId]; bytes32 kickoffTxid = BitvmTxParser.computeTxid(rawKickoffTx); if (kickoffTxid != graphData.kickoffTxid) revert TxidMismatch(); - (bytes32 blockHash, bytes32 merkleRoot) = MerkleProof - .parseBtcBlockHeader(kickoffProof.rawHeader); - if (bitcoinSPV.blockHash(kickoffProof.height) != blockHash) - revert InvalidHeader(); - if ( - !MerkleProof.verifyMerkleProof( - merkleRoot, - kickoffProof.proof, - kickoffTxid, - kickoffProof.index - ) - ) { - revert MerkleVerifyFail(); - } + _verifyMerkleInclusion(kickoffProof, kickoffTxid, false); // once kickoff is braodcasted , operator will not be able to cancel withdrawal withdrawData.status = WithdrawStatus.Processing; @@ -600,48 +651,13 @@ contract GatewayUpgradeable is BitvmPolicy, Initializable, IGateway { BitvmTxParser.BitcoinTx calldata rawTake1Tx, MerkleProof.BitcoinTxProof calldata take1Proof ) external onlyCommittee { - WithdrawData storage withdrawData = withdrawDataMap[graphId]; - bytes16 instanceId = withdrawData.instanceId; - PeginDataInner storage peginData = peginDataMap[instanceId]; - if (withdrawData.status != WithdrawStatus.Processing) - revert WithdrawStatusInvalid(); - GraphData storage graphData = graphDataMap[graphId]; - bytes32 take1Txid = BitvmTxParser.computeTxid(rawTake1Tx); - if (take1Txid != graphData.take1Txid) revert TxidMismatch(); - (bytes32 blockHash, bytes32 merkleRoot) = MerkleProof - .parseBtcBlockHeader(take1Proof.rawHeader); - if (bitcoinSPV.blockHash(take1Proof.height) != blockHash) - revert InvalidHeader(); - if ( - !MerkleProof.verifyMerkleProof( - merkleRoot, - take1Proof.proof, - take1Txid, - take1Proof.index - ) - ) { - revert MerkleVerifyFail(); - } - - peginData.status = PeginStatus.Claimed; - withdrawData.status = WithdrawStatus.Complete; - - // incentive mechanism for honest Operators - uint64 rewardAmountSats = minOperatorRewardSats + - (peginData.peginAmountSats * operatorRewardRate) / - rateMultiplier; - pegBTC.transfer( - withdrawData.operatorAddress, - Converter.amountFromSats(rewardAmountSats) - ); - - emit WithdrawHappyPath( - instanceId, + _finalizeWithdraw( graphId, - take1Txid, - withdrawData.operatorAddress, - rewardAmountSats + rawTake1Tx, + take1Proof, + graphData.take1Txid, + true ); } @@ -650,48 +666,13 @@ contract GatewayUpgradeable is BitvmPolicy, Initializable, IGateway { BitvmTxParser.BitcoinTx calldata rawTake2Tx, MerkleProof.BitcoinTxProof calldata take2Proof ) external onlyCommittee { - WithdrawData storage withdrawData = withdrawDataMap[graphId]; - bytes16 instanceId = withdrawData.instanceId; - PeginDataInner storage peginData = peginDataMap[instanceId]; - if (withdrawData.status != WithdrawStatus.Processing) - revert WithdrawStatusInvalid(); - GraphData storage graphData = graphDataMap[graphId]; - bytes32 take2Txid = BitvmTxParser.computeTxid(rawTake2Tx); - if (take2Txid != graphData.take2Txid) revert TxidMismatch(); - (bytes32 blockHash, bytes32 merkleRoot) = MerkleProof - .parseBtcBlockHeader(take2Proof.rawHeader); - if (bitcoinSPV.blockHash(take2Proof.height) != blockHash) - revert InvalidHeader(); - if ( - !MerkleProof.verifyMerkleProof( - merkleRoot, - take2Proof.proof, - take2Txid, - take2Proof.index - ) - ) { - revert MerkleVerifyFail(); - } - - peginData.status = PeginStatus.Claimed; - withdrawData.status = WithdrawStatus.Complete; - - // incentive mechanism for honest Operators - uint64 rewardAmountSats = minOperatorRewardSats + - (peginData.peginAmountSats * operatorRewardRate) / - rateMultiplier; - pegBTC.transfer( - withdrawData.operatorAddress, - Converter.amountFromSats(rewardAmountSats) - ); - - emit WithdrawUnhappyPath( - instanceId, + _finalizeWithdraw( graphId, - take2Txid, - withdrawData.operatorAddress, - rewardAmountSats + rawTake2Tx, + take2Proof, + graphData.take2Txid, + false ); } @@ -717,8 +698,6 @@ contract GatewayUpgradeable is BitvmPolicy, Initializable, IGateway { address challengerAddress; bytes32 kickoffTxid; uint32 kickoffVout; - bytes32 blockHash; - bytes32 merkleRoot; if ( (disproveTxType == DisproveTxType.QuickChallenge || disproveTxType == DisproveTxType.ChallengeIncompeleteKickoff) && @@ -735,19 +714,11 @@ contract GatewayUpgradeable is BitvmPolicy, Initializable, IGateway { if (kickoffTxid != graphData.kickoffTxid) revert TxidMismatch(); if (kickoffVout != BitvmTxParser.CHALLENGE_CONNECTOR_VOUT) revert TxidMismatch(); - (blockHash, merkleRoot) = MerkleProof.parseBtcBlockHeader( - challengeStartTxProof.rawHeader + _verifyMerkleInclusion( + challengeStartTxProof, + challengeStartTxid, + true ); - if (bitcoinSPV.blockHash(challengeStartTxProof.height) != blockHash) - revert DisproveInvalidHeader(); - if ( - !MerkleProof.verifyMerkleProof( - merkleRoot, - challengeStartTxProof.proof, - challengeStartTxid, - challengeStartTxProof.index - ) - ) revert MerkleVerifyFail(); } // verify ChallengeFinish tx @@ -812,19 +783,11 @@ contract GatewayUpgradeable is BitvmPolicy, Initializable, IGateway { } else { revert UnknownDisproveType(); } - (blockHash, merkleRoot) = MerkleProof.parseBtcBlockHeader( - challengeFinishTxProof.rawHeader + _verifyMerkleInclusion( + challengeFinishTxProof, + challengeFinishTxid, + true ); - if (bitcoinSPV.blockHash(challengeFinishTxProof.height) != blockHash) - revert DisproveInvalidHeader(); - if ( - !MerkleProof.verifyMerkleProof( - merkleRoot, - challengeFinishTxProof.proof, - challengeFinishTxid, - challengeFinishTxProof.index - ) - ) revert MerkleVerifyFail(); withdrawData.status = WithdrawStatus.Disproved; // slash Operator & reward Challenger and Disprover From 5afd48d77229da5f05e65c203aef373906b7c3f5 Mon Sep 17 00:00:00 2001 From: Li-Qing Wang Date: Thu, 18 Dec 2025 00:27:32 +0800 Subject: [PATCH 06/11] fix: add prefix to internal functions --- script/InitWithdraw.sol | 2 +- src/CommitteeManagement.sol | 24 +++---- src/Gateway.sol | 47 +++++++------ src/StakeManagement.sol | 1 + src/libraries/BitvmTxParser.sol | 118 ++++++++++++++++---------------- src/libraries/Converter.sol | 4 +- src/libraries/MerkleProof.sol | 4 +- 7 files changed, 104 insertions(+), 96 deletions(-) diff --git a/script/InitWithdraw.sol b/script/InitWithdraw.sol index 67fc510..a0837fc 100644 --- a/script/InitWithdraw.sol +++ b/script/InitWithdraw.sol @@ -42,7 +42,7 @@ contract InitWithdraw is Script { // Read pegin data to compute the exact lock amount in PegBTC GatewayUpgradeable.PeginData memory pegin = gateway.getPeginData(instanceId); require(pegin.peginAmountSats > 0, "pegin not found or amount=0"); - uint256 lockAmount = Converter.amountFromSats(pegin.peginAmountSats); + uint256 lockAmount = Converter._amountFromSats(pegin.peginAmountSats); // derive sender address from private key in a view-only way address operator = vm.addr(pk); diff --git a/src/CommitteeManagement.sol b/src/CommitteeManagement.sol index 195f653..7ac4383 100644 --- a/src/CommitteeManagement.sol +++ b/src/CommitteeManagement.sol @@ -183,7 +183,7 @@ contract CommitteeManagement is MultiSigVerifier { uint256 nonce, bytes[] memory authSignatures ) external { - bytes32 msgHash = getAddWatchtowerDigest(watchtower); + bytes32 msgHash = _getAddWatchtowerDigest(watchtower); _executeNoncedSignatures(msgHash, nonce, authSignatures); watchtowerList.add(watchtower); } @@ -194,14 +194,14 @@ contract CommitteeManagement is MultiSigVerifier { uint256 nonce, bytes[] memory authSignatures ) external { - bytes32 msgHash = getRemoveWatchtowerDigest(watchtower); + bytes32 msgHash = _getRemoveWatchtowerDigest(watchtower); _executeNoncedSignatures(msgHash, nonce, authSignatures); watchtowerList.remove(watchtower); } // ========== Digest Helpers (Watchtower) ========== /// @dev Returns the domain-bound message hash for adding a watchtower (without nonce) - function getAddWatchtowerDigest( + function _getAddWatchtowerDigest( bytes32 watchtower ) internal view returns (bytes32) { bytes32 typeHash = keccak256("ADD_WATCHTOWER(bytes32 watchtower)"); @@ -213,12 +213,12 @@ contract CommitteeManagement is MultiSigVerifier { bytes32 watchtower, uint256 nonce ) public view returns (bytes32) { - bytes32 msgHash = getAddWatchtowerDigest(watchtower); + bytes32 msgHash = _getAddWatchtowerDigest(watchtower); return getNoncedDigest(msgHash, nonce); } /// @dev Returns the domain-bound message hash for removing a watchtower (without nonce) - function getRemoveWatchtowerDigest( + function _getRemoveWatchtowerDigest( bytes32 watchtower ) internal view returns (bytes32) { bytes32 typeHash = keccak256("REMOVE_WATCHTOWER(bytes32 watchtower)"); @@ -230,7 +230,7 @@ contract CommitteeManagement is MultiSigVerifier { bytes32 watchtower, uint256 nonce ) public view returns (bytes32) { - bytes32 msgHash = getRemoveWatchtowerDigest(watchtower); + bytes32 msgHash = _getRemoveWatchtowerDigest(watchtower); return getNoncedDigest(msgHash, nonce); } @@ -264,7 +264,7 @@ contract CommitteeManagement is MultiSigVerifier { uint256 nonce, bytes[] memory authSignatures ) external { - bytes32 msgHash = getAddAuthorizedCallerDigest(caller); + bytes32 msgHash = _getAddAuthorizedCallerDigest(caller); _executeNoncedSignatures(msgHash, nonce, authSignatures); authorizedCallers.add(caller); emit AuthorizedCallerAdded(caller); @@ -276,7 +276,7 @@ contract CommitteeManagement is MultiSigVerifier { uint256 nonce, bytes[] memory authSignatures ) external { - bytes32 msgHash = getRemoveAuthorizedCallerDigest(caller); + bytes32 msgHash = _getRemoveAuthorizedCallerDigest(caller); _executeNoncedSignatures(msgHash, nonce, authSignatures); authorizedCallers.remove(caller); emit AuthorizedCallerRemoved(caller); @@ -284,7 +284,7 @@ contract CommitteeManagement is MultiSigVerifier { // ========== Digest Helpers (Authorized Callers) ========== /// @dev Returns the domain-bound message hash for adding an authorized external caller (without nonce) - function getAddAuthorizedCallerDigest( + function _getAddAuthorizedCallerDigest( address caller ) internal view returns (bytes32) { bytes32 typeHash = keccak256("ADD_AUTH_CALLER(address caller)"); @@ -296,12 +296,12 @@ contract CommitteeManagement is MultiSigVerifier { address caller, uint256 nonce ) public view returns (bytes32) { - bytes32 msgHash = getAddAuthorizedCallerDigest(caller); + bytes32 msgHash = _getAddAuthorizedCallerDigest(caller); return getNoncedDigest(msgHash, nonce); } /// @dev Returns the domain-bound message hash for removing an authorized external caller (without nonce) - function getRemoveAuthorizedCallerDigest( + function _getRemoveAuthorizedCallerDigest( address caller ) internal view returns (bytes32) { bytes32 typeHash = keccak256("REMOVE_AUTH_CALLER(address caller)"); @@ -313,7 +313,7 @@ contract CommitteeManagement is MultiSigVerifier { address caller, uint256 nonce ) public view returns (bytes32) { - bytes32 msgHash = getRemoveAuthorizedCallerDigest(caller); + bytes32 msgHash = _getRemoveAuthorizedCallerDigest(caller); return getNoncedDigest(msgHash, nonce); } diff --git a/src/Gateway.sol b/src/Gateway.sol index 44777e1..ec7ab97 100644 --- a/src/Gateway.sol +++ b/src/Gateway.sol @@ -68,6 +68,10 @@ contract GatewayUpgradeable is BitvmPolicy, Initializable, IGateway { mapping(bytes16 graphId => GraphData) public graphDataMap; mapping(bytes16 graphId => WithdrawData) public withdrawDataMap; + constructor() { + _disableInitializers(); + } + // initializer function initialize( IPegBTC _pegBTC, @@ -191,7 +195,7 @@ contract GatewayUpgradeable is BitvmPolicy, Initializable, IGateway { ); } - function getCancelWithdrawDigest( + function _getCancelWithdrawDigest( bytes16 graphId ) internal view returns (bytes32) { return @@ -204,11 +208,11 @@ contract GatewayUpgradeable is BitvmPolicy, Initializable, IGateway { bytes16 graphId, uint256 nonce ) public view returns (bytes32) { - bytes32 msgHash = getCancelWithdrawDigest(graphId); + bytes32 msgHash = _getCancelWithdrawDigest(graphId); return committeeManagement.getNoncedDigest(msgHash, nonce); } - function getUnlockStakeDigest( + function _getUnlockStakeDigest( address operator, uint256 amount ) internal view returns (bytes32) { @@ -228,7 +232,7 @@ contract GatewayUpgradeable is BitvmPolicy, Initializable, IGateway { uint256 amount, uint256 nonce ) public view returns (bytes32) { - bytes32 msgHash = getUnlockStakeDigest(operator, amount); + bytes32 msgHash = _getUnlockStakeDigest(operator, amount); return committeeManagement.getNoncedDigest(msgHash, nonce); } @@ -277,7 +281,7 @@ contract GatewayUpgradeable is BitvmPolicy, Initializable, IGateway { if (withdrawData.status != WithdrawStatus.Processing) revert WithdrawStatusInvalid(); - bytes32 takeTxid = BitvmTxParser.computeTxid(rawTakeTx); + bytes32 takeTxid = BitvmTxParser._computeTxid(rawTakeTx); if (takeTxid != expectedTxid) revert TxidMismatch(); _verifyMerkleInclusion(takeProof, takeTxid, false); @@ -287,7 +291,7 @@ contract GatewayUpgradeable is BitvmPolicy, Initializable, IGateway { uint64 rewardAmountSats = _operatorReward(peginData.peginAmountSats); pegBTC.transfer( withdrawData.operatorAddress, - Converter.amountFromSats(rewardAmountSats) + Converter._amountFromSats(rewardAmountSats) ); if (happyPath) { @@ -331,6 +335,7 @@ contract GatewayUpgradeable is BitvmPolicy, Initializable, IGateway { string calldata userChangeAddress, string calldata userRefundAddress ) external payable { + // TODO: check if request already exists PeginDataInner storage peginData = peginDataMap[instanceId]; if (peginData.status != PeginStatus.None) revert InstanceUsed(); // TODO: check peginAmount,feeRate,userInputs @@ -449,7 +454,7 @@ contract GatewayUpgradeable is BitvmPolicy, Initializable, IGateway { uint64 peginAmountSats, address depositorAddress, bytes16 parsedInstanceId - ) = BitvmTxParser.parsePegin(rawPeginTx); + ) = BitvmTxParser._parsePegin(rawPeginTx); if (parsedInstanceId != instanceId) revert InstanceMismatch(); if (peginAmountSats != peginData.peginAmountSats) revert PeginAmountMismatch(); @@ -481,9 +486,9 @@ contract GatewayUpgradeable is BitvmPolicy, Initializable, IGateway { if (feeAmountSats >= peginAmountSats) revert FeeTooHigh(); pegBTC.mint( depositorAddress, - Converter.amountFromSats(peginAmountSats - feeAmountSats) + Converter._amountFromSats(peginAmountSats - feeAmountSats) ); - pegBTC.mint(address(this), Converter.amountFromSats(feeAmountSats)); + pegBTC.mint(address(this), Converter._amountFromSats(feeAmountSats)); emit BridgeIn( depositorAddress, @@ -554,7 +559,7 @@ contract GatewayUpgradeable is BitvmPolicy, Initializable, IGateway { peginData.status = PeginStatus.Locked; // lock operator's pegBTC - uint256 lockAmount = Converter.amountFromSats( + uint256 lockAmount = Converter._amountFromSats( peginData.peginAmountSats ); pegBTC.transferFrom(msg.sender, address(this), lockAmount); @@ -588,6 +593,7 @@ contract GatewayUpgradeable is BitvmPolicy, Initializable, IGateway { revert TimelockNotExpired(); } withdrawData.status = WithdrawStatus.Canceled; + // FIXME: transfer to operator or gateway? pegBTC.transfer(msg.sender, withdrawData.lockAmount); peginData.status = PeginStatus.Withdrawbale; @@ -601,7 +607,7 @@ contract GatewayUpgradeable is BitvmPolicy, Initializable, IGateway { ) external { // validate committeeSigs WithdrawData storage withdrawData = withdrawDataMap[graphId]; - bytes32 cancel_digest = getCancelWithdrawDigest(graphId); + bytes32 cancel_digest = _getCancelWithdrawDigest(graphId); committeeManagement.executeNoncedSignatures( cancel_digest, nonce, @@ -614,6 +620,7 @@ contract GatewayUpgradeable is BitvmPolicy, Initializable, IGateway { if (withdrawData.status != WithdrawStatus.Initialized) revert WithdrawStatusInvalid(); withdrawData.status = WithdrawStatus.Canceled; + // FIXME: transfer to operator or gateway? pegBTC.transfer(msg.sender, withdrawData.lockAmount); peginData.status = PeginStatus.Withdrawbale; emit CancelWithdraw(withdrawData.instanceId, graphId, msg.sender); @@ -633,7 +640,7 @@ contract GatewayUpgradeable is BitvmPolicy, Initializable, IGateway { revert KickoffHeightLow(); GraphData storage graphData = graphDataMap[graphId]; - bytes32 kickoffTxid = BitvmTxParser.computeTxid(rawKickoffTx); + bytes32 kickoffTxid = BitvmTxParser._computeTxid(rawKickoffTx); if (kickoffTxid != graphData.kickoffTxid) revert TxidMismatch(); _verifyMerkleInclusion(kickoffProof, kickoffTxid, false); @@ -710,7 +717,7 @@ contract GatewayUpgradeable is BitvmPolicy, Initializable, IGateway { kickoffTxid, kickoffVout, challengerAddress - ) = BitvmTxParser.parseChallengeTx(rawChallengeStartTx); + ) = BitvmTxParser._parseChallengeTx(rawChallengeStartTx); if (kickoffTxid != graphData.kickoffTxid) revert TxidMismatch(); if (kickoffVout != BitvmTxParser.CHALLENGE_CONNECTOR_VOUT) revert TxidMismatch(); @@ -725,7 +732,7 @@ contract GatewayUpgradeable is BitvmPolicy, Initializable, IGateway { bytes32 challengeFinishTxid; address disproverAddress; if (disproveTxType == DisproveTxType.AssertTimeout) { - (challengeFinishTxid) = BitvmTxParser.computeTxid( + (challengeFinishTxid) = BitvmTxParser._computeTxid( rawChallengeFinishTx ); if (graphData.assertTimoutTxids.length <= txnIndex) @@ -733,13 +740,13 @@ contract GatewayUpgradeable is BitvmPolicy, Initializable, IGateway { if (challengeFinishTxid != graphData.assertTimoutTxids[txnIndex]) revert TxidMismatch(); } else if (disproveTxType == DisproveTxType.OperatorCommitTimeout) { - (challengeFinishTxid) = BitvmTxParser.computeTxid( + (challengeFinishTxid) = BitvmTxParser._computeTxid( rawChallengeFinishTx ); if (challengeFinishTxid != graphData.commitTimoutTxid) revert TxidMismatch(); } else if (disproveTxType == DisproveTxType.OperatorNack) { - (challengeFinishTxid) = BitvmTxParser.computeTxid( + (challengeFinishTxid) = BitvmTxParser._computeTxid( rawChallengeFinishTx ); if (graphData.NackTxids.length <= txnIndex) @@ -752,7 +759,7 @@ contract GatewayUpgradeable is BitvmPolicy, Initializable, IGateway { kickoffTxid, kickoffVout, disproverAddress - ) = BitvmTxParser.parseDisproveTx(rawChallengeFinishTx); + ) = BitvmTxParser._parseDisproveTx(rawChallengeFinishTx); if (kickoffTxid != graphData.kickoffTxid) revert TxidMismatch(); if (kickoffVout != BitvmTxParser.DISPROVE_CONNECTOR_VOUT) revert TxidMismatch(); @@ -762,7 +769,7 @@ contract GatewayUpgradeable is BitvmPolicy, Initializable, IGateway { kickoffTxid, kickoffVout, disproverAddress - ) = BitvmTxParser.parseQuickChallengeTx(rawChallengeFinishTx); + ) = BitvmTxParser._parseQuickChallengeTx(rawChallengeFinishTx); if (kickoffTxid != graphData.kickoffTxid) revert TxidMismatch(); if (kickoffVout != BitvmTxParser.GUARDIAN_CONNECTOR_VOUT) revert TxidMismatch(); @@ -774,7 +781,7 @@ contract GatewayUpgradeable is BitvmPolicy, Initializable, IGateway { kickoffTxid, kickoffVout, disproverAddress - ) = BitvmTxParser.parseChallengeIncompleteKickoffTx( + ) = BitvmTxParser._parseChallengeIncompleteKickoffTx( rawChallengeFinishTx ); if (kickoffTxid != graphData.kickoffTxid) revert TxidMismatch(); @@ -835,7 +842,7 @@ contract GatewayUpgradeable is BitvmPolicy, Initializable, IGateway { uint256 nonce, bytes[] calldata committeeSigs ) external { - bytes32 msgHash = getUnlockStakeDigest(operator, amount); + bytes32 msgHash = _getUnlockStakeDigest(operator, amount); committeeManagement.executeNoncedSignatures( msgHash, nonce, diff --git a/src/StakeManagement.sol b/src/StakeManagement.sol index 1fa585d..16afbba 100644 --- a/src/StakeManagement.sol +++ b/src/StakeManagement.sol @@ -47,6 +47,7 @@ contract StakeManagement is IStakeManagement, Initializable { return lockedStakes[operator]; } + // TODO: add caller authentication? function slashStake(address operator, uint256 amount) external override { require(stakes[operator] >= amount, "insufficient stake to slash"); if (lockedStakes[operator] > amount) { diff --git a/src/libraries/BitvmTxParser.sol b/src/libraries/BitvmTxParser.sol index 50674be..416416a 100644 --- a/src/libraries/BitvmTxParser.sol +++ b/src/libraries/BitvmTxParser.sol @@ -15,50 +15,50 @@ library BitvmTxParser { uint32 constant DISPROVE_CONNECTOR_VOUT = 3; uint32 constant GUARDIAN_CONNECTOR_VOUT = 4; - function parsePegin(BitcoinTx memory bitcoinTx) + function _parsePegin(BitcoinTx memory bitcoinTx) internal pure returns (bytes32 peginTxid, uint64 peginAmountSats, address depositorAddress, bytes16 instanceId) { - peginTxid = computeTxid(bitcoinTx); + peginTxid = _computeTxid(bitcoinTx); bytes memory txouts = bitcoinTx.outputVector; // memory layout of bitcoinTx.outputVector: // | outputVector.length(32-bytes) | outputcount(compact-size).[amount(8-bytes).scriptpubkeysize(compact-size).scriptpubkey(x-bytes); n] // peginAmountSats is the amount of txout[0] - (, uint256 offset) = parseCompactSize(txouts, 32); - uint64 peginAmountSatsRev = uint64(bytes8(memLoad(txouts, offset))); + (, uint256 offset) = _parseCompactSize(txouts, 32); + uint64 peginAmountSatsRev = uint64(bytes8(_memLoad(txouts, offset))); uint256 scriptpubkeysize; - (scriptpubkeysize, offset) = parseCompactSize(txouts, offset + 8); + (scriptpubkeysize, offset) = _parseCompactSize(txouts, offset + 8); uint256 nextTxoutOffset = scriptpubkeysize + offset; // instance-id & depositorAddress is op_return data of txout[1] // Bitvm pegin OP_RETURN script (46-bytes): // OP_RETURN OP_PUSHBYTES44 {magic-bytes(8-bytes)} {instance-id(16-bytes)} {depositorAddress(20-bytes)} - (uint256 opReturnScriptSize, uint256 opReturnScriptOffset) = parseCompactSize(txouts, nextTxoutOffset + 8); - bytes2 firstTwoOpcode = bytes2(memLoad(txouts, opReturnScriptOffset)); + (uint256 opReturnScriptSize, uint256 opReturnScriptOffset) = _parseCompactSize(txouts, nextTxoutOffset + 8); + bytes2 firstTwoOpcode = bytes2(_memLoad(txouts, opReturnScriptOffset)); require(opReturnScriptSize == 46 && firstTwoOpcode == 0x6a2c, "invalid pegin OP_RETURN script"); - require(bytes8(memLoad(txouts, opReturnScriptOffset + 2)) == Constants.magic_bytes, "magic_bytes mismatch"); - instanceId = bytes16(memLoad(txouts, opReturnScriptOffset + 10)); - depositorAddress = address(bytes20(memLoad(txouts, opReturnScriptOffset + 26))); - peginAmountSats = reverseUint64(peginAmountSatsRev); + require(bytes8(_memLoad(txouts, opReturnScriptOffset + 2)) == Constants.magic_bytes, "magic_bytes mismatch"); + instanceId = bytes16(_memLoad(txouts, opReturnScriptOffset + 10)); + depositorAddress = address(bytes20(_memLoad(txouts, opReturnScriptOffset + 26))); + peginAmountSats = _reverseUint64(peginAmountSatsRev); } - function parseChallengeTx(BitcoinTx memory bitcoinTx) + function _parseChallengeTx(BitcoinTx memory bitcoinTx) internal pure returns (bytes32 challengeTxid, bytes32 kickoffTxid, uint32 kickoffVout, address challengerAddress) { - challengeTxid = computeTxid(bitcoinTx); + challengeTxid = _computeTxid(bitcoinTx); // kickoffTxid is txid of the txin[0] // memory layout of bitcoinTx.inputVector: // | inputVector.length(32-bytes) | inputcount(compact-size).input_0_txid(32-bytes).input_0_vout(4-bytes little-endian)... bytes memory txin = bitcoinTx.inputVector; - (, uint256 offset) = parseCompactSize(txin, 32); - kickoffTxid = memLoad(txin, offset); + (, uint256 offset) = _parseCompactSize(txin, 32); + kickoffTxid = _memLoad(txin, offset); // kickoffVout is vout of the txin[0] - kickoffVout = reverseUint32(uint32(bytes4(memLoad(txin, offset + 32)))); + kickoffVout = _reverseUint32(uint32(bytes4(_memLoad(txin, offset + 32)))); // challengerAddress is op_return data of txout[1] // if txout[1] is not op_return, return address(0) @@ -67,32 +67,32 @@ library BitvmTxParser { challengerAddress = address(0); bytes memory txouts = bitcoinTx.outputVector; uint256 outputCount; - (outputCount, offset) = parseCompactSize(txouts, 32); + (outputCount, offset) = _parseCompactSize(txouts, 32); if (outputCount >= 2) { uint256 scriptpubkeysize; - (scriptpubkeysize, offset) = parseCompactSize(txouts, offset + 8); + (scriptpubkeysize, offset) = _parseCompactSize(txouts, offset + 8); uint256 nextTxoutOffset = scriptpubkeysize + offset; - (uint256 opReturnScriptSize, uint256 opReturnScriptOffset) = parseCompactSize(txouts, nextTxoutOffset + 8); - bytes2 firstTwoOpcode = bytes2(memLoad(txouts, opReturnScriptOffset)); + (uint256 opReturnScriptSize, uint256 opReturnScriptOffset) = _parseCompactSize(txouts, nextTxoutOffset + 8); + bytes2 firstTwoOpcode = bytes2(_memLoad(txouts, opReturnScriptOffset)); if (opReturnScriptSize == 22 && firstTwoOpcode == 0x6a14) { - challengerAddress = address(bytes20(memLoad(txouts, opReturnScriptOffset + 2))); + challengerAddress = address(bytes20(_memLoad(txouts, opReturnScriptOffset + 2))); } } } - function parseDisproveTx(BitcoinTx memory bitcoinTx) + function _parseDisproveTx(BitcoinTx memory bitcoinTx) internal pure returns (bytes32 disproveTxid, bytes32 kickoffTxid, uint32 kickoffVout, address challengerAddress) { - disproveTxid = computeTxid(bitcoinTx); + disproveTxid = _computeTxid(bitcoinTx); // kickoffTxid is txid of the txin[0] bytes memory txin = bitcoinTx.inputVector; - (, uint256 offset) = parseCompactSize(txin, 32); - kickoffTxid = memLoad(txin, offset); + (, uint256 offset) = _parseCompactSize(txin, 32); + kickoffTxid = _memLoad(txin, offset); // kickoffVout is vout of the txin[0] - kickoffVout = reverseUint32(uint32(bytes4(memLoad(txin, offset + 32)))); + kickoffVout = _reverseUint32(uint32(bytes4(_memLoad(txin, offset + 32)))); // challengerAddress is op_return data of txout[0] // if txout[0] is not op_return, return address(0) @@ -100,27 +100,27 @@ library BitvmTxParser { // OP_RETURN OP_PUSHBYTES20 {challengerAddress(20-bytes)} challengerAddress = address(0); bytes memory txouts = bitcoinTx.outputVector; - (, offset) = parseCompactSize(txouts, 32); - (uint256 opReturnScriptSize, uint256 opReturnScriptOffset) = parseCompactSize(txouts, offset + 8); - bytes2 firstTwoOpcode = bytes2(memLoad(txouts, opReturnScriptOffset)); + (, offset) = _parseCompactSize(txouts, 32); + (uint256 opReturnScriptSize, uint256 opReturnScriptOffset) = _parseCompactSize(txouts, offset + 8); + bytes2 firstTwoOpcode = bytes2(_memLoad(txouts, opReturnScriptOffset)); if (opReturnScriptSize == 22 && firstTwoOpcode == 0x6a14) { - challengerAddress = address(bytes20(memLoad(txouts, opReturnScriptOffset + 2))); + challengerAddress = address(bytes20(_memLoad(txouts, opReturnScriptOffset + 2))); } } - function parseQuickChallengeTx(BitcoinTx memory bitcoinTx) + function _parseQuickChallengeTx(BitcoinTx memory bitcoinTx) internal pure returns (bytes32 quickChallengeTxid, bytes32 kickoffTxid, uint32 kickoffVout, address challengerAddress) { - quickChallengeTxid = computeTxid(bitcoinTx); + quickChallengeTxid = _computeTxid(bitcoinTx); // kickoffTxid is txid of the txin[0] bytes memory txin = bitcoinTx.inputVector; - (, uint256 offset) = parseCompactSize(txin, 32); - kickoffTxid = memLoad(txin, offset); + (, uint256 offset) = _parseCompactSize(txin, 32); + kickoffTxid = _memLoad(txin, offset); // kickoffVout is vout of the txin[0] - kickoffVout = reverseUint32(uint32(bytes4(memLoad(txin, offset + 32)))); + kickoffVout = _reverseUint32(uint32(bytes4(_memLoad(txin, offset + 32)))); // challengerAddress is op_return data of txout[0] // if txout[0] is not op_return, return address(0) @@ -128,15 +128,15 @@ library BitvmTxParser { // OP_RETURN OP_PUSHBYTES20 {challengerAddress(20-bytes)} challengerAddress = address(0); bytes memory txouts = bitcoinTx.outputVector; - (, offset) = parseCompactSize(txouts, 32); - (uint256 opReturnScriptSize, uint256 opReturnScriptOffset) = parseCompactSize(txouts, offset + 8); - bytes2 firstTwoOpcode = bytes2(memLoad(txouts, opReturnScriptOffset)); + (, offset) = _parseCompactSize(txouts, 32); + (uint256 opReturnScriptSize, uint256 opReturnScriptOffset) = _parseCompactSize(txouts, offset + 8); + bytes2 firstTwoOpcode = bytes2(_memLoad(txouts, opReturnScriptOffset)); if (opReturnScriptSize == 22 && firstTwoOpcode == 0x6a14) { - challengerAddress = address(bytes20(memLoad(txouts, opReturnScriptOffset + 2))); + challengerAddress = address(bytes20(_memLoad(txouts, opReturnScriptOffset + 2))); } } - function parseChallengeIncompleteKickoffTx(BitcoinTx memory bitcoinTx) + function _parseChallengeIncompleteKickoffTx(BitcoinTx memory bitcoinTx) internal pure returns ( @@ -146,14 +146,14 @@ library BitvmTxParser { address challengerAddress ) { - challengeIncompleteKickoffTxid = computeTxid(bitcoinTx); + challengeIncompleteKickoffTxid = _computeTxid(bitcoinTx); // kickoffTxid is txid of the txin[0] bytes memory txin = bitcoinTx.inputVector; - (, uint256 offset) = parseCompactSize(txin, 32); - kickoffTxid = memLoad(txin, offset); + (, uint256 offset) = _parseCompactSize(txin, 32); + kickoffTxid = _memLoad(txin, offset); // kickoffVout is vout of the txin[0] - kickoffVout = reverseUint32(uint32(bytes4(memLoad(txin, offset + 32)))); + kickoffVout = _reverseUint32(uint32(bytes4(_memLoad(txin, offset + 32)))); // challengerAddress is op_return data of txout[0] // if txout[0] is not op_return, return address(0) @@ -161,31 +161,31 @@ library BitvmTxParser { // OP_RETURN OP_PUSHBYTES20 {challengerAddress(20-bytes)} challengerAddress = address(0); bytes memory txouts = bitcoinTx.outputVector; - (, offset) = parseCompactSize(txouts, 32); - (uint256 opReturnScriptSize, uint256 opReturnScriptOffset) = parseCompactSize(txouts, offset + 8); - bytes2 firstTwoOpcode = bytes2(memLoad(txouts, opReturnScriptOffset)); + (, offset) = _parseCompactSize(txouts, 32); + (uint256 opReturnScriptSize, uint256 opReturnScriptOffset) = _parseCompactSize(txouts, offset + 8); + bytes2 firstTwoOpcode = bytes2(_memLoad(txouts, opReturnScriptOffset)); if (opReturnScriptSize == 22 && firstTwoOpcode == 0x6a14) { - challengerAddress = address(bytes20(memLoad(txouts, opReturnScriptOffset + 2))); + challengerAddress = address(bytes20(_memLoad(txouts, opReturnScriptOffset + 2))); } } - function computeTxid(BitcoinTx memory bitcoinTx) internal pure returns (bytes32) { + function _computeTxid(BitcoinTx memory bitcoinTx) internal pure returns (bytes32) { bytes memory rawTx = abi.encodePacked(bitcoinTx.version, bitcoinTx.inputVector, bitcoinTx.outputVector, bitcoinTx.locktime); - return hash256(rawTx); + return _hash256(rawTx); } - function hash256(bytes memory raw) internal pure returns (bytes32) { + function _hash256(bytes memory raw) internal pure returns (bytes32) { return sha256(abi.encodePacked(sha256(raw))); } - function memLoad(bytes memory data, uint256 offset) internal pure returns (bytes32 res) { + function _memLoad(bytes memory data, uint256 offset) internal pure returns (bytes32 res) { assembly { res := mload(add(data, offset)) } } - function reverseUint64(uint64 _b) internal pure returns (uint64 v) { + function _reverseUint64(uint64 _b) internal pure returns (uint64 v) { v = _b; // swap bytes v = ((v >> 8) & 0x00FF00FF00FF00FF) | ((v & 0x00FF00FF00FF00FF) << 8); @@ -195,7 +195,7 @@ library BitvmTxParser { v = (v >> 32) | (v << 32); } - function reverseUint32(uint32 _b) internal pure returns (uint32 v) { + function _reverseUint32(uint32 _b) internal pure returns (uint32 v) { v = _b; // swap bytes @@ -204,11 +204,11 @@ library BitvmTxParser { v = (v >> 16) | (v << 16); } - function reverseUint16(uint16 _b) internal pure returns (uint16 v) { + function _reverseUint16(uint16 _b) internal pure returns (uint16 v) { v = (_b << 8) | (_b >> 8); } - function parseCompactSize(bytes memory data, uint256 offset) + function _parseCompactSize(bytes memory data, uint256 offset) internal pure returns (uint256 size, uint256 nextOffset) @@ -221,7 +221,7 @@ library BitvmTxParser { assembly { sizeRev := mload(sub(add(data, offset), 23)) // -23 = 1 + 8 - 32 } - size = reverseUint64(sizeRev); + size = _reverseUint64(sizeRev); } if (uint8(data[offset - 32]) == 0xfe) { nextOffset = offset + 5; // one-byte flag, 4 bytes data @@ -229,7 +229,7 @@ library BitvmTxParser { assembly { sizeRev := mload(sub(add(data, offset), 27)) // -27 = 1 + 4 - 32 } - size = reverseUint32(sizeRev); + size = _reverseUint32(sizeRev); } if (uint8(data[offset - 32]) == 0xfd) { nextOffset = offset + 3; // one-byte flag, 2 bytes data @@ -237,7 +237,7 @@ library BitvmTxParser { assembly { sizeRev := mload(sub(add(data, offset), 29)) // -29 = 1 + 2 - 32 } - size = reverseUint16(sizeRev); + size = _reverseUint16(sizeRev); } nextOffset = offset + 1; // one-byte flag, 0 bytes data size = uint8(data[offset - 32]); diff --git a/src/libraries/Converter.sol b/src/libraries/Converter.sol index 4f67b16..c7e74a9 100644 --- a/src/libraries/Converter.sol +++ b/src/libraries/Converter.sol @@ -6,7 +6,7 @@ import {Constants} from "../Constants.sol"; library Converter { uint8 constant BtcDecimals = 8; - function amountFromSats(uint64 amountSats) internal pure returns (uint256) { + function _amountFromSats(uint64 amountSats) internal pure returns (uint256) { uint8 TokenDecimals = Constants.TokenDecimals; if (TokenDecimals >= BtcDecimals) { return uint256(amountSats * uint64(10 ** (TokenDecimals - BtcDecimals))); @@ -15,7 +15,7 @@ library Converter { } } - function amountToSats(uint256 amount) internal pure returns (uint64) { + function _amountToSats(uint256 amount) internal pure returns (uint64) { uint8 TokenDecimals = Constants.TokenDecimals; if (TokenDecimals >= BtcDecimals) { return uint64(amount / uint256(10 ** (TokenDecimals - BtcDecimals))); diff --git a/src/libraries/MerkleProof.sol b/src/libraries/MerkleProof.sol index 4367ee1..d3b668f 100644 --- a/src/libraries/MerkleProof.sol +++ b/src/libraries/MerkleProof.sol @@ -16,8 +16,8 @@ library MerkleProof { pure returns (bytes32 blockHash, bytes32 merkleRoot) { - blockHash = BitvmTxParser.hash256(rawHeader); - merkleRoot = BitvmTxParser.memLoad(rawHeader, 0x44); + blockHash = BitvmTxParser._hash256(rawHeader); + merkleRoot = BitvmTxParser._memLoad(rawHeader, 0x44); } function verifyMerkleProof(bytes32 root, bytes32[] memory proof, bytes32 leaf, uint256 index) From a1c4f9ea71e5095b958bd3283aaf60e4ccd3f366 Mon Sep 17 00:00:00 2001 From: Li-Qing Wang Date: Fri, 19 Dec 2025 15:12:15 +0800 Subject: [PATCH 07/11] fix: add validation to deploy script, initialize when deploy, update deploy guide --- .env.example | 17 +++++++++++++++++ Makefile | 6 ++++++ script/DeployContractDebug.sol | 34 ++++++++++++++++------------------ script/DeployGateway.sol | 34 ++++++++++++++++------------------ script/README.md | 27 ++++++++++++++++++++------- src/MultiSigVerifier.sol | 4 +--- 6 files changed, 76 insertions(+), 46 deletions(-) create mode 100644 .env.example create mode 100644 Makefile diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..96aae60 --- /dev/null +++ b/.env.example @@ -0,0 +1,17 @@ +### Replace placeholder values before running scripts + +# Private key used by forge script (hex without quotes) +PRIVATE_KEY=0xabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcd + +# External RPC endpoint to broadcast transactions +RPC_URL=https://rpc.testnet3.goat.network + +# Previously deployed Bitcoin SPV verifier contract +BITCOINSPV_ADDR=0x0000000000000000000000000000000000000001 + +# Committee member addresses (add or remove sequentially numbered keys) +COMMITTEE_0=0x0000000000000000000000000000000000000001 +COMMITTEE_1=0x0000000000000000000000000000000000000002 + +# Watchtower identifiers (32-byte hex strings). Add WATCHTOWER_2, etc. as needed. +WATCHTOWER_0=0x0000000000000000000000000000000000000000000000000000000000000001 diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..176bb8f --- /dev/null +++ b/Makefile @@ -0,0 +1,6 @@ +deployMain: + forge script script/DeployGateway.sol:DeployGateway --rpc-url goatMainnet --broadcast -vvvv --verify --verifier blockscout --verifier-url https://explorer.goat.network/api/ + +deployTest: + forge script script/DeployGateway.sol:DeployGateway --rpc-url goatTestnet --broadcast -vvvv --verify --verifier blockscout --verifier-url https://explorer.testnet3.goat.network/api/ + diff --git a/script/DeployContractDebug.sol b/script/DeployContractDebug.sol index ba495e3..88ba35f 100644 --- a/script/DeployContractDebug.sol +++ b/script/DeployContractDebug.sol @@ -54,6 +54,8 @@ contract DeployGateway is Script { bytes32[] memory initialWatchtowers = _readSequentialBytes32( "WATCHTOWER" ); + require(initialMembers.length > 0, "COMMITTEE list empty"); + require(initialWatchtowers.length > 0, "WATCHTOWER list empty"); address[] memory initialAuthorizedCallers = new address[](1); initialAuthorizedCallers[0] = address(gateway); @@ -62,24 +64,22 @@ contract DeployGateway is Script { "CommitteeManagement implementation contract address: ", address(committeeImpl) ); + bytes memory committeeInitData = abi.encodeWithSelector( + CommitteeManagement.initialize.selector, + initialMembers, + initialRequired, + initialAuthorizedCallers, + initialWatchtowers + ); UpgradeableProxy committeeProxy = new UpgradeableProxy( address(committeeImpl), deployer, - "" + committeeInitData ); console.log( "CommitteeManagement proxy contract address: ", address(committeeProxy) ); - CommitteeManagement committeeManagementImpl = CommitteeManagement( - address(committeeProxy) - ); - committeeManagementImpl.initialize( - initialMembers, - initialRequired, - initialAuthorizedCallers, - initialWatchtowers - ); ICommitteeManagement committeeManagement = ICommitteeManagement( address(committeeProxy) ); @@ -89,22 +89,20 @@ contract DeployGateway is Script { "StakeManagement implementation contract address: ", address(stakeImpl) ); + bytes memory stakeInitData = abi.encodeWithSelector( + StakeManagement.initialize.selector, + IERC20(address(pegBTC)), + address(gateway) + ); UpgradeableProxy stakeProxy = new UpgradeableProxy( address(stakeImpl), deployer, - "" + stakeInitData ); console.log( "StakeManagement proxy contract address: ", address(stakeProxy) ); - StakeManagement stakeManagementImpl = StakeManagement( - address(stakeProxy) - ); - stakeManagementImpl.initialize( - IERC20(address(pegBTC)), - address(gateway) - ); IStakeManagement stakeManagement = IStakeManagement( address(stakeProxy) ); diff --git a/script/DeployGateway.sol b/script/DeployGateway.sol index 8285ba2..067d55a 100644 --- a/script/DeployGateway.sol +++ b/script/DeployGateway.sol @@ -56,6 +56,8 @@ contract DeployGateway is Script { bytes32[] memory initialWatchtowers = _readSequentialBytes32( "WATCHTOWER" ); + require(initialMembers.length > 0, "COMMITTEE list empty"); + require(initialWatchtowers.length > 0, "WATCHTOWER list empty"); address[] memory initialAuthorizedCallers = new address[](1); initialAuthorizedCallers[0] = address(gateway); @@ -65,24 +67,22 @@ contract DeployGateway is Script { "CommitteeManagement implementation contract address: ", address(committeeImpl) ); + bytes memory committeeInitData = abi.encodeWithSelector( + CommitteeManagement.initialize.selector, + initialMembers, + initialRequired, + initialAuthorizedCallers, + initialWatchtowers + ); UpgradeableProxy committeeProxy = new UpgradeableProxy( address(committeeImpl), deployer, - "" + committeeInitData ); console.log( "CommitteeManagement proxy contract address: ", address(committeeProxy) ); - CommitteeManagement committeeManagementImpl = CommitteeManagement( - address(committeeProxy) - ); - committeeManagementImpl.initialize( - initialMembers, - initialRequired, - initialAuthorizedCallers, - initialWatchtowers - ); ICommitteeManagement committeeManagement = ICommitteeManagement( address(committeeProxy) ); @@ -93,22 +93,20 @@ contract DeployGateway is Script { "StakeManagement implementation contract address: ", address(stakeImpl) ); + bytes memory stakeInitData = abi.encodeWithSelector( + StakeManagement.initialize.selector, + IERC20(address(pegBTC)), + address(gateway) + ); UpgradeableProxy stakeProxy = new UpgradeableProxy( address(stakeImpl), deployer, - "" + stakeInitData ); console.log( "StakeManagement proxy contract address: ", address(stakeProxy) ); - StakeManagement stakeManagementImpl = StakeManagement( - address(stakeProxy) - ); - stakeManagementImpl.initialize( - IERC20(address(pegBTC)), - address(gateway) - ); IStakeManagement stakeManagement = IStakeManagement( address(stakeProxy) ); diff --git a/script/README.md b/script/README.md index c876aac..805123d 100644 --- a/script/README.md +++ b/script/README.md @@ -1,13 +1,26 @@ -# Deploy +# Deployment Scripts +## Environment setup +1. Copy `.env.example` to `.env` and replace the placeholder values. +2. Add as many `COMMITTEE_` and `WATCHTOWER_` entries as needed. Indexes must be sequential (e.g., `_0`, `_1`, `_2`). +3. Export the variables into your shell (`set -a && source .env && set +a`) or let Foundry load them via `--env .env`. + +## Deploying the Gateway stack + +The `DeployGateway` script provisions the PegBTC token, Gateway (implementation + proxy), CommitteeManagement proxy, and StakeManagement proxy. Each proxy is initialized via constructor-calldata so there is no uninitialized window. + +```bash +make deployTest +# or for mainnet +make deployMain ``` -export prv=... -export OWNER=0x8943545177806ED17B9F23F0a21ee5948eCaa776 -forge script script/SSPDeploy.s.sol:Deploy \ - --rpc-url https://rpc.testnet3.goat.network --private-key=$prv --broadcast --legacy +Under the hood these targets expand to `forge script script/DeployGateway.sol:DeployGateway` with the Goat RPC aliases defined in `foundry.toml`, `--broadcast -vvvv`, and Blockscout verification flags (`--verifier blockscout --verifier-url ...). Provide `PRIVATE_KEY`, `BITCOINSPV_ADDR`, committee, and watchtower env vars before invoking the Makefile. -forge verify-contract --compiler-version 0.8.28 0x3901C4670aA92a626636f7Ea1e3F029A0ECd6b68 SequencerSetPublisher --verifier blockscout --verifier-url 'https://explorer.testnet3.goat.network/api/' +Key environment variables consumed by the script: -``` \ No newline at end of file +- `PRIVATE_KEY`: broadcaster key (hex string, no quotes). +- `BITCOINSPV_ADDR`: already deployed Bitcoin SPV contract on the target chain. +- `COMMITTEE_`: sequential list of committee member addresses (at least one required). +- `WATCHTOWER_`: sequential list of 32-byte watchtower identifiers (at least one required). diff --git a/src/MultiSigVerifier.sol b/src/MultiSigVerifier.sol index bb17c14..0111cd5 100644 --- a/src/MultiSigVerifier.sol +++ b/src/MultiSigVerifier.sol @@ -132,9 +132,7 @@ contract MultiSigVerifier is Initializable { for (uint256 i = 0; i < newOwners.length; i++) { address o = newOwners[i]; require(o != address(0), "Zero address"); - for (uint256 j = 0; j < i; j++) { - require(newOwners[j] != o, "Duplicate owner"); - } + require(!isOwner[o], "Duplicate owner"); isOwner[o] = true; ownerList.push(o); } From 367715ae52f004ae54f4b536e167ef7b63e3465f Mon Sep 17 00:00:00 2001 From: Li-Qing Wang Date: Fri, 19 Dec 2025 15:16:26 +0800 Subject: [PATCH 08/11] fix: fix readme typo --- script/README.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/script/README.md b/script/README.md index 805123d..2535d9b 100644 --- a/script/README.md +++ b/script/README.md @@ -12,11 +12,15 @@ The `DeployGateway` script provisions the PegBTC token, Gateway (implementation ```bash make deployTest -# or for mainnet +``` + +or for mainnet + +```bash make deployMain ``` -Under the hood these targets expand to `forge script script/DeployGateway.sol:DeployGateway` with the Goat RPC aliases defined in `foundry.toml`, `--broadcast -vvvv`, and Blockscout verification flags (`--verifier blockscout --verifier-url ...). Provide `PRIVATE_KEY`, `BITCOINSPV_ADDR`, committee, and watchtower env vars before invoking the Makefile. +Under the hood these targets expand to `forge script script/DeployGateway.sol:DeployGateway` with the Goat RPC aliases defined in `foundry.toml`, `--broadcast -vvvv`, and Blockscout verification flags (`--verifier blockscout --verifier-url ...`). Provide `PRIVATE_KEY`, `BITCOINSPV_ADDR`, committee, and watchtower env vars before invoking the Makefile. Key environment variables consumed by the script: From 01b9062a4d885e3a6f388236e7114cd50680e81f Mon Sep 17 00:00:00 2001 From: KSlashh <48985735+KSlashh@users.noreply.github.com> Date: Fri, 19 Dec 2025 21:50:43 +0800 Subject: [PATCH 09/11] fix bugs --- src/Gateway.sol | 23 ++++++++++------------- src/StakeManagement.sol | 2 +- src/interfaces/IGateway.sol | 4 ++-- 3 files changed, 13 insertions(+), 16 deletions(-) diff --git a/src/Gateway.sol b/src/Gateway.sol index ec7ab97..ab43521 100644 --- a/src/Gateway.sol +++ b/src/Gateway.sol @@ -25,10 +25,10 @@ contract BitvmPolicy { uint64 public minOperatorRewardSats; uint64 public operatorRewardRate; - uint64 public minStakeAmount; // TODO: uint256 - uint64 public minChallengerReward; // TODO: uint256 - uint64 public minDisproverReward; // TODO: uint256 - uint64 public minSlashAmount; // TODO: uint256 + uint256 public minStakeAmount; + uint256 public minChallengerReward; + uint256 public minDisproverReward; + uint256 public minSlashAmount; // TODO Initializer & setters } @@ -335,7 +335,6 @@ contract GatewayUpgradeable is BitvmPolicy, Initializable, IGateway { string calldata userChangeAddress, string calldata userRefundAddress ) external payable { - // TODO: check if request already exists PeginDataInner storage peginData = peginDataMap[instanceId]; if (peginData.status != PeginStatus.None) revert InstanceUsed(); // TODO: check peginAmount,feeRate,userInputs @@ -593,11 +592,10 @@ contract GatewayUpgradeable is BitvmPolicy, Initializable, IGateway { revert TimelockNotExpired(); } withdrawData.status = WithdrawStatus.Canceled; - // FIXME: transfer to operator or gateway? - pegBTC.transfer(msg.sender, withdrawData.lockAmount); + pegBTC.transfer(withdrawData.operatorAddress, withdrawData.lockAmount); peginData.status = PeginStatus.Withdrawbale; - emit CancelWithdraw(withdrawData.instanceId, graphId, msg.sender); + emit CancelWithdraw(withdrawData.instanceId, graphId, withdrawData.operatorAddress); } function committeeCancelWithdraw( @@ -620,10 +618,9 @@ contract GatewayUpgradeable is BitvmPolicy, Initializable, IGateway { if (withdrawData.status != WithdrawStatus.Initialized) revert WithdrawStatusInvalid(); withdrawData.status = WithdrawStatus.Canceled; - // FIXME: transfer to operator or gateway? - pegBTC.transfer(msg.sender, withdrawData.lockAmount); + pegBTC.transfer(withdrawData.operatorAddress, withdrawData.lockAmount); peginData.status = PeginStatus.Withdrawbale; - emit CancelWithdraw(withdrawData.instanceId, graphId, msg.sender); + emit CancelWithdraw(withdrawData.instanceId, graphId, withdrawData.operatorAddress); } // post kickoff tx @@ -807,8 +804,8 @@ contract GatewayUpgradeable is BitvmPolicy, Initializable, IGateway { if (operatorStake < slashAmount) slashAmount = operatorStake; stakeManagement.slashStake(operatorStakeAddress, slashAmount); - uint64 challengerRewardAmount = minChallengerReward; - uint64 disproverRewardAmount = minDisproverReward; + uint256 challengerRewardAmount = minChallengerReward; + uint256 disproverRewardAmount = minDisproverReward; if (challengerAddress != address(0)) { stakeToken.transfer(challengerAddress, challengerRewardAmount); } diff --git a/src/StakeManagement.sol b/src/StakeManagement.sol index 16afbba..489e7b7 100644 --- a/src/StakeManagement.sol +++ b/src/StakeManagement.sol @@ -47,8 +47,8 @@ contract StakeManagement is IStakeManagement, Initializable { return lockedStakes[operator]; } - // TODO: add caller authentication? function slashStake(address operator, uint256 amount) external override { + require(msg.sender == gatewayAddress, "only gateway can slash stake"); require(stakes[operator] >= amount, "insufficient stake to slash"); if (lockedStakes[operator] > amount) { lockedStakes[operator] -= amount; diff --git a/src/interfaces/IGateway.sol b/src/interfaces/IGateway.sol index 1b458a3..0da8365 100644 --- a/src/interfaces/IGateway.sol +++ b/src/interfaces/IGateway.sol @@ -187,7 +187,7 @@ interface IGateway { bytes32 challengeFinishTxid, address challengerAddress, address disproverAddress, - uint64 challengerRewardAmount, - uint64 disproverRewardAmount + uint256 challengerRewardAmount, + uint256 disproverRewardAmount ); } From d253b72caed0b72f7b6566d4d566ba9d48e6b2a5 Mon Sep 17 00:00:00 2001 From: KSlashh <48985735+KSlashh@users.noreply.github.com> Date: Wed, 24 Dec 2025 18:16:58 +0800 Subject: [PATCH 10/11] update scripts --- script/CommitteeRegisterPeerId.sol | 30 +++++++ script/DebugCancelWithdraw.sol | 36 -------- script/DebugMintPegBTC.sol | 41 ---------- script/ListInstance.sol | 127 +++++++++++++++++++++++++++++ script/MockProceedWithdraw.sol | 43 ---------- script/OperatorRegisterPubkey.sol | 30 +++++++ script/OperatorStake.sol | 96 ++++++++++++++++++++++ 7 files changed, 283 insertions(+), 120 deletions(-) create mode 100644 script/CommitteeRegisterPeerId.sol delete mode 100644 script/DebugCancelWithdraw.sol delete mode 100644 script/DebugMintPegBTC.sol create mode 100644 script/ListInstance.sol delete mode 100644 script/MockProceedWithdraw.sol create mode 100644 script/OperatorRegisterPubkey.sol create mode 100644 script/OperatorStake.sol diff --git a/script/CommitteeRegisterPeerId.sol b/script/CommitteeRegisterPeerId.sol new file mode 100644 index 0000000..3e8b910 --- /dev/null +++ b/script/CommitteeRegisterPeerId.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +import {Script, console} from "forge-std/Script.sol"; +import {CommitteeManagement} from "../src/CommitteeManagement.sol"; +import {GatewayUpgradeable} from "../src/Gateway.sol"; + +contract RegisterPeerId is Script { + function run() public { + uint256 committeePrivateKey = vm.envUint("PRIVATE_KEY"); + address committee = vm.addr(committeePrivateKey); + + address gatewayAddr = vm.envAddress("GATEWAY_ADDR"); + address committeeManagementAddr = address(GatewayUpgradeable(payable(gatewayAddr)).committeeManagement()); + bytes memory peerId = vm.envBytes("PEER_ID"); + + console.log("Committee:", committee); + console.log("Gateway:", gatewayAddr); + console.log("Committee Management:", committeeManagementAddr); + console.log("Peer ID:", vm.toString(peerId)); + + vm.startBroadcast(committeePrivateKey); + + CommitteeManagement(committeeManagementAddr).registerPeerId(peerId); + + console.log("Peer ID registered successfully"); + + vm.stopBroadcast(); + } +} diff --git a/script/DebugCancelWithdraw.sol b/script/DebugCancelWithdraw.sol deleted file mode 100644 index 95bbf5c..0000000 --- a/script/DebugCancelWithdraw.sol +++ /dev/null @@ -1,36 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.28; - -import {Script, console} from "forge-std/Script.sol"; - -import {GatewayDebug} from "../src/GatewayDebug.sol"; - -/* - Script: call GatewayDebug.debugCancelWithdraw(graphId) as an operator. - Required env vars: - - PRIVATE_KEY: uint256 private key of the operator who initialized the withdraw - - GATEWAY_ADDR: address of the Gateway (proxy) deployed on the network - - GRAPH_ID: bytes16 hex -*/ -contract DebugCancelWithdraw is Script { - function run() external { - uint256 pk = vm.envUint("PRIVATE_KEY"); - address gatewayAddr = vm.envAddress("GATEWAY_ADDR"); - bytes memory graphRaw = vm.envBytes("GRAPH_ID"); - require(graphRaw.length == 16, "GRAPH_ID must be 16 bytes hex"); - bytes16 graphId; - assembly { - graphId := mload(add(graphRaw, 0x20)) - } - address operator = vm.addr(pk); - console.log("Gateway:", gatewayAddr); - console.log("Operator:", operator); - console.log("GraphId:", vm.toString(graphId)); - - vm.startBroadcast(pk); - GatewayDebug(gatewayAddr).debugCancelWithdraw(graphId); - vm.stopBroadcast(); - - console.log("debugCancelWithdraw sent"); - } -} \ No newline at end of file diff --git a/script/DebugMintPegBTC.sol b/script/DebugMintPegBTC.sol deleted file mode 100644 index ed49129..0000000 --- a/script/DebugMintPegBTC.sol +++ /dev/null @@ -1,41 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; - -import {Script, console} from "forge-std/Script.sol"; -import {GatewayDebug} from "../src/GatewayDebug.sol"; - -/* - Required env vars: - - PRIVATE_KEY: uint256 private key of the operator who initialized the withdraw - - GATEWAY_ADDR: address of the Gateway (proxy) deployed on the network - - TO: recipient_address - - AMOUNT: amount_to_mint -*/ -contract DebugMintPegBTC is Script { - address public sender; - address payable public gateway; - - function setUp() public virtual { - gateway = payable(vm.envAddress("GATEWAY_ADDR")); - } - - function run() public { - uint256 senderPrivateKey = vm.envUint("PRIVATE_KEY"); - sender = vm.createWallet(senderPrivateKey).addr; - console.log("sender address: ", sender); - - vm.startBroadcast(senderPrivateKey); - _mintPegBTC(); - vm.stopBroadcast(); - } - - function _mintPegBTC() public { - address to = vm.envAddress("TO"); - uint256 amount = vm.envUint("AMOUNT"); - require(to != address(0), "TO env required"); - require(amount > 0, "AMOUNT env required"); - - GatewayDebug(gateway).debugMintPegBTC(to, amount); - console.log("debugMintPegBTC called with to:", to, "amount:", amount); - } -} \ No newline at end of file diff --git a/script/ListInstance.sol b/script/ListInstance.sol new file mode 100644 index 0000000..1ca12b9 --- /dev/null +++ b/script/ListInstance.sol @@ -0,0 +1,127 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +import {Script, console} from "forge-std/Script.sol"; +import {GatewayUpgradeable} from "../src/Gateway.sol"; +import {IGateway} from "../src/interfaces/IGateway.sol"; + +contract DebugListInstance is Script { + GatewayUpgradeable gateway; + int256 filterPeginStatus; + int256 filterWithdrawStatus; + + function run() public { + address gatewayAddr = vm.envAddress("GATEWAY_ADDR"); + gateway = GatewayUpgradeable(gatewayAddr); + + console.log("Listing instances for Gateway at:", gatewayAddr); + + uint256 startIndex = vm.envOr("START_INDEX", uint256(0)); + uint256 endIndex = vm.envOr("END_INDEX", type(uint256).max); + + filterPeginStatus = vm.envOr("FILTER_PEGIN_STATUS", int256(-1)); + filterWithdrawStatus = vm.envOr("FILTER_WITHDRAW_STATUS", int256(-1)); + + uint256 i = startIndex; + while (i <= endIndex) { + try gateway.instanceIds(i) returns (bytes16 instanceId) { + listInstance(i, instanceId); + i++; + } catch { + break; + } + } + } + + function getPeginStatusString(uint256 status) internal pure returns (string memory) { + if (status == uint256(IGateway.PeginStatus.None)) return "None"; + if (status == uint256(IGateway.PeginStatus.Pending)) return "Pending"; + if (status == uint256(IGateway.PeginStatus.Withdrawbale)) return "Withdrawbale"; + if (status == uint256(IGateway.PeginStatus.Processing)) return "Processing"; + if (status == uint256(IGateway.PeginStatus.Locked)) return "Locked"; + if (status == uint256(IGateway.PeginStatus.Claimed)) return "Claimed"; + if (status == uint256(IGateway.PeginStatus.Discarded)) return "Discarded"; + return "Unknown"; + } + + function getWithdrawStatusString(uint256 status) internal pure returns (string memory) { + if (status == uint256(IGateway.WithdrawStatus.None)) return "None"; + if (status == uint256(IGateway.WithdrawStatus.Processing)) return "Processing"; + if (status == uint256(IGateway.WithdrawStatus.Initialized)) return "Initialized"; + if (status == uint256(IGateway.WithdrawStatus.Canceled)) return "Canceled"; + if (status == uint256(IGateway.WithdrawStatus.Complete)) return "Complete"; + if (status == uint256(IGateway.WithdrawStatus.Disproved)) return "Disproved"; + return "Unknown"; + } + + function listInstance(uint256 index, bytes16 instanceId) internal view { + // Get Pegin Status + (bool success, bytes memory data) = address(gateway).staticcall( + abi.encodeWithSelector(gateway.peginDataMap.selector, instanceId) + ); + + uint256 statusVal; + bool statusFetched = false; + if (success && data.length >= 32) { + statusVal = abi.decode(data, (uint256)); + statusFetched = true; + } + + if (filterPeginStatus != -1) { + if (!statusFetched || int256(statusVal) != filterPeginStatus) { + return; + } + } + + console.log("--------------------------------------------------"); + console.log("Index:", index); + console.log("Instance ID:"); + console.logBytes16(instanceId); + + if (statusFetched) { + console.log("Pegin Status:", getPeginStatusString(statusVal)); + } else { + console.log("Failed to fetch Pegin Status"); + } + + // List graphs + uint256 j = 0; + while (true) { + try gateway.instanceIdToGraphIds(instanceId, j) returns (bytes16 graphId) { + listGraph(graphId); + j++; + } catch { + break; + } + } + } + + function listGraph(bytes16 graphId) internal view { + // Get Withdraw Status + (bool success, bytes memory data) = address(gateway).staticcall( + abi.encodeWithSelector(gateway.withdrawDataMap.selector, graphId) + ); + + uint256 statusVal; + bool statusFetched = false; + if (success && data.length >= 32) { + statusVal = abi.decode(data, (uint256)); + statusFetched = true; + } + + if (filterWithdrawStatus != -1) { + if (!statusFetched || int256(statusVal) != filterWithdrawStatus) { + return; + } + } + + console.log(" Graph ID:"); + console.logBytes16(graphId); + + if (statusFetched) { + console.log(" Withdraw Status:", getWithdrawStatusString(statusVal)); + } else { + console.log(" Failed to fetch Withdraw Status"); + } + } +} \ No newline at end of file diff --git a/script/MockProceedWithdraw.sol b/script/MockProceedWithdraw.sol deleted file mode 100644 index 47bfcaf..0000000 --- a/script/MockProceedWithdraw.sol +++ /dev/null @@ -1,43 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; - -import {Script, console} from "forge-std/Script.sol"; -import {GatewayDebug} from "../src/GatewayDebug.sol"; - -/* - Required env vars: - - PRIVATE_KEY: uint256 private key to broadcast from (operator) - - GATEWAY_ADDR: address of deployed Gateway (proxy) - - GRAPH_ID: bytes16 hex -*/ -contract MockProceedWithdraw is Script { - address public sender; - address payable public gateway; - - function setUp() public virtual { - gateway = payable(vm.envAddress("GATEWAY_ADDR")); - } - - function run() public { - uint256 senderPrivateKey = vm.envUint("PRIVATE_KEY"); - sender = vm.createWallet(senderPrivateKey).addr; - console.log("sender address:", sender); - - vm.startBroadcast(senderPrivateKey); - _mockProceedWithdraw(); - vm.stopBroadcast(); - } - - function _mockProceedWithdraw() public { - bytes memory graphRaw = vm.envBytes("GRAPH_ID"); - require(graphRaw.length == 16, "GRAPH_ID must be 16 bytes hex"); - bytes16 graphId; - assembly { - graphId := mload(add(graphRaw, 0x20)) - } - console.log("\ngraphId:", vm.toString(graphId)); - - GatewayDebug(gateway).mockProceedWithdraw(graphId); - console.log("mockProceedWithdraw sent for graphId:", vm.toString(uint256(uint128(bytes16(graphId))))); - } -} \ No newline at end of file diff --git a/script/OperatorRegisterPubkey.sol b/script/OperatorRegisterPubkey.sol new file mode 100644 index 0000000..76141a7 --- /dev/null +++ b/script/OperatorRegisterPubkey.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +import {Script, console} from "forge-std/Script.sol"; +import {GatewayUpgradeable} from "../src/Gateway.sol"; +import {StakeManagement} from "../src/StakeManagement.sol"; + +contract OperatorRegisterPubkey is Script { + function run() public { + uint256 operatorPrivateKey = vm.envUint("PRIVATE_KEY"); + address operator = vm.addr(operatorPrivateKey); + + address gatewayAddr = vm.envAddress("GATEWAY_ADDR"); + bytes32 pubkey = vm.envBytes32("PUBKEY"); + + console.log("Operator:", operator); + console.log("Gateway:", gatewayAddr); + console.log("Pubkey:", vm.toString(pubkey)); + + vm.startBroadcast(operatorPrivateKey); + address stakeManagementAddr = address(GatewayUpgradeable(payable(gatewayAddr)).stakeManagement()); + console.log("StakeManagement:", stakeManagementAddr); + + StakeManagement(stakeManagementAddr).registerPubkey(pubkey); + + console.log("Pubkey registered successfully"); + + vm.stopBroadcast(); + } +} diff --git a/script/OperatorStake.sol b/script/OperatorStake.sol new file mode 100644 index 0000000..6f0013e --- /dev/null +++ b/script/OperatorStake.sol @@ -0,0 +1,96 @@ +pragma solidity ^0.8.0; + +import {Script, console} from "forge-std/Script.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import {StakeManagement} from "../src/StakeManagement.sol"; +import {IStakeManagement} from "../src/interfaces/IStakeManagement.sol"; +import {GatewayUpgradeable} from "../src/Gateway.sol"; + +/* +# Operator stake and lock + +Stake PBTC as an operator and lock it via StakeManagement fetched from the Gateway. + +Env vars: +- GATEWAY_ADDR: address of the Gateway proxy +- PRIVATE_KEY: operator's private key +- STAKE_AMOUNT: amount of PBTC to stake (wei) +- LOCK_AMOUNT: amount to lock (wei), optional; defaults to STAKE_AMOUNT when omitted or 0 + +Example: + +```sh +export GATEWAY_ADDR=0x... +export PRIVATE_KEY=... +export STAKE_AMOUNT=60000000000000000 +export LOCK_AMOUNT=60000000000000000 # optional +``` +*/ +contract OperatorStake is Script { + address public operator; + address public stakeManagementAddr; + address public gatewayAddr; + + uint256 public stakeAmount; + uint256 public lockAmount; + + function setUp() public virtual { + // Read Gateway address from env, then fetch StakeManagement from it + gatewayAddr = vm.envAddress("GATEWAY_ADDR"); + IStakeManagement sm = GatewayUpgradeable(payable(gatewayAddr)).stakeManagement(); + stakeManagementAddr = address(sm); + + stakeAmount = vm.envUint("STAKE_AMOUNT"); + uint256 lockAmt = vm.envOr("LOCK_AMOUNT", uint256(0)); + lockAmount = lockAmt == 0 ? stakeAmount : lockAmt; + } + + function run() public { + uint256 operatorPk = vm.envUint("PRIVATE_KEY"); + operator = vm.createWallet(operatorPk).addr; + console.log("operator:", operator); + console.log("Gateway:", gatewayAddr); + console.log("StakeManagement:", stakeManagementAddr); + + vm.startBroadcast(operatorPk); + _stakeAndLock(); + vm.stopBroadcast(); + } + + function _stakeAndLock() internal { + // Resolve token from StakeManagement + address tokenAddr = IStakeManagement(stakeManagementAddr).stakeTokenAddress(); + IERC20 token = IERC20(tokenAddr); + + console.log("stake token:", tokenAddr); + console.log("stake amount:", stakeAmount); + console.log("lock amount:", lockAmount); + + // Check balance for a friendly message (not a hard requirement here) + uint256 bal = token.balanceOf(operator); + console.log("operator balance:", bal); + + // Approve if needed + uint256 allowance = token.allowance(operator, stakeManagementAddr); + if (allowance < stakeAmount) { + // Approve exactly the shortfall to minimize allowance; simple path: set to stakeAmount + // If an allowance already exists, some ERC20s require resetting to 0 first; PegBTC is standard OpenZeppelin ERC20, so direct set is fine. + bool ok = token.approve(stakeManagementAddr, stakeAmount); + require(ok, "approve failed"); + console.log("approved:", stakeAmount); + } else { + console.log("sufficient allowance, skipping approve"); + } + + // Stake + StakeManagement(stakeManagementAddr).stake(stakeAmount); + console.log("staked"); + + // Lock if requested (> 0) + if (lockAmount > 0) { + StakeManagement(stakeManagementAddr).lockStake(operator, lockAmount); + console.log("locked"); + } + } +} From d66d6fa672dca06e44d0aae4d23f086e5ac2ebcb Mon Sep 17 00:00:00 2001 From: KSlashh <48985735+KSlashh@users.noreply.github.com> Date: Thu, 25 Dec 2025 16:09:30 +0800 Subject: [PATCH 11/11] add descriptions for script required env --- script/CommitteeRegisterPeerId.sol | 7 +++++++ script/DeployContractDebug.sol | 6 ++++++ script/DeployGateway.sol | 6 ++++++ script/ListInstance.sol | 9 +++++++++ script/OperatorRegisterPubkey.sol | 7 +++++++ script/SSPDeploy.s.sol | 5 +++++ script/UpgradeGateway.sol | 7 +++++++ script/UpgradeGatewayDebug.sol | 7 +++++++ 8 files changed, 54 insertions(+) diff --git a/script/CommitteeRegisterPeerId.sol b/script/CommitteeRegisterPeerId.sol index 3e8b910..0d75b05 100644 --- a/script/CommitteeRegisterPeerId.sol +++ b/script/CommitteeRegisterPeerId.sol @@ -5,6 +5,13 @@ import {Script, console} from "forge-std/Script.sol"; import {CommitteeManagement} from "../src/CommitteeManagement.sol"; import {GatewayUpgradeable} from "../src/Gateway.sol"; +/* + Script: Register a peer ID for the committee. + Required env vars: + - PRIVATE_KEY: uint256 private key to broadcast from (committee member) + - GATEWAY_ADDR: address of deployed Gateway (proxy) + - PEER_ID: bytes peer ID to register +*/ contract RegisterPeerId is Script { function run() public { uint256 committeePrivateKey = vm.envUint("PRIVATE_KEY"); diff --git a/script/DeployContractDebug.sol b/script/DeployContractDebug.sol index 88ba35f..0107e55 100644 --- a/script/DeployContractDebug.sol +++ b/script/DeployContractDebug.sol @@ -14,6 +14,12 @@ import {PegBTC} from "../src/PegBTC.sol"; import {UpgradeableProxy} from "../src/UpgradeableProxy.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +/* + Script: Deploy the GatewayDebug contract and related contracts. + Required env vars: + - PRIVATE_KEY: uint256 private key to broadcast from (deployer) + - BITCOINSPV_ADDR: address of the BitcoinSPV contract +*/ contract DeployGateway is Script { address public deployer; address public bitcoinSPV; diff --git a/script/DeployGateway.sol b/script/DeployGateway.sol index 067d55a..325e842 100644 --- a/script/DeployGateway.sol +++ b/script/DeployGateway.sol @@ -14,6 +14,12 @@ import {PegBTC} from "../src/PegBTC.sol"; import {UpgradeableProxy} from "../src/UpgradeableProxy.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +/* + Script: Deploy the Gateway contract and related contracts. + Required env vars: + - PRIVATE_KEY: uint256 private key to broadcast from (deployer) + - BITCOINSPV_ADDR: address of the BitcoinSPV contract +*/ contract DeployGateway is Script { address public deployer; address public bitcoinSPV; diff --git a/script/ListInstance.sol b/script/ListInstance.sol index 1ca12b9..a1af434 100644 --- a/script/ListInstance.sol +++ b/script/ListInstance.sol @@ -5,6 +5,15 @@ import {Script, console} from "forge-std/Script.sol"; import {GatewayUpgradeable} from "../src/Gateway.sol"; import {IGateway} from "../src/interfaces/IGateway.sol"; +/* + Script: List instances from the Gateway. + Required env vars: + - GATEWAY_ADDR: address of deployed Gateway (proxy) + - START_INDEX: (optional) uint256 start index for listing (default: 0) + - END_INDEX: (optional) uint256 end index for listing (default: max) + - FILTER_PEGIN_STATUS: (optional) int256 filter by pegin status (default: -1 for all) + - FILTER_WITHDRAW_STATUS: (optional) int256 filter by withdraw status (default: -1 for all) +*/ contract DebugListInstance is Script { GatewayUpgradeable gateway; int256 filterPeginStatus; diff --git a/script/OperatorRegisterPubkey.sol b/script/OperatorRegisterPubkey.sol index 76141a7..348a612 100644 --- a/script/OperatorRegisterPubkey.sol +++ b/script/OperatorRegisterPubkey.sol @@ -5,6 +5,13 @@ import {Script, console} from "forge-std/Script.sol"; import {GatewayUpgradeable} from "../src/Gateway.sol"; import {StakeManagement} from "../src/StakeManagement.sol"; +/* + Script: Register a public key for the operator. + Required env vars: + - PRIVATE_KEY: uint256 private key to broadcast from (operator) + - GATEWAY_ADDR: address of deployed Gateway (proxy) + - PUBKEY: bytes32 public key to register +*/ contract OperatorRegisterPubkey is Script { function run() public { uint256 operatorPrivateKey = vm.envUint("PRIVATE_KEY"); diff --git a/script/SSPDeploy.s.sol b/script/SSPDeploy.s.sol index d4fbde3..524ff84 100644 --- a/script/SSPDeploy.s.sol +++ b/script/SSPDeploy.s.sol @@ -6,6 +6,11 @@ import {console} from "forge-std/console.sol"; import {SequencerSetPublisher} from "../src/SequencerSetPublisher.sol"; import {MultiSigVerifier} from "../src/MultiSigVerifier.sol"; +/* + Script: Deploy SequencerSetPublisher and MultiSigVerifier. + Required env vars: + - OWNER: address of the initial owner +*/ contract Deploy is Script { function run() external { // Load from environment variables or replace inline diff --git a/script/UpgradeGateway.sol b/script/UpgradeGateway.sol index c768c9c..c7c0c37 100644 --- a/script/UpgradeGateway.sol +++ b/script/UpgradeGateway.sol @@ -5,6 +5,13 @@ import {GatewayUpgradeable} from "../src/Gateway.sol"; import {ProxyAdmin} from "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol"; import {ITransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; +/* + Script: Upgrade the Gateway contract. + Required env vars: + - PRIVATE_KEY: uint256 private key to broadcast from (sender) + - PROXYADMIN_ADDR: address of the ProxyAdmin contract + - GATEWAY_ADDR: address of the Gateway proxy +*/ contract UpgradeGateway is Script { address public sender; address public proxyAdmin; diff --git a/script/UpgradeGatewayDebug.sol b/script/UpgradeGatewayDebug.sol index aae4127..d45526f 100644 --- a/script/UpgradeGatewayDebug.sol +++ b/script/UpgradeGatewayDebug.sol @@ -5,6 +5,13 @@ import {GatewayDebug} from "../src/GatewayDebug.sol"; import {ProxyAdmin} from "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol"; import {ITransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; +/* + Script: Upgrade the Gateway contract to GatewayDebug. + Required env vars: + - PRIVATE_KEY: uint256 private key to broadcast from (sender) + - PROXYADMIN_ADDR: address of the ProxyAdmin contract + - GATEWAY_ADDR: address of the Gateway proxy +*/ contract UpgradeGateway is Script { address public sender; address public proxyAdmin;