diff --git a/slither.config.json b/slither.config.json index 8f098a6c..293d8b58 100644 --- a/slither.config.json +++ b/slither.config.json @@ -1,5 +1,5 @@ { - "detectors_to_exclude": "pragma,assembly,solc-version,naming-convention,timestamp,low-level-calls,too-many-digits,similar-names,calls-loop,reentrancy-benign,reentrancy-events,dead-code", + "detectors_to_exclude": "pragma,assembly,solc-version,naming-convention,timestamp,low-level-calls,too-many-digits,similar-names,calls-loop,reentrancy-benign,reentrancy-events,dead-code,mapping-deletion", "filter_paths": "lib/|test/|mocks/|BytesLib|script/", "solc_remaps": [ "forge-std/=lib/forge-std/src/", diff --git a/src/core/ExocoreBtcGateway.sol b/src/core/ExocoreBtcGateway.sol deleted file mode 100644 index c8a4968c..00000000 --- a/src/core/ExocoreBtcGateway.sol +++ /dev/null @@ -1,711 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.19; - -import {ASSETS_CONTRACT} from "../interfaces/precompiles/IAssets.sol"; - -import {DELEGATION_CONTRACT} from "../interfaces/precompiles/IDelegation.sol"; -import {REWARD_CONTRACT} from "../interfaces/precompiles/IReward.sol"; -import {SignatureVerifier} from "../libraries/SignatureVerifier.sol"; -import {ExocoreBtcGatewayStorage} from "../storage/ExocoreBtcGatewayStorage.sol"; -import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; -import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; -import {PausableUpgradeable} from "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol"; -import {ReentrancyGuardUpgradeable} from "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; -// import "forge-std/console.sol"; -/** - * @title ExocoreBtcGateway - * @dev This contract manages the gateway between Bitcoin and the Exocore system. - * It handles deposits, delegations, withdrawals, and peg-out requests for BTC. - */ - -contract ExocoreBtcGateway is - Initializable, - PausableUpgradeable, - OwnableUpgradeable, - ReentrancyGuardUpgradeable, - ExocoreBtcGatewayStorage -{ - - uint32 internal CLIENT_CHAIN_ID; - address internal constant BTC_ADDR = address(0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599); - bytes internal constant BTC_TOKEN = abi.encodePacked(bytes32(bytes20(BTC_ADDR))); - - /** - * @dev Modifier to restrict access to authorized witnesses only. - */ - modifier onlyAuthorizedWitness() { - if (!_isAuthorizedWitness(msg.sender)) { - revert UnauthorizedWitness(); - } - _; - } - - /** - * @notice Pauses the contract. - * @dev Can only be called by the contract owner. - */ - function pause() external onlyOwner { - _pause(); - } - - /** - * @notice Unpauses the contract. - * @dev Can only be called by the contract owner. - */ - function unpause() external onlyOwner { - _unpause(); - } - - /** - * @notice Constructor to initialize the contract with the client chain ID. - * @dev Sets up initial configuration for testing purposes. - */ - constructor() { - // todo: for test. - _registerClientChain(111); - authorizedWitnesses[EXOCORE_WITNESS] = true; - isWhitelistedToken[BTC_ADDR] = true; - _disableInitializers(); - } - - /** - * @notice Initializes the contract with the Exocore witness address. - * @param _witness The address of the Exocore witness . - */ - function initialize(address _witness) external initializer { - addWitness(_witness); - __Pausable_init_unchained(); - } - - /** - * @notice Adds a new authorized witness. - * @param _witness The address of the witness to be added. - * @dev Can only be called by the contract owner. - */ - function addWitness(address _witness) public onlyOwner { - if (_witness == address(0)) { - revert ZeroAddressNotAllowed(); - } - require(!authorizedWitnesses[_witness], "Witness already authorized"); - authorizedWitnesses[_witness] = true; - emit WitnessAdded(_witness); - } - - /** - * @notice Removes an authorized witness. - * @param _witness The address of the witness to be removed. - * @dev Can only be called by the contract owner. - */ - function removeWitness(address _witness) public onlyOwner { - require(authorizedWitnesses[_witness], "Witness not authorized"); - authorizedWitnesses[_witness] = false; - emit WitnessRemoved(_witness); - } - - /** - * @notice Updates the bridge fee. - * @param _newFee The new fee to be set (in basis points, max 1000 or 10%). - * @dev Can only be called by the contract owner. - */ - function updateBridgeFee(uint256 _newFee) public onlyOwner { - require(_newFee <= 1000, "Fee cannot exceed 10%"); // Max fee of 10% - bridgeFee = _newFee; - emit BridgeFeeUpdated(_newFee); - } - - /** - * @notice Checks if the proofs for a transaction are consistent. - * @param _txTag The transaction tag to check. - * @return bool True if proofs are consistent, false otherwise. - */ - function _areProofsConsistent(bytes memory _txTag) internal view returns (bool) { - Proof[] storage txProofs = proofs[_txTag]; - if (txProofs.length < REQUIRED_PROOFS) { - return false; - } - - InterchainMsg memory firstMsg = txProofs[0].message; - for (uint256 i = 1; i < txProofs.length; i++) { - InterchainMsg memory currentMsg = txProofs[i].message; - if ( - firstMsg.srcChainID != currentMsg.srcChainID || firstMsg.dstChainID != currentMsg.dstChainID - || keccak256(firstMsg.srcAddress) != keccak256(currentMsg.srcAddress) - || keccak256(firstMsg.dstAddress) != keccak256(currentMsg.dstAddress) - || firstMsg.token != currentMsg.token || firstMsg.amount != currentMsg.amount - || firstMsg.nonce != currentMsg.nonce || keccak256(firstMsg.txTag) != keccak256(currentMsg.txTag) - || keccak256(firstMsg.payload) != keccak256(currentMsg.payload) - ) { - return false; - } - } - return true; - } - - /** - * @notice Checks and updates expired transactions. - * @param _txTags An array of transaction tags to check. - */ - function checkExpiredTransactions(bytes[] calldata _txTags) public { - for (uint256 i = 0; i < _txTags.length; i++) { - Transaction storage txn = transactions[_txTags[i]]; - if (txn.status == TxStatus.Pending && block.timestamp >= txn.expiryTime) { - txn.status = TxStatus.Expired; - emit TransactionExpired(_txTags[i]); - } - } - } - - /** - * @notice Registers the client chain ID with the Exocore system. - * @param clientChainId The ID of the client chain. - * @dev This function should be implemented in ExocoreGateway. - */ - function _registerClientChain(uint32 clientChainId) internal { - if (clientChainId == 0) { - revert ZeroAddressNotAllowed(); - } - // if (!ASSETS_CONTRACT.registerClientChain(clientChainId)) { - // revert RegisterClientChainToExocoreFailed(clientChainId); - // } - CLIENT_CHAIN_ID = clientChainId; - } - - /** - * @notice Registers a BTC address with an Exocore address. - * @param depositor The BTC address to register. - * @param exocoreAddress The corresponding Exocore address. - * @dev Can only be called by an authorized witness. - */ - function registerAddress(bytes calldata depositor, bytes calldata exocoreAddress) external onlyAuthorizedWitness { - require(depositor.length > 0 && exocoreAddress.length > 0, "Invalid address"); - require(btcToExocoreAddress[depositor].length == 0, "Depositor address already registered"); - require(exocoreToBtcAddress[exocoreAddress].length == 0, "Exocore address already registered"); - - btcToExocoreAddress[depositor] = exocoreAddress; - exocoreToBtcAddress[exocoreAddress] = depositor; - - emit AddressRegistered(depositor, exocoreAddress); - } - - /** - * @notice Verifies the signature of an interchain message. - * @param _msg The interchain message. - * @param signature The signature to verify. - */ - function _verifySignature(InterchainMsg calldata _msg, bytes memory signature) internal view { - // InterchainMsg, EIP721 is preferred next step. - bytes memory encodeMsg = abi.encode( - _msg.srcChainID, - _msg.dstChainID, - _msg.srcAddress, - _msg.dstAddress, - _msg.token, - _msg.amount, - _msg.nonce, - _msg.txTag, - _msg.payload - ); - bytes32 messageHash = keccak256(encodeMsg); - - SignatureVerifier.verifyMsgSig(msg.sender, messageHash, signature); - } - - /** - * @notice Converts a bytes32 to a string. - * @param _bytes32 The bytes32 to convert. - * @return string The resulting string. - */ - function bytes32ToString(bytes32 _bytes32) public pure returns (string memory) { - bytes memory bytesArray = new bytes(32); - for (uint256 i; i < 32; i++) { - bytesArray[i] = _bytes32[i]; - } - return string(bytesArray); - } - /** - * @notice Processes and verifies an interchain message. - * @param _msg The interchain message. - * @param signature The signature to verify. - * @return btcTxTag The lowercase of BTC txid-vout. - * @return depositor The BTC address. - */ - - function _processAndVerify(InterchainMsg calldata _msg, bytes calldata signature) - internal - returns (bytes memory btcTxTag, bytes memory depositor) - { - btcTxTag = _msg.txTag; - depositor = btcToExocoreAddress[_msg.srcAddress]; - if (depositor.length == 0) { - revert BtcAddressNotRegistered(); - } - - if (processedBtcTxs[btcTxTag].processed) { - revert BtcTxAlreadyProcessed(); - } - - // Verify nonce - _verifyAndUpdateBytesNonce(_msg.srcChainID, depositor, _msg.nonce); - - // Verify signature - _verifySignature(_msg, signature); - } - - /** - * @notice Submits a proof for a transaction. - * @param _message The interchain message. - * @param _signature The signature of the message. - */ - function submitProof(InterchainMsg calldata _message, bytes calldata _signature) - public - nonReentrant - whenNotPaused - { - // Verify the signature - if (processedBtcTxs[_message.txTag].processed) { - revert BtcTxAlreadyProcessed(); - } - - // Verify nonce - _verifyAndUpdateBytesNonce(_message.srcChainID, _message.srcAddress, _message.nonce); - - // Verify signature - _verifySignature(_message, _signature); - - bytes memory txTag = _message.txTag; - Transaction storage txn = transactions[txTag]; - - if (txn.status == TxStatus.Pending) { - require(!txn.hasWitnessed[msg.sender], "Witness has already submitted proof"); - txn.hasWitnessed[msg.sender] = true; - txn.proofCount++; - } else { - txn.status = TxStatus.Pending; - txn.amount = _message.amount; - txn.recipient = address(bytes20(_message.dstAddress)); - txn.expiryTime = block.timestamp + PROOF_TIMEOUT; - txn.proofCount = 1; - txn.hasWitnessed[msg.sender] = true; - } - - proofs[txTag].push( - Proof({witness: msg.sender, message: _message, timestamp: block.timestamp, signature: _signature}) - ); - - emit ProofSubmitted(txTag, msg.sender, _message); - - // Check for consensus - if (txn.proofCount >= REQUIRED_PROOFS) { - _processDeposit(txTag); - } - } - - /** - * @notice Processes a deposit after sufficient proofs have been submitted. - * @param _txTag The transaction tag of the deposit to process. - */ - function _processDeposit(bytes memory _txTag) internal { - Transaction storage txn = transactions[_txTag]; - require(txn.status == TxStatus.Pending, "Transaction not pending"); - require(txn.proofCount >= REQUIRED_PROOFS, "Insufficient proofs"); - - // Verify proof consistency - require(_areProofsConsistent(_txTag), "Inconsistent proofs"); - - // Calculate fee - uint256 fee = (txn.amount * bridgeFee) / 10_000; - uint256 amountAfterFee = txn.amount - fee; - - //todo:call precompile depositTo - - txn.status = TxStatus.Processed; - - // totalDeposited += txn.amount; - - emit DepositProcessed(_txTag, txn.recipient, amountAfterFee); - } - - /** - * @notice Deposits BTC to the Exocore system. - * @param _msg The interchain message containing the deposit details. - * @param signature The signature to verify. - */ - function depositTo(InterchainMsg calldata _msg, bytes calldata signature) - external - nonReentrant - whenNotPaused - isTokenWhitelisted(_msg.token) - isValidAmount(_msg.amount) - onlyAuthorizedWitness - { - require(authorizedWitnesses[msg.sender], "Not an authorized witness"); - (bytes memory btcTxTag, bytes memory depositorExoAddr) = _processAndVerify(_msg, signature); - - processedBtcTxs[btcTxTag] = TxInfo(true, block.timestamp); - - //TODO: this depositor can be exocore address or btc address. - (bool success, uint256 updatedBalance) = - ASSETS_CONTRACT.depositLST(_msg.srcChainID, BTC_TOKEN, depositorExoAddr, _msg.amount); - if (!success) { - revert DepositFailed(btcTxTag); - } - // console.log("depositTo success"); - emit DepositCompleted(btcTxTag, depositorExoAddr, BTC_ADDR, _msg.srcAddress, _msg.amount, updatedBalance); - } - - /** - * @notice Delegates BTC to an operator. - * @param token The token address. - * @param operator The operator's exocore address. - * @param amount The amount to delegate. - */ - function delegateTo(address token, bytes calldata operator, uint256 amount) - external - nonReentrant - whenNotPaused - isTokenWhitelisted(token) - isValidAmount(amount) - { - bytes memory delegator = abi.encodePacked(bytes32(bytes20(msg.sender))); - _nextNonce(CLIENT_CHAIN_ID, delegator); - try DELEGATION_CONTRACT.delegateToThroughBtcGateway(CLIENT_CHAIN_ID, BTC_TOKEN, delegator, operator, amount) - returns (bool success) { - if (!success) { - revert DelegationFailed(); - } - emit DelegationCompleted(token, delegator, operator, amount); - } catch { - emit ExocorePrecompileError(address(DELEGATION_CONTRACT)); - revert DelegationFailed(); - } - } - - /** - * @notice Undelegates BTC from an operator. - * @param token The token address. - * @param operator The operator's exocore address. - * @param amount The amount to undelegate. - */ - function undelegateFrom(address token, bytes calldata operator, uint256 amount) - external - nonReentrant - whenNotPaused - isTokenWhitelisted(token) - isValidAmount(amount) - { - bytes memory delegator = abi.encodePacked(bytes32(bytes20(msg.sender))); - _nextNonce(CLIENT_CHAIN_ID, delegator); - try DELEGATION_CONTRACT.undelegateFromThroughBtcGateway(CLIENT_CHAIN_ID, BTC_TOKEN, delegator, operator, amount) - returns (bool success) { - if (!success) { - revert UndelegationFailed(); - } - emit UndelegationCompleted(token, delegator, operator, amount); - } catch { - emit ExocorePrecompileError(address(DELEGATION_CONTRACT)); - revert UndelegationFailed(); - } - } - - /** - * @notice Withdraws the principal BTC. - * @param token The token address. - * @param amount The amount to withdraw. - */ - function withdrawPrincipal(address token, uint256 amount) - external - nonReentrant - whenNotPaused - isTokenWhitelisted(token) - isValidAmount(amount) - { - bytes memory withdrawer = abi.encodePacked(bytes32(bytes20(msg.sender))); - _nextNonce(CLIENT_CHAIN_ID, withdrawer); - (bool success, uint256 updatedBalance) = - ASSETS_CONTRACT.withdrawLST(CLIENT_CHAIN_ID, BTC_TOKEN, withdrawer, amount); - if (!success) { - revert WithdrawPrincipalFailed(); - } - (bytes32 requestId, bytes memory _btcAddress) = - _initiatePegOut(token, amount, withdrawer, WithdrawType.WithdrawPrincipal); - emit WithdrawPrincipalRequested(requestId, msg.sender, token, _btcAddress, amount, updatedBalance); - } - - /** - * @notice Withdraws the reward BTC. - * @param token The token address. - * @param amount The amount to withdraw. - */ - function withdrawReward(address token, uint256 amount) - external - nonReentrant - whenNotPaused - isTokenWhitelisted(token) - isValidAmount(amount) - { - bytes memory withdrawer = abi.encodePacked(bytes32(bytes20(msg.sender))); - _nextNonce(CLIENT_CHAIN_ID, withdrawer); - (bool success, uint256 updatedBalance) = - REWARD_CONTRACT.claimReward(CLIENT_CHAIN_ID, BTC_TOKEN, withdrawer, amount); - if (!success) { - revert WithdrawRewardFailed(); - } - (bytes32 requestId, bytes memory _btcAddress) = - _initiatePegOut(token, amount, withdrawer, WithdrawType.WithdrawReward); - - emit WithdrawRewardRequested(requestId, msg.sender, token, _btcAddress, amount, updatedBalance); - } - - /** - * @notice Initiates a peg-out request for a given token amount to a Bitcoin address - * @dev This function creates a new peg-out request and stores it in the contract's state - * @param _token The address of the token to be pegged out - * @param _amount The amount of tokens to be pegged out - * @param withdrawer The Exocore address associated with the Bitcoin address - * @param _withdrawType The type of withdrawal (e.g., normal, fast) - * @return requestId The unique identifier for the peg-out request - * @return _btcAddress The Bitcoin address for the peg-out - * @custom:throws BtcAddressNotRegistered if the Bitcoin address is not registered for the given Exocore address - * @custom:throws RequestAlreadyExists if a request with the same parameters already exists - */ - function _initiatePegOut(address _token, uint256 _amount, bytes memory withdrawer, WithdrawType _withdrawType) - internal - returns (bytes32 requestId, bytes memory _btcAddress) - { - // Use storage pointer to reduce gas consumption - PegOutRequest storage request; - - // 1. Check BTC address - _btcAddress = exocoreToBtcAddress[withdrawer]; - if (_btcAddress.length == 0) { - revert BtcAddressNotRegistered(); - } - - // 2. Generate unique requestId - requestId = keccak256(abi.encodePacked(_token, msg.sender, _btcAddress, _amount, block.number)); - - // 3. Check if request already exists - request = pegOutRequests[requestId]; - if (request.requester != address(0)) { - revert RequestAlreadyExists(requestId); - } - - // 4. Create new PegOutRequest - request.token = _token; - request.requester = msg.sender; - request.btcAddress = _btcAddress; - request.amount = _amount; - request.withdrawType = _withdrawType; - request.status = TxStatus.Pending; - request.timestamp = block.timestamp; - } - - /** - * @notice Process a pending peg-out request - * @dev Only authorized witnesses can call this function - * @param _requestId The unique identifier of the peg-out request - * @param _btcTxTag The Bitcoin transaction tag associated with the peg-out - * @custom:throws InvalidRequestStatus if the request status is not Pending - * @custom:throws RequestNotFound if the request does not exist - */ - function processPegOut(bytes32 _requestId, bytes32 _btcTxTag) - public - onlyAuthorizedWitness - nonReentrant - whenNotPaused - { - PegOutRequest storage request = pegOutRequests[_requestId]; - - // Check if the request exists and has the correct status - if (request.requester == address(0)) { - revert RequestNotFound(_requestId); - } - if (request.status != TxStatus.Pending) { - revert InvalidRequestStatus(_requestId); - } - - // Update request status - request.status = TxStatus.Processed; - - // Emit event - emit PegOutProcessed(_requestId, _btcTxTag); - } - - // Function to check and update expired peg-out requests - function checkExpiredPegOutRequests(bytes32[] calldata _requestIds) public { - for (uint256 i = 0; i < _requestIds.length; i++) { - PegOutRequest storage request = pegOutRequests[_requestIds[i]]; - if (request.status == TxStatus.Pending && block.timestamp >= request.timestamp + PROOF_TIMEOUT) { - request.status = TxStatus.Expired; - // Refund the tokens - // require(token.mint(request.requester, request.amount), "Token minting failed"); - emit PegOutTransactionExpired(_requestIds[i]); - } - } - } - - /** - * @notice Deposits BTC and then delegates it to an operator. - * @param _msg The interchain message containing the deposit details. - * @param operator The operator's address. - * @param signature The signature to verify. - */ - function depositThenDelegateTo(InterchainMsg calldata _msg, bytes calldata operator, bytes calldata signature) - external - nonReentrant - whenNotPaused - isTokenWhitelisted(BTC_ADDR) - isValidAmount(_msg.amount) - onlyAuthorizedWitness - { - (bytes memory btcTxTag, bytes memory depositor) = _processAndVerify(_msg, signature); - _depositToAssetContract(CLIENT_CHAIN_ID, BTC_TOKEN, depositor, _msg.amount, btcTxTag, operator); - } - - /** - * @notice Internal function to deposit BTC to the asset contract. - * @param clientChainId The client chain ID. - * @param btcToken The BTC token. - * @param depositor The BTC address. - * @param amount The amount to deposit. - * @param btcTxTag The BTC transaction tag. - * @param operator The operator's address. - */ - function _depositToAssetContract( - uint32 clientChainId, - bytes memory btcToken, - bytes memory depositor, - uint256 amount, - bytes memory btcTxTag, - bytes memory operator - ) internal { - try ASSETS_CONTRACT.depositLST(clientChainId, btcToken, depositor, amount) returns ( - bool depositSuccess, uint256 updatedBalance - ) { - if (!depositSuccess) { - revert DepositFailed(btcTxTag); - } - processedBtcTxs[btcTxTag] = TxInfo(true, block.timestamp); - _delegateToDelegationContract(clientChainId, btcToken, depositor, operator, amount, updatedBalance); - } catch { - emit ExocorePrecompileError(address(ASSETS_CONTRACT)); - revert DepositFailed(btcTxTag); - } - } - - /** - * @notice Internal function to delegate BTC to the delegation contract. - * @param clientChainId The client chain ID. - * @param btcToken The BTC token. - * @param depositor The BTC address. - * @param operator The operator's address. - * @param amount The amount to delegate. - * @param updatedBalance The updated balance after delegation. - */ - function _delegateToDelegationContract( - uint32 clientChainId, - bytes memory btcToken, - bytes memory depositor, - bytes memory operator, - uint256 amount, - uint256 updatedBalance - ) internal { - try DELEGATION_CONTRACT.delegateToThroughBtcGateway(clientChainId, btcToken, depositor, operator, amount) - returns (bool delegateSuccess) { - if (!delegateSuccess) { - revert DelegationFailed(); - } - emit DepositAndDelegationCompleted(BTC_ADDR, depositor, operator, amount, updatedBalance); - } catch { - emit ExocorePrecompileError(address(DELEGATION_CONTRACT)); - revert DelegationFailed(); - } - } - - /** - * @notice Gets the BTC address corresponding to an Exocore address. - * @param exocoreAddress The Exocore address. - * @return The corresponding BTC address. - */ - function getBtcAddress(bytes calldata exocoreAddress) external view returns (bytes memory) { - return exocoreToBtcAddress[exocoreAddress]; - } - - /** - * @notice Gets the current nonce for a given BTC address. - * @param srcChainId The source chain ID. - * @param depositor The BTC address as a string. - * @return The current nonce. - */ - function getCurrentNonce(uint32 srcChainId, string calldata depositor) external view returns (uint64) { - bytes memory bytesBtcAddr = _stringToBytes(depositor); - return inboundBytesNonce[srcChainId][bytesBtcAddr]; - } - - /** - * @notice Retrieves a PegOutRequest by its requestId. - * @param requestId The unique identifier of the request. - * @return The PegOutRequest struct associated with the given requestId. - */ - function getPegOutRequest(bytes32 requestId) public view returns (PegOutRequest memory) { - return pegOutRequests[requestId]; - } - - /** - * @notice Sets the status of a PegOutRequest. - * @param requestId The unique identifier of the request. - * @param newStatus The new status to set. - */ - function setPegOutRequestStatus(bytes32 requestId, TxStatus newStatus) - external - nonReentrant - whenNotPaused - onlyAuthorizedWitness - { - require(pegOutRequests[requestId].requester != address(0), "Request does not exist"); - pegOutRequests[requestId].status = newStatus; - emit PegOutRequestStatusUpdated(requestId, newStatus); - } - - /** - * @notice Converts an address to bytes. - * @param addr The address to convert. - * @return The address as bytes. - */ - function _addressToBytes(address addr) internal pure returns (bytes memory) { - return abi.encodePacked(addr); - } - - /** - * @notice Increments and gets the next nonce for a given source address. - * @param srcChainId The source chain ID. - * @param exoSrcAddress The exocore source address. - * @return The next nonce for corresponding btcAddress. - */ - function _nextNonce(uint32 srcChainId, bytes memory exoSrcAddress) internal view returns (uint64) { - bytes memory depositor = exocoreToBtcAddress[exoSrcAddress]; - return inboundBytesNonce[srcChainId][depositor] + 1; - } - - /** - * @notice Checks if a witness is authorized. - * @param witness The witness address. - * @return True if the witness is authorized, false otherwise. - */ - function _isAuthorizedWitness(address witness) internal view returns (bool) { - // Implementation depends on how you determine if a witness is authorized - // For example, you might check against a list of authorized witnesss - // or query another contract - return authorizedWitnesses[witness]; - } - - /** - * @notice Converts a string to bytes. - * @param source The string to convert. - * @return The string as bytes. - */ - function _stringToBytes(string memory source) internal pure returns (bytes memory) { - return abi.encodePacked(source); - } - -} diff --git a/src/core/UTXOGateway.sol b/src/core/UTXOGateway.sol new file mode 100644 index 00000000..bf2aada5 --- /dev/null +++ b/src/core/UTXOGateway.sol @@ -0,0 +1,791 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import {Errors} from "../libraries/Errors.sol"; +import {ExocoreBytes} from "../libraries/ExocoreBytes.sol"; + +import {ASSETS_CONTRACT} from "../interfaces/precompiles/IAssets.sol"; +import {DELEGATION_CONTRACT} from "../interfaces/precompiles/IDelegation.sol"; +import {REWARD_CONTRACT} from "../interfaces/precompiles/IReward.sol"; +import {SignatureVerifier} from "../libraries/SignatureVerifier.sol"; +import {UTXOGatewayStorage} from "../storage/UTXOGatewayStorage.sol"; +import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import {PausableUpgradeable} from "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol"; +import {ReentrancyGuardUpgradeable} from "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; + +/** + * @title UTXOGateway + * @dev This contract manages the gateway between Bitcoin like chains and the Exocore system. + * It handles deposits, delegations, withdrawals, and peg-out requests for BTC like tokens. + */ +contract UTXOGateway is + Initializable, + PausableUpgradeable, + OwnableUpgradeable, + ReentrancyGuardUpgradeable, + UTXOGatewayStorage +{ + + using ExocoreBytes for address; + using SignatureVerifier for bytes32; + + /** + * @notice Constructor to initialize the contract with the client chain ID. + * @dev Sets up initial configuration for testing purposes. + */ + constructor() { + _disableInitializers(); + } + + /** + * @notice Initializes the contract with the Exocore witness address, owner address and required proofs. + * @dev If the witnesses length is greater or equal to the required proofs, the consensus requirement for stake + * message would be activated. + * @param owner_ The address of the owner. + * @param witnesses The addresses of the witnesses. + * @param requiredProofs_ The number of required proofs. + */ + function initialize(address owner_, address[] calldata witnesses, uint256 requiredProofs_) external initializer { + if (owner_ == address(0) || witnesses.length == 0) { + revert Errors.ZeroAddress(); + } + if (requiredProofs_ < MIN_REQUIRED_PROOFS || requiredProofs_ > MAX_REQUIRED_PROOFS) { + revert Errors.InvalidRequiredProofs(); + } + + requiredProofs = requiredProofs_; + for (uint256 i = 0; i < witnesses.length; i++) { + _addWitness(witnesses[i]); + } + __Pausable_init_unchained(); + __ReentrancyGuard_init_unchained(); + _transferOwnership(owner_); + } + + /** + * @notice Pauses the contract. + * @dev Can only be called by the contract owner. + */ + function pause() external onlyOwner { + _pause(); + } + + /** + * @notice Unpauses the contract. + * @dev Can only be called by the contract owner. + */ + function unpause() external onlyOwner { + _unpause(); + } + + /** + * @notice Activates token staking by registering or updating the chain and token with the Exocore system. + */ + function activateStakingForClientChain(ClientChainID clientChainId) external onlyOwner whenNotPaused { + if (clientChainId == ClientChainID.Bitcoin) { + _registerOrUpdateClientChain( + clientChainId, STAKER_ACCOUNT_LENGTH, BITCOIN_NAME, BITCOIN_METADATA, BITCOIN_SIGNATURE_SCHEME + ); + _registerOrUpdateToken(clientChainId, VIRTUAL_TOKEN, BTC_DECIMALS, BTC_NAME, BTC_METADATA, BTC_ORACLE_INFO); + } else { + revert Errors.InvalidClientChain(); + } + } + + /** + * @notice Updates the required proofs for consensus. + * @notice The consensus requirement for stake message would be activated if the current authorized witness count is + * greater than or equal to the new required proofs. + * @dev Can only be called by the contract owner. + * @param newRequiredProofs The new required proofs. + */ + function updateRequiredProofs(uint256 newRequiredProofs) external onlyOwner whenNotPaused { + if (newRequiredProofs < MIN_REQUIRED_PROOFS || newRequiredProofs > MAX_REQUIRED_PROOFS) { + revert Errors.InvalidRequiredProofs(); + } + + bool wasConsensusRequired = _isConsensusRequired(); + uint256 oldRequiredProofs = requiredProofs; + requiredProofs = newRequiredProofs; + + emit RequiredProofsUpdated(oldRequiredProofs, newRequiredProofs); + + // Check if consensus state changed due to new requirement + bool isConsensusRequired_ = _isConsensusRequired(); + if (!wasConsensusRequired && isConsensusRequired_) { + emit ConsensusActivated(requiredProofs, authorizedWitnessCount); + } else if (wasConsensusRequired && !isConsensusRequired_) { + emit ConsensusDeactivated(requiredProofs, authorizedWitnessCount); + } + } + + /** + * @notice Adds a group of authorized witnesses. + * @notice This could potentially activate consensus for stake message if the total witness count is greater than or + * equal to the required proofs. + * @param witnesses The addresses of the witnesses to be added. + * @dev Can only be called by the contract owner. + */ + function addWitnesses(address[] calldata witnesses) external onlyOwner whenNotPaused { + for (uint256 i = 0; i < witnesses.length; i++) { + _addWitness(witnesses[i]); + } + } + + /** + * @notice Removes a group of authorized witnesses. + * @notice This could potentially deactivate consensus for stake message if the total witness count is less than the + * required proofs. + * @param witnesses The addresses of the witnesses to be removed. + * @dev Can only be called by the contract owner. + */ + function removeWitnesses(address[] calldata witnesses) external onlyOwner whenNotPaused { + for (uint256 i = 0; i < witnesses.length; i++) { + _removeWitness(witnesses[i]); + } + } + + /** + * @notice Updates the bridge fee rate. + * @param bridgeFeeRate_ The new bridge fee rate, with basis as 10000, so 100 means 1% + * @dev Can only be called by the contract owner. + */ + function updateBridgeFeeRate(uint256 bridgeFeeRate_) external onlyOwner whenNotPaused { + require(bridgeFeeRate_ <= MAX_BRIDGE_FEE_RATE, "Fee cannot exceed max bridge fee rate"); + bridgeFeeRate = bridgeFeeRate_; + emit BridgeFeeRateUpdated(bridgeFeeRate_); + } + + /** + * @notice Submits a proof for a stake message. + * @notice The submitted message would be processed after collecting enough proofs from withnesses. + * @dev The stake message would be deleted after it has been processed to refund some gas, though the mapping + * inside it cannot be deleted. + * @param witness The witness address that signed the message. + * @param _message The stake message. + * @param _signature The signature of the message. + */ + // slither-disable-next-line reentrancy-no-eth + function submitProofForStakeMsg(address witness, StakeMsg calldata _message, bytes calldata _signature) + external + nonReentrant + whenNotPaused + { + if (!_isConsensusRequired()) { + revert Errors.ConsensusNotRequired(); + } + + if (!_isAuthorizedWitness(witness)) { + revert Errors.WitnessNotAuthorized(witness); + } + + bytes32 messageHash = _verifyStakeMessage(witness, _message, _signature); + + // we should revoke the tx by setting it as expired if it has expired + _revokeTxIfExpired(messageHash); + + Transaction storage txn = transactions[messageHash]; + + if (txn.status == TxStatus.Pending) { + // if the witness has already submitted proof at or after the start of the proof window, they cannot submit + // again + if (txn.witnessTime[witness] >= txn.expiryTime - PROOF_TIMEOUT) { + revert Errors.WitnessAlreadySubmittedProof(); + } + txn.witnessTime[witness] = block.timestamp; + txn.proofCount++; + } else { + txn.status = TxStatus.Pending; + txn.expiryTime = block.timestamp + PROOF_TIMEOUT; + txn.proofCount = 1; + txn.witnessTime[witness] = block.timestamp; + txn.stakeMsg = _message; + } + + emit ProofSubmitted(messageHash, witness); + + // Check for consensus + if (txn.proofCount >= requiredProofs) { + processedTransactions[messageHash] = true; + _processStakeMsg(txn.stakeMsg); + // we delete the transaction after it has been processed to refund some gas, so no need to worry about + // reentrancy + delete transactions[messageHash]; + + emit TransactionProcessed(messageHash); + } + } + + /** + * @notice Deposits BTC like tokens to the Exocore system. + * @param witness The witness address that signed the message. + * @param _msg The stake message. + * @param signature The signature of the message. + */ + function processStakeMessage(address witness, StakeMsg calldata _msg, bytes calldata signature) + external + nonReentrant + whenNotPaused + { + if (_isConsensusRequired()) { + revert Errors.ConsensusRequired(); + } + + if (!_isAuthorizedWitness(witness)) { + revert Errors.WitnessNotAuthorized(witness); + } + _verifyStakeMessage(witness, _msg, signature); + + _processStakeMsg(_msg); + } + + /** + * @notice Delegates BTC like tokens to an operator. + * @param token The value of the token enum. + * @param operator The operator's exocore address. + * @param amount The amount to delegate. + */ + function delegateTo(Token token, string calldata operator, uint256 amount) + external + nonReentrant + whenNotPaused + isValidAmount(amount) + isRegistered(token, msg.sender) + { + if (!isValidOperatorAddress(operator)) { + revert Errors.InvalidOperator(); + } + + ClientChainID clientChainId = ClientChainID(uint8(token)); + + bool success = _delegate(clientChainId, msg.sender, operator, amount); + if (!success) { + revert Errors.DelegationFailed(); + } + + emit DelegationCompleted(clientChainId, msg.sender, operator, amount); + } + + /** + * @notice Undelegates BTC like tokens from an operator. + * @param token The value of the token enum. + * @param operator The operator's exocore address. + * @param amount The amount to undelegate. + */ + function undelegateFrom(Token token, string calldata operator, uint256 amount) + external + nonReentrant + whenNotPaused + isValidAmount(amount) + isRegistered(token, msg.sender) + { + if (!isValidOperatorAddress(operator)) { + revert Errors.InvalidOperator(); + } + + ClientChainID clientChainId = ClientChainID(uint8(token)); + + uint64 nonce = ++delegationNonce[clientChainId]; + bool success = DELEGATION_CONTRACT.undelegate( + uint32(uint8(clientChainId)), nonce, VIRTUAL_TOKEN, msg.sender.toExocoreBytes(), bytes(operator), amount + ); + if (!success) { + revert Errors.UndelegationFailed(); + } + emit UndelegationCompleted(clientChainId, msg.sender, operator, amount); + } + + /** + * @notice Withdraws the principal BTC like tokens. + * @param token The value of the token enum. + * @param amount The amount to withdraw. + */ + function withdrawPrincipal(Token token, uint256 amount) external nonReentrant whenNotPaused isValidAmount(amount) { + ClientChainID clientChainId = ClientChainID(uint8(token)); + + bytes memory clientAddress = outboundRegistry[clientChainId][msg.sender]; + if (clientAddress.length == 0) { + revert Errors.AddressNotRegistered(); + } + + (bool success, uint256 updatedBalance) = ASSETS_CONTRACT.withdrawLST( + uint32(uint8(clientChainId)), VIRTUAL_TOKEN, msg.sender.toExocoreBytes(), amount + ); + if (!success) { + revert Errors.WithdrawPrincipalFailed(); + } + + uint64 requestId = + _initiatePegOut(clientChainId, amount, msg.sender, clientAddress, WithdrawType.WithdrawPrincipal); + emit WithdrawPrincipalRequested(clientChainId, requestId, msg.sender, clientAddress, amount, updatedBalance); + } + + /** + * @notice Withdraws the reward BTC like tokens. + * @param token The value of the token enum. + * @param amount The amount to withdraw. + */ + function withdrawReward(Token token, uint256 amount) external nonReentrant whenNotPaused isValidAmount(amount) { + ClientChainID clientChainId = ClientChainID(uint8(token)); + bytes memory clientAddress = outboundRegistry[clientChainId][msg.sender]; + if (clientAddress.length == 0) { + revert Errors.AddressNotRegistered(); + } + + (bool success, uint256 updatedBalance) = REWARD_CONTRACT.claimReward( + uint32(uint8(clientChainId)), VIRTUAL_TOKEN, msg.sender.toExocoreBytes(), amount + ); + if (!success) { + revert Errors.WithdrawRewardFailed(); + } + + uint64 requestId = + _initiatePegOut(clientChainId, amount, msg.sender, clientAddress, WithdrawType.WithdrawReward); + emit WithdrawRewardRequested(clientChainId, requestId, msg.sender, clientAddress, amount, updatedBalance); + } + + /** + * @notice Process a pending peg-out request + * @dev Only authorized witnesses can call this function + * @dev the processed request would be deleted from the pegOutRequests mapping + * @param clientChainId The client chain ID + * @return nextRequest The next PegOutRequest + * @custom:throws InvalidRequestStatus if the request status is not Pending + * @custom:throws RequestNotFound if the request does not exist + */ + function processNextPegOut(ClientChainID clientChainId) + external + onlyAuthorizedWitness + nonReentrant + whenNotPaused + returns (PegOutRequest memory nextRequest) + { + uint64 requestId = ++outboundNonce[clientChainId]; + nextRequest = pegOutRequests[clientChainId][requestId]; + + // Check if the request exists + if (nextRequest.requester == address(0)) { + revert Errors.RequestNotFound(requestId); + } + + // delete the request + delete pegOutRequests[clientChainId][requestId]; + + // Emit event + emit PegOutProcessed( + uint8(nextRequest.withdrawType), + clientChainId, + requestId, + nextRequest.requester, + nextRequest.clientAddress, + nextRequest.amount + ); + } + + /** + * @notice Gets the client chain address for a given Exocore address + * @param clientChainId The client chain ID + * @param exocoreAddress The Exocore address + * @return The client chain address + */ + function getClientAddress(ClientChainID clientChainId, address exocoreAddress) + external + view + returns (bytes memory) + { + return outboundRegistry[clientChainId][exocoreAddress]; + } + + /** + * @notice Gets the Exocore address for a given client chain address + * @param clientChainId The client chain ID + * @param clientAddress The client chain address + * @return The Exocore address + */ + function getExocoreAddress(ClientChainID clientChainId, bytes calldata clientAddress) + external + view + returns (address) + { + return inboundRegistry[clientChainId][clientAddress]; + } + + /** + * @notice Gets the next inbound nonce for a given source chain ID. + * @param clientChainId The client chain ID. + * @return The next inbound nonce. + */ + function nextInboundNonce(ClientChainID clientChainId) external view returns (uint64) { + return inboundNonce[clientChainId] + 1; + } + + /** + * @notice Retrieves a PegOutRequest by client chain id and request id + * @param clientChainId The client chain ID + * @param requestId The unique identifier of the request. + * @return The PegOutRequest struct associated with the given requestId. + */ + function getPegOutRequest(ClientChainID clientChainId, uint64 requestId) + public + view + returns (PegOutRequest memory) + { + return pegOutRequests[clientChainId][requestId]; + } + + /** + * @notice Retrieves the status of a transaction. + * @param messageHash The hash of the transaction. + * @return The status of the transaction. + */ + function getTransactionStatus(bytes32 messageHash) public view returns (TxStatus) { + return transactions[messageHash].status; + } + + /** + * @notice Retrieves the proof count of a transaction. + * @param messageHash The hash of the transaction. + * @return The proof count of the transaction. + */ + function getTransactionProofCount(bytes32 messageHash) public view returns (uint256) { + return transactions[messageHash].proofCount; + } + + /** + * @notice Retrieves the expiry time of a transaction. + * @param messageHash The hash of the transaction. + * @return The expiry time of the transaction. + */ + function getTransactionExpiryTime(bytes32 messageHash) public view returns (uint256) { + return transactions[messageHash].expiryTime; + } + + /** + * @notice Retrieves the witness time of a transaction. + * @param messageHash The hash of the transaction. + * @param witness The witness address. + * @return The witness time of the transaction. + */ + function getTransactionWitnessTime(bytes32 messageHash, address witness) public view returns (uint256) { + return transactions[messageHash].witnessTime[witness]; + } + + function isConsensusRequired() external view returns (bool) { + return _isConsensusRequired(); + } + + /** + * @notice Checks if consensus is required for a stake message. + * @return True if count of authorized witnesses is greater than or equal to REQUIRED_PROOFS, false otherwise. + */ + function _isConsensusRequired() internal view returns (bool) { + return authorizedWitnessCount >= requiredProofs; + } + + /** + * @notice Checks if a witness is authorized. + * @param witness The witness address. + * @return True if the witness is authorized, false otherwise. + */ + function _isAuthorizedWitness(address witness) internal view returns (bool) { + return authorizedWitnesses[witness]; + } + + function _addWitness(address _witness) internal { + if (_witness == address(0)) { + revert Errors.ZeroAddress(); + } + if (_isAuthorizedWitness(_witness)) { + revert Errors.WitnessAlreadyAuthorized(_witness); + } + + bool wasConsensusRequired = _isConsensusRequired(); + + authorizedWitnesses[_witness] = true; + authorizedWitnessCount++; + emit WitnessAdded(_witness); + + // Emit only when crossing the threshold from false to true + if (!wasConsensusRequired && _isConsensusRequired()) { + emit ConsensusActivated(requiredProofs, authorizedWitnessCount); + } + } + + function _removeWitness(address _witness) internal { + if (authorizedWitnessCount <= 1) { + revert Errors.CannotRemoveLastWitness(); + } + if (!authorizedWitnesses[_witness]) { + revert Errors.WitnessNotAuthorized(_witness); + } + + bool wasConsensusRequired = _isConsensusRequired(); + + authorizedWitnesses[_witness] = false; + authorizedWitnessCount--; + emit WitnessRemoved(_witness); + + // Emit only when crossing the threshold from true to false + if (wasConsensusRequired && !_isConsensusRequired()) { + emit ConsensusDeactivated(requiredProofs, authorizedWitnessCount); + } + } + + /** + * @notice Registers or updates the Bitcoin chain with the Exocore system. + */ + function _registerOrUpdateClientChain( + ClientChainID clientChainId, + uint8 stakerAccountLength, + string memory name, + string memory metadata, + string memory signatureScheme + ) internal { + (bool success, bool updated) = ASSETS_CONTRACT.registerOrUpdateClientChain( + uint32(uint8(clientChainId)), stakerAccountLength, name, metadata, signatureScheme + ); + if (!success) { + revert Errors.RegisterClientChainToExocoreFailed(uint32(uint8(clientChainId))); + } + if (updated) { + emit ClientChainUpdated(clientChainId); + } else { + emit ClientChainRegistered(clientChainId); + } + } + + function _registerOrUpdateToken( + ClientChainID clientChainId, + bytes memory token, + uint8 decimals, + string memory name, + string memory metadata, + string memory oracleInfo + ) internal { + uint32 clientChainIdUint32 = uint32(uint8(clientChainId)); + bool registered = + ASSETS_CONTRACT.registerToken(clientChainIdUint32, token, decimals, name, metadata, oracleInfo); + if (!registered) { + bool updated = ASSETS_CONTRACT.updateToken(clientChainIdUint32, token, metadata); + if (!updated) { + revert Errors.AddWhitelistTokenFailed(clientChainIdUint32, bytes32(token)); + } + emit WhitelistTokenUpdated(clientChainId, VIRTUAL_TOKEN_ADDRESS); + } else { + emit WhitelistTokenAdded(clientChainId, VIRTUAL_TOKEN_ADDRESS); + } + } + + /** + * @notice Verifies the signature of a stake message. + * @param signer The signer address. + * @param _msg The stake message. + * @param signature The signature to verify. + */ + function _verifySignature(address signer, StakeMsg calldata _msg, bytes memory signature) + internal + pure + returns (bytes32 messageHash) + { + // StakeMsg, EIP721 is preferred next step. + bytes memory encodeMsg = abi.encode( + _msg.clientChainId, + _msg.clientAddress, + _msg.exocoreAddress, + _msg.operator, + _msg.amount, + _msg.nonce, + _msg.txTag + ); + messageHash = keccak256(encodeMsg); + + SignatureVerifier.verifyMsgSig(signer, messageHash, signature); + } + + /** + * @dev Verifies that all required fields in StakeMsg are valid + * @param _msg The stake message to verify + */ + function _verifyStakeMsgFields(StakeMsg calldata _msg) internal pure { + // Combine all non-zero checks into a single value + uint256 nonZeroCheck = + uint8(_msg.clientChainId) | _msg.clientAddress.length | _msg.amount | _msg.nonce | _msg.txTag.length; + + if (nonZeroCheck == 0) { + revert Errors.InvalidStakeMessage(); + } + + if (bytes(_msg.operator).length > 0 && !isValidOperatorAddress(_msg.operator)) { + revert Errors.InvalidOperator(); + } + } + + function _verifyTxTagNotProcessed(ClientChainID clientChainId, bytes calldata txTag) internal view { + if (processedClientChainTxs[clientChainId][txTag]) { + revert Errors.TxTagAlreadyProcessed(); + } + } + + /** + * @notice Verifies a stake message. + * @param witness The witness address that signed the message. + * @param _msg The stake message. + * @param signature The signature to verify. + */ + function _verifyStakeMessage(address witness, StakeMsg calldata _msg, bytes calldata signature) + internal + view + returns (bytes32 messageHash) + { + // verify that the stake message fields are valid + _verifyStakeMsgFields(_msg); + + // Verify nonce + _verifyInboundNonce(_msg.clientChainId, _msg.nonce); + + // Verify that the txTag has not been processed + _verifyTxTagNotProcessed(_msg.clientChainId, _msg.txTag); + + // Verify signature + messageHash = _verifySignature(witness, _msg, signature); + } + + /** + * @notice Initiates a peg-out request for a given token amount to a Bitcoin address + * @dev This function creates a new peg-out request and stores it in the contract's state + * @param clientChainId The client chain to be pegged out + * @param _amount The amount of tokens to be pegged out + * @param withdrawer The Exocore address associated with the Bitcoin address + * @param clientAddress The client chain address + * @param _withdrawType The type of withdrawal (e.g., normal, fast) + * @return requestId The unique identifier for the peg-out request + * @custom:throws RequestAlreadyExists if a request with the same parameters already exists + */ + function _initiatePegOut( + ClientChainID clientChainId, + uint256 _amount, + address withdrawer, + bytes memory clientAddress, + WithdrawType _withdrawType + ) internal returns (uint64 requestId) { + // 2. increase the peg-out nonce for the client chain and return as requestId + requestId = ++pegOutNonce[clientChainId]; + + // 3. Check if request already exists + PegOutRequest storage request = pegOutRequests[clientChainId][requestId]; + if (request.requester != address(0)) { + revert Errors.RequestAlreadyExists(uint32(uint8(clientChainId)), requestId); + } + + // 4. Create new PegOutRequest + request.clientChainId = clientChainId; + request.nonce = requestId; + request.requester = withdrawer; + request.clientAddress = clientAddress; + request.amount = _amount; + request.withdrawType = _withdrawType; + } + + /** + * @notice Internal function to deposit BTC like token. + * @param clientChainId The client chain ID. + * @param srcAddress The source address. + * @param depositorExoAddr The Exocore address. + * @param amount The amount to deposit. + * @param txTag The transaction tag. + */ + function _deposit( + ClientChainID clientChainId, + bytes memory srcAddress, + address depositorExoAddr, + uint256 amount, + bytes memory txTag + ) internal { + (bool success, uint256 updatedBalance) = ASSETS_CONTRACT.depositLST( + uint32(uint8(clientChainId)), VIRTUAL_TOKEN, depositorExoAddr.toExocoreBytes(), amount + ); + if (!success) { + revert Errors.DepositFailed(txTag); + } + + emit DepositCompleted(clientChainId, txTag, depositorExoAddr, srcAddress, amount, updatedBalance); + } + + /** + * @notice Internal function to delegate BTC like token. + * @param clientChainId The client chain ID. + * @param delegator The Exocore address. + * @param operator The operator's address. + * @param amount The amount to delegate. + * @return success True if the delegation was successful, false otherwise. + * @dev Sometimes we may not want to revert on failure, so we return a boolean. + */ + function _delegate(ClientChainID clientChainId, address delegator, string memory operator, uint256 amount) + internal + returns (bool success) + { + uint64 nonce = ++delegationNonce[clientChainId]; + success = DELEGATION_CONTRACT.delegate( + uint32(uint8(clientChainId)), nonce, VIRTUAL_TOKEN, delegator.toExocoreBytes(), bytes(operator), amount + ); + } + + function _revokeTxIfExpired(bytes32 txid) internal { + Transaction storage txn = transactions[txid]; + if (txn.status == TxStatus.Pending && block.timestamp >= txn.expiryTime) { + txn.status = TxStatus.Expired; + emit TransactionExpired(txid); + } + } + + function _registerAddress(ClientChainID clientChainId, bytes memory depositor, address exocoreAddress) internal { + require(depositor.length > 0 && exocoreAddress != address(0), "Invalid address"); + require(inboundRegistry[clientChainId][depositor] == address(0), "Depositor address already registered"); + require(outboundRegistry[clientChainId][exocoreAddress].length == 0, "Exocore address already registered"); + + inboundRegistry[clientChainId][depositor] = exocoreAddress; + outboundRegistry[clientChainId][exocoreAddress] = depositor; + + emit AddressRegistered(clientChainId, depositor, exocoreAddress); + } + + function _processStakeMsg(StakeMsg memory _msg) internal { + // increment inbound nonce for the client chain and mark the tx as processed + inboundNonce[_msg.clientChainId]++; + processedClientChainTxs[_msg.clientChainId][_msg.txTag] = true; + + // register address if not already registered + if ( + inboundRegistry[_msg.clientChainId][_msg.clientAddress] == address(0) + && outboundRegistry[_msg.clientChainId][_msg.exocoreAddress].length == 0 + ) { + if (_msg.exocoreAddress == address(0)) { + revert Errors.ZeroAddress(); + } + _registerAddress(_msg.clientChainId, _msg.clientAddress, _msg.exocoreAddress); + } + + address stakerExoAddr = inboundRegistry[_msg.clientChainId][_msg.clientAddress]; + uint256 fee = _msg.amount * bridgeFeeRate / BASIS_POINTS; + uint256 amountAfterFee = _msg.amount - fee; + + // we use registered exocore address as the depositor + // this should always succeed and never revert, otherwise something is wrong. + _deposit(_msg.clientChainId, _msg.clientAddress, stakerExoAddr, amountAfterFee, _msg.txTag); + + // delegate to operator if operator is provided, and do not revert if it fails since we need to count the stake + // as deposited + if (bytes(_msg.operator).length > 0) { + bool success = _delegate(_msg.clientChainId, stakerExoAddr, _msg.operator, amountAfterFee); + if (!success) { + emit DelegationFailedForStake(_msg.clientChainId, stakerExoAddr, _msg.operator, amountAfterFee); + } else { + emit DelegationCompleted(_msg.clientChainId, stakerExoAddr, _msg.operator, amountAfterFee); + } + } + + emit StakeMsgExecuted(_msg.clientChainId, _msg.nonce, stakerExoAddr, amountAfterFee); + } + +} diff --git a/src/libraries/Errors.sol b/src/libraries/Errors.sol index 653ab3ed..f79cdd84 100644 --- a/src/libraries/Errors.sol +++ b/src/libraries/Errors.sol @@ -311,4 +311,71 @@ library Errors { /// @dev RewardVault: insufficient balance error InsufficientBalance(); + /* -------------------------------------------------------------------------- */ + /* UTXOGateway Errors */ + /* -------------------------------------------------------------------------- */ + + /// @dev UTXOGateway: witness has already submitted proof + error WitnessAlreadySubmittedProof(); + + /// @dev UTXOGateway: invalid stake message + error InvalidStakeMessage(); + + /// @dev UTXOGateway: transaction tag has already been processed + error TxTagAlreadyProcessed(); + + /// @dev UTXOGateway: invalid operator address + error InvalidOperator(); + + /// @dev UTXOGateway: invalid token + error InvalidToken(); + + /// @dev UTXOGateway: witness has already been authorized + error WitnessAlreadyAuthorized(address witness); + + /// @dev UTXOGateway: witness has not been authorized + error WitnessNotAuthorized(address witness); + + /// @dev UTXOGateway: cannot remove the last witness + error CannotRemoveLastWitness(); + + /// @dev UTXOGateway: invalid client chain + error InvalidClientChain(); + + /// @dev UTXOGateway: deposit failed + error DepositFailed(bytes txTag); + + /// @dev UTXOGateway: address not registered + error AddressNotRegistered(); + + /// @dev UTXOGateway: delegation failed + error DelegationFailed(); + + /// @dev UTXOGateway: withdraw principal failed + error WithdrawPrincipalFailed(); + + /// @dev UTXOGateway: undelegation failed + error UndelegationFailed(); + + /// @dev UTXOGateway: withdraw reward failed + error WithdrawRewardFailed(); + + /// @dev UTXOGateway: request not found + error RequestNotFound(uint64 requestId); + + /// @dev UTXOGateway: request already exists + error RequestAlreadyExists(uint32 clientChain, uint64 requestId); + + /// @dev UTXOGateway: witness not authorized + error UnauthorizedWitness(); + + /// @dev UTXOGateway: consensus is not activated + error ConsensusNotRequired(); + + /// @dev UTXOGateway: consensus is required + error ConsensusRequired(); + + /// @dev UTXOGateway: invalid required proofs + error InvalidRequiredProofs(); + } diff --git a/src/libraries/ExocoreBytes.sol b/src/libraries/ExocoreBytes.sol new file mode 100644 index 00000000..7d5ee374 --- /dev/null +++ b/src/libraries/ExocoreBytes.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +library ExocoreBytes { + + /// @notice Converts an Ethereum address to Exocore's 32-byte address format + /// @param addr The Ethereum address to convert + /// @return The address as 32-byte Exocore format (20 bytes + right padding) + function toExocoreBytes(address addr) internal pure returns (bytes memory) { + return abi.encodePacked(bytes32(bytes20(addr))); + } + +} diff --git a/src/storage/ExocoreBtcGatewayStorage.sol b/src/storage/ExocoreBtcGatewayStorage.sol deleted file mode 100644 index ba52896f..00000000 --- a/src/storage/ExocoreBtcGatewayStorage.sol +++ /dev/null @@ -1,466 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.19; - -/** - * @title ExocoreBtcGatewayStorage - * @dev This contract manages the storage for the Exocore-Bitcoin gateway - */ -contract ExocoreBtcGatewayStorage { - - /** - * @dev Enum to represent the status of a transaction - */ - enum TxStatus { - Pending, - Processed, - Expired - } - - /** - * @dev Enum to represent the WithdrawType - */ - enum WithdrawType { - Undefined, - WithdrawPrincipal, - WithdrawReward - } - - /** - * @dev Struct to store transaction information - */ - struct TxInfo { - bool processed; - uint256 timestamp; - } - - /** - * @dev Struct to store interchain message information - */ - struct InterchainMsg { - uint32 srcChainID; - uint32 dstChainID; - bytes srcAddress; - bytes dstAddress; - address token; // btc virtual token - uint256 amount; // btc deposit amount - uint64 nonce; - bytes txTag; // btc lowercase(txid-vout) - bytes payload; - } - - /** - * @dev Struct to store proof information - */ - struct Proof { - address witness; - InterchainMsg message; - uint256 timestamp; - bytes signature; - } - - /** - * @dev Struct to store transaction information - */ - struct Transaction { - TxStatus status; - uint256 amount; - address recipient; - uint256 expiryTime; - uint256 proofCount; - mapping(address => bool) hasWitnessed; - } - - /** - * @dev Struct for peg-out requests - */ - struct PegOutRequest { - address token; - address requester; - bytes btcAddress; - uint256 amount; - WithdrawType withdrawType; - TxStatus status; - uint256 timestamp; - } - - // Constants - address public constant EXOCORE_WITNESS = address(0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266); - uint256 public constant REQUIRED_PROOFS = 2; - uint256 public constant PROOF_TIMEOUT = 1 days; - uint256 public bridgeFee; // Fee percentage (in basis points, e.g., 100 = 1%) - - // Mappings - /** - * @dev Mapping to store proofs submitted by witnesses - */ - mapping(bytes => Proof[]) public proofs; - - /** - * @dev Mapping to store transaction information - */ - mapping(bytes => Transaction) public transactions; - - /** - * @dev Mapping to store processed Bitcoin transactions - */ - mapping(bytes => TxInfo) public processedBtcTxs; - - /** - * @dev Mapping to store peg-out requests - */ - mapping(bytes32 => PegOutRequest) public pegOutRequests; - - /** - * @dev Mapping to store authorized witnesses - */ - mapping(address => bool) public authorizedWitnesses; - - /** - * @dev Mapping to store Bitcoin to Exocore address mappings - */ - mapping(bytes => bytes) public btcToExocoreAddress; - - /** - * @dev Mapping to store Exocore to Bitcoin address mappings - */ - mapping(bytes => bytes) public exocoreToBtcAddress; - - /** - * @dev Mapping to store whitelisted tokens - */ - mapping(address => bool) public isWhitelistedToken; - - /** - * @dev Mapping to store inbound bytes nonce for each chain and sender - */ - mapping(uint32 => mapping(bytes => uint64)) public inboundBytesNonce; - - // Events - /** - * @dev Emitted when a deposit is completed - * @param btcTxTag The Bitcoin transaction tag - * @param depositorExoAddr The depositor's Exocore address - * @param token The token address - * @param depositorBtcAddr The depositor's Bitcoin address - * @param amount The amount deposited - * @param updatedBalance The updated balance after deposit - */ - event DepositCompleted( - bytes btcTxTag, - bytes depositorExoAddr, - address indexed token, - bytes depositorBtcAddr, - uint256 amount, - uint256 updatedBalance - ); - - /** - * @dev Emitted when a principal withdrawal is requested - * @param requestId The unique identifier for the withdrawal request - * @param withdrawerExoAddr The withdrawer's Exocore address - * @param token The token address - * @param withdrawerBtcAddr The withdrawer's Bitcoin address - * @param amount The amount to withdraw - * @param updatedBalance The updated balance after withdrawal request - */ - event WithdrawPrincipalRequested( - bytes32 indexed requestId, - address indexed withdrawerExoAddr, - address indexed token, - bytes withdrawerBtcAddr, - uint256 amount, - uint256 updatedBalance - ); - - /** - * @dev Emitted when a reward withdrawal is requested - * @param requestId The unique identifier for the withdrawal request - * @param withdrawerExoAddr The withdrawer's Exocore address - * @param token The token address - * @param withdrawerBtcAddr The withdrawer's Bitcoin address - * @param amount The amount to withdraw - * @param updatedBalance The updated balance after withdrawal request - */ - event WithdrawRewardRequested( - bytes32 indexed requestId, - address indexed withdrawerExoAddr, - address indexed token, - bytes withdrawerBtcAddr, - uint256 amount, - uint256 updatedBalance - ); - - /** - * @dev Emitted when a principal withdrawal is completed - * @param requestId The unique identifier for the withdrawal request - * @param withdrawerExoAddr The withdrawer's Exocore address - * @param token The token address - * @param withdrawerBtcAddr The withdrawer's Bitcoin address - * @param amount The amount withdrawn - * @param updatedBalance The updated balance after withdrawal - */ - event WithdrawPrincipalCompleted( - bytes32 indexed requestId, - address indexed withdrawerExoAddr, - address indexed token, - bytes withdrawerBtcAddr, - uint256 amount, - uint256 updatedBalance - ); - - /** - * @dev Emitted when a reward withdrawal is completed - * @param requestId The unique identifier for the withdrawal request - * @param withdrawerExoAddr The withdrawer's Exocore address - * @param token The token address - * @param withdrawerBtcAddr The withdrawer's Bitcoin address - * @param amount The amount withdrawn - * @param updatedBalance The updated balance after withdrawal - */ - event WithdrawRewardCompleted( - bytes32 indexed requestId, - address indexed withdrawerExoAddr, - address indexed token, - bytes withdrawerBtcAddr, - uint256 amount, - uint256 updatedBalance - ); - - /** - * @dev Emitted when a delegation is completed - * @param token The token address - * @param delegator The delegator's address - * @param operator The operator's address - * @param amount The amount delegated - */ - event DelegationCompleted(address token, bytes delegator, bytes operator, uint256 amount); - - /** - * @dev Emitted when an undelegation is completed - * @param token The token address - * @param delegator The delegator's address - * @param operator The operator's address - * @param amount The amount undelegated - */ - event UndelegationCompleted(address token, bytes delegator, bytes operator, uint256 amount); - - /** - * @dev Emitted when a deposit and delegation is completed - * @param token The token address - * @param depositor The depositor's address - * @param operator The operator's address - * @param amount The amount deposited and delegated - * @param updatedBalance The updated balance after the operation - */ - event DepositAndDelegationCompleted( - address token, bytes depositor, bytes operator, uint256 amount, uint256 updatedBalance - ); - - /** - * @dev Emitted when an address is registered - * @param depositor The depositor's address - * @param exocoreAddress The corresponding Exocore address - */ - event AddressRegistered(bytes depositor, bytes exocoreAddress); - - /** - * @dev Emitted when an Exocore precompile error occurs - * @param precompileAddress The address of the precompile that caused the error - */ - event ExocorePrecompileError(address precompileAddress); - - /** - * @dev Emitted when a new witness is added - * @param witness The address of the added witness - */ - event WitnessAdded(address indexed witness); - - /** - * @dev Emitted when a witness is removed - * @param witness The address of the removed witness - */ - event WitnessRemoved(address indexed witness); - - /** - * @dev Emitted when a proof is submitted - * @param btcTxTag The Bitcoin transaction tag - * @param witness The address of the witness submitting the proof - * @param message The interchain message associated with the proof - */ - event ProofSubmitted(bytes btcTxTag, address indexed witness, InterchainMsg message); - - /** - * @dev Emitted when a deposit is processed - * @param btcTxTag The Bitcoin transaction tag - * @param recipient The address of the recipient - * @param amount The amount processed - */ - event DepositProcessed(bytes btcTxTag, address indexed recipient, uint256 amount); - - /** - * @dev Emitted when a transaction expires - * @param btcTxTag The Bitcoin transaction tag of the expired transaction - */ - event TransactionExpired(bytes btcTxTag); - - /** - * @dev Emitted when a peg-out transaction expires - * @param requestId The unique identifier of the expired peg-out request - */ - event PegOutTransactionExpired(bytes32 requestId); - - /** - * @dev Emitted when the bridge fee is updated - * @param newFee The new bridge fee - */ - event BridgeFeeUpdated(uint256 newFee); - - /** - * @dev Emitted when the deposit limit is updated - * @param newLimit The new deposit limit - */ - event DepositLimitUpdated(uint256 newLimit); - - /** - * @dev Emitted when the withdrawal limit is updated - * @param newLimit The new withdrawal limit - */ - event WithdrawalLimitUpdated(uint256 newLimit); - - /** - * @dev Emitted when a peg-out is processed - * @param requestId The unique identifier of the processed peg-out request - * @param btcTxTag The Bitcoin transaction tag associated with the peg-out - */ - event PegOutProcessed(bytes32 indexed requestId, bytes32 btcTxTag); - - /** - * @dev Emitted when a peg-out request status is updated - * @param requestId The unique identifier of the peg-out request - * @param newStatus The new status of the peg-out request - */ - event PegOutRequestStatusUpdated(bytes32 indexed requestId, TxStatus newStatus); - - // Errors - /** - * @dev Thrown when an unauthorized witness attempts an action - */ - error UnauthorizedWitness(); - - /** - * @dev Thrown when registering a client chain to Exocore fails - * @param clientChainId The ID of the client chain that failed to register - */ - error RegisterClientChainToExocoreFailed(uint32 clientChainId); - - /** - * @dev Thrown when a zero address is provided where it's not allowed - */ - error ZeroAddressNotAllowed(); - - /** - * @dev Thrown when attempting to process a Bitcoin transaction that has already been processed - */ - error BtcTxAlreadyProcessed(); - - /** - * @dev Thrown when a Bitcoin address is not registered - */ - error BtcAddressNotRegistered(); - - /** - * @dev Thrown when trying to process a request with an invalid status - * @param requestId The ID of the request with the invalid status - */ - error InvalidRequestStatus(bytes32 requestId); - - /** - * @dev Thrown when the requested peg-out does not exist - * @param requestId The ID of the non-existent request - */ - error RequestNotFound(bytes32 requestId); - - /** - * @dev Thrown when attempting to create a request that already exists - * @param requestId The ID of the existing request - */ - error RequestAlreadyExists(bytes32 requestId); - - /** - * @dev Thrown when a deposit operation fails - * @param btcTxTag The Bitcoin transaction tag of the failed deposit - */ - error DepositFailed(bytes btcTxTag); - - /** - * @dev Thrown when a principal withdrawal operation fails - */ - error WithdrawPrincipalFailed(); - - /** - * @dev Thrown when a reward withdrawal operation fails - */ - error WithdrawRewardFailed(); - - /** - * @dev Thrown when a delegation operation fails - */ - error DelegationFailed(); - - /** - * @dev Thrown when an undelegation operation fails - */ - error UndelegationFailed(); - - /** - * @dev Thrown when an Ether transfer fails - */ - error EtherTransferFailed(); - - /** - * @dev Thrown when an invalid signature is provided - */ - error InvalidSignature(); - - /** - * @dev Thrown when an unexpected inbound nonce is encountered - * @param expectedNonce The expected nonce - * @param actualNonce The actual nonce received - */ - error UnexpectedInboundNonce(uint64 expectedNonce, uint64 actualNonce); - - /** - * @dev Modifier to check if a token is whitelisted - * @param token The address of the token to check - */ - modifier isTokenWhitelisted(address token) { - require(isWhitelistedToken[token], "ExocoreBtcGatewayStorage: token is not whitelisted"); - _; - } - - /** - * @dev Modifier to check if an amount is valid - * @param amount The amount to check - */ - modifier isValidAmount(uint256 amount) { - require(amount > 0, "ExocoreBtcGatewayStorage: amount should be greater than zero"); - _; - } - - /** - * @dev Internal function to verify and update the inbound bytes nonce - * @param srcChainId The source chain ID - * @param srcAddress The source address - * @param nonce The nonce to verify - */ - function _verifyAndUpdateBytesNonce(uint32 srcChainId, bytes memory srcAddress, uint64 nonce) internal { - uint64 expectedNonce = inboundBytesNonce[srcChainId][srcAddress] + 1; - if (nonce != expectedNonce) { - revert UnexpectedInboundNonce(expectedNonce, nonce); - } - inboundBytesNonce[srcChainId][srcAddress] = nonce; - } - - uint256[40] private __gap; - -} diff --git a/src/storage/UTXOGatewayStorage.sol b/src/storage/UTXOGatewayStorage.sol new file mode 100644 index 00000000..2531f975 --- /dev/null +++ b/src/storage/UTXOGatewayStorage.sol @@ -0,0 +1,527 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import {Errors} from "../libraries/Errors.sol"; + +/** + * @title UTXOGatewayStorage + * @dev This contract manages the storage for the UTXO gateway + */ +contract UTXOGatewayStorage { + + /** + * @notice Enum to represent the type of supported token + * @dev Each field should be matched with the corresponding field of ClientChainID + */ + enum Token { + None, // 0: Invalid/uninitialized token + BTC // 1: Bitcoin token, matches with ClientChainID.Bitcoin + + } + + /** + * @notice Enum to represent the supported client chain ID + * @dev Each field should be matched with the corresponding field of Token + */ + enum ClientChainID { + None, // 0: Invalid/uninitialized chain + Bitcoin // 1: Bitcoin chain, matches with Token.BTC + + } + + /** + * @dev Enum to represent the status of a transaction + */ + enum TxStatus { + NotStartedOrProcessed, // 0: Default state - transaction hasn't started collecting proofs + Pending, // 1: Currently collecting witness proofs + Expired // 2: Failed due to timeout, but can be retried + + } + + /** + * @dev Enum to represent the WithdrawType + */ + enum WithdrawType { + Undefined, + WithdrawPrincipal, + WithdrawReward + } + + /** + * @dev Struct to store stake message information + * @param clientChainId The client chain ID + * @param clientAddress The client chain address + * @param exocoreAddress The Exocore address + * @param operator The operator + * @param amount The amount + * @param nonce The nonce + * @param txTag The tx tag + */ + struct StakeMsg { + ClientChainID clientChainId; + bytes clientAddress; + address exocoreAddress; + string operator; + uint256 amount; + uint64 nonce; + bytes txTag; + } + + /** + * @dev Struct to store proof information + */ + struct Proof { + address witness; + StakeMsg message; + uint256 timestamp; + bytes signature; + } + + /** + * @dev Struct to store transaction information + */ + struct Transaction { + TxStatus status; + uint256 proofCount; + uint256 expiryTime; + mapping(address => uint256) witnessTime; + StakeMsg stakeMsg; + } + + /** + * @dev Struct for peg-out requests + */ + struct PegOutRequest { + ClientChainID clientChainId; + uint64 nonce; + address requester; + bytes clientAddress; + uint256 amount; + WithdrawType withdrawType; + } + + /* -------------------------------------------------------------------------- */ + /* Constants */ + /* -------------------------------------------------------------------------- */ + /// @notice the human readable prefix for Exocore bech32 encoded address. + bytes public constant EXO_ADDRESS_PREFIX = bytes("exo1"); + + // the virtual chain id for Bitcoin, compatible with other chain ids(endpoint ids) maintained by layerzero + string public constant BITCOIN_NAME = "Bitcoin"; + string public constant BITCOIN_METADATA = "Bitcoin"; + string public constant BITCOIN_SIGNATURE_SCHEME = "ECDSA"; + uint8 public constant STAKER_ACCOUNT_LENGTH = 20; + + // virtual token address and token, shared for tokens supported by the gateway + address public constant VIRTUAL_TOKEN_ADDRESS = 0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB; + bytes public constant VIRTUAL_TOKEN = abi.encodePacked(bytes32(bytes20(VIRTUAL_TOKEN_ADDRESS))); + + uint8 public constant BTC_DECIMALS = 8; + string public constant BTC_NAME = "BTC"; + string public constant BTC_METADATA = "BTC"; + string public constant BTC_ORACLE_INFO = "BTC,BITCOIN,8"; + + uint256 public constant PROOF_TIMEOUT = 1 days; + uint256 public bridgeFeeRate; // e.g., 100 (basis points) means 1% + uint256 public constant BASIS_POINTS = 10_000; // 100% = 10000 basis points + uint256 public constant MAX_BRIDGE_FEE_RATE = 1000; // 10% + + // Add min/max bounds for safety + uint256 public constant MIN_REQUIRED_PROOFS = 1; + uint256 public constant MAX_REQUIRED_PROOFS = 10; + + /// @notice The number of proofs required for consensus + uint256 public requiredProofs; + + /// @notice The count of authorized witnesses + uint256 public authorizedWitnessCount; + + /** + * @dev Mapping to store transaction information, key is the message hash + */ + mapping(bytes32 => Transaction) public transactions; + + /** + * @dev Mapping to store processed transactions + */ + mapping(bytes32 => bool) public processedTransactions; + + /** + * @dev Mapping to store processed ClientChain transactions + */ + mapping(ClientChainID => mapping(bytes => bool)) public processedClientChainTxs; + + /** + * @dev Mapping to store peg-out requests + * @dev Key1: ClientChainID + * @dev Key2: nonce + * @dev Value: PegOutRequest + */ + mapping(ClientChainID => mapping(uint64 => PegOutRequest)) public pegOutRequests; + + /** + * @dev Mapping to store authorized witnesses + */ + mapping(address => bool) public authorizedWitnesses; + + /** + * @dev Maps client chain addresses to their registered Exocore addresses + * @dev Key1: Client chain ID (Bitcoin, etc.) + * @dev Key2: Client chain address in bytes + * @dev Value: Registered Exocore address + */ + mapping(ClientChainID => mapping(bytes => address)) public inboundRegistry; + + /** + * @dev Maps Exocore addresses to their registered client chain addresses + * @dev Key1: Client chain ID (Bitcoin, etc.) + * @dev Key2: Exocore address + * @dev Value: Registered client chain address in bytes + */ + mapping(ClientChainID => mapping(address => bytes)) public outboundRegistry; + + /** + * @dev Mapping to store inbound nonce for each chain + */ + mapping(ClientChainID => uint64) public inboundNonce; + + /** + * @notice Mapping to store outbound nonce for each chain + */ + mapping(ClientChainID => uint64) public outboundNonce; + + /** + * @notice Mapping to store peg-out nonce for each chain + */ + mapping(ClientChainID => uint64) public pegOutNonce; + + /** + * @notice Mapping to store delegation nonce for each chain + * @dev The nonce is incremented for each delegate/undelegate operation + * @dev The nonce is provided to the precompile as operation id + */ + mapping(ClientChainID => uint64) public delegationNonce; + + uint256[40] private __gap; + + // Events + + /** + * @dev Emitted when the required proofs is updated + * @param oldRequired The old required proofs + * @param newRequired The new required proofs + */ + event RequiredProofsUpdated(uint256 oldRequired, uint256 newRequired); + + /** + * @dev Emitted when a stake message is executed + * @param clientChainId The chain ID of the client chain, should not violate the layerzero chain id + * @param nonce The nonce of the stake message + * @param exocoreAddress The Exocore address of the depositor + * @param amount The amount deposited(delegated) + */ + event StakeMsgExecuted( + ClientChainID indexed clientChainId, uint64 nonce, address indexed exocoreAddress, uint256 amount + ); + + /** + * @dev Emitted when a transaction is processed + * @param txId The hash of the stake message + */ + event TransactionProcessed(bytes32 indexed txId); + + /** + * @dev Emitted when a deposit is completed + * @param clientChainId The client chain ID + * @param txTag The txid + vout-index + * @param depositorExoAddr The depositor's Exocore address + * @param depositorClientChainAddr The depositor's client chain address + * @param amount The amount deposited + * @param updatedBalance The updated balance after deposit + */ + event DepositCompleted( + ClientChainID indexed clientChainId, + bytes txTag, + address indexed depositorExoAddr, + bytes depositorClientChainAddr, + uint256 amount, + uint256 updatedBalance + ); + + /** + * @dev Emitted when a principal withdrawal is requested + * @param requestId The unique identifier for the withdrawal request + * @param clientChainId The client chain ID + * @param withdrawerExoAddr The withdrawer's Exocore address + * @param withdrawerClientChainAddr The withdrawer's client chain address + * @param amount The amount to withdraw + * @param updatedBalance The updated balance after withdrawal request + */ + event WithdrawPrincipalRequested( + ClientChainID indexed clientChainId, + uint64 indexed requestId, + address indexed withdrawerExoAddr, + bytes withdrawerClientChainAddr, + uint256 amount, + uint256 updatedBalance + ); + + /** + * @dev Emitted when a reward withdrawal is requested + * @param requestId The unique identifier for the withdrawal request + * @param clientChainId The client chain ID + * @param withdrawerExoAddr The withdrawer's Exocore address + * @param withdrawerClientChainAddr The withdrawer's client chain address + * @param amount The amount to withdraw + * @param updatedBalance The updated balance after withdrawal request + */ + event WithdrawRewardRequested( + ClientChainID indexed clientChainId, + uint64 indexed requestId, + address indexed withdrawerExoAddr, + bytes withdrawerClientChainAddr, + uint256 amount, + uint256 updatedBalance + ); + + /** + * @dev Emitted when a principal withdrawal is completed + * @param clientChainId The client chain ID + * @param requestId The unique identifier for the withdrawal request + * @param withdrawerExoAddr The withdrawer's Exocore address + * @param withdrawerClientChainAddr The withdrawer's client chain address + * @param amount The amount withdrawn + * @param updatedBalance The updated balance after withdrawal + */ + event WithdrawPrincipalCompleted( + ClientChainID indexed clientChainId, + bytes32 indexed requestId, + address indexed withdrawerExoAddr, + bytes withdrawerClientChainAddr, + uint256 amount, + uint256 updatedBalance + ); + + /** + * @dev Emitted when a reward withdrawal is completed + * @param clientChainId The client chain ID + * @param requestId The unique identifier for the withdrawal request + * @param withdrawerExoAddr The withdrawer's Exocore address + * @param withdrawerClientChainAddr The withdrawer's client chain address + * @param amount The amount withdrawn + * @param updatedBalance The updated balance after withdrawal + */ + event WithdrawRewardCompleted( + ClientChainID indexed clientChainId, + bytes32 indexed requestId, + address indexed withdrawerExoAddr, + bytes withdrawerClientChainAddr, + uint256 amount, + uint256 updatedBalance + ); + + /** + * @dev Emitted when a delegation is completed + * @param clientChainId The chain ID of the client chain, should not violate the layerzero chain id + * @param exoDelegator The delegator's Exocore address + * @param operator The operator's address + * @param amount The amount delegated + */ + event DelegationCompleted( + ClientChainID indexed clientChainId, address indexed exoDelegator, string operator, uint256 amount + ); + + /** + * @dev Emitted when a delegation fails for a stake message + * @param clientChainId The chain ID of the client chain, should not violate the layerzero chain id + * @param exoDelegator The delegator's Exocore address + * @param operator The operator's address + * @param amount The amount delegated + */ + event DelegationFailedForStake( + ClientChainID indexed clientChainId, address indexed exoDelegator, string operator, uint256 amount + ); + + /** + * @dev Emitted when an undelegation is completed + * @param clientChainId The chain ID of the client chain, should not violate the layerzero chain id + * @param exoDelegator The delegator's Exocore address + * @param operator The operator's address + * @param amount The amount undelegated + */ + event UndelegationCompleted( + ClientChainID indexed clientChainId, address indexed exoDelegator, string operator, uint256 amount + ); + + /** + * @dev Emitted when an address is registered + * @param clientChainId The client chain ID + * @param depositor The depositor's address + * @param exocoreAddress The corresponding Exocore address + */ + event AddressRegistered(ClientChainID indexed clientChainId, bytes depositor, address indexed exocoreAddress); + + /** + * @dev Emitted when a new witness is added + * @param witness The address of the added witness + */ + event WitnessAdded(address indexed witness); + + /** + * @dev Emitted when a witness is removed + * @param witness The address of the removed witness + */ + event WitnessRemoved(address indexed witness); + + /** + * @dev Emitted when a proof is submitted + * @param messageHash The hash of the stake message + * @param witness The address of the witness submitting the proof + */ + event ProofSubmitted(bytes32 indexed messageHash, address indexed witness); + + /** + * @dev Emitted when a deposit is processed + * @param txTag The txid + vout-index + * @param recipient The address of the recipient + * @param amount The amount processed + */ + event DepositProcessed(bytes txTag, address indexed recipient, uint256 amount); + + /** + * @dev Emitted when a transaction expires + * @param txid The message hash of the expired transaction + */ + event TransactionExpired(bytes32 txid); + + /** + * @dev Emitted when the bridge rate is updated + * @param newRate The new bridge rate + */ + event BridgeFeeRateUpdated(uint256 newRate); + + /** + * @dev Emitted when the deposit limit is updated + * @param newLimit The new deposit limit + */ + event DepositLimitUpdated(uint256 newLimit); + + /** + * @dev Emitted when the withdrawal limit is updated + * @param newLimit The new withdrawal limit + */ + event WithdrawalLimitUpdated(uint256 newLimit); + + /** + * @dev Emitted when a peg-out is processed + * @param withdrawType The type of withdrawal + * @param clientChain The client chain ID + * @param nonce The nonce of the peg-out request + * @param requester The requester's address + * @param clientChainAddress The client chain address + * @param amount The amount to withdraw + */ + event PegOutProcessed( + uint8 indexed withdrawType, + ClientChainID indexed clientChain, + uint64 nonce, + address indexed requester, + bytes clientChainAddress, + uint256 amount + ); + + /** + * @dev Emitted when a peg-out request status is updated + * @param requestId The unique identifier of the peg-out request + * @param newStatus The new status of the peg-out request + */ + event PegOutRequestStatusUpdated(bytes32 indexed requestId, TxStatus newStatus); + + /// @notice Emitted upon the registration of a new client chain. + /// @param clientChainId The chain ID of the client chain. + event ClientChainRegistered(ClientChainID clientChainId); + + /// @notice Emitted upon the update of a client chain. + /// @param clientChainId The chain ID of the client chain. + event ClientChainUpdated(ClientChainID clientChainId); + + /// @notice Emitted when a token is added to the whitelist. + /// @param clientChainId The chain ID of the client chain. + /// @param token The address of the token. + event WhitelistTokenAdded(ClientChainID clientChainId, address indexed token); + + /// @notice Emitted when a token is updated in the whitelist. + /// @param clientChainId The chain ID of the client chain. + /// @param token The address of the token. + event WhitelistTokenUpdated(ClientChainID clientChainId, address indexed token); + + /// @notice Emitted when consensus is activated + /// @param requiredWitnessesCount The number of required witnesses + /// @param authorizedWitnessesCount The number of authorized witnesses + event ConsensusActivated(uint256 requiredWitnessesCount, uint256 authorizedWitnessesCount); + + /// @notice Emitted when consensus is deactivated + /// @param requiredWitnessesCount The number of required witnesses + /// @param authorizedWitnessesCount The number of authorized witnesses + event ConsensusDeactivated(uint256 requiredWitnessesCount, uint256 authorizedWitnessesCount); + + /** + * @dev Modifier to check if an amount is valid + * @param amount The amount to check + */ + modifier isValidAmount(uint256 amount) { + if (amount == 0) { + revert Errors.ZeroAmount(); + } + _; + } + + modifier isRegistered(Token token, address exocoreAddress) { + if (outboundRegistry[ClientChainID(uint8(token))][exocoreAddress].length == 0) { + revert Errors.AddressNotRegistered(); + } + _; + } + + /** + * @dev Modifier to restrict access to authorized witnesses only. + */ + modifier onlyAuthorizedWitness() { + if (!authorizedWitnesses[msg.sender]) { + revert Errors.UnauthorizedWitness(); + } + _; + } + + /// @notice Checks if the provided string is a valid Exocore address. + /// @param addressToValidate The string to check. + /// @return True if the string is valid, false otherwise. + /// @dev Since implementation of bech32 is difficult in Solidity, this function only + /// checks that the address is 42 characters long and starts with "exo1". + function isValidOperatorAddress(string calldata addressToValidate) public pure returns (bool) { + bytes memory stringBytes = bytes(addressToValidate); + if (stringBytes.length != 42) { + return false; + } + for (uint256 i = 0; i < EXO_ADDRESS_PREFIX.length; ++i) { + if (stringBytes[i] != EXO_ADDRESS_PREFIX[i]) { + return false; + } + } + + return true; + } + + /** + * @dev Internal function to verify and update the inbound bytes nonce + * @param srcChainId The source chain ID + * @param nonce The nonce to verify + */ + function _verifyInboundNonce(ClientChainID srcChainId, uint64 nonce) internal view { + if (nonce != inboundNonce[srcChainId] + 1) { + revert Errors.UnexpectedInboundNonce(inboundNonce[srcChainId] + 1, nonce); + } + } + +} diff --git a/test/foundry/unit/ExocoreBtcGateway.t.sol b/test/foundry/unit/ExocoreBtcGateway.t.sol deleted file mode 100644 index 0b3915ff..00000000 --- a/test/foundry/unit/ExocoreBtcGateway.t.sol +++ /dev/null @@ -1,419 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.19; - -import "src/core/ExocoreBtcGateway.sol"; -import "src/interfaces/precompiles/IAssets.sol"; - -import "src/interfaces/precompiles/IDelegation.sol"; -import "src/interfaces/precompiles/IReward.sol"; -import "src/libraries/SignatureVerifier.sol"; -import "src/storage/ExocoreBtcGatewayStorage.sol"; - -import "forge-std/Test.sol"; - -contract ExocoreBtcGatewayTest is ExocoreBtcGatewayStorage, Test { - - ExocoreBtcGateway internal exocoreBtcGateway; - - uint32 internal exocoreChainId = 2; - uint32 internal clientBtcChainId = 111; - - address internal validator = address(0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266); - address internal btcToken = address(0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599); - address internal delegatorAddr = address(0x70997970C51812dc3A010C7d01b50e0d17dc79C8); - bytes internal BTC_TOKEN = abi.encodePacked(bytes32(bytes20(btcToken))); - - using stdStorage for StdStorage; - - // Mock contracts - IDelegation internal mockDelegation; - IAssets internal mockAssets; - IReward internal mockClaimReward; - - function setUp() public { - // Deploy mock contracts - _bindPrecompileMocks(); - - // Deploy the main contract - exocoreBtcGateway = new ExocoreBtcGateway(); - - // Whitelist the btcToken - // Calculate the storage slot for the mapping - bytes32 whitelistedSlot = bytes32( - stdstore.target(address(exocoreBtcGateway)).sig("isWhitelistedToken(address)").with_key(btcToken).find() - ); - - // Set the storage value to true (1) - vm.store(address(exocoreBtcGateway), whitelistedSlot, bytes32(uint256(1))); - } - - function _bindPrecompileMocks() internal { - // bind precompile mock contracts code to constant precompile address so that local simulation could pass - bytes memory AssetsMockCode = vm.getDeployedCode("AssetsMock.sol"); - vm.etch(ASSETS_PRECOMPILE_ADDRESS, AssetsMockCode); - - bytes memory DelegationMockCode = vm.getDeployedCode("DelegationMock.sol"); - vm.etch(DELEGATION_PRECOMPILE_ADDRESS, DelegationMockCode); - - bytes memory WithdrawRewardMockCode = vm.getDeployedCode("RewardMock.sol"); - vm.etch(REWARD_PRECOMPILE_ADDRESS, WithdrawRewardMockCode); - } - - /** - * @notice Test the depositTo function with the first InterchainMsg. - */ - function testDepositToWithFirstMessage() public { - assertTrue(exocoreBtcGateway.isWhitelistedToken(btcToken)); - - bytes memory btcAddress = _stringToBytes("tb1pdwf5ar0kxr2sdhxw28wqhjwzynzlkdrqlgx8ju3sr02hkldqmlfspm0mmh"); - bytes memory exocoreAddress = _addressToBytes(delegatorAddr); - console.logBytes(btcAddress); - - // Get the inboundBytesNonce - uint256 nonce = exocoreBtcGateway.inboundBytesNonce(clientBtcChainId, btcAddress) + 1; - assertEq(nonce, 1, "Nonce should be 1"); - - // register address. - vm.prank(validator); - exocoreBtcGateway.registerAddress(btcAddress, exocoreAddress); - InterchainMsg memory _msg = InterchainMsg({ - srcChainID: clientBtcChainId, - dstChainID: exocoreChainId, - srcAddress: btcAddress, - dstAddress: _stringToBytes("tb1qqytgqkzvg48p700s46n57wfgaf04h7ca5m03qcschaawv9qqw2vsp67ku4"), - token: btcToken, - amount: 39_900_000_000_000, - nonce: 1, - txTag: _stringToBytes("b2c4366e29da536bd1ca5ac1790ba1d3a5e706a2b5e2674dee2678a669432ffc-3"), - payload: "0x" - }); - - bytes memory signature = - hex"aa70b655593f96d19dca3ef0bfc6602b6597a3b6253de2b709b81306a09d46867f857e8a44e64f0c1be6f4ec90a66e28401e007b7efb6fd344164af8316e1f571b"; - - // Check if the event is emitted correctly - vm.expectEmit(true, true, true, true); - emit DepositCompleted(_msg.txTag, exocoreAddress, btcToken, btcAddress, _msg.amount, 39_900_000_000_000); - - // Simulate the validator calling the depositTo function - vm.prank(validator); - exocoreBtcGateway.depositTo(_msg, signature); - } - - /** - * @notice Test the depositTo function with the second InterchainMsg. - */ - function testDepositToWithSecondMessage() public { - assertTrue(exocoreBtcGateway.isWhitelistedToken(btcToken)); - bytes memory btcAddress = _stringToBytes("tb1p43yswl96qlz9v9m6wtvv9c7s0jv7g6dktwfcuzle6nflyyhrqhpqtdacpy"); - bytes memory exocoreAddress = _addressToBytes(address(0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC)); - - console.logBytes(btcAddress); - - // Get the inboundBytesNonce - uint256 nonce = exocoreBtcGateway.inboundBytesNonce(clientBtcChainId, btcAddress) + 1; - assertEq(nonce, 1, "Nonce should be 1"); - - // register address. - vm.prank(validator); - exocoreBtcGateway.registerAddress(btcAddress, exocoreAddress); - - InterchainMsg memory _msg = InterchainMsg({ - srcChainID: clientBtcChainId, - dstChainID: exocoreChainId, - srcAddress: btcAddress, - dstAddress: _stringToBytes("tb1qqytgqkzvg48p700s46n57wfgaf04h7ca5m03qcschaawv9qqw2vsp67ku4"), - token: btcToken, - amount: 49_000_000_000_000, // 0.000049 BTC - nonce: 1, - txTag: _stringToBytes("102f5578c65f78cda5b1c4b35b58281b66c27a4929bb4f938fd15fa8f2d1c58b-1"), - payload: "0x" - }); - // This is a placeholder signature. In a real scenario, you would need to generate a valid signature. - bytes memory signature = - hex"4eb94c22acf431262f040dbb99bec5acc6b8288c61d4acbe6a8ba7969ab0cea91613579684c664cd81dd876a385c0c493646267fbbdd58f9408d784e8b8e616d1b"; - // Check if the event is emitted correctly - vm.expectEmit(true, true, true, true); - emit DepositCompleted(_msg.txTag, exocoreAddress, btcToken, btcAddress, _msg.amount, 49_000_000_000_000); - - // Simulate the validator calling the depositTo function - vm.prank(validator); - exocoreBtcGateway.depositTo(_msg, signature); - } - - function testEstimateGas() public { - bytes memory data = - hex"016322c3000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000002e000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000001800000000000000000000000002260fac5e5542a773aa44fbcfedf7c193bc2c59900000000000000000000000000000000000000000000000000002449f1539800000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000001e00000000000000000000000000000000000000000000000000000000000000260000000000000000000000000000000000000000000000000000000000000003e74623170647766356172306b787232736468787732387771686a777a796e7a6c6b6472716c6778386a753373723032686b6c64716d6c6673706d306d6d680000000000000000000000000000000000000000000000000000000000000000003e7462317171797467716b7a76673438703730307334366e35377766676166303468376361356d3033716373636861617776397171773276737036376b753400000000000000000000000000000000000000000000000000000000000000000042623263343336366532396461353336626431636135616331373930626131643361356537303661326235653236373464656532363738613636393433326666632d330000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002307800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000411b599ef9aebf5d2a65c6e8288e1e1d3fbcbe30d891a016110c5dbba48a91037f34c5b1b5cc5903b59a19ae5b58ebd3eb659deaf651b74bf4b50ca5bc22e8f7b11c00000000000000000000000000000000000000000000000000000000000000"; - - bytes memory btcAddress = _stringToBytes("tb1pdwf5ar0kxr2sdhxw28wqhjwzynzlkdrqlgx8ju3sr02hkldqmlfspm0mmh"); - bytes memory exocoreAddress = _stringToBytes("0x70997970C51812dc3A010C7d01b50e0d17dc79C8"); - // register address. - vm.prank(validator); - exocoreBtcGateway.registerAddress(btcAddress, exocoreAddress); - // Estimate gas - vm.prank(validator); - (bool success, bytes memory returnData) = - address(0x5FC8d32690cc91D4c39d9d3abcBD16989F875707).call{gas: 1_000_000}(data); - if (!success) { - // Decode revert reason - if (returnData.length > 0) { - // The call reverted with a reason or a custom error - assembly { - let returndata_size := mload(returnData) - revert(add(32, returnData), returndata_size) - } - } else { - revert("Call failed without a reason"); - } - } - } - - /** - * @notice Test the delegateTo function - */ - function testDelegateTo() public { - bytes memory delegator = _addressToBytes(delegatorAddr); - bytes memory operator = _stringToBytes("exo13hasr43vvq8v44xpzh0l6yuym4kca98f87j7ac"); - uint256 amount = 1_000_000_000; // 10 BTC - vm.expectEmit(true, true, true, true); - emit DelegationCompleted(btcToken, delegator, operator, amount); - vm.prank(delegatorAddr); - exocoreBtcGateway.delegateTo(btcToken, operator, amount); - } - - /** - * @notice Test the undelegateFrom function - */ - function testUndelegateFrom() public { - bytes memory delegator = _addressToBytes(delegatorAddr); - bytes memory operator = _stringToBytes("exo13hasr43vvq8v44xpzh0l6yuym4kca98f87j7ac"); - uint256 amount = 500_000_000; // 5 BTC - - // Use exocoreBtcGateway's interface to set the initial delegation amount - vm.prank(delegatorAddr); // Assume only the validator can call this function - exocoreBtcGateway.delegateTo(btcToken, operator, amount); - - vm.expectEmit(true, true, true, true); - emit UndelegationCompleted(btcToken, delegator, operator, amount); - - vm.prank(delegatorAddr); - exocoreBtcGateway.undelegateFrom(btcToken, operator, amount); - - // Verify that the delegation amount has not changed - vm.prank(validator); - (bool success, bytes memory data) = DELEGATION_PRECOMPILE_ADDRESS.call( - abi.encodeWithSignature( - "getDelegateAmount(address,string,uint32,address)", - delegatorAddr, - "exo13hasr43vvq8v44xpzh0l6yuym4kca98f87j7ac", - clientBtcChainId, - btcToken - ) - ); - require(success, "Low-level call failed"); - uint256 invalidAmount = 0; - uint256 retrievedAmount = abi.decode(data, (uint256)); - assertEq(retrievedAmount, invalidAmount); - } - /** - * @notice Test the withdrawPrincipal function - */ - - function testWithdrawPrincipal() public { - testDepositToWithFirstMessage(); - bytes memory btcAddress = _stringToBytes("tb1pdwf5ar0kxr2sdhxw28wqhjwzynzlkdrqlgx8ju3sr02hkldqmlfspm0mmh"); - uint256 amount = 39_900_000_000_000; - bytes32 requestId = keccak256(abi.encodePacked(btcToken, delegatorAddr, btcAddress, amount, block.number)); - vm.expectEmit(true, true, true, true); - emit WithdrawPrincipalRequested(requestId, delegatorAddr, btcToken, btcAddress, amount, 0); - vm.prank(delegatorAddr); - exocoreBtcGateway.withdrawPrincipal(btcToken, amount); - - // Retrieve and log the PegOutRequest state - ExocoreBtcGatewayStorage.PegOutRequest memory request = exocoreBtcGateway.getPegOutRequest(requestId); - - console.log("PegOutRequest status:"); - console.log("Token: ", request.token); - console.log("Requester: ", request.requester); - console.log("Amount: ", request.amount); - console.log("WithdrawType: ", uint256(request.withdrawType)); - console.log("Status: ", uint256(request.status)); - console.log("Timestamp: ", request.timestamp); - } - /** - * @notice Test the withdrawReward function - */ - - function testWithdrawRewardInsufficientBalance() public { - testDepositToWithFirstMessage(); - bytes memory btcAddress = _stringToBytes("tb1pdwf5ar0kxr2sdhxw28wqhjwzynzlkdrqlgx8ju3sr02hkldqmlfspm0mmh"); - uint256 amount = 500; - bytes32 requestId = keccak256(abi.encodePacked(btcToken, delegatorAddr, btcAddress, amount, block.number)); - vm.expectRevert("insufficient reward"); - vm.prank(delegatorAddr); - exocoreBtcGateway.withdrawReward(btcToken, amount); - - // Retrieve and log the PegOutRequest state - ExocoreBtcGatewayStorage.PegOutRequest memory request = exocoreBtcGateway.getPegOutRequest(requestId); - - console.log("PegOutRequest status:"); - console.log("Token: ", request.token); - console.log("Requester: ", request.requester); - console.log("Amount: ", request.amount); - console.log("WithdrawType: ", uint256(request.withdrawType)); - console.log("Status: ", uint256(request.status)); - console.log("Timestamp: ", request.timestamp); - } - /** - * @notice Test delegateTo with invalid token - */ - - function testDelegateToInvalidToken() public { - bytes memory delegator = _addressToBytes(delegatorAddr); - bytes memory operator = _stringToBytes("exo13hasr43vvq8v44xpzh0l6yuym4kca98f87j7ac"); - uint256 amount = 1_000_000_000; // 10 BTC - address invalidToken = address(0x1111111111111111111111111111111111111111); - vm.expectRevert("ExocoreBtcGatewayStorage: token is not whitelisted"); - vm.prank(delegatorAddr); - exocoreBtcGateway.delegateTo(invalidToken, operator, amount); - } - /** - * @notice Test undelegateFrom with invalid amount - */ - - function testUndelegateFromInvalidAmount() public { - bytes memory delegator = _addressToBytes(delegatorAddr); - bytes memory operator = _stringToBytes("exo13hasr43vvq8v44xpzh0l6yuym4kca98f87j7ac"); - uint256 invalidAmount = 0; - vm.expectRevert("ExocoreBtcGatewayStorage: amount should be greater than zero"); - vm.prank(delegatorAddr); - exocoreBtcGateway.undelegateFrom(btcToken, operator, invalidAmount); - } - /** - * @notice Test withdrawPrincipal when paused - */ - - function testWithdrawPrincipalWhenPaused() public { - bytes memory withdrawer = _addressToBytes(delegatorAddr); - uint256 amount = 300_000_000; // 3 BTC - vm.prank(exocoreBtcGateway.owner()); - exocoreBtcGateway.pause(); - vm.expectRevert("Pausable: paused"); - vm.prank(delegatorAddr); - exocoreBtcGateway.withdrawPrincipal(btcToken, amount); - } - - /** - * @notice Test depositThenDelegateTo function - */ - function testDepositThenDelegateTo() public { - bytes memory btcAddress = _stringToBytes("tb1pdwf5ar0kxr2sdhxw28wqhjwzynzlkdrqlgx8ju3sr02hkldqmlfspm0mmh"); - bytes memory exocoreAddress = _addressToBytes(delegatorAddr); - // bytes memory exocoreAddress = _stringToBytes("0x70997970C51812dc3A010C7d01b50e0d17dc79C8"); - bytes memory operator = _stringToBytes("0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC"); - // Register address - vm.prank(validator); - exocoreBtcGateway.registerAddress(btcAddress, exocoreAddress); - InterchainMsg memory _msg = InterchainMsg({ - srcChainID: clientBtcChainId, - dstChainID: exocoreChainId, - srcAddress: btcAddress, - dstAddress: _stringToBytes("tb1qqytgqkzvg48p700s46n57wfgaf04h7ca5m03qcschaawv9qqw2vsp67ku4"), - token: btcToken, - amount: 39_900_000_000_000, - nonce: 1, - txTag: _stringToBytes("b2c4366e29da536bd1ca5ac1790ba1d3a5e706a2b5e2674dee2678a669432ffc-3"), - payload: "0x" - }); - bytes memory signature = - hex"aa70b655593f96d19dca3ef0bfc6602b6597a3b6253de2b709b81306a09d46867f857e8a44e64f0c1be6f4ec90a66e28401e007b7efb6fd344164af8316e1f571b"; - vm.expectEmit(true, true, true, true); - emit DepositAndDelegationCompleted(btcToken, exocoreAddress, operator, _msg.amount, 39_900_000_000_000); - vm.prank(validator); - exocoreBtcGateway.depositThenDelegateTo(_msg, operator, signature); - } - /** - * @notice Test registerAddress function - */ - - function testRegisterAddress() public { - bytes memory btcAddress = _stringToBytes("tb1pdwf5ar0kxr2sdhxw28wqhjwzynzlkdrqlgx8ju3sr02hkldqmlfspm0mmh"); - bytes memory exocoreAddress = _stringToBytes("0x70997970C51812dc3A010C7d01b50e0d17dc79C8"); - vm.expectEmit(true, true, true, true); - emit AddressRegistered(btcAddress, exocoreAddress); - vm.prank(validator); - exocoreBtcGateway.registerAddress(btcAddress, exocoreAddress); - assertEq(exocoreBtcGateway.btcToExocoreAddress(btcAddress), exocoreAddress); - assertEq(exocoreBtcGateway.exocoreToBtcAddress(exocoreAddress), btcAddress); - } - /** - * @notice Test registerAddress with already registered addresses - */ - - function testRegisterAddressAlreadyRegistered() public { - bytes memory btcAddress = _stringToBytes("tb1pdwf5ar0kxr2sdhxw28wqhjwzynzlkdrqlgx8ju3sr02hkldqmlfspm0mmh"); - bytes memory exocoreAddress = _stringToBytes("0x70997970C51812dc3A010C7d01b50e0d17dc79C8"); - vm.prank(validator); - exocoreBtcGateway.registerAddress(btcAddress, exocoreAddress); - vm.expectRevert("Depositor address already registered"); - vm.prank(validator); - exocoreBtcGateway.registerAddress(btcAddress, exocoreAddress); - } - - /** - * @notice Helper function to create a PegOutRequest - * @return bytes32 The requestId of the created PegOutRequest - */ - function _createPegOutRequest() internal returns (bytes32) { - testDepositToWithFirstMessage(); - bytes memory btcAddress = _stringToBytes("tb1pdwf5ar0kxr2sdhxw28wqhjwzynzlkdrqlgx8ju3sr02hkldqmlfspm0mmh"); - uint256 amount = 39_900_000_000_000; - bytes32 requestId = keccak256(abi.encodePacked(btcToken, delegatorAddr, btcAddress, amount, block.number)); - vm.expectEmit(true, true, true, true); - emit WithdrawPrincipalRequested(requestId, delegatorAddr, btcToken, btcAddress, amount, 0); - vm.prank(delegatorAddr); - exocoreBtcGateway.withdrawPrincipal(btcToken, amount); - return requestId; - } - - /** - * @notice Test successful status update for a PegOutRequest - * @dev This test creates a PegOutRequest, updates its status, and verifies the update - */ - function testSetPegOutRequestStatusSuccess() public { - bytes32 requestId = _createPegOutRequest(); - vm.prank(validator); - vm.expectEmit(true, true, true, true); - emit PegOutRequestStatusUpdated(requestId, ExocoreBtcGatewayStorage.TxStatus.Processed); - exocoreBtcGateway.setPegOutRequestStatus(requestId, ExocoreBtcGatewayStorage.TxStatus.Processed); - - ExocoreBtcGatewayStorage.PegOutRequest memory request = exocoreBtcGateway.getPegOutRequest(requestId); - assertEq( - uint256(request.status), - uint256(ExocoreBtcGatewayStorage.TxStatus.Processed), - "Status was not updated correctly" - ); - } - - /** - * @notice Test pause and unpause functions - */ - function testPauseUnpause() public { - vm.prank(exocoreBtcGateway.owner()); - exocoreBtcGateway.pause(); - assertTrue(exocoreBtcGateway.paused()); - vm.prank(exocoreBtcGateway.owner()); - exocoreBtcGateway.unpause(); - assertFalse(exocoreBtcGateway.paused()); - } - - // Helper function to convert string to bytes - function _stringToBytes(string memory source) internal pure returns (bytes memory) { - return abi.encodePacked(source); - } - - function _addressToBytes(address _addr) internal pure returns (bytes memory) { - return abi.encodePacked(bytes32(bytes20(_addr))); - } - -} diff --git a/test/foundry/unit/UTXOGateway.t.sol b/test/foundry/unit/UTXOGateway.t.sol new file mode 100644 index 00000000..0523c1d2 --- /dev/null +++ b/test/foundry/unit/UTXOGateway.t.sol @@ -0,0 +1,1818 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import "forge-std/Test.sol"; +import {UTXOGateway} from "src/core/UTXOGateway.sol"; + +import "src/interfaces/precompiles/IAssets.sol"; +import "src/interfaces/precompiles/IDelegation.sol"; +import "src/interfaces/precompiles/IReward.sol"; +import {Errors} from "src/libraries/Errors.sol"; + +import {ExocoreBytes} from "src/libraries/ExocoreBytes.sol"; +import {SignatureVerifier} from "src/libraries/SignatureVerifier.sol"; +import {UTXOGatewayStorage} from "src/storage/UTXOGatewayStorage.sol"; +import "test/mocks/AssetsMock.sol"; +import "test/mocks/DelegationMock.sol"; +import "test/mocks/RewardMock.sol"; + +import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; + +contract UTXOGatewayTest is Test { + + using stdStorage for StdStorage; + using SignatureVerifier for bytes32; + using ExocoreBytes for address; + + struct Player { + uint256 privateKey; + address addr; + } + + UTXOGateway gateway; + UTXOGateway gatewayLogic; + address owner; + address user; + address relayer; + Player[3] witnesses; + bytes btcAddress; + string operator; + + address public constant EXOCORE_WITNESS = address(0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266); + + // chain id from layerzero, virtual for bitcoin since it's not yet a layerzero chain + string public constant BITCOIN_NAME = "Bitcoin"; + string public constant BITCOIN_METADATA = "Bitcoin"; + string public constant BITCOIN_SIGNATURE_SCHEME = "ECDSA"; + uint8 public constant STAKER_ACCOUNT_LENGTH = 20; + + // virtual token address and token, shared for tokens supported by the gateway + address public constant VIRTUAL_TOKEN_ADDRESS = 0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB; + bytes public constant VIRTUAL_TOKEN = abi.encodePacked(bytes32(bytes20(VIRTUAL_TOKEN_ADDRESS))); + + uint8 public constant BTC_DECIMALS = 8; + string public constant BTC_NAME = "BTC"; + string public constant BTC_METADATA = "BTC"; + string public constant BTC_ORACLE_INFO = "BTC,BITCOIN,8"; + + uint256 public initialRequiredProofs = 3; + uint256 public constant PROOF_TIMEOUT = 1 days; + + event WitnessAdded(address indexed witness); + event WitnessRemoved(address indexed witness); + event AddressRegistered( + UTXOGatewayStorage.ClientChainID indexed chainId, bytes depositor, address indexed exocoreAddress + ); + event DepositCompleted( + UTXOGatewayStorage.ClientChainID indexed chainId, + bytes txTag, + address indexed exocoreAddress, + bytes srcAddress, + uint256 amount, + uint256 updatedBalance + ); + event DelegationCompleted( + UTXOGatewayStorage.ClientChainID indexed chainId, address indexed delegator, string operator, uint256 amount + ); + event UndelegationCompleted( + UTXOGatewayStorage.ClientChainID indexed clientChainId, + address indexed exoDelegator, + string operator, + uint256 amount + ); + event ProofSubmitted(bytes32 indexed messageHash, address indexed witness); + event StakeMsgExecuted(bytes32 indexed txId); + event BridgeFeeRateUpdated(uint256 newRate); + + event ClientChainRegistered(UTXOGatewayStorage.ClientChainID clientChainId); + event ClientChainUpdated(UTXOGatewayStorage.ClientChainID clientChainId); + event WhitelistTokenAdded(UTXOGatewayStorage.ClientChainID clientChainId, address indexed token); + event WhitelistTokenUpdated(UTXOGatewayStorage.ClientChainID clientChainId, address indexed token); + event DelegationFailedForStake( + UTXOGatewayStorage.ClientChainID indexed clientChainId, + address indexed exoDelegator, + string operator, + uint256 amount + ); + event StakeMsgExecuted( + UTXOGatewayStorage.ClientChainID indexed chainId, uint64 nonce, address indexed exocoreAddress, uint256 amount + ); + event TransactionProcessed(bytes32 indexed txId); + + event WithdrawPrincipalRequested( + UTXOGatewayStorage.ClientChainID indexed srcChainId, + uint64 indexed requestId, + address indexed withdrawerExoAddr, + bytes withdrawerClientChainAddr, + uint256 amount, + uint256 updatedBalance + ); + event WithdrawRewardRequested( + UTXOGatewayStorage.ClientChainID indexed srcChainId, + uint64 indexed requestId, + address indexed withdrawerExoAddr, + bytes withdrawerClientChainAddr, + uint256 amount, + uint256 updatedBalance + ); + event PegOutProcessed( + uint8 indexed withdrawType, + UTXOGatewayStorage.ClientChainID indexed clientChain, + uint64 nonce, + address indexed requester, + bytes clientChainAddress, + uint256 amount + ); + + event ConsensusActivated(uint256 requiredProofs, uint256 authorizedWitnessCount); + event ConsensusDeactivated(uint256 requiredProofs, uint256 authorizedWitnessCount); + event RequiredProofsUpdated(uint256 oldRequired, uint256 newRequired); + + function setUp() public { + owner = address(1); + user = address(2); + relayer = address(3); + witnesses[0] = Player({privateKey: 0xa, addr: vm.addr(0xa)}); + witnesses[1] = Player({privateKey: 0xb, addr: vm.addr(0xb)}); + witnesses[2] = Player({privateKey: 0xc, addr: vm.addr(0xc)}); + + btcAddress = bytes("1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa"); + operator = "exo13hasr43vvq8v44xpzh0l6yuym4kca98f87j7ac"; + + // Deploy and initialize gateway + gatewayLogic = new UTXOGateway(); + gateway = UTXOGateway(address(new TransparentUpgradeableProxy(address(gatewayLogic), address(0xab), ""))); + address[] memory initialWitnesses = new address[](1); + initialWitnesses[0] = witnesses[0].addr; + gateway.initialize(owner, initialWitnesses, initialRequiredProofs); + } + + function test_initialize() public { + assertEq(gateway.owner(), owner); + assertTrue(gateway.authorizedWitnesses(witnesses[0].addr)); + assertEq(gateway.authorizedWitnessCount(), 1); + assertEq(gateway.requiredProofs(), initialRequiredProofs); + assertFalse(gateway.isConsensusRequired()); + } + + function test_UpdateRequiredProofs_Success() public { + uint256 oldRequiredProofs = gateway.requiredProofs(); + + vm.prank(owner); + vm.expectEmit(true, true, true, true); + emit RequiredProofsUpdated(oldRequiredProofs, 2); + gateway.updateRequiredProofs(2); + + assertEq(gateway.requiredProofs(), 2); + } + + function test_UpdateRequiredProofs_ConsensusStateChange() public { + // Initially consensus should be inactive (1 witnesses < 3 required) + assertFalse(gateway.isConsensusRequired()); + uint256 oldRequiredProofs = gateway.requiredProofs(); + uint256 witnessCount = gateway.authorizedWitnessCount(); + + // Lower required proofs to 1, should activate consensus + vm.prank(owner); + vm.expectEmit(true, true, true, true); + emit RequiredProofsUpdated(oldRequiredProofs, witnessCount); + vm.expectEmit(true, true, true, true); + emit ConsensusActivated(witnessCount, witnessCount); + gateway.updateRequiredProofs(witnessCount); + + assertTrue(gateway.isConsensusRequired()); + } + + function test_UpdateRequiredProofs_RevertInvalidValue() public { + vm.startPrank(owner); + vm.expectRevert(Errors.InvalidRequiredProofs.selector); + gateway.updateRequiredProofs(0); // Below minimum + + vm.expectRevert(Errors.InvalidRequiredProofs.selector); + gateway.updateRequiredProofs(11); // Above maximum + vm.stopPrank(); + } + + function test_UpdateRequiredProofs_RevertNotOwner() public { + vm.prank(user); + vm.expectRevert("Ownable: caller is not the owner"); + gateway.updateRequiredProofs(2); + } + + function test_UpdateRequiredProofs_RevertWhenPaused() public { + vm.startPrank(owner); + gateway.pause(); + + vm.expectRevert("Pausable: paused"); + gateway.updateRequiredProofs(2); + vm.stopPrank(); + } + + function test_AddWitnesses_Success() public { + vm.prank(owner); + + vm.expectEmit(true, false, false, false); + emit WitnessAdded(witnesses[1].addr); + + address[] memory witnessesToAdd = new address[](1); + witnessesToAdd[0] = witnesses[1].addr; + gateway.addWitnesses(witnessesToAdd); + assertTrue(gateway.authorizedWitnesses(witnesses[1].addr)); + assertEq(gateway.authorizedWitnessCount(), 2); + } + + function test_AddWitnesses_RevertNotOwner() public { + address[] memory witnessesToAdd = new address[](1); + witnessesToAdd[0] = witnesses[1].addr; + + vm.prank(user); + vm.expectRevert("Ownable: caller is not the owner"); + gateway.addWitnesses(witnessesToAdd); + } + + function test_AddWitnesses_RevertZeroAddress() public { + address[] memory witnessesToAdd = new address[](1); + witnessesToAdd[0] = address(0); + + vm.prank(owner); + vm.expectRevert(Errors.ZeroAddress.selector); + gateway.addWitnesses(witnessesToAdd); + } + + function test_AddWitnesses_RevertAlreadyAuthorized() public { + address[] memory witnessesToAdd = new address[](1); + witnessesToAdd[0] = witnesses[1].addr; + + vm.startPrank(owner); + gateway.addWitnesses(witnessesToAdd); + + // Try to add the same witness again + vm.expectRevert(abi.encodeWithSelector(Errors.WitnessAlreadyAuthorized.selector, witnesses[1].addr)); + gateway.addWitnesses(witnessesToAdd); + vm.stopPrank(); + } + + function test_AddWitnesses_RevertWhenPaused() public { + address[] memory witnessesToAdd = new address[](1); + witnessesToAdd[0] = witnesses[1].addr; + + vm.startPrank(owner); + gateway.pause(); + + vm.expectRevert("Pausable: paused"); + gateway.addWitnesses(witnessesToAdd); + vm.stopPrank(); + } + + function test_AddWitnesses_ConsensusActivation() public { + // initially we have 1 witness, and required proofs is 3 + assertEq(gateway.authorizedWitnessCount(), 1); + assertEq(gateway.requiredProofs(), 3); + assertFalse(gateway.isConsensusRequired()); + + address[] memory witnessesToAdd = new address[](1); + + vm.startPrank(owner); + // Add second witness - no consensus event + witnessesToAdd[0] = witnesses[1].addr; + gateway.addWitnesses(witnessesToAdd); + + // Add third witness - should emit ConsensusActivated + witnessesToAdd[0] = witnesses[2].addr; + vm.expectEmit(true, true, true, true); + emit ConsensusActivated(gateway.requiredProofs(), gateway.authorizedWitnessCount() + 1); + gateway.addWitnesses(witnessesToAdd); + + // Add fourth witness - no consensus event + witnessesToAdd[0] = address(0xaa); + gateway.addWitnesses(witnessesToAdd); + + vm.stopPrank(); + } + + function test_RemoveWitnesses() public { + vm.startPrank(owner); + address[] memory witnessesToAdd = new address[](1); + + // we need to add a witness before removing the first witness, since we cannot remove the last witness + witnessesToAdd[0] = witnesses[1].addr; + gateway.addWitnesses(witnessesToAdd); + assertTrue(gateway.authorizedWitnesses(witnesses[1].addr)); + assertEq(gateway.authorizedWitnessCount(), 2); + + vm.expectEmit(true, false, false, false); + emit WitnessRemoved(witnesses[0].addr); + + address[] memory witnessesToRemove = new address[](1); + witnessesToRemove[0] = witnesses[0].addr; + gateway.removeWitnesses(witnessesToRemove); + assertFalse(gateway.authorizedWitnesses(witnesses[0].addr)); + assertEq(gateway.authorizedWitnessCount(), 1); + } + + function test_RemoveWitnesses_RevertNotOwner() public { + address[] memory witnessesToRemove = new address[](1); + witnessesToRemove[0] = witnesses[0].addr; + + vm.prank(user); + vm.expectRevert("Ownable: caller is not the owner"); + gateway.removeWitnesses(witnessesToRemove); + } + + function test_RemoveWitnesses_RevertWitnessNotAuthorized() public { + // first add another witness to make total witnesses count 2 + address[] memory witnessesToAdd = new address[](1); + witnessesToAdd[0] = witnesses[1].addr; + vm.startPrank(owner); + gateway.addWitnesses(witnessesToAdd); + + // try to remove the unauthorized one + address[] memory witnessesToRemove = new address[](1); + witnessesToRemove[0] = witnesses[2].addr; + vm.expectRevert(abi.encodeWithSelector(Errors.WitnessNotAuthorized.selector, witnesses[2].addr)); + gateway.removeWitnesses(witnessesToRemove); + vm.stopPrank(); + } + + function test_RemoveWitnesses_RevertWhenPaused() public { + address[] memory witnessesToRemove = new address[](1); + witnessesToRemove[0] = witnesses[0].addr; + + vm.startPrank(owner); + gateway.pause(); + + vm.expectRevert("Pausable: paused"); + gateway.removeWitnesses(witnessesToRemove); + vm.stopPrank(); + } + + function test_RemoveWitnesses_CannotRemoveLastWitness() public { + // there should be only one witness added + assertEq(gateway.authorizedWitnessCount(), 1); + + address[] memory witnessesToRemove = new address[](1); + witnessesToRemove[0] = witnesses[0].addr; + + vm.startPrank(owner); + // Try to remove the hardcoded witness + vm.expectRevert(Errors.CannotRemoveLastWitness.selector); + gateway.removeWitnesses(witnessesToRemove); + vm.stopPrank(); + } + + function test_RemoveWitnesses_MultipleRemovals() public { + vm.startPrank(owner); + + // First add another 2 witnesses + address[] memory witnessesToAdd = new address[](2); + witnessesToAdd[0] = witnesses[1].addr; + witnessesToAdd[1] = witnesses[2].addr; + gateway.addWitnesses(witnessesToAdd); + assertTrue(gateway.authorizedWitnesses(witnesses[1].addr)); + assertTrue(gateway.authorizedWitnesses(witnesses[2].addr)); + assertEq(gateway.authorizedWitnessCount(), 3); + + // Remove first witness + address[] memory witnessesToRemove = new address[](1); + witnessesToRemove[0] = witnesses[0].addr; + gateway.removeWitnesses(witnessesToRemove); + assertFalse(gateway.authorizedWitnesses(witnesses[0].addr)); + assertTrue(gateway.authorizedWitnesses(witnesses[1].addr)); + assertEq(gateway.authorizedWitnessCount(), 2); + + // Remove second witness + witnessesToRemove[0] = witnesses[1].addr; + gateway.removeWitnesses(witnessesToRemove); + assertFalse(gateway.authorizedWitnesses(witnesses[1].addr)); + assertEq(gateway.authorizedWitnessCount(), 1); + + vm.stopPrank(); + } + + function test_RemoveWitnesses_ConsensusDeactivation() public { + // add total 3 witnesses + _addAllWitnesses(); + + // set required proofs to 2 + vm.startPrank(owner); + gateway.updateRequiredProofs(2); + + // Remove one witness - no consensus event + address[] memory witnessesToRemove = new address[](1); + witnessesToRemove[0] = witnesses[2].addr; + gateway.removeWitnesses(witnessesToRemove); + + // Remove another witness - should emit ConsensusDeactivated + vm.expectEmit(true, true, true, true); + emit ConsensusDeactivated(gateway.requiredProofs(), gateway.authorizedWitnessCount() - 1); + witnessesToRemove[0] = witnesses[1].addr; + gateway.removeWitnesses(witnessesToRemove); + vm.stopPrank(); + } + + function test_UpdateBridgeFee() public { + uint256 newFee = 500; // 5% + + vm.prank(owner); + vm.expectEmit(true, false, false, true); + emit BridgeFeeRateUpdated(newFee); + + gateway.updateBridgeFeeRate(newFee); + assertEq(gateway.bridgeFeeRate(), newFee); + } + + function test_UpdateBridgeFee_Zero() public { + vm.prank(owner); + vm.expectEmit(true, false, false, true); + emit BridgeFeeRateUpdated(0); + + gateway.updateBridgeFeeRate(0); + assertEq(gateway.bridgeFeeRate(), 0); + } + + function test_UpdateBridgeFee_MaxFee() public { + uint256 maxFee = 1000; // 10% + + vm.prank(owner); + vm.expectEmit(true, false, false, true); + emit BridgeFeeRateUpdated(maxFee); + + gateway.updateBridgeFeeRate(maxFee); + assertEq(gateway.bridgeFeeRate(), maxFee); + } + + function test_UpdateBridgeFee_RevertExceedMax() public { + vm.prank(owner); + vm.expectRevert("Fee cannot exceed max bridge fee rate"); + gateway.updateBridgeFeeRate(1001); // 10.01% + } + + function test_UpdateBridgeFee_RevertNotOwner() public { + vm.prank(user); + vm.expectRevert("Ownable: caller is not the owner"); + gateway.updateBridgeFeeRate(500); + } + + function test_UpdateBridgeFee_RevertWhenPaused() public { + vm.startPrank(owner); + gateway.pause(); + + vm.expectRevert("Pausable: paused"); + gateway.updateBridgeFeeRate(500); + vm.stopPrank(); + } + + function test_UpdateBridgeFee_MultipleFeeUpdates() public { + vm.startPrank(owner); + + // First update + uint256 firstFee = 300; + vm.expectEmit(true, false, false, true); + emit BridgeFeeRateUpdated(firstFee); + gateway.updateBridgeFeeRate(firstFee); + assertEq(gateway.bridgeFeeRate(), firstFee); + + // Second update + uint256 secondFee = 700; + vm.expectEmit(true, false, false, true); + emit BridgeFeeRateUpdated(secondFee); + gateway.updateBridgeFeeRate(secondFee); + assertEq(gateway.bridgeFeeRate(), secondFee); + + vm.stopPrank(); + } + + function test_ActivateStakingForClientChain_Success() public { + vm.startPrank(owner); + + // Mock successful chain registration + bytes memory chainRegisterCall = abi.encodeWithSelector( + IAssets.registerOrUpdateClientChain.selector, + uint32(uint8(UTXOGatewayStorage.ClientChainID.Bitcoin)), + STAKER_ACCOUNT_LENGTH, + BITCOIN_NAME, + BITCOIN_METADATA, + BITCOIN_SIGNATURE_SCHEME + ); + vm.mockCall( + ASSETS_PRECOMPILE_ADDRESS, + chainRegisterCall, + abi.encode(true, false) // success = true, updated = false (new registration) + ); + + // Mock successful token registration + bytes memory tokenRegisterCall = abi.encodeWithSelector( + IAssets.registerToken.selector, + uint32(uint8(UTXOGatewayStorage.ClientChainID.Bitcoin)), + VIRTUAL_TOKEN, + BTC_DECIMALS, + BTC_NAME, + BTC_METADATA, + BTC_ORACLE_INFO + ); + vm.mockCall( + ASSETS_PRECOMPILE_ADDRESS, + tokenRegisterCall, + abi.encode(true) // success = true + ); + + vm.expectEmit(true, false, false, false); + emit ClientChainRegistered(UTXOGatewayStorage.ClientChainID.Bitcoin); + vm.expectEmit(true, false, false, false); + emit WhitelistTokenAdded(UTXOGatewayStorage.ClientChainID.Bitcoin, VIRTUAL_TOKEN_ADDRESS); + + gateway.activateStakingForClientChain(UTXOGatewayStorage.ClientChainID.Bitcoin); + vm.stopPrank(); + } + + function test_ActivateStakingForClientChain_UpdateExisting() public { + vm.startPrank(owner); + + // Mock chain update + bytes memory chainRegisterCall = abi.encodeWithSelector( + IAssets.registerOrUpdateClientChain.selector, + uint32(uint8(UTXOGatewayStorage.ClientChainID.Bitcoin)), + STAKER_ACCOUNT_LENGTH, + BITCOIN_NAME, + BITCOIN_METADATA, + BITCOIN_SIGNATURE_SCHEME + ); + vm.mockCall( + ASSETS_PRECOMPILE_ADDRESS, + chainRegisterCall, + abi.encode(true, true) // success = true, updated = true (updating existing) + ); + + // Mock token update + bytes memory tokenRegisterCall = abi.encodeWithSelector( + IAssets.registerToken.selector, + uint32(uint8(UTXOGatewayStorage.ClientChainID.Bitcoin)), + VIRTUAL_TOKEN, + BTC_DECIMALS, + BTC_NAME, + BTC_METADATA, + BTC_ORACLE_INFO + ); + vm.mockCall( + ASSETS_PRECOMPILE_ADDRESS, + tokenRegisterCall, + abi.encode(false) // registration fails, indicating existing token + ); + + // Mock token update call + bytes memory tokenUpdateCall = abi.encodeWithSelector( + IAssets.updateToken.selector, + uint32(uint8(UTXOGatewayStorage.ClientChainID.Bitcoin)), + VIRTUAL_TOKEN, + BTC_METADATA + ); + vm.mockCall( + ASSETS_PRECOMPILE_ADDRESS, + tokenUpdateCall, + abi.encode(true) // update succeeds + ); + + vm.expectEmit(true, false, false, false); + emit ClientChainUpdated(UTXOGatewayStorage.ClientChainID.Bitcoin); + vm.expectEmit(true, false, false, false); + emit WhitelistTokenUpdated(UTXOGatewayStorage.ClientChainID.Bitcoin, VIRTUAL_TOKEN_ADDRESS); + + gateway.activateStakingForClientChain(UTXOGatewayStorage.ClientChainID.Bitcoin); + vm.stopPrank(); + } + + function test_ActivateStakingForClientChain_RevertChainRegistrationFailed() public { + vm.startPrank(owner); + + // Mock failed chain registration + bytes memory chainRegisterCall = abi.encodeWithSelector( + IAssets.registerOrUpdateClientChain.selector, + uint32(uint8(UTXOGatewayStorage.ClientChainID.Bitcoin)), + STAKER_ACCOUNT_LENGTH, + BITCOIN_NAME, + BITCOIN_METADATA, + BITCOIN_SIGNATURE_SCHEME + ); + vm.mockCall( + ASSETS_PRECOMPILE_ADDRESS, + chainRegisterCall, + abi.encode(false, false) // registration failed + ); + + vm.expectRevert( + abi.encodeWithSelector( + Errors.RegisterClientChainToExocoreFailed.selector, + uint32(uint8(UTXOGatewayStorage.ClientChainID.Bitcoin)) + ) + ); + gateway.activateStakingForClientChain(UTXOGatewayStorage.ClientChainID.Bitcoin); + vm.stopPrank(); + } + + function test_ActivateStakingForClientChain_RevertTokenRegistrationAndUpdateFailed() public { + vm.startPrank(owner); + + // Mock successful chain registration + bytes memory chainRegisterCall = abi.encodeWithSelector( + IAssets.registerOrUpdateClientChain.selector, + uint32(uint8(UTXOGatewayStorage.ClientChainID.Bitcoin)), + STAKER_ACCOUNT_LENGTH, + BITCOIN_NAME, + BITCOIN_METADATA, + BITCOIN_SIGNATURE_SCHEME + ); + vm.mockCall(ASSETS_PRECOMPILE_ADDRESS, chainRegisterCall, abi.encode(true, false)); + + // Mock failed token registration + bytes memory tokenRegisterCall = abi.encodeWithSelector( + IAssets.registerToken.selector, + uint32(uint8(UTXOGatewayStorage.ClientChainID.Bitcoin)), + VIRTUAL_TOKEN, + BTC_DECIMALS, + BTC_NAME, + BTC_METADATA, + BTC_ORACLE_INFO + ); + vm.mockCall(ASSETS_PRECOMPILE_ADDRESS, tokenRegisterCall, abi.encode(false)); + + // Mock failed token update + bytes memory tokenUpdateCall = abi.encodeWithSelector( + IAssets.updateToken.selector, + uint32(uint8(UTXOGatewayStorage.ClientChainID.Bitcoin)), + VIRTUAL_TOKEN, + BTC_METADATA + ); + vm.mockCall(ASSETS_PRECOMPILE_ADDRESS, tokenUpdateCall, abi.encode(false)); + + vm.expectRevert( + abi.encodeWithSelector( + Errors.AddWhitelistTokenFailed.selector, + uint32(uint8(UTXOGatewayStorage.ClientChainID.Bitcoin)), + bytes32(VIRTUAL_TOKEN) + ) + ); + gateway.activateStakingForClientChain(UTXOGatewayStorage.ClientChainID.Bitcoin); + vm.stopPrank(); + } + + function test_ActivateStakingForClientChain_RevertInvalidChain() public { + vm.prank(owner); + vm.expectRevert(Errors.InvalidClientChain.selector); + gateway.activateStakingForClientChain(UTXOGatewayStorage.ClientChainID.None); + } + + function test_ActivateStakingForClientChain_RevertNotOwner() public { + vm.prank(user); + vm.expectRevert("Ownable: caller is not the owner"); + gateway.activateStakingForClientChain(UTXOGatewayStorage.ClientChainID.Bitcoin); + } + + function test_ActivateStakingForClientChain_RevertWhenPaused() public { + vm.startPrank(owner); + gateway.pause(); + + vm.expectRevert("Pausable: paused"); + gateway.activateStakingForClientChain(UTXOGatewayStorage.ClientChainID.Bitcoin); + vm.stopPrank(); + } + + function test_SubmitProofForStakeMsg_Success() public { + _addAllWitnesses(); + _activateConsensus(); + + // Create stake message + UTXOGatewayStorage.StakeMsg memory stakeMsg = UTXOGatewayStorage.StakeMsg({ + clientChainId: UTXOGatewayStorage.ClientChainID.Bitcoin, + clientAddress: btcAddress, + exocoreAddress: user, + operator: operator, + amount: 1 ether, + nonce: 1, + txTag: bytes("tx1-0") + }); + + bytes32 txId = _getMessageHash(stakeMsg); + bytes memory signature = _generateSignature(stakeMsg, witnesses[0].privateKey); + + // Submit proof from first witness + vm.prank(relayer); + vm.expectEmit(true, true, false, true); + emit ProofSubmitted(txId, witnesses[0].addr); + gateway.submitProofForStakeMsg(witnesses[0].addr, stakeMsg, signature); + + // Submit proof from second witness + signature = _generateSignature(stakeMsg, witnesses[1].privateKey); + vm.prank(relayer); + vm.expectEmit(true, true, false, true); + emit ProofSubmitted(txId, witnesses[1].addr); + gateway.submitProofForStakeMsg(witnesses[1].addr, stakeMsg, signature); + + // Submit proof from thrid witness and trigger message execution as we have enough proofs + // mock Assets precompile deposit success and Delegation precompile delegate success + vm.mockCall( + ASSETS_PRECOMPILE_ADDRESS, + abi.encodeWithSelector(IAssets.depositLST.selector), + abi.encode(true, stakeMsg.amount) + ); + vm.mockCall( + DELEGATION_PRECOMPILE_ADDRESS, abi.encodeWithSelector(IDelegation.delegate.selector), abi.encode(true) + ); + + signature = _generateSignature(stakeMsg, witnesses[2].privateKey); + vm.prank(relayer); + vm.expectEmit(true, false, false, false); + emit StakeMsgExecuted(stakeMsg.clientChainId, stakeMsg.nonce, stakeMsg.exocoreAddress, stakeMsg.amount); + vm.expectEmit(true, false, false, false); + emit TransactionProcessed(txId); + gateway.submitProofForStakeMsg(witnesses[2].addr, stakeMsg, signature); + + // Verify message was processed + assertTrue(gateway.processedClientChainTxs(stakeMsg.clientChainId, stakeMsg.txTag)); + assertTrue(gateway.processedTransactions(txId)); + } + + function test_SubmitProofForStakeMsg_RevertConsensusDeactivated() public { + _addAllWitnesses(); + + // deactivate consensus for stake message by updating the value of requiredProofs + _deactivateConsensus(); + + UTXOGatewayStorage.StakeMsg memory stakeMsg = UTXOGatewayStorage.StakeMsg({ + clientChainId: UTXOGatewayStorage.ClientChainID.Bitcoin, + clientAddress: btcAddress, + exocoreAddress: user, + operator: operator, + amount: 1 ether, + nonce: 1, + txTag: bytes("tx1") + }); + + // First witness submits proof + bytes memory signature = _generateSignature(stakeMsg, witnesses[0].privateKey); + + vm.prank(relayer); + vm.expectRevert(abi.encodeWithSelector(Errors.ConsensusNotRequired.selector)); + gateway.submitProofForStakeMsg(witnesses[0].addr, stakeMsg, signature); + } + + function test_SubmitProofForStakeMsg_RevertInvalidSignature() public { + _addAllWitnesses(); + _activateConsensus(); + + UTXOGatewayStorage.StakeMsg memory stakeMsg = UTXOGatewayStorage.StakeMsg({ + clientChainId: UTXOGatewayStorage.ClientChainID.Bitcoin, + clientAddress: btcAddress, + exocoreAddress: user, + operator: operator, + amount: 1 ether, + nonce: 1, + txTag: bytes("tx1-0") + }); + + bytes memory invalidSignature = bytes("invalid"); + + vm.prank(relayer); + vm.expectRevert(SignatureVerifier.InvalidSignature.selector); + gateway.submitProofForStakeMsg(witnesses[0].addr, stakeMsg, invalidSignature); + } + + function test_SubmitProofForStakeMsg_RevertUnauthorizedWitness() public { + _addAllWitnesses(); + _activateConsensus(); + + UTXOGatewayStorage.StakeMsg memory stakeMsg = UTXOGatewayStorage.StakeMsg({ + clientChainId: UTXOGatewayStorage.ClientChainID.Bitcoin, + clientAddress: btcAddress, + exocoreAddress: user, + operator: operator, + amount: 1 ether, + nonce: 1, + txTag: bytes("tx1") + }); + + Player memory unauthorizedWitness = Player({privateKey: 99, addr: vm.addr(99)}); + bytes memory signature = _generateSignature(stakeMsg, unauthorizedWitness.privateKey); + + vm.prank(unauthorizedWitness.addr); + vm.expectRevert(abi.encodeWithSelector(Errors.WitnessNotAuthorized.selector, unauthorizedWitness.addr)); + gateway.submitProofForStakeMsg(unauthorizedWitness.addr, stakeMsg, signature); + } + + function test_SubmitProofForStakeMsg_ExpiredBeforeConsensus() public { + _addAllWitnesses(); + _activateConsensus(); + + UTXOGatewayStorage.StakeMsg memory stakeMsg = UTXOGatewayStorage.StakeMsg({ + clientChainId: UTXOGatewayStorage.ClientChainID.Bitcoin, + clientAddress: btcAddress, + exocoreAddress: user, + operator: operator, + amount: 1 ether, + nonce: 1, + txTag: bytes("tx1") + }); + + // Submit proofs from requiredProofs - 1 witnesses + for (uint256 i = 0; i < gateway.requiredProofs() - 1; i++) { + bytes memory signature = _generateSignature(stakeMsg, witnesses[i].privateKey); + vm.prank(relayer); + gateway.submitProofForStakeMsg(witnesses[i].addr, stakeMsg, signature); + } + + // Move time forward past expiry + vm.warp(block.timestamp + PROOF_TIMEOUT + 1); + + // Submit the last proof + bytes memory lastSignature = _generateSignature(stakeMsg, witnesses[gateway.requiredProofs() - 1].privateKey); + vm.prank(relayer); + gateway.submitProofForStakeMsg(witnesses[gateway.requiredProofs() - 1].addr, stakeMsg, lastSignature); + + // Verify transaction is restarted owing to expired and not processed + bytes32 messageHash = _getMessageHash(stakeMsg); + assertEq(uint8(gateway.getTransactionStatus(messageHash)), uint8(UTXOGatewayStorage.TxStatus.Pending)); + assertEq(gateway.getTransactionProofCount(messageHash), 1); + assertFalse(gateway.processedClientChainTxs(stakeMsg.clientChainId, stakeMsg.txTag)); + } + + function test_SubmitProofForStakeMsg_RestartExpiredTransaction() public { + _addAllWitnesses(); + _activateConsensus(); + + UTXOGatewayStorage.StakeMsg memory stakeMsg = UTXOGatewayStorage.StakeMsg({ + clientChainId: UTXOGatewayStorage.ClientChainID.Bitcoin, + clientAddress: btcAddress, + exocoreAddress: user, + operator: operator, + amount: 1 ether, + nonce: 1, + txTag: bytes("tx1") + }); + + // First witness submits proof + bytes memory signature0 = _generateSignature(stakeMsg, witnesses[0].privateKey); + vm.prank(relayer); + gateway.submitProofForStakeMsg(witnesses[0].addr, stakeMsg, signature0); + + // Move time forward past expiry + vm.warp(block.timestamp + PROOF_TIMEOUT + 1); + + // Same witness submits proof again to restart transaction + bytes memory signature0Restart = _generateSignature(stakeMsg, witnesses[0].privateKey); + vm.prank(relayer); + gateway.submitProofForStakeMsg(witnesses[0].addr, stakeMsg, signature0Restart); + + bytes32 messageHash = _getMessageHash(stakeMsg); + + // Verify transaction is restarted + assertEq(uint8(gateway.getTransactionStatus(messageHash)), uint8(UTXOGatewayStorage.TxStatus.Pending)); + assertEq(gateway.getTransactionProofCount(messageHash), 1); + assertTrue(gateway.getTransactionWitnessTime(messageHash, witnesses[0].addr) > 0); + assertFalse(gateway.processedTransactions(messageHash)); + } + + function test_SubmitProofForStakeMsg_JoinRestartedTransaction() public { + _addAllWitnesses(); + _activateConsensus(); + // afater activating consensus, required proofs should be set as 3 + assertEq(gateway.requiredProofs(), 3); + + UTXOGatewayStorage.StakeMsg memory stakeMsg = UTXOGatewayStorage.StakeMsg({ + clientChainId: UTXOGatewayStorage.ClientChainID.Bitcoin, + clientAddress: btcAddress, + exocoreAddress: user, + operator: operator, + amount: 1 ether, + nonce: 1, + txTag: bytes("tx1") + }); + + // First witness submits proof + bytes memory signature0 = _generateSignature(stakeMsg, witnesses[0].privateKey); + vm.prank(relayer); + gateway.submitProofForStakeMsg(witnesses[0].addr, stakeMsg, signature0); + + // Move time forward past expiry + vm.warp(block.timestamp + PROOF_TIMEOUT + 1); + + // Second witness restarts transaction + bytes memory signature1 = _generateSignature(stakeMsg, witnesses[1].privateKey); + vm.prank(relayer); + gateway.submitProofForStakeMsg(witnesses[1].addr, stakeMsg, signature1); + + // First witness can submit proof again in new round + // as requiredProofs is 3, the transaction should not be processed even if the first witness submits proof + bytes memory signature0New = _generateSignature(stakeMsg, witnesses[0].privateKey); + vm.prank(relayer); + gateway.submitProofForStakeMsg(witnesses[0].addr, stakeMsg, signature0New); + + bytes32 messageHash = _getMessageHash(stakeMsg); + + // Verify both witnesses' proofs are counted + assertEq(uint8(gateway.getTransactionStatus(messageHash)), uint8(UTXOGatewayStorage.TxStatus.Pending)); + assertEq(gateway.getTransactionProofCount(messageHash), 2); + assertFalse(gateway.processedTransactions(messageHash)); + assertFalse(gateway.processedClientChainTxs(stakeMsg.clientChainId, stakeMsg.txTag)); + assertTrue(gateway.getTransactionWitnessTime(messageHash, witnesses[0].addr) > 0); + assertTrue(gateway.getTransactionWitnessTime(messageHash, witnesses[1].addr) > 0); + } + + function test_SubmitProofForStakeMsg_RevertDuplicateProofInSameRound() public { + _addAllWitnesses(); + _activateConsensus(); + + UTXOGatewayStorage.StakeMsg memory stakeMsg = UTXOGatewayStorage.StakeMsg({ + clientChainId: UTXOGatewayStorage.ClientChainID.Bitcoin, + clientAddress: btcAddress, + exocoreAddress: user, + operator: operator, + amount: 1 ether, + nonce: 1, + txTag: bytes("tx1") + }); + + // First submission + bytes memory signature = _generateSignature(stakeMsg, witnesses[0].privateKey); + vm.prank(relayer); + gateway.submitProofForStakeMsg(witnesses[0].addr, stakeMsg, signature); + + // Try to submit again in same round + bytes memory signatureSecond = _generateSignature(stakeMsg, witnesses[0].privateKey); + vm.prank(relayer); + vm.expectRevert(Errors.WitnessAlreadySubmittedProof.selector); + gateway.submitProofForStakeMsg(witnesses[0].addr, stakeMsg, signatureSecond); + } + + function test_ProcessStakeMessage_RevertConsensusActivated() public { + _activateConsensus(); + + UTXOGatewayStorage.StakeMsg memory stakeMsg = UTXOGatewayStorage.StakeMsg({ + clientChainId: UTXOGatewayStorage.ClientChainID.Bitcoin, + clientAddress: btcAddress, + exocoreAddress: user, + operator: operator, + amount: 1 ether, + nonce: 1, + txTag: bytes("tx1") + }); + + bytes memory signature = _generateSignature(stakeMsg, witnesses[0].privateKey); + + vm.prank(relayer); + vm.expectRevert(Errors.ConsensusRequired.selector); + gateway.processStakeMessage(witnesses[0].addr, stakeMsg, signature); + } + + function test_ProcessStakeMessage_RegisterNewAddress() public { + _deactivateConsensus(); + + UTXOGatewayStorage.StakeMsg memory stakeMsg = UTXOGatewayStorage.StakeMsg({ + clientChainId: UTXOGatewayStorage.ClientChainID.Bitcoin, + clientAddress: btcAddress, + exocoreAddress: user, + operator: "", + amount: 1 ether, + nonce: 1, + txTag: bytes("tx1") + }); + + // mock Assets precompile deposit success and Delegation precompile delegate success + vm.mockCall( + ASSETS_PRECOMPILE_ADDRESS, + abi.encodeWithSelector(IAssets.depositLST.selector), + abi.encode(true, stakeMsg.amount) + ); + vm.mockCall( + DELEGATION_PRECOMPILE_ADDRESS, abi.encodeWithSelector(IDelegation.delegate.selector), abi.encode(true) + ); + + bytes memory signature = _generateSignature(stakeMsg, witnesses[0].privateKey); + + vm.prank(relayer); + vm.expectEmit(true, true, true, true); + emit AddressRegistered(UTXOGatewayStorage.ClientChainID.Bitcoin, btcAddress, user); + vm.expectEmit(true, true, true, true); + emit StakeMsgExecuted(UTXOGatewayStorage.ClientChainID.Bitcoin, stakeMsg.nonce, user, stakeMsg.amount); + gateway.processStakeMessage(witnesses[0].addr, stakeMsg, signature); + + // Verify address registration + assertEq(gateway.getClientAddress(UTXOGatewayStorage.ClientChainID.Bitcoin, user), btcAddress); + assertEq(gateway.getExocoreAddress(UTXOGatewayStorage.ClientChainID.Bitcoin, btcAddress), user); + } + + function test_ProcessStakeMessage_WithBridgeFee() public { + _deactivateConsensus(); + + UTXOGatewayStorage.StakeMsg memory stakeMsg = UTXOGatewayStorage.StakeMsg({ + clientChainId: UTXOGatewayStorage.ClientChainID.Bitcoin, + clientAddress: btcAddress, + exocoreAddress: user, + operator: operator, + amount: 1 ether, + nonce: 1, + txTag: bytes("tx1") + }); + + // first owner updates bridge fee + vm.prank(owner); + gateway.updateBridgeFeeRate(100); + + // then relayer submits proof and we should see the bridge fee deducted from the amount + bytes memory signature = _generateSignature(stakeMsg, witnesses[0].privateKey); + uint256 amountAfterFee = 1 ether - 1 ether * 100 / 10_000; + + // mock Assets precompile deposit success and Delegation precompile delegate success + vm.mockCall( + ASSETS_PRECOMPILE_ADDRESS, + abi.encodeWithSelector(IAssets.depositLST.selector), + abi.encode(true, amountAfterFee) + ); + vm.mockCall( + DELEGATION_PRECOMPILE_ADDRESS, abi.encodeWithSelector(IDelegation.delegate.selector), abi.encode(true) + ); + + vm.expectEmit(true, true, true, true, address(gateway)); + emit DepositCompleted( + UTXOGatewayStorage.ClientChainID.Bitcoin, + stakeMsg.txTag, + user, + stakeMsg.clientAddress, + amountAfterFee, + amountAfterFee + ); + + vm.expectEmit(true, true, true, true, address(gateway)); + emit DelegationCompleted(UTXOGatewayStorage.ClientChainID.Bitcoin, user, operator, amountAfterFee); + + vm.prank(relayer); + gateway.processStakeMessage(witnesses[0].addr, stakeMsg, signature); + } + + function test_ProcessStakeMessage_WithDelegation() public { + _deactivateConsensus(); + + UTXOGatewayStorage.StakeMsg memory stakeMsg = UTXOGatewayStorage.StakeMsg({ + clientChainId: UTXOGatewayStorage.ClientChainID.Bitcoin, + clientAddress: btcAddress, + exocoreAddress: user, + operator: operator, + amount: 1 ether, + nonce: 1, + txTag: bytes("tx1") + }); + + // mock Assets precompile deposit success and Delegation precompile delegate success + vm.mockCall( + ASSETS_PRECOMPILE_ADDRESS, + abi.encodeWithSelector(IAssets.depositLST.selector), + abi.encode(true, stakeMsg.amount) + ); + vm.mockCall( + DELEGATION_PRECOMPILE_ADDRESS, abi.encodeWithSelector(IDelegation.delegate.selector), abi.encode(true) + ); + + bytes memory signature = _generateSignature(stakeMsg, witnesses[0].privateKey); + + vm.prank(relayer); + vm.expectEmit(true, true, true, true); + emit DelegationCompleted(UTXOGatewayStorage.ClientChainID.Bitcoin, user, operator, 1 ether); + gateway.processStakeMessage(witnesses[0].addr, stakeMsg, signature); + } + + function test_ProcessStakeMessage_DelegationFailureNotRevert() public { + UTXOGatewayStorage.StakeMsg memory stakeMsg = UTXOGatewayStorage.StakeMsg({ + clientChainId: UTXOGatewayStorage.ClientChainID.Bitcoin, + clientAddress: btcAddress, + exocoreAddress: user, + operator: operator, + amount: 1 ether, + nonce: 1, + txTag: bytes("tx1") + }); + + // mock Assets precompile deposit success and Delegation precompile delegate failure + vm.mockCall( + ASSETS_PRECOMPILE_ADDRESS, + abi.encodeWithSelector(IAssets.depositLST.selector), + abi.encode(true, stakeMsg.amount) + ); + vm.mockCall( + DELEGATION_PRECOMPILE_ADDRESS, abi.encodeWithSelector(IDelegation.delegate.selector), abi.encode(false) + ); + + bytes memory signature = _generateSignature(stakeMsg, witnesses[0].privateKey); + + vm.prank(relayer); + // deposit should be successful + vm.expectEmit(true, true, true, true); + emit DepositCompleted( + UTXOGatewayStorage.ClientChainID.Bitcoin, + stakeMsg.txTag, + user, + stakeMsg.clientAddress, + 1 ether, + stakeMsg.amount + ); + + // delegation should fail + vm.expectEmit(true, true, true, true); + emit DelegationFailedForStake(UTXOGatewayStorage.ClientChainID.Bitcoin, user, operator, 1 ether); + + gateway.processStakeMessage(witnesses[0].addr, stakeMsg, signature); + } + + function test_ProcessStakeMessage_RevertOnDepositFailure() public { + _deactivateConsensus(); + + UTXOGatewayStorage.StakeMsg memory stakeMsg = UTXOGatewayStorage.StakeMsg({ + clientChainId: UTXOGatewayStorage.ClientChainID.Bitcoin, + clientAddress: btcAddress, + exocoreAddress: user, + operator: "", + amount: 1 ether, + nonce: 1, + txTag: bytes("tx1") + }); + + // mock Assets precompile deposit failure + vm.mockCall( + ASSETS_PRECOMPILE_ADDRESS, abi.encodeWithSelector(IAssets.depositLST.selector), abi.encode(false, 0) + ); + + bytes memory signature = _generateSignature(stakeMsg, witnesses[0].privateKey); + + vm.prank(relayer); + vm.expectRevert(abi.encodeWithSelector(Errors.DepositFailed.selector, bytes("tx1"))); + gateway.processStakeMessage(witnesses[0].addr, stakeMsg, signature); + } + + function test_ProcessStakeMessage_RevertWhenPaused() public { + _deactivateConsensus(); + + UTXOGatewayStorage.StakeMsg memory stakeMsg = UTXOGatewayStorage.StakeMsg({ + clientChainId: UTXOGatewayStorage.ClientChainID.Bitcoin, + clientAddress: btcAddress, + exocoreAddress: user, + operator: "", + amount: 1 ether, + nonce: 1, + txTag: bytes("tx1") + }); + + bytes memory signature = _generateSignature(stakeMsg, witnesses[0].privateKey); + + vm.prank(owner); + gateway.pause(); + + vm.prank(relayer); + vm.expectRevert("Pausable: paused"); + gateway.processStakeMessage(witnesses[0].addr, stakeMsg, signature); + } + + function test_ProcessStakeMessage_RevertUnauthorizedWitness() public { + _deactivateConsensus(); + + UTXOGatewayStorage.StakeMsg memory stakeMsg = UTXOGatewayStorage.StakeMsg({ + clientChainId: UTXOGatewayStorage.ClientChainID.Bitcoin, + clientAddress: btcAddress, + exocoreAddress: user, + operator: "", + amount: 1 ether, + nonce: 1, + txTag: bytes("tx1") + }); + + Player memory unauthorizedWitness = Player({addr: vm.addr(0x999), privateKey: 0x999}); + bytes memory signature = _generateSignature(stakeMsg, unauthorizedWitness.privateKey); + + vm.prank(unauthorizedWitness.addr); + vm.expectRevert(abi.encodeWithSelector(Errors.WitnessNotAuthorized.selector, unauthorizedWitness.addr)); + gateway.processStakeMessage(unauthorizedWitness.addr, stakeMsg, signature); + } + + function test_ProcessStakeMessage_RevertInvalidStakeMessage() public { + _deactivateConsensus(); + + // Create invalid message with all zero values + UTXOGatewayStorage.StakeMsg memory stakeMsg = UTXOGatewayStorage.StakeMsg({ + clientChainId: UTXOGatewayStorage.ClientChainID.None, + clientAddress: bytes(""), + exocoreAddress: address(0), + operator: "", + amount: 0, + nonce: 0, + txTag: bytes("") + }); + + bytes memory signature = _generateSignature(stakeMsg, witnesses[0].privateKey); + + vm.prank(relayer); + vm.expectRevert(Errors.InvalidStakeMessage.selector); + gateway.processStakeMessage(witnesses[0].addr, stakeMsg, signature); + } + + function test_ProcessStakeMessage_RevertZeroExocoreAddressBeforeRegistration() public { + _deactivateConsensus(); + + UTXOGatewayStorage.StakeMsg memory stakeMsg = UTXOGatewayStorage.StakeMsg({ + clientChainId: UTXOGatewayStorage.ClientChainID.Bitcoin, + clientAddress: btcAddress, + exocoreAddress: address(0), // Zero address + operator: "", + amount: 1 ether, + nonce: 1, + txTag: bytes("tx1") + }); + + bytes memory signature = _generateSignature(stakeMsg, witnesses[0].privateKey); + + vm.prank(relayer); + vm.expectRevert(Errors.ZeroAddress.selector); + gateway.processStakeMessage(witnesses[0].addr, stakeMsg, signature); + } + + function test_ProcessStakeMessage_RevertInvalidNonce() public { + _deactivateConsensus(); + + UTXOGatewayStorage.StakeMsg memory stakeMsg = UTXOGatewayStorage.StakeMsg({ + clientChainId: UTXOGatewayStorage.ClientChainID.Bitcoin, + clientAddress: btcAddress, + exocoreAddress: user, + operator: "", + amount: 1 ether, + nonce: gateway.nextInboundNonce(UTXOGatewayStorage.ClientChainID.Bitcoin) + 1, + txTag: bytes("tx1") + }); + + bytes memory signature = _generateSignature(stakeMsg, witnesses[0].privateKey); + + vm.prank(relayer); + vm.expectRevert( + abi.encodeWithSelector( + Errors.UnexpectedInboundNonce.selector, gateway.nextInboundNonce(stakeMsg.clientChainId), stakeMsg.nonce + ) + ); + gateway.processStakeMessage(witnesses[0].addr, stakeMsg, signature); + } + + function test_DelegateTo_Success() public { + // Setup: Register user's client chain address first + _mockRegisterAddress(user, btcAddress); + + // mock delegation precompile delegate success + vm.mockCall( + DELEGATION_PRECOMPILE_ADDRESS, abi.encodeWithSelector(IDelegation.delegate.selector), abi.encode(true) + ); + + vm.prank(user); + vm.expectEmit(true, true, true, true); + emit DelegationCompleted(UTXOGatewayStorage.ClientChainID.Bitcoin, user, operator, 1 ether); + + gateway.delegateTo(UTXOGatewayStorage.Token.BTC, operator, 1 ether); + + // Verify nonce increment + assertEq(gateway.delegationNonce(UTXOGatewayStorage.ClientChainID.Bitcoin), 1); + } + + function test_DelegateTo_RevertZeroAmount() public { + _mockRegisterAddress(user, btcAddress); + + vm.prank(user); + vm.expectRevert(Errors.ZeroAmount.selector); + gateway.delegateTo(UTXOGatewayStorage.Token.BTC, operator, 0); + } + + function test_DelegateTo_RevertWhenPaused() public { + _mockRegisterAddress(user, btcAddress); + + vm.prank(owner); + gateway.pause(); + + vm.prank(user); + vm.expectRevert("Pausable: paused"); + gateway.delegateTo(UTXOGatewayStorage.Token.BTC, operator, 1 ether); + } + + function test_DelegateTo_RevertNotRegistered() public { + // Don't register user's address + + vm.prank(user); + vm.expectRevert(Errors.AddressNotRegistered.selector); + gateway.delegateTo(UTXOGatewayStorage.Token.BTC, operator, 1 ether); + } + + function test_DelegateTo_RevertInvalidOperator() public { + _mockRegisterAddress(user, btcAddress); + + string memory invalidOperator = "not-a-bech32-address"; + + vm.prank(user); + vm.expectRevert(Errors.InvalidOperator.selector); + gateway.delegateTo(UTXOGatewayStorage.Token.BTC, invalidOperator, 1 ether); + } + + function test_DelegateTo_RevertDelegationFailed() public { + _mockRegisterAddress(user, btcAddress); + + // Mock delegation failure + vm.mockCall( + DELEGATION_PRECOMPILE_ADDRESS, abi.encodeWithSelector(IDelegation.delegate.selector), abi.encode(false) + ); + + vm.prank(user); + vm.expectRevert(abi.encodeWithSelector(Errors.DelegationFailed.selector)); + gateway.delegateTo(UTXOGatewayStorage.Token.BTC, operator, 1 ether); + } + + function test_UndelegateFrom_Success() public { + // Setup: Register user's client chain address first + _mockRegisterAddress(user, btcAddress); + + // mock delegation precompile undelegate success + vm.mockCall( + DELEGATION_PRECOMPILE_ADDRESS, abi.encodeWithSelector(IDelegation.undelegate.selector), abi.encode(true) + ); + + vm.prank(user); + vm.expectEmit(true, true, true, true); + emit UndelegationCompleted(UTXOGatewayStorage.ClientChainID.Bitcoin, user, operator, 1 ether); + + gateway.undelegateFrom(UTXOGatewayStorage.Token.BTC, operator, 1 ether); + + // Verify nonce increment + assertEq(gateway.delegationNonce(UTXOGatewayStorage.ClientChainID.Bitcoin), 1); + } + + function test_UndelegateFrom_RevertZeroAmount() public { + _mockRegisterAddress(user, btcAddress); + + vm.prank(user); + vm.expectRevert(Errors.ZeroAmount.selector); + gateway.undelegateFrom(UTXOGatewayStorage.Token.BTC, operator, 0); + } + + function test_UndelegateFrom_RevertWhenPaused() public { + _mockRegisterAddress(user, btcAddress); + + vm.prank(owner); + gateway.pause(); + + vm.prank(user); + vm.expectRevert("Pausable: paused"); + gateway.undelegateFrom(UTXOGatewayStorage.Token.BTC, operator, 1 ether); + } + + function test_UndelegateFrom_RevertNotRegistered() public { + // Don't register user's address + + vm.prank(user); + vm.expectRevert(Errors.AddressNotRegistered.selector); + gateway.undelegateFrom(UTXOGatewayStorage.Token.BTC, operator, 1 ether); + } + + function test_UndelegateFrom_RevertInvalidOperator() public { + _mockRegisterAddress(user, btcAddress); + + string memory invalidOperator = "not-a-bech32-address"; + + vm.prank(user); + vm.expectRevert(Errors.InvalidOperator.selector); + gateway.undelegateFrom(UTXOGatewayStorage.Token.BTC, invalidOperator, 1 ether); + } + + function test_UndelegateFrom_RevertUndelegationFailed() public { + _mockRegisterAddress(user, btcAddress); + + // mock delegation precompile undelegate failure + vm.mockCall( + DELEGATION_PRECOMPILE_ADDRESS, abi.encodeWithSelector(IDelegation.undelegate.selector), abi.encode(false) + ); + + vm.prank(user); + vm.expectRevert(Errors.UndelegationFailed.selector); + gateway.undelegateFrom(UTXOGatewayStorage.Token.BTC, operator, 1 ether); + } + + function test_WithdrawPrincipal_Success() public { + // Setup: Register user's client chain address first + _mockRegisterAddress(user, btcAddress); + + // mock assets precompile withdrawLST success and return updated balance + vm.mockCall( + ASSETS_PRECOMPILE_ADDRESS, abi.encodeWithSelector(IAssets.withdrawLST.selector), abi.encode(true, 2 ether) + ); + + vm.prank(user); + vm.expectEmit(true, true, true, true); + emit WithdrawPrincipalRequested( + UTXOGatewayStorage.ClientChainID.Bitcoin, + 1, // first request ID + user, + btcAddress, + 1 ether, + 2 ether + ); + + gateway.withdrawPrincipal(UTXOGatewayStorage.Token.BTC, 1 ether); + + // Verify pegOutNonce increment + assertEq(gateway.pegOutNonce(UTXOGatewayStorage.ClientChainID.Bitcoin), 1); + } + + function test_WithdrawPrincipal_RevertWhenPaused() public { + _mockRegisterAddress(user, btcAddress); + + vm.prank(owner); + gateway.pause(); + + vm.prank(user); + vm.expectRevert("Pausable: paused"); + gateway.withdrawPrincipal(UTXOGatewayStorage.Token.BTC, 1 ether); + } + + function test_WithdrawPrincipal_RevertZeroAmount() public { + _mockRegisterAddress(user, btcAddress); + + vm.prank(user); + vm.expectRevert(Errors.ZeroAmount.selector); + gateway.withdrawPrincipal(UTXOGatewayStorage.Token.BTC, 0); + } + + function test_WithdrawPrincipal_RevertWithdrawFailed() public { + _mockRegisterAddress(user, btcAddress); + + // mock assets precompile withdrawLST failure + vm.mockCall( + ASSETS_PRECOMPILE_ADDRESS, abi.encodeWithSelector(IAssets.withdrawLST.selector), abi.encode(false, 0) + ); + + vm.prank(user); + vm.expectRevert(Errors.WithdrawPrincipalFailed.selector); + gateway.withdrawPrincipal(UTXOGatewayStorage.Token.BTC, 1 ether); + } + + function test_WithdrawPrincipal_RevertNotRegistered() public { + // Don't register user's address + + vm.prank(user); + vm.expectRevert(Errors.AddressNotRegistered.selector); + gateway.withdrawPrincipal(UTXOGatewayStorage.Token.BTC, 1 ether); + } + + function test_WithdrawPrincipal_VerifyPegOutRequest() public { + _mockRegisterAddress(user, btcAddress); + + // mock Assets precompile withdrawLST success and return updated balance + vm.mockCall( + ASSETS_PRECOMPILE_ADDRESS, abi.encodeWithSelector(IAssets.withdrawLST.selector), abi.encode(true, 2 ether) + ); + + vm.prank(user); + gateway.withdrawPrincipal(UTXOGatewayStorage.Token.BTC, 1 ether); + + // Verify peg-out request details + UTXOGatewayStorage.PegOutRequest memory request = + gateway.getPegOutRequest(UTXOGatewayStorage.ClientChainID.Bitcoin, 1); + assertEq(uint8(request.clientChainId), uint8(UTXOGatewayStorage.ClientChainID.Bitcoin)); + assertEq(request.nonce, 1); + assertEq(request.requester, user); + assertEq(request.clientAddress, btcAddress); + assertEq(request.amount, 1 ether); + assertEq(uint8(request.withdrawType), uint8(UTXOGatewayStorage.WithdrawType.WithdrawPrincipal)); + } + + function test_WithdrawReward_Success() public { + // Setup: Register user's client chain address first + _mockRegisterAddress(user, btcAddress); + + // mock Reward precompile claimReward success and return updated balance + vm.mockCall( + REWARD_PRECOMPILE_ADDRESS, abi.encodeWithSelector(IReward.claimReward.selector), abi.encode(true, 2 ether) + ); + + vm.prank(user); + vm.expectEmit(true, true, true, true); + emit WithdrawRewardRequested( + UTXOGatewayStorage.ClientChainID.Bitcoin, + 1, // first request ID + user, + btcAddress, + 1 ether, + 2 ether + ); + + gateway.withdrawReward(UTXOGatewayStorage.Token.BTC, 1 ether); + + // Verify pegOutNonce increment + assertEq(gateway.pegOutNonce(UTXOGatewayStorage.ClientChainID.Bitcoin), 1); + } + + function test_WithdrawReward_RevertWhenPaused() public { + _mockRegisterAddress(user, btcAddress); + + vm.prank(owner); + gateway.pause(); + + vm.prank(user); + vm.expectRevert("Pausable: paused"); + gateway.withdrawReward(UTXOGatewayStorage.Token.BTC, 1 ether); + } + + function test_WithdrawReward_RevertZeroAmount() public { + _mockRegisterAddress(user, btcAddress); + + vm.prank(user); + vm.expectRevert(Errors.ZeroAmount.selector); + gateway.withdrawReward(UTXOGatewayStorage.Token.BTC, 0); + } + + function test_WithdrawReward_RevertClaimFailed() public { + _mockRegisterAddress(user, btcAddress); + + // mock claimReward failure + vm.mockCall( + REWARD_PRECOMPILE_ADDRESS, abi.encodeWithSelector(IReward.claimReward.selector), abi.encode(false, 0) + ); + + vm.prank(user); + vm.expectRevert(Errors.WithdrawRewardFailed.selector); + gateway.withdrawReward(UTXOGatewayStorage.Token.BTC, 1 ether); + } + + function test_WithdrawReward_RevertAddressNotRegistered() public { + // Don't register user's address - try to withdraw without registration + + vm.prank(user); + vm.expectRevert(Errors.AddressNotRegistered.selector); + gateway.withdrawReward(UTXOGatewayStorage.Token.BTC, 1 ether); + } + + function test_WithdrawReward_VerifyPegOutRequest() public { + _mockRegisterAddress(user, btcAddress); + + // mock Reward precompile claimReward success and return updated balance + vm.mockCall( + REWARD_PRECOMPILE_ADDRESS, abi.encodeWithSelector(IReward.claimReward.selector), abi.encode(true, 2 ether) + ); + + vm.prank(user); + gateway.withdrawReward(UTXOGatewayStorage.Token.BTC, 1 ether); + + // Verify peg-out request details + UTXOGatewayStorage.PegOutRequest memory request = + gateway.getPegOutRequest(UTXOGatewayStorage.ClientChainID.Bitcoin, 1); + assertEq(uint8(request.clientChainId), uint8(UTXOGatewayStorage.ClientChainID.Bitcoin)); + assertEq(request.nonce, 1); + assertEq(request.requester, user); + assertEq(request.clientAddress, btcAddress); + assertEq(request.amount, 1 ether); + assertEq(uint8(request.withdrawType), uint8(UTXOGatewayStorage.WithdrawType.WithdrawReward)); + } + + function test_WithdrawReward_MultipleRequests() public { + _mockRegisterAddress(user, btcAddress); + + // Mock successful claimReward + bytes memory claimCall1 = abi.encodeWithSelector( + IReward.claimReward.selector, + uint32(uint8(UTXOGatewayStorage.ClientChainID.Bitcoin)), + VIRTUAL_TOKEN, + user.toExocoreBytes(), + 1 ether + ); + vm.mockCall(REWARD_PRECOMPILE_ADDRESS, claimCall1, abi.encode(true, 2 ether)); + + bytes memory claimCall2 = abi.encodeWithSelector( + IReward.claimReward.selector, + uint32(uint8(UTXOGatewayStorage.ClientChainID.Bitcoin)), + VIRTUAL_TOKEN, + user.toExocoreBytes(), + 0.5 ether + ); + vm.mockCall(REWARD_PRECOMPILE_ADDRESS, claimCall2, abi.encode(true, 1.5 ether)); + + vm.startPrank(user); + + // First withdrawal + gateway.withdrawReward(UTXOGatewayStorage.Token.BTC, 1 ether); + + // Second withdrawal + gateway.withdrawReward(UTXOGatewayStorage.Token.BTC, 0.5 ether); + + vm.stopPrank(); + + // Verify both requests exist with correct details + UTXOGatewayStorage.PegOutRequest memory request1 = + gateway.getPegOutRequest(UTXOGatewayStorage.ClientChainID.Bitcoin, 1); + assertEq(request1.amount, 1 ether); + + UTXOGatewayStorage.PegOutRequest memory request2 = + gateway.getPegOutRequest(UTXOGatewayStorage.ClientChainID.Bitcoin, 2); + assertEq(request2.amount, 0.5 ether); + + // Verify nonce increment + assertEq(gateway.pegOutNonce(UTXOGatewayStorage.ClientChainID.Bitcoin), 2); + } + + function test_ProcessNextPegOut_Success() public { + // Setup: Create a peg-out request first + _setupPegOutRequest(); + + // Now process the peg-out request + vm.prank(witnesses[0].addr); + vm.expectEmit(true, true, true, true); + emit PegOutProcessed( + uint8(UTXOGatewayStorage.WithdrawType.WithdrawPrincipal), + UTXOGatewayStorage.ClientChainID.Bitcoin, + 1, // requestId + user, + btcAddress, + 1 ether + ); + + UTXOGatewayStorage.PegOutRequest memory request = + gateway.processNextPegOut(UTXOGatewayStorage.ClientChainID.Bitcoin); + + // Verify returned request contents + assertEq(uint8(request.clientChainId), uint8(UTXOGatewayStorage.ClientChainID.Bitcoin)); + assertEq(request.nonce, 1); + assertEq(request.requester, user); + assertEq(request.clientAddress, btcAddress); + assertEq(request.amount, 1 ether); + assertEq(uint8(request.withdrawType), uint8(UTXOGatewayStorage.WithdrawType.WithdrawPrincipal)); + + // Verify request was deleted + UTXOGatewayStorage.PegOutRequest memory deletedRequest = + gateway.getPegOutRequest(UTXOGatewayStorage.ClientChainID.Bitcoin, 1); + assertEq(deletedRequest.requester, address(0)); + assertEq(deletedRequest.amount, 0); + assertEq(deletedRequest.clientAddress, ""); + + // Verify outbound nonce increment + assertEq(gateway.outboundNonce(UTXOGatewayStorage.ClientChainID.Bitcoin), 1); + } + + function test_ProcessNextPegOut_RevertUnauthorizedWitness() public { + // Setup a peg-out request + _setupPegOutRequest(); + + address unauthorizedWitness = address(0x9999); + vm.prank(unauthorizedWitness); + vm.expectRevert(Errors.UnauthorizedWitness.selector); + gateway.processNextPegOut(UTXOGatewayStorage.ClientChainID.Bitcoin); + } + + function test_ProcessNextPegOut_RevertWhenPaused() public { + // Setup a peg-out request + _setupPegOutRequest(); + + vm.prank(owner); + gateway.pause(); + + vm.prank(witnesses[0].addr); + vm.expectRevert("Pausable: paused"); + gateway.processNextPegOut(UTXOGatewayStorage.ClientChainID.Bitcoin); + } + + function test_ProcessNextPegOut_RevertRequestNotFound() public { + // Don't create any peg-out request + + vm.prank(witnesses[0].addr); + vm.expectRevert(abi.encodeWithSelector(Errors.RequestNotFound.selector, 1)); + gateway.processNextPegOut(UTXOGatewayStorage.ClientChainID.Bitcoin); + } + + function test_ProcessNextPegOut_MultipleRequests() public { + // Setup multiple peg-out requests + _setupPegOutRequest(); // First request + _setupPegOutRequest(); // Second request + + vm.startPrank(witnesses[0].addr); + + // Process first request + UTXOGatewayStorage.PegOutRequest memory request1 = + gateway.processNextPegOut(UTXOGatewayStorage.ClientChainID.Bitcoin); + assertEq(request1.amount, 1 ether); + + // Process second request + UTXOGatewayStorage.PegOutRequest memory request2 = + gateway.processNextPegOut(UTXOGatewayStorage.ClientChainID.Bitcoin); + assertEq(request2.amount, 1 ether); + + vm.stopPrank(); + + // Verify both requests were deleted + UTXOGatewayStorage.PegOutRequest memory deleted1 = + gateway.getPegOutRequest(UTXOGatewayStorage.ClientChainID.Bitcoin, 1); + UTXOGatewayStorage.PegOutRequest memory deleted2 = + gateway.getPegOutRequest(UTXOGatewayStorage.ClientChainID.Bitcoin, 2); + assertEq(deleted1.requester, address(0)); + assertEq(deleted2.requester, address(0)); + + // Verify outbound nonce + assertEq(gateway.outboundNonce(UTXOGatewayStorage.ClientChainID.Bitcoin), 2); + } + + // Helper function to setup a peg-out request + function _setupPegOutRequest() internal { + if (gateway.getClientAddress(UTXOGatewayStorage.ClientChainID.Bitcoin, user).length == 0) { + _mockRegisterAddress(user, btcAddress); + } + + // mock withdrawLST success + vm.mockCall( + ASSETS_PRECOMPILE_ADDRESS, abi.encodeWithSelector(IAssets.withdrawLST.selector), abi.encode(true, 2 ether) + ); + + vm.prank(user); + gateway.withdrawPrincipal(UTXOGatewayStorage.Token.BTC, 1 ether); + assertEq(gateway.getPegOutRequest(UTXOGatewayStorage.ClientChainID.Bitcoin, 1).amount, 1 ether); + } + + // Helper functions + function _mockRegisterAddress(address exocoreAddr, bytes memory btcAddr) internal { + UTXOGatewayStorage.StakeMsg memory stakeMsg = UTXOGatewayStorage.StakeMsg({ + clientChainId: UTXOGatewayStorage.ClientChainID.Bitcoin, + clientAddress: btcAddr, + exocoreAddress: exocoreAddr, + operator: "", + amount: 1 ether, + nonce: gateway.nextInboundNonce(UTXOGatewayStorage.ClientChainID.Bitcoin), + txTag: bytes("tx1") + }); + + // mock Assets precompile deposit success and Delegation precompile delegate success + vm.mockCall( + ASSETS_PRECOMPILE_ADDRESS, + abi.encodeWithSelector(IAssets.depositLST.selector), + abi.encode(true, stakeMsg.amount) + ); + vm.mockCall( + DELEGATION_PRECOMPILE_ADDRESS, abi.encodeWithSelector(IDelegation.delegate.selector), abi.encode(true) + ); + + bytes memory signature = _generateSignature(stakeMsg, witnesses[0].privateKey); + + vm.expectEmit(true, true, true, true, address(gateway)); + emit AddressRegistered(UTXOGatewayStorage.ClientChainID.Bitcoin, btcAddr, exocoreAddr); + + vm.prank(relayer); + gateway.processStakeMessage(witnesses[0].addr, stakeMsg, signature); + + // Verify address registration + assertEq(gateway.getClientAddress(UTXOGatewayStorage.ClientChainID.Bitcoin, exocoreAddr), btcAddr); + assertEq(gateway.getExocoreAddress(UTXOGatewayStorage.ClientChainID.Bitcoin, btcAddr), exocoreAddr); + } + + function _addAllWitnesses() internal { + address[] memory witnessesToAdd = new address[](1); + for (uint256 i = 0; i < witnesses.length; i++) { + if (!gateway.authorizedWitnesses(witnesses[i].addr)) { + witnessesToAdd[0] = witnesses[i].addr; + vm.prank(owner); + gateway.addWitnesses(witnessesToAdd); + } + } + } + + function _getMessageHash(UTXOGatewayStorage.StakeMsg memory msg_) internal pure returns (bytes32) { + return keccak256( + abi.encode( + msg_.clientChainId, // ClientChainID + msg_.clientAddress, // bytes - Bitcoin address + msg_.exocoreAddress, // address + msg_.operator, // string + msg_.amount, // uint256 + msg_.nonce, // uint64 + msg_.txTag // bytes + ) + ); + } + + function _generateSignature(UTXOGatewayStorage.StakeMsg memory msg_, uint256 privateKey) + internal + pure + returns (bytes memory) + { + // Encode all fields of StakeMsg in order + bytes32 messageHash = _getMessageHash(msg_); + + // Sign the encoded message hash + (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, messageHash.toEthSignedMessageHash()); + + // Return the signature in the format expected by the contract + return abi.encodePacked(r, s, v); + } + + function _activateConsensus() internal { + vm.startPrank(owner); + gateway.updateRequiredProofs(gateway.authorizedWitnessCount()); + vm.stopPrank(); + } + + function _deactivateConsensus() internal { + vm.startPrank(owner); + gateway.updateRequiredProofs(gateway.authorizedWitnessCount() + 1); + vm.stopPrank(); + } + +}