From 5023f31275e600aebf7e4ccde56475ca91236f52 Mon Sep 17 00:00:00 2001 From: adu Date: Mon, 28 Oct 2024 15:29:23 +0800 Subject: [PATCH 01/11] refactor: fine tune ExocoreBTCGateway --- src/core/ExocoreBtcGateway.sol | 579 ++++++++++++----------- src/storage/ExocoreBtcGatewayStorage.sol | 49 +- 2 files changed, 349 insertions(+), 279 deletions(-) diff --git a/src/core/ExocoreBtcGateway.sol b/src/core/ExocoreBtcGateway.sol index c8a4968c..6e74d41d 100644 --- a/src/core/ExocoreBtcGateway.sol +++ b/src/core/ExocoreBtcGateway.sol @@ -1,8 +1,9 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.19; -import {ASSETS_CONTRACT} from "../interfaces/precompiles/IAssets.sol"; +import {Errors} from "../libraries/Errors.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"; @@ -26,10 +27,6 @@ contract ExocoreBtcGateway is 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. */ @@ -61,8 +58,6 @@ contract ExocoreBtcGateway is * @dev Sets up initial configuration for testing purposes. */ constructor() { - // todo: for test. - _registerClientChain(111); authorizedWitnesses[EXOCORE_WITNESS] = true; isWhitelistedToken[BTC_ADDR] = true; _disableInitializers(); @@ -77,12 +72,37 @@ contract ExocoreBtcGateway is __Pausable_init_unchained(); } + /** + * @notice Activates token staking by registering or updating the chain and token with the Exocore system. + */ + function activateStakingForToken(TokenType _tokenType) external { + if (_tokenType == TokenType.BTC) { + _registerOrUpdateBitcoinChain( + BITCOIN_CHAIN_ID, + BITCOIN_STAKER_ACCOUNT_LENGTH, + BITCOIN_NAME, + BITCOIN_METADATA, + BITCOIN_SIGNATURE_SCHEME + ); + _registerOrUpdateBTC( + BITCOIN_CHAIN_ID, + VIRTUAL_BTC_TOKEN, + BTC_DECIMALS, + BTC_NAME, + BTC_METADATA, + BTC_ORACLE_INFO + ); + } else { + revert InvalidTokenType(); + } + } + /** * @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 { + function addWitness(address _witness) external onlyOwner { if (_witness == address(0)) { revert ZeroAddressNotAllowed(); } @@ -96,7 +116,7 @@ contract ExocoreBtcGateway is * @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 { + function removeWitness(address _witness) external onlyOwner { require(authorizedWitnesses[_witness], "Witness not authorized"); authorizedWitnesses[_witness] = false; emit WitnessRemoved(_witness); @@ -107,45 +127,17 @@ contract ExocoreBtcGateway is * @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 { + function updateBridgeFee(uint256 _newFee) external 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 { + function checkExpiredTransactions(bytes[] calldata _txTags) external { for (uint256 i = 0; i < _txTags.length; i++) { Transaction storage txn = transactions[_txTags[i]]; if (txn.status == TxStatus.Pending && block.timestamp >= txn.expiryTime) { @@ -155,21 +147,6 @@ contract ExocoreBtcGateway is } } - /** - * @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. @@ -187,77 +164,13 @@ contract ExocoreBtcGateway is 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 + external nonReentrant whenNotPaused { @@ -300,31 +213,6 @@ contract ExocoreBtcGateway is } } - /** - * @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. @@ -367,17 +255,12 @@ contract ExocoreBtcGateway is 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)); + + bool success = DELEGATION_CONTRACT.delegate(CLIENT_CHAIN_ID, BTC_TOKEN, delegator, operator, amount); + if (!success) { revert DelegationFailed(); } + emit DelegationCompleted(token, delegator, operator, amount); } /** @@ -394,17 +277,11 @@ contract ExocoreBtcGateway is 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)); + bool success = DELEGATION_CONTRACT.undelegate(CLIENT_CHAIN_ID, BTC_TOKEN, delegator, operator, amount); + if (!success) { revert UndelegationFailed(); } + emit UndelegationCompleted(token, delegator, operator, amount); } /** @@ -420,7 +297,6 @@ contract ExocoreBtcGateway is 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) { @@ -456,50 +332,6 @@ contract ExocoreBtcGateway is 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 @@ -509,7 +341,7 @@ contract ExocoreBtcGateway is * @custom:throws RequestNotFound if the request does not exist */ function processPegOut(bytes32 _requestId, bytes32 _btcTxTag) - public + external onlyAuthorizedWitness nonReentrant whenNotPaused @@ -532,7 +364,7 @@ contract ExocoreBtcGateway is } // Function to check and update expired peg-out requests - function checkExpiredPegOutRequests(bytes32[] calldata _requestIds) public { + function checkExpiredPegOutRequests(bytes32[] calldata _requestIds) external { 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) { @@ -562,66 +394,6 @@ contract ExocoreBtcGateway is _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. @@ -642,15 +414,6 @@ contract ExocoreBtcGateway is 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. @@ -667,6 +430,28 @@ contract ExocoreBtcGateway is emit PegOutRequestStatusUpdated(requestId, newStatus); } + /** + * @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 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 Converts an address to bytes. * @param addr The address to convert. @@ -708,4 +493,242 @@ contract ExocoreBtcGateway is return abi.encodePacked(source); } + /** + * @notice Registers or updates the Bitcoin chain with the Exocore system. + */ + function _registerOrUpdateClientChain(uint32 chainId, uint8 stakerAccountLength, string storage name, string storage metadata, string storage signatureScheme) internal { + (bool success, bool updated) = ASSETS_CONTRACT.registerOrUpdateClientChain( + chainId, stakerAccountLength, name, metadata, signatureScheme + ); + if (!success) { + revert Errors.RegisterClientChainToExocoreFailed(chainId); + } + if (updated) { + emit ClientChainUpdated(chainId); + } else { + emit ClientChainRegistered(chainId); + } + } + + function _registerOrUpdateToken(uint32 chainId, bytes storage token, uint8 decimals, string storage name, string storage metadata, bytes memory oracleInfo) internal { + bool registered = ASSETS_CONTRACT.registerToken(chainId, token, decimals, name, metadata, oracleInfo); + if (!registered) { + bool updated = ASSETS_CONTRACT.updateToken(BITCOIN_CHAIN_ID, VIRTUAL_BTC_TOKEN, BTC_DECIMALS, BTC_NAME, BTC_METADATA, BTC_ORACLE_INFO); + if (!updated) { + revert Errors.RegisterTokenToExocoreFailed(BITCOIN_CHAIN_ID, VIRTUAL_BTC_TOKEN); + } + emit WhitelistTokenUpdated(chainId, token); + } else { + emit WhitelistTokenAdded(chainId, token); + } + } + + /** + * @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 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 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 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 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 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(); + } + } + } diff --git a/src/storage/ExocoreBtcGatewayStorage.sol b/src/storage/ExocoreBtcGatewayStorage.sol index ba52896f..88e46f46 100644 --- a/src/storage/ExocoreBtcGatewayStorage.sol +++ b/src/storage/ExocoreBtcGatewayStorage.sol @@ -7,6 +7,13 @@ pragma solidity ^0.8.19; */ contract ExocoreBtcGatewayStorage { + /** + * @notice Enum to represent the type of supported token + */ + enum TokenType { + BTC + } + /** * @dev Enum to represent the status of a transaction */ @@ -84,6 +91,18 @@ contract ExocoreBtcGatewayStorage { } // Constants + uint32 public constant BITCOIN_CHAIN_ID = 1; + uint8 public constant BITCOIN_STAKER_ACCOUNT_LENGTH = 20; + string public constant BITCOIN_NAME = "Bitcoin"; + string public constant BITCOIN_METADATA = "Bitcoin"; + string public constant BITCOIN_SIGNATURE_SCHEME = "ECDSA"; + address public constant VIRTUAL_BTC_ADDRESS = 0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB; + bytes public constant VIRTUAL_BTC_TOKEN = abi.encodePacked(bytes32(bytes20(VIRTUAL_BTC_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"; + address public constant EXOCORE_WITNESS = address(0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266); uint256 public constant REQUIRED_PROOFS = 2; uint256 public constant PROOF_TIMEOUT = 1 days; @@ -135,6 +154,8 @@ contract ExocoreBtcGatewayStorage { */ mapping(uint32 => mapping(bytes => uint64)) public inboundBytesNonce; + uint256[40] private __gap; + // Events /** * @dev Emitted when a deposit is completed @@ -341,6 +362,24 @@ contract ExocoreBtcGatewayStorage { */ event PegOutRequestStatusUpdated(bytes32 indexed requestId, TxStatus newStatus); + /// @notice Emitted upon the registration of a new client chain. + /// @param clientChainId The LayerZero chain ID of the client chain. + event ClientChainRegistered(uint32 clientChainId); + + /// @notice Emitted upon the update of a client chain. + /// @param clientChainId The LayerZero chain ID of the client chain. + event ClientChainUpdated(uint32 clientChainId); + + /// @notice Emitted when a token is added to the whitelist. + /// @param clientChainId The LayerZero chain ID of the client chain. + /// @param token The address of the token. + event WhitelistTokenAdded(uint32 clientChainId, bytes32 token); + + /// @notice Emitted when a token is updated in the whitelist. + /// @param clientChainId The LayerZero chain ID of the client chain. + /// @param token The address of the token. + event WhitelistTokenUpdated(uint32 clientChainId, bytes32 token); + // Errors /** * @dev Thrown when an unauthorized witness attempts an action @@ -429,6 +468,8 @@ contract ExocoreBtcGatewayStorage { */ error UnexpectedInboundNonce(uint64 expectedNonce, uint64 actualNonce); + error InvalidTokenType(); + /** * @dev Modifier to check if a token is whitelisted * @param token The address of the token to check @@ -461,6 +502,12 @@ contract ExocoreBtcGatewayStorage { inboundBytesNonce[srcChainId][srcAddress] = nonce; } - uint256[40] private __gap; + function getTokenByType(TokenType _tokenType) public pure returns (bytes memory) { + if (_tokenType == TokenType.BTC) { + return VIRTUAL_BTC_TOKEN; + } else { + revert InvalidTokenType(); + } + } } From f906243f468b0399bcda0b526d9542ae444008d7 Mon Sep 17 00:00:00 2001 From: adu Date: Wed, 6 Nov 2024 18:01:48 +0800 Subject: [PATCH 02/11] refactor: optimize enums --- src/core/ExocoreBtcGateway.sol | 317 +++++++++++------------ src/libraries/Errors.sol | 7 + src/libraries/ExocoreBytes.sol | 11 + src/storage/ExocoreBtcGatewayStorage.sol | 184 ++++++------- 4 files changed, 257 insertions(+), 262 deletions(-) create mode 100644 src/libraries/ExocoreBytes.sol diff --git a/src/core/ExocoreBtcGateway.sol b/src/core/ExocoreBtcGateway.sol index 6e74d41d..60b0cd04 100644 --- a/src/core/ExocoreBtcGateway.sol +++ b/src/core/ExocoreBtcGateway.sol @@ -2,6 +2,7 @@ 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"; @@ -26,6 +27,7 @@ contract ExocoreBtcGateway is ReentrancyGuardUpgradeable, ExocoreBtcGatewayStorage { + using ExocoreBytes for address; /** * @dev Modifier to restrict access to authorized witnesses only. @@ -59,7 +61,6 @@ contract ExocoreBtcGateway is */ constructor() { authorizedWitnesses[EXOCORE_WITNESS] = true; - isWhitelistedToken[BTC_ADDR] = true; _disableInitializers(); } @@ -75,18 +76,18 @@ contract ExocoreBtcGateway is /** * @notice Activates token staking by registering or updating the chain and token with the Exocore system. */ - function activateStakingForToken(TokenType _tokenType) external { - if (_tokenType == TokenType.BTC) { - _registerOrUpdateBitcoinChain( - BITCOIN_CHAIN_ID, - BITCOIN_STAKER_ACCOUNT_LENGTH, + function activateStakingForClientChain(ClientChain clientChain_) external { + if (clientChain_ == ClientChain.Bitcoin) { + _registerOrUpdateClientChain( + getChainId(clientChain_), + STAKER_ACCOUNT_LENGTH, BITCOIN_NAME, BITCOIN_METADATA, BITCOIN_SIGNATURE_SCHEME ); - _registerOrUpdateBTC( - BITCOIN_CHAIN_ID, - VIRTUAL_BTC_TOKEN, + _registerOrUpdateToken( + getChainId(clientChain_), + VIRTUAL_TOKEN, BTC_DECIMALS, BTC_NAME, BTC_METADATA, @@ -102,7 +103,7 @@ contract ExocoreBtcGateway is * @param _witness The address of the witness to be added. * @dev Can only be called by the contract owner. */ - function addWitness(address _witness) external onlyOwner { + function addWitness(address _witness) public onlyOwner { if (_witness == address(0)) { revert ZeroAddressNotAllowed(); } @@ -139,11 +140,7 @@ contract ExocoreBtcGateway is */ function checkExpiredTransactions(bytes[] calldata _txTags) external { 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]); - } + _revokeTxIfExpired(_txTags[i]); } } @@ -153,9 +150,9 @@ contract ExocoreBtcGateway is * @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"); + function registerAddress(bytes calldata depositor, address exocoreAddress) external onlyAuthorizedWitness { + require(depositor.length > 0 && exocoreAddress != address(0), "Invalid address"); + require(btcToExocoreAddress[depositor] != address(0), "Depositor address already registered"); require(exocoreToBtcAddress[exocoreAddress].length == 0, "Exocore address already registered"); btcToExocoreAddress[depositor] = exocoreAddress; @@ -169,7 +166,7 @@ contract ExocoreBtcGateway is * @param _message The interchain message. * @param _signature The signature of the message. */ - function submitProof(InterchainMsg calldata _message, bytes calldata _signature) + function submitProof(StakeMsg calldata _message, bytes calldata _signature) external nonReentrant whenNotPaused @@ -179,8 +176,11 @@ contract ExocoreBtcGateway is revert BtcTxAlreadyProcessed(); } + // we should revoke the tx by setting it as expired if it has expired + _revokeTxIfExpired(_message.txTag); + // Verify nonce - _verifyAndUpdateBytesNonce(_message.srcChainID, _message.srcAddress, _message.nonce); + _verifyAndUpdateBytesNonce(getChainId(_message.clientChain), _message.srcAddress, _message.nonce); // Verify signature _verifySignature(_message, _signature); @@ -189,16 +189,20 @@ contract ExocoreBtcGateway is 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; + // if the witness has already submitted proof at or after the start of the proof window, they cannot submit again + if (txn.witnessTime[msg.sender] >= txn.expiryTime - PROOF_TIMEOUT) { + revert Errors.WitnessAlreadySubmittedProof(); + } + txn.witnessTime[msg.sender] = block.timestamp; txn.proofCount++; } else { txn.status = TxStatus.Pending; + txn.clientChain = _message.clientChain; txn.amount = _message.amount; - txn.recipient = address(bytes20(_message.dstAddress)); + txn.recipient = address(bytes20(_message.exocoreAddress)); txn.expiryTime = block.timestamp + PROOF_TIMEOUT; txn.proofCount = 1; - txn.hasWitnessed[msg.sender] = true; + txn.witnessTime[msg.sender] = block.timestamp; } proofs[txTag].push( @@ -218,118 +222,104 @@ contract ExocoreBtcGateway is * @param _msg The interchain message containing the deposit details. * @param signature The signature to verify. */ - function depositTo(InterchainMsg calldata _msg, bytes calldata signature) + function depositTo(StakeMsg 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); + (bytes memory txTag, address depositorExoAddr) = _processAndVerify(_msg, signature); - processedBtcTxs[btcTxTag] = TxInfo(true, block.timestamp); + processedBtcTxs[txTag] = 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); + // we use registered exocore address as the depositor + _deposit(getChainId(_msg.clientChain), _msg.srcAddress, depositorExoAddr, _msg.amount, _msg.txTag); } /** * @notice Delegates BTC to an operator. - * @param token The token address. + * @param token The value of the token enum. * @param operator The operator's exocore address. * @param amount The amount to delegate. */ - function delegateTo(address token, bytes calldata operator, uint256 amount) + function delegateTo(Token token, string calldata operator, uint256 amount) external nonReentrant whenNotPaused - isTokenWhitelisted(token) isValidAmount(amount) { - bytes memory delegator = abi.encodePacked(bytes32(bytes20(msg.sender))); - - bool success = DELEGATION_CONTRACT.delegate(CLIENT_CHAIN_ID, BTC_TOKEN, delegator, operator, amount); - if (!success) { - revert DelegationFailed(); - } - emit DelegationCompleted(token, delegator, operator, amount); + uint32 chainId = getChainIdByToken(token); + uint64 nonce = ++delegationNonce[chainId][msg.sender]; + _delegate(chainId, nonce, msg.sender, operator, amount); } /** * @notice Undelegates BTC from an operator. - * @param token The token address. + * @param token The value of the token enum. * @param operator The operator's exocore address. * @param amount The amount to undelegate. */ - function undelegateFrom(address token, bytes calldata operator, uint256 amount) + function undelegateFrom(Token token, string memory operator, uint256 amount) external nonReentrant whenNotPaused - isTokenWhitelisted(token) isValidAmount(amount) { - bytes memory delegator = abi.encodePacked(bytes32(bytes20(msg.sender))); - bool success = DELEGATION_CONTRACT.undelegate(CLIENT_CHAIN_ID, BTC_TOKEN, delegator, operator, amount); + uint32 chainId = getChainIdByToken(token); + uint64 nonce = ++delegationNonce[chainId][msg.sender]; + bool success = DELEGATION_CONTRACT.undelegate(chainId, nonce, VIRTUAL_TOKEN, msg.sender.toExocoreBytes(), bytes(operator), amount); if (!success) { revert UndelegationFailed(); } - emit UndelegationCompleted(token, delegator, operator, amount); + emit UndelegationCompleted(chainId, msg.sender, operator, amount); } /** * @notice Withdraws the principal BTC. - * @param token The token address. + * @param token The value of the token enum. * @param amount The amount to withdraw. */ - function withdrawPrincipal(address token, uint256 amount) + function withdrawPrincipal(Token token, uint256 amount) external nonReentrant whenNotPaused - isTokenWhitelisted(token) isValidAmount(amount) { - bytes memory withdrawer = abi.encodePacked(bytes32(bytes20(msg.sender))); + uint32 chainId = getChainIdByToken(token); (bool success, uint256 updatedBalance) = - ASSETS_CONTRACT.withdrawLST(CLIENT_CHAIN_ID, BTC_TOKEN, withdrawer, amount); + ASSETS_CONTRACT.withdrawLST(chainId, VIRTUAL_TOKEN, msg.sender.toExocoreBytes(), 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); + + (bytes32 requestId, bytes memory clientChainAddress) = + _initiatePegOut(ClientChain(uint8(token)), amount, msg.sender, WithdrawType.WithdrawPrincipal); + emit WithdrawPrincipalRequested(chainId, requestId, msg.sender, clientChainAddress, amount, updatedBalance); } /** * @notice Withdraws the reward BTC. - * @param token The token address. + * @param token The value of the token enum. * @param amount The amount to withdraw. */ - function withdrawReward(address token, uint256 amount) + function withdrawReward(Token token, uint256 amount) external nonReentrant whenNotPaused - isTokenWhitelisted(token) isValidAmount(amount) { - bytes memory withdrawer = abi.encodePacked(bytes32(bytes20(msg.sender))); - _nextNonce(CLIENT_CHAIN_ID, withdrawer); + uint32 chainId = getChainIdByToken(token); (bool success, uint256 updatedBalance) = - REWARD_CONTRACT.claimReward(CLIENT_CHAIN_ID, BTC_TOKEN, withdrawer, amount); + REWARD_CONTRACT.claimReward(chainId, VIRTUAL_TOKEN, msg.sender.toExocoreBytes(), amount); if (!success) { revert WithdrawRewardFailed(); } - (bytes32 requestId, bytes memory _btcAddress) = - _initiatePegOut(token, amount, withdrawer, WithdrawType.WithdrawReward); + (bytes32 requestId, bytes memory clientChainAddress) = + _initiatePegOut(ClientChain(uint8(token)), amount, msg.sender, WithdrawType.WithdrawReward); - emit WithdrawRewardRequested(requestId, msg.sender, token, _btcAddress, amount, updatedBalance); + emit WithdrawRewardRequested(chainId, requestId, msg.sender, clientChainAddress, amount, updatedBalance); } /** @@ -379,19 +369,21 @@ contract ExocoreBtcGateway is /** * @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) + function depositThenDelegateTo(StakeMsg calldata _msg, 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); + (bytes memory txTag, address depositorExoAddr) = _processAndVerify(_msg, signature); + uint32 srcChainId = getChainId(_msg.clientChain); + _deposit(srcChainId, _msg.srcAddress, depositorExoAddr, _msg.amount, txTag); + + uint64 nonce = ++delegationNonce[srcChainId][msg.sender]; + _delegate(srcChainId, nonce, depositorExoAddr, _msg.operator, _msg.amount); } /** @@ -399,7 +391,7 @@ contract ExocoreBtcGateway is * @param exocoreAddress The Exocore address. * @return The corresponding BTC address. */ - function getBtcAddress(bytes calldata exocoreAddress) external view returns (bytes memory) { + function getBtcAddress(address exocoreAddress) external view returns (bytes memory) { return exocoreToBtcAddress[exocoreAddress]; } @@ -446,29 +438,20 @@ contract ExocoreBtcGateway is */ 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]; + assembly { + mstore(add(bytesArray, 32), _bytes32) } return string(bytesArray); } - /** - * @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. + * @param exocoreAddress The exocore address. * @return The next nonce for corresponding btcAddress. */ - function _nextNonce(uint32 srcChainId, bytes memory exoSrcAddress) internal view returns (uint64) { - bytes memory depositor = exocoreToBtcAddress[exoSrcAddress]; + function _nextNonce(uint32 srcChainId, address exocoreAddress) internal view returns (uint64) { + bytes memory depositor = exocoreToBtcAddress[exocoreAddress]; return inboundBytesNonce[srcChainId][depositor] + 1; } @@ -496,7 +479,7 @@ contract ExocoreBtcGateway is /** * @notice Registers or updates the Bitcoin chain with the Exocore system. */ - function _registerOrUpdateClientChain(uint32 chainId, uint8 stakerAccountLength, string storage name, string storage metadata, string storage signatureScheme) internal { + function _registerOrUpdateClientChain(uint32 chainId, uint8 stakerAccountLength, string memory name, string memory metadata, string memory signatureScheme) internal { (bool success, bool updated) = ASSETS_CONTRACT.registerOrUpdateClientChain( chainId, stakerAccountLength, name, metadata, signatureScheme ); @@ -510,16 +493,16 @@ contract ExocoreBtcGateway is } } - function _registerOrUpdateToken(uint32 chainId, bytes storage token, uint8 decimals, string storage name, string storage metadata, bytes memory oracleInfo) internal { + function _registerOrUpdateToken(uint32 chainId, bytes memory token, uint8 decimals, string memory name, string memory metadata, string memory oracleInfo) internal { bool registered = ASSETS_CONTRACT.registerToken(chainId, token, decimals, name, metadata, oracleInfo); if (!registered) { - bool updated = ASSETS_CONTRACT.updateToken(BITCOIN_CHAIN_ID, VIRTUAL_BTC_TOKEN, BTC_DECIMALS, BTC_NAME, BTC_METADATA, BTC_ORACLE_INFO); + bool updated = ASSETS_CONTRACT.updateToken(chainId, token, metadata); if (!updated) { - revert Errors.RegisterTokenToExocoreFailed(BITCOIN_CHAIN_ID, VIRTUAL_BTC_TOKEN); + revert Errors.AddWhitelistTokenFailed(chainId, bytes32(token)); } - emit WhitelistTokenUpdated(chainId, token); + emit WhitelistTokenUpdated(chainId, VIRTUAL_TOKEN_ADDRESS); } else { - emit WhitelistTokenAdded(chainId, token); + emit WhitelistTokenAdded(chainId, VIRTUAL_TOKEN_ADDRESS); } } @@ -534,16 +517,15 @@ contract ExocoreBtcGateway is return false; } - InterchainMsg memory firstMsg = txProofs[0].message; + StakeMsg memory firstMsg = txProofs[0].message; for (uint256 i = 1; i < txProofs.length; i++) { - InterchainMsg memory currentMsg = txProofs[i].message; + StakeMsg 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.clientChain != currentMsg.clientChain + || firstMsg.exocoreAddress != currentMsg.exocoreAddress + || keccak256(bytes(firstMsg.operator)) != keccak256(bytes(currentMsg.operator)) + || firstMsg.amount != currentMsg.amount || firstMsg.nonce != currentMsg.nonce || keccak256(firstMsg.txTag) != keccak256(currentMsg.txTag) - || keccak256(firstMsg.payload) != keccak256(currentMsg.payload) ) { return false; } @@ -556,18 +538,15 @@ contract ExocoreBtcGateway is * @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. + function _verifySignature(StakeMsg calldata _msg, bytes memory signature) internal view { + // StakeMsg, EIP721 is preferred next step. bytes memory encodeMsg = abi.encode( - _msg.srcChainID, - _msg.dstChainID, + _msg.clientChain, _msg.srcAddress, - _msg.dstAddress, - _msg.token, + _msg.operator, _msg.amount, _msg.nonce, - _msg.txTag, - _msg.payload + _msg.txTag ); bytes32 messageHash = keccak256(encodeMsg); @@ -578,25 +557,25 @@ contract ExocoreBtcGateway is * @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. + * @return txTag The lowercase of BTC txid-vout. + * @return depositorExoAddress The Exocore address of the depositor. */ - function _processAndVerify(InterchainMsg calldata _msg, bytes calldata signature) + function _processAndVerify(StakeMsg calldata _msg, bytes calldata signature) internal - returns (bytes memory btcTxTag, bytes memory depositor) + returns (bytes memory txTag, address depositorExoAddress) { - btcTxTag = _msg.txTag; - depositor = btcToExocoreAddress[_msg.srcAddress]; - if (depositor.length == 0) { + txTag = _msg.txTag; + depositorExoAddress = btcToExocoreAddress[_msg.srcAddress]; + if (depositorExoAddress == address(0)) { revert BtcAddressNotRegistered(); } - if (processedBtcTxs[btcTxTag].processed) { + if (processedBtcTxs[txTag].processed) { revert BtcTxAlreadyProcessed(); } // Verify nonce - _verifyAndUpdateBytesNonce(_msg.srcChainID, depositor, _msg.nonce); + _verifyAndUpdateBytesNonce(getChainId(_msg.clientChain), _msg.srcAddress, _msg.nonce); // Verify signature _verifySignature(_msg, signature); @@ -619,7 +598,7 @@ contract ExocoreBtcGateway is uint256 amountAfterFee = txn.amount - fee; //todo:call precompile depositTo - + _deposit(getChainId(txn.clientChain), txn.srcAddress, txn.recipient, txn.amount, _txTag); txn.status = TxStatus.Processed; // totalDeposited += txn.amount; @@ -630,30 +609,30 @@ contract ExocoreBtcGateway is /** * @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 clientChain 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 _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 + * @return clientChainAddress The client chain 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) + function _initiatePegOut(ClientChain clientChain, uint256 _amount, address withdrawer, WithdrawType _withdrawType) internal - returns (bytes32 requestId, bytes memory _btcAddress) + returns (bytes32 requestId, bytes memory clientChainAddress) { // Use storage pointer to reduce gas consumption PegOutRequest storage request; - // 1. Check BTC address - _btcAddress = exocoreToBtcAddress[withdrawer]; - if (_btcAddress.length == 0) { + // 1. Check client c address + clientChainAddress = exocoreToBtcAddress[withdrawer]; + if (clientChainAddress.length == 0) { revert BtcAddressNotRegistered(); } // 2. Generate unique requestId - requestId = keccak256(abi.encodePacked(_token, msg.sender, _btcAddress, _amount, block.number)); + requestId = keccak256(abi.encodePacked(clientChain, withdrawer, clientChainAddress, _amount, block.number)); // 3. Check if request already exists request = pegOutRequests[requestId]; @@ -662,9 +641,9 @@ contract ExocoreBtcGateway is } // 4. Create new PegOutRequest - request.token = _token; - request.requester = msg.sender; - request.btcAddress = _btcAddress; + request.clientChain = clientChain; + request.requester = withdrawer; + request.clientChainAddress = clientChainAddress; request.amount = _amount; request.withdrawType = _withdrawType; request.status = TxStatus.Pending; @@ -672,63 +651,61 @@ contract ExocoreBtcGateway is } /** - * @notice Internal function to deposit BTC to the asset contract. + * @notice Internal function to deposit BTC like token. * @param clientChainId The client chain ID. - * @param btcToken The BTC token. - * @param depositor The BTC address. + * @param srcAddress The source address. + * @param depositorExoAddr The Exocore address. * @param amount The amount to deposit. - * @param btcTxTag The BTC transaction tag. - * @param operator The operator's address. + * @param txTag The transaction tag. */ - function _depositToAssetContract( + function _deposit( uint32 clientChainId, - bytes memory btcToken, - bytes memory depositor, + bytes memory srcAddress, + address depositorExoAddr, uint256 amount, - bytes memory btcTxTag, - bytes memory operator + bytes memory txTag ) 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); + (bool success, uint256 updatedBalance) = + ASSETS_CONTRACT.depositLST(clientChainId, VIRTUAL_TOKEN, depositorExoAddr.toExocoreBytes(), amount); + if (!success) { + revert DepositFailed(txTag); } + + emit DepositCompleted(clientChainId, txTag, depositorExoAddr, srcAddress, amount, updatedBalance); } /** - * @notice Internal function to delegate BTC to the delegation contract. + * @notice Internal function to delegate BTC like token. * @param clientChainId The client chain ID. - * @param btcToken The BTC token. - * @param depositor The BTC address. + * @param delegator The Exocore address. * @param operator The operator's address. * @param amount The amount to delegate. - * @param updatedBalance The updated balance after delegation. */ - function _delegateToDelegationContract( + function _delegate( uint32 clientChainId, - bytes memory btcToken, - bytes memory depositor, - bytes memory operator, - uint256 amount, - uint256 updatedBalance + uint64 nonce, + address delegator, + string memory operator, + uint256 amount ) 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)); + bool success = DELEGATION_CONTRACT.delegate(clientChainId, nonce, VIRTUAL_TOKEN, delegator.toExocoreBytes(), bytes(operator), amount); + if (!success) { revert DelegationFailed(); } + emit DelegationCompleted(clientChainId, delegator, operator, amount); + } + + function _revokeTxIfExpired(bytes calldata txTag) internal { + Transaction storage txn = transactions[txTag]; + if (txn.status == TxStatus.Pending && block.timestamp >= txn.expiryTime) { + txn.status = TxStatus.Expired; + emit TransactionExpired(txTag); + } + } + + // encode address as byte array with 32 bytes, and pad with zeros from right + function addressToExocoreBytes(address addr) internal pure returns (bytes memory) { + return abi.encodePacked(bytes32(bytes20(addr))); } } diff --git a/src/libraries/Errors.sol b/src/libraries/Errors.sol index 653ab3ed..00eff708 100644 --- a/src/libraries/Errors.sol +++ b/src/libraries/Errors.sol @@ -311,4 +311,11 @@ library Errors { /// @dev RewardVault: insufficient balance error InsufficientBalance(); + /* -------------------------------------------------------------------------- */ + /* ExocoreBtcGateway Errors */ + /* -------------------------------------------------------------------------- */ + + /// @dev ExocoreBtcGateway: witness has already submitted proof + error WitnessAlreadySubmittedProof(); + } diff --git a/src/libraries/ExocoreBytes.sol b/src/libraries/ExocoreBytes.sol new file mode 100644 index 00000000..78e328b6 --- /dev/null +++ b/src/libraries/ExocoreBytes.sol @@ -0,0 +1,11 @@ +// 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 index 88e46f46..7be1deeb 100644 --- a/src/storage/ExocoreBtcGatewayStorage.sol +++ b/src/storage/ExocoreBtcGatewayStorage.sol @@ -9,18 +9,28 @@ contract ExocoreBtcGatewayStorage { /** * @notice Enum to represent the type of supported token + * @dev Each field should be matched with the corresponding field of ClientChainID */ - enum TokenType { + enum Token { BTC } + /** + * @notice Enum to represent the supported client chain ID + * @dev Each field should be matched with the corresponding field of TokenType + */ + enum ClientChain { + Bitcoin + } + /** * @dev Enum to represent the status of a transaction */ enum TxStatus { - Pending, - Processed, - Expired + NotStarted, // 0: Default state - transaction hasn't started collecting proofs + Pending, // 1: Currently collecting witness proofs + Processed, // 2: Successfully processed + Expired // 3: Failed due to timeout, but can be retried } /** @@ -43,16 +53,14 @@ contract ExocoreBtcGatewayStorage { /** * @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 + struct StakeMsg { + ClientChain clientChain; + bytes srcAddress; // the address of the depositor on the source chain + address exocoreAddress; // the address of the depositor on the Exocore chain + string operator; // the operator to delegate to, would only deposit to exocore address if operator is empty + uint256 amount; // deposit amount uint64 nonce; - bytes txTag; // btc lowercase(txid-vout) - bytes payload; + bytes txTag; // lowercase(txid-vout) } /** @@ -60,7 +68,7 @@ contract ExocoreBtcGatewayStorage { */ struct Proof { address witness; - InterchainMsg message; + StakeMsg message; uint256 timestamp; bytes signature; } @@ -70,34 +78,41 @@ contract ExocoreBtcGatewayStorage { */ struct Transaction { TxStatus status; + ClientChain clientChain; uint256 amount; + bytes srcAddress; address recipient; uint256 expiryTime; uint256 proofCount; - mapping(address => bool) hasWitnessed; + mapping(address => uint256) witnessTime; } /** * @dev Struct for peg-out requests */ struct PegOutRequest { - address token; + ClientChain clientChain; address requester; - bytes btcAddress; + bytes clientChainAddress; uint256 amount; WithdrawType withdrawType; TxStatus status; uint256 timestamp; } - // Constants - uint32 public constant BITCOIN_CHAIN_ID = 1; - uint8 public constant BITCOIN_STAKER_ACCOUNT_LENGTH = 20; + /* -------------------------------------------------------------------------- */ + /* Constants */ + /* -------------------------------------------------------------------------- */ + // 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"; - address public constant VIRTUAL_BTC_ADDRESS = 0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB; - bytes public constant VIRTUAL_BTC_TOKEN = abi.encodePacked(bytes32(bytes20(VIRTUAL_BTC_ADDRESS))); + 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"; @@ -137,40 +152,42 @@ contract ExocoreBtcGatewayStorage { /** * @dev Mapping to store Bitcoin to Exocore address mappings */ - mapping(bytes => bytes) public btcToExocoreAddress; + mapping(bytes => address) public btcToExocoreAddress; /** * @dev Mapping to store Exocore to Bitcoin address mappings */ - mapping(bytes => bytes) public exocoreToBtcAddress; + mapping(address => bytes) public exocoreToBtcAddress; /** - * @dev Mapping to store whitelisted tokens + * @dev Mapping to store inbound bytes nonce for each chain and sender */ - mapping(address => bool) public isWhitelistedToken; + mapping(uint32 => mapping(bytes => uint64)) public inboundBytesNonce; /** - * @dev Mapping to store inbound bytes nonce for each chain and sender + * @notice Mapping to store delegation nonce for each chain and delegator + * @dev The nonce is incremented for each delegate/undelegate operation + * @dev The nonce is provided to the precompile as operation id */ - mapping(uint32 => mapping(bytes => uint64)) public inboundBytesNonce; + mapping(uint32 => mapping(address => uint64)) public delegationNonce; uint256[40] private __gap; // Events /** * @dev Emitted when a deposit is completed - * @param btcTxTag The Bitcoin transaction tag + * @param srcChainId The source chain ID + * @param txTag The txid + vout-index * @param depositorExoAddr The depositor's Exocore address - * @param token The token address - * @param depositorBtcAddr The depositor's Bitcoin address + * @param depositorClientChainAddr The depositor's client chain address * @param amount The amount deposited * @param updatedBalance The updated balance after deposit */ event DepositCompleted( - bytes btcTxTag, - bytes depositorExoAddr, - address indexed token, - bytes depositorBtcAddr, + uint32 indexed srcChainId, + bytes txTag, + address indexed depositorExoAddr, + bytes depositorClientChainAddr, uint256 amount, uint256 updatedBalance ); @@ -178,17 +195,17 @@ contract ExocoreBtcGatewayStorage { /** * @dev Emitted when a principal withdrawal is requested * @param requestId The unique identifier for the withdrawal request + * @param srcChainId The source chain ID * @param withdrawerExoAddr The withdrawer's Exocore address - * @param token The token address - * @param withdrawerBtcAddr The withdrawer's Bitcoin 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( + uint32 indexed srcChainId, bytes32 indexed requestId, address indexed withdrawerExoAddr, - address indexed token, - bytes withdrawerBtcAddr, + bytes withdrawerClientChainAddr, uint256 amount, uint256 updatedBalance ); @@ -196,99 +213,91 @@ contract ExocoreBtcGatewayStorage { /** * @dev Emitted when a reward withdrawal is requested * @param requestId The unique identifier for the withdrawal request + * @param srcChainId The source chain ID * @param withdrawerExoAddr The withdrawer's Exocore address - * @param token The token address - * @param withdrawerBtcAddr The withdrawer's Bitcoin 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( + uint32 indexed srcChainId, bytes32 indexed requestId, address indexed withdrawerExoAddr, - address indexed token, - bytes withdrawerBtcAddr, + bytes withdrawerClientChainAddr, uint256 amount, uint256 updatedBalance ); /** * @dev Emitted when a principal withdrawal is completed + * @param srcChainId The source chain ID * @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 withdrawerClientChainAddr The withdrawer's client chain address * @param amount The amount withdrawn * @param updatedBalance The updated balance after withdrawal */ event WithdrawPrincipalCompleted( + uint32 indexed srcChainId, bytes32 indexed requestId, address indexed withdrawerExoAddr, - address indexed token, - bytes withdrawerBtcAddr, + bytes withdrawerClientChainAddr, uint256 amount, uint256 updatedBalance ); /** * @dev Emitted when a reward withdrawal is completed + * @param srcChainId The source chain ID * @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 withdrawerClientChainAddr The withdrawer's client chain address * @param amount The amount withdrawn * @param updatedBalance The updated balance after withdrawal */ event WithdrawRewardCompleted( + uint32 indexed srcChainId, bytes32 indexed requestId, address indexed withdrawerExoAddr, - address indexed token, - bytes withdrawerBtcAddr, + bytes withdrawerClientChainAddr, uint256 amount, uint256 updatedBalance ); /** * @dev Emitted when a delegation is completed - * @param token The token address - * @param delegator The delegator's address + * @param clientChainId The LayerZero chain ID of the client chain + * @param exoDelegator The delegator's Exocore address * @param operator The operator's address * @param amount The amount delegated */ - event DelegationCompleted(address token, bytes delegator, bytes operator, uint256 amount); + event DelegationCompleted(uint32 clientChainId, address exoDelegator, string operator, uint256 amount); /** * @dev Emitted when an undelegation is completed - * @param token The token address - * @param delegator The delegator's address + * @param clientChainId The LayerZero chain ID of the client chain + * @param exoDelegator The delegator's Exocore address * @param operator The operator's address * @param amount The amount undelegated */ - event UndelegationCompleted(address token, bytes delegator, bytes operator, uint256 amount); + event UndelegationCompleted(uint32 clientChainId, address exoDelegator, string operator, uint256 amount); /** * @dev Emitted when a deposit and delegation is completed - * @param token The token address - * @param depositor The depositor's address + * @param clientChainId The LayerZero chain ID of the client chain + * @param exoDepositor The depositor's Exocore 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 - ); + event DepositAndDelegationCompleted(uint32 clientChainId, address exoDepositor, string 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); + event AddressRegistered(bytes depositor, address indexed exocoreAddress); /** * @dev Emitted when a new witness is added @@ -304,25 +313,25 @@ contract ExocoreBtcGatewayStorage { /** * @dev Emitted when a proof is submitted - * @param btcTxTag The Bitcoin transaction tag + * @param txTag The txid + vout-index * @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); + event ProofSubmitted(bytes txTag, address indexed witness, StakeMsg message); /** * @dev Emitted when a deposit is processed - * @param btcTxTag The Bitcoin transaction tag + * @param txTag The txid + vout-index * @param recipient The address of the recipient * @param amount The amount processed */ - event DepositProcessed(bytes btcTxTag, address indexed recipient, uint256 amount); + event DepositProcessed(bytes txTag, address indexed recipient, uint256 amount); /** * @dev Emitted when a transaction expires - * @param btcTxTag The Bitcoin transaction tag of the expired transaction + * @param txTag The txid + vout-index of the expired transaction */ - event TransactionExpired(bytes btcTxTag); + event TransactionExpired(bytes txTag); /** * @dev Emitted when a peg-out transaction expires @@ -373,12 +382,12 @@ contract ExocoreBtcGatewayStorage { /// @notice Emitted when a token is added to the whitelist. /// @param clientChainId The LayerZero chain ID of the client chain. /// @param token The address of the token. - event WhitelistTokenAdded(uint32 clientChainId, bytes32 token); + event WhitelistTokenAdded(uint32 clientChainId, address indexed token); /// @notice Emitted when a token is updated in the whitelist. /// @param clientChainId The LayerZero chain ID of the client chain. /// @param token The address of the token. - event WhitelistTokenUpdated(uint32 clientChainId, bytes32 token); + event WhitelistTokenUpdated(uint32 clientChainId, address indexed token); // Errors /** @@ -469,15 +478,7 @@ contract ExocoreBtcGatewayStorage { error UnexpectedInboundNonce(uint64 expectedNonce, uint64 actualNonce); error InvalidTokenType(); - - /** - * @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"); - _; - } + error InvalidClientChainId(); /** * @dev Modifier to check if an amount is valid @@ -502,12 +503,11 @@ contract ExocoreBtcGatewayStorage { inboundBytesNonce[srcChainId][srcAddress] = nonce; } - function getTokenByType(TokenType _tokenType) public pure returns (bytes memory) { - if (_tokenType == TokenType.BTC) { - return VIRTUAL_BTC_TOKEN; - } else { - revert InvalidTokenType(); - } + function getChainIdByToken(Token token) public pure returns (uint32) { + return uint32(uint8(token)) + 1; } + function getChainId(ClientChain clientChain) public pure returns (uint32) { + return uint32(uint8(clientChain)) + 1; + } } From c33e5f8a77a746ba4a08aeb1b11b8164851757f2 Mon Sep 17 00:00:00 2001 From: adu Date: Thu, 7 Nov 2024 17:08:12 +0800 Subject: [PATCH 03/11] refactor: process stakemsg including deposit and delegate --- src/core/ExocoreBtcGateway.sol | 413 +++++++++-------------- src/libraries/Errors.sol | 6 + src/storage/ExocoreBtcGatewayStorage.sol | 123 +++---- 3 files changed, 214 insertions(+), 328 deletions(-) diff --git a/src/core/ExocoreBtcGateway.sol b/src/core/ExocoreBtcGateway.sol index 60b0cd04..20546327 100644 --- a/src/core/ExocoreBtcGateway.sol +++ b/src/core/ExocoreBtcGateway.sol @@ -76,17 +76,17 @@ contract ExocoreBtcGateway is /** * @notice Activates token staking by registering or updating the chain and token with the Exocore system. */ - function activateStakingForClientChain(ClientChain clientChain_) external { - if (clientChain_ == ClientChain.Bitcoin) { + function activateStakingForClientChain(ClientChainID clientChain_) external { + if (clientChain_ == ClientChainID.Bitcoin) { _registerOrUpdateClientChain( - getChainId(clientChain_), + clientChain_, STAKER_ACCOUNT_LENGTH, BITCOIN_NAME, BITCOIN_METADATA, BITCOIN_SIGNATURE_SCHEME ); _registerOrUpdateToken( - getChainId(clientChain_), + clientChain_, VIRTUAL_TOKEN, BTC_DECIMALS, BTC_NAME, @@ -134,16 +134,6 @@ contract ExocoreBtcGateway is emit BridgeFeeUpdated(_newFee); } - /** - * @notice Checks and updates expired transactions. - * @param _txTags An array of transaction tags to check. - */ - function checkExpiredTransactions(bytes[] calldata _txTags) external { - for (uint256 i = 0; i < _txTags.length; i++) { - _revokeTxIfExpired(_txTags[i]); - } - } - /** * @notice Registers a BTC address with an Exocore address. * @param depositor The BTC address to register. @@ -151,42 +141,26 @@ contract ExocoreBtcGateway is * @dev Can only be called by an authorized witness. */ function registerAddress(bytes calldata depositor, address exocoreAddress) external onlyAuthorizedWitness { - require(depositor.length > 0 && exocoreAddress != address(0), "Invalid address"); - require(btcToExocoreAddress[depositor] != address(0), "Depositor address already registered"); - require(exocoreToBtcAddress[exocoreAddress].length == 0, "Exocore address already registered"); - - btcToExocoreAddress[depositor] = exocoreAddress; - exocoreToBtcAddress[exocoreAddress] = depositor; - - emit AddressRegistered(depositor, exocoreAddress); + _registerAddress(depositor, exocoreAddress); } /** - * @notice Submits a proof for a transaction. + * @notice Submits a proof for a stake message. + * @notice The submitted message would be processed after collecting enough proofs from withnesses. * @param _message The interchain message. * @param _signature The signature of the message. */ - function submitProof(StakeMsg calldata _message, bytes calldata _signature) + function submitProofForStakeMsg(StakeMsg calldata _message, bytes calldata _signature) external nonReentrant whenNotPaused { - // Verify the signature - if (processedBtcTxs[_message.txTag].processed) { - revert BtcTxAlreadyProcessed(); - } + bytes32 messageHash = _verifyStakeMessage(_message, _signature); // we should revoke the tx by setting it as expired if it has expired - _revokeTxIfExpired(_message.txTag); + _revokeTxIfExpired(messageHash); - // Verify nonce - _verifyAndUpdateBytesNonce(getChainId(_message.clientChain), _message.srcAddress, _message.nonce); - - // Verify signature - _verifySignature(_message, _signature); - - bytes memory txTag = _message.txTag; - Transaction storage txn = transactions[txTag]; + 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 @@ -197,23 +171,18 @@ contract ExocoreBtcGateway is txn.proofCount++; } else { txn.status = TxStatus.Pending; - txn.clientChain = _message.clientChain; - txn.amount = _message.amount; - txn.recipient = address(bytes20(_message.exocoreAddress)); txn.expiryTime = block.timestamp + PROOF_TIMEOUT; txn.proofCount = 1; txn.witnessTime[msg.sender] = block.timestamp; + txn.stakeMsg = _message; } - proofs[txTag].push( - Proof({witness: msg.sender, message: _message, timestamp: block.timestamp, signature: _signature}) - ); - - emit ProofSubmitted(txTag, msg.sender, _message); + emit ProofSubmitted(messageHash, msg.sender, _message); // Check for consensus if (txn.proofCount >= REQUIRED_PROOFS) { - _processDeposit(txTag); + _processStakeMsg(txn.stakeMsg); + delete transactions[messageHash]; } } @@ -222,7 +191,7 @@ contract ExocoreBtcGateway is * @param _msg The interchain message containing the deposit details. * @param signature The signature to verify. */ - function depositTo(StakeMsg calldata _msg, bytes calldata signature) + function processStakeMessage(StakeMsg calldata _msg, bytes calldata signature) external nonReentrant whenNotPaused @@ -230,12 +199,9 @@ contract ExocoreBtcGateway is onlyAuthorizedWitness { require(authorizedWitnesses[msg.sender], "Not an authorized witness"); - (bytes memory txTag, address depositorExoAddr) = _processAndVerify(_msg, signature); - - processedBtcTxs[txTag] = TxInfo(true, block.timestamp); + _verifyStakeMessage(_msg, signature); - // we use registered exocore address as the depositor - _deposit(getChainId(_msg.clientChain), _msg.srcAddress, depositorExoAddr, _msg.amount, _msg.txTag); + _processStakeMsg(_msg); } /** @@ -250,9 +216,13 @@ contract ExocoreBtcGateway is whenNotPaused isValidAmount(amount) { - uint32 chainId = getChainIdByToken(token); - uint64 nonce = ++delegationNonce[chainId][msg.sender]; - _delegate(chainId, nonce, msg.sender, operator, amount); + ClientChainID chainId = ClientChainID(uint8(token)); + bool success = _delegate(chainId, msg.sender, operator, amount); + if (!success) { + revert DelegationFailed(); + } + + emit DelegationCompleted(chainId, msg.sender, operator, amount); } /** @@ -267,9 +237,9 @@ contract ExocoreBtcGateway is whenNotPaused isValidAmount(amount) { - uint32 chainId = getChainIdByToken(token); + ClientChainID chainId = ClientChainID(uint8(token)); uint64 nonce = ++delegationNonce[chainId][msg.sender]; - bool success = DELEGATION_CONTRACT.undelegate(chainId, nonce, VIRTUAL_TOKEN, msg.sender.toExocoreBytes(), bytes(operator), amount); + bool success = DELEGATION_CONTRACT.undelegate(uint32(uint8(chainId)), nonce, VIRTUAL_TOKEN, msg.sender.toExocoreBytes(), bytes(operator), amount); if (!success) { revert UndelegationFailed(); } @@ -287,15 +257,15 @@ contract ExocoreBtcGateway is whenNotPaused isValidAmount(amount) { - uint32 chainId = getChainIdByToken(token); + ClientChainID chainId = ClientChainID(uint8(token)); (bool success, uint256 updatedBalance) = - ASSETS_CONTRACT.withdrawLST(chainId, VIRTUAL_TOKEN, msg.sender.toExocoreBytes(), amount); + ASSETS_CONTRACT.withdrawLST(uint32(uint8(chainId)), VIRTUAL_TOKEN, msg.sender.toExocoreBytes(), amount); if (!success) { revert WithdrawPrincipalFailed(); } - (bytes32 requestId, bytes memory clientChainAddress) = - _initiatePegOut(ClientChain(uint8(token)), amount, msg.sender, WithdrawType.WithdrawPrincipal); + (uint64 requestId, bytes memory clientChainAddress) = + _initiatePegOut(chainId, amount, msg.sender, WithdrawType.WithdrawPrincipal); emit WithdrawPrincipalRequested(chainId, requestId, msg.sender, clientChainAddress, amount, updatedBalance); } @@ -310,14 +280,14 @@ contract ExocoreBtcGateway is whenNotPaused isValidAmount(amount) { - uint32 chainId = getChainIdByToken(token); + ClientChainID chainId = ClientChainID(uint8(token)); (bool success, uint256 updatedBalance) = - REWARD_CONTRACT.claimReward(chainId, VIRTUAL_TOKEN, msg.sender.toExocoreBytes(), amount); + REWARD_CONTRACT.claimReward(uint32(uint8(chainId)), VIRTUAL_TOKEN, msg.sender.toExocoreBytes(), amount); if (!success) { revert WithdrawRewardFailed(); } - (bytes32 requestId, bytes memory clientChainAddress) = - _initiatePegOut(ClientChain(uint8(token)), amount, msg.sender, WithdrawType.WithdrawReward); + (uint64 requestId, bytes memory clientChainAddress) = + _initiatePegOut(chainId, amount, msg.sender, WithdrawType.WithdrawReward); emit WithdrawRewardRequested(chainId, requestId, msg.sender, clientChainAddress, amount, updatedBalance); } @@ -325,65 +295,29 @@ contract ExocoreBtcGateway is /** * @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) + function processNextPegOut(ClientChainID clientChain) external onlyAuthorizedWitness nonReentrant whenNotPaused + returns (uint64 requestId) { - PegOutRequest storage request = pegOutRequests[_requestId]; + requestId = ++outboundNonce[clientChain]; + PegOutRequest storage request = pegOutRequests[requestId]; - // Check if the request exists and has the correct status + // Check if the request exists if (request.requester == address(0)) { - revert RequestNotFound(_requestId); - } - if (request.status != TxStatus.Pending) { - revert InvalidRequestStatus(_requestId); + revert RequestNotFound(requestId); } - // Update request status - request.status = TxStatus.Processed; + // delete the request + delete pegOutRequests[requestId]; // Emit event - emit PegOutProcessed(_requestId, _btcTxTag); - } - - // Function to check and update expired peg-out requests - function checkExpiredPegOutRequests(bytes32[] calldata _requestIds) external { - 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 signature The signature to verify. - */ - function depositThenDelegateTo(StakeMsg calldata _msg, bytes calldata signature) - external - nonReentrant - whenNotPaused - isValidAmount(_msg.amount) - onlyAuthorizedWitness - { - (bytes memory txTag, address depositorExoAddr) = _processAndVerify(_msg, signature); - uint32 srcChainId = getChainId(_msg.clientChain); - _deposit(srcChainId, _msg.srcAddress, depositorExoAddr, _msg.amount, txTag); - - uint64 nonce = ++delegationNonce[srcChainId][msg.sender]; - _delegate(srcChainId, nonce, depositorExoAddr, _msg.operator, _msg.amount); + emit PegOutProcessed(requestId); } /** @@ -398,28 +332,10 @@ contract ExocoreBtcGateway is /** * @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 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); + function nextInboundNonce(ClientChainID srcChainId) external view returns (uint64) { + return inboundNonce[srcChainId]+1; } /** @@ -427,34 +343,10 @@ contract ExocoreBtcGateway is * @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) { + function getPegOutRequest(uint64 requestId) public view returns (PegOutRequest memory) { return pegOutRequests[requestId]; } - /** - * @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); - assembly { - mstore(add(bytesArray, 32), _bytes32) - } - return string(bytesArray); - } - - /** - * @notice Increments and gets the next nonce for a given source address. - * @param srcChainId The source chain ID. - * @param exocoreAddress The exocore address. - * @return The next nonce for corresponding btcAddress. - */ - function _nextNonce(uint32 srcChainId, address exocoreAddress) internal view returns (uint64) { - bytes memory depositor = exocoreToBtcAddress[exocoreAddress]; - return inboundBytesNonce[srcChainId][depositor] + 1; - } - /** * @notice Checks if a witness is authorized. * @param witness The witness address. @@ -479,58 +371,33 @@ contract ExocoreBtcGateway is /** * @notice Registers or updates the Bitcoin chain with the Exocore system. */ - function _registerOrUpdateClientChain(uint32 chainId, uint8 stakerAccountLength, string memory name, string memory metadata, string memory signatureScheme) internal { + function _registerOrUpdateClientChain(ClientChainID chainId, uint8 stakerAccountLength, string memory name, string memory metadata, string memory signatureScheme) internal { + uint32 chainIdUint32 = uint32(uint8(chainId)); (bool success, bool updated) = ASSETS_CONTRACT.registerOrUpdateClientChain( - chainId, stakerAccountLength, name, metadata, signatureScheme + chainIdUint32, stakerAccountLength, name, metadata, signatureScheme ); if (!success) { - revert Errors.RegisterClientChainToExocoreFailed(chainId); + revert Errors.RegisterClientChainToExocoreFailed(chainIdUint32); } if (updated) { - emit ClientChainUpdated(chainId); + emit ClientChainUpdated(chainIdUint32); } else { - emit ClientChainRegistered(chainId); + emit ClientChainRegistered(chainIdUint32); } } - function _registerOrUpdateToken(uint32 chainId, bytes memory token, uint8 decimals, string memory name, string memory metadata, string memory oracleInfo) internal { - bool registered = ASSETS_CONTRACT.registerToken(chainId, token, decimals, name, metadata, oracleInfo); + function _registerOrUpdateToken(ClientChainID chainId, bytes memory token, uint8 decimals, string memory name, string memory metadata, string memory oracleInfo) internal { + uint32 chainIdUint32 = uint32(uint8(chainId)); + bool registered = ASSETS_CONTRACT.registerToken(chainIdUint32, token, decimals, name, metadata, oracleInfo); if (!registered) { - bool updated = ASSETS_CONTRACT.updateToken(chainId, token, metadata); + bool updated = ASSETS_CONTRACT.updateToken(chainIdUint32, token, metadata); if (!updated) { - revert Errors.AddWhitelistTokenFailed(chainId, bytes32(token)); + revert Errors.AddWhitelistTokenFailed(chainIdUint32, bytes32(token)); } - emit WhitelistTokenUpdated(chainId, VIRTUAL_TOKEN_ADDRESS); + emit WhitelistTokenUpdated(chainIdUint32, VIRTUAL_TOKEN_ADDRESS); } else { - emit WhitelistTokenAdded(chainId, VIRTUAL_TOKEN_ADDRESS); - } - } - - /** - * @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; - } - - StakeMsg memory firstMsg = txProofs[0].message; - for (uint256 i = 1; i < txProofs.length; i++) { - StakeMsg memory currentMsg = txProofs[i].message; - if ( - firstMsg.clientChain != currentMsg.clientChain - || firstMsg.exocoreAddress != currentMsg.exocoreAddress - || keccak256(bytes(firstMsg.operator)) != keccak256(bytes(currentMsg.operator)) - || firstMsg.amount != currentMsg.amount - || firstMsg.nonce != currentMsg.nonce || keccak256(firstMsg.txTag) != keccak256(currentMsg.txTag) - ) { - return false; - } + emit WhitelistTokenAdded(chainIdUint32, VIRTUAL_TOKEN_ADDRESS); } - return true; } /** @@ -538,72 +405,63 @@ contract ExocoreBtcGateway is * @param _msg The interchain message. * @param signature The signature to verify. */ - function _verifySignature(StakeMsg calldata _msg, bytes memory signature) internal view { + function _verifySignature(StakeMsg calldata _msg, bytes memory signature) internal view returns (bytes32 messageHash) { // StakeMsg, EIP721 is preferred next step. bytes memory encodeMsg = abi.encode( - _msg.clientChain, + _msg.chainId, _msg.srcAddress, _msg.operator, _msg.amount, _msg.nonce, _msg.txTag ); - bytes32 messageHash = keccak256(encodeMsg); + messageHash = keccak256(encodeMsg); SignatureVerifier.verifyMsgSig(msg.sender, messageHash, signature); } /** - * @notice Processes and verifies an interchain message. - * @param _msg The interchain message. - * @param signature The signature to verify. - * @return txTag The lowercase of BTC txid-vout. - * @return depositorExoAddress The Exocore address of the depositor. + * @dev Verifies that all required fields in StakeMsg are valid + * @param _msg The stake message to verify */ - function _processAndVerify(StakeMsg calldata _msg, bytes calldata signature) - internal - returns (bytes memory txTag, address depositorExoAddress) - { - txTag = _msg.txTag; - depositorExoAddress = btcToExocoreAddress[_msg.srcAddress]; - if (depositorExoAddress == address(0)) { - revert BtcAddressNotRegistered(); - } + function _verifyStakeMsgFields(StakeMsg calldata _msg) internal pure { + // Combine all non-zero checks into a single value + uint256 validityCheck = uint8(_msg.chainId) + | _msg.srcAddress.length + | _msg.amount + | _msg.nonce + | _msg.txTag.length; + + if (validityCheck == 0) revert Errors.InvalidStakeMessage(); + } + function _verifyTxTagNotProcessed(bytes calldata txTag) internal view { if (processedBtcTxs[txTag].processed) { - revert BtcTxAlreadyProcessed(); + revert Errors.TxTagAlreadyProcessed(); } - - // Verify nonce - _verifyAndUpdateBytesNonce(getChainId(_msg.clientChain), _msg.srcAddress, _msg.nonce); - - // Verify signature - _verifySignature(_msg, signature); } /** - * @notice Processes a deposit after sufficient proofs have been submitted. - * @param _txTag The transaction tag of the deposit to process. + * @notice Verifies a stake message. + * @param _msg The stake message. + * @param signature The signature to verify. */ - 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; + function _verifyStakeMessage(StakeMsg calldata _msg, bytes calldata signature) + internal + view + returns (bytes32 messageHash) + { + // verify that the stake message fields are valid + _verifyStakeMsgFields(_msg); - //todo:call precompile depositTo - _deposit(getChainId(txn.clientChain), txn.srcAddress, txn.recipient, txn.amount, _txTag); - txn.status = TxStatus.Processed; + // Verify nonce + _verifyInboundNonce(_msg.chainId, _msg.nonce); - // totalDeposited += txn.amount; + // Verify that the txTag has not been processed + _verifyTxTagNotProcessed(_msg.txTag); - emit DepositProcessed(_txTag, txn.recipient, amountAfterFee); + // Verify signature + messageHash = _verifySignature(_msg, signature); } /** @@ -618,12 +476,10 @@ contract ExocoreBtcGateway is * @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(ClientChain clientChain, uint256 _amount, address withdrawer, WithdrawType _withdrawType) + function _initiatePegOut(ClientChainID clientChain, uint256 _amount, address withdrawer, WithdrawType _withdrawType) internal - returns (bytes32 requestId, bytes memory clientChainAddress) - { - // Use storage pointer to reduce gas consumption - PegOutRequest storage request; + returns (uint64 requestId, bytes memory clientChainAddress) + { // 1. Check client c address clientChainAddress = exocoreToBtcAddress[withdrawer]; @@ -631,22 +487,21 @@ contract ExocoreBtcGateway is revert BtcAddressNotRegistered(); } - // 2. Generate unique requestId - requestId = keccak256(abi.encodePacked(clientChain, withdrawer, clientChainAddress, _amount, block.number)); + // 2. increase the peg-out nonce for the client chain and return as requestId + requestId = ++pegOutNonce[clientChain]; // 3. Check if request already exists - request = pegOutRequests[requestId]; + PegOutRequest storage request = pegOutRequests[requestId]; if (request.requester != address(0)) { revert RequestAlreadyExists(requestId); } // 4. Create new PegOutRequest - request.clientChain = clientChain; + request.chainId = clientChain; request.requester = withdrawer; request.clientChainAddress = clientChainAddress; request.amount = _amount; request.withdrawType = _withdrawType; - request.status = TxStatus.Pending; request.timestamp = block.timestamp; } @@ -659,14 +514,14 @@ contract ExocoreBtcGateway is * @param txTag The transaction tag. */ function _deposit( - uint32 clientChainId, + ClientChainID clientChainId, bytes memory srcAddress, address depositorExoAddr, uint256 amount, bytes memory txTag ) internal { (bool success, uint256 updatedBalance) = - ASSETS_CONTRACT.depositLST(clientChainId, VIRTUAL_TOKEN, depositorExoAddr.toExocoreBytes(), amount); + ASSETS_CONTRACT.depositLST(uint32(uint8(clientChainId)), VIRTUAL_TOKEN, depositorExoAddr.toExocoreBytes(), amount); if (!success) { revert DepositFailed(txTag); } @@ -680,32 +535,68 @@ contract ExocoreBtcGateway is * @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( - uint32 clientChainId, - uint64 nonce, + ClientChainID clientChainId, address delegator, string memory operator, uint256 amount - ) internal { - bool success = DELEGATION_CONTRACT.delegate(clientChainId, nonce, VIRTUAL_TOKEN, delegator.toExocoreBytes(), bytes(operator), amount); - if (!success) { - revert DelegationFailed(); - } - emit DelegationCompleted(clientChainId, delegator, operator, amount); + ) internal returns(bool success){ + uint64 nonce = ++delegationNonce[clientChainId][delegator]; + success = DELEGATION_CONTRACT.delegate(uint32(uint8(clientChainId)), nonce, VIRTUAL_TOKEN, delegator.toExocoreBytes(), bytes(operator), amount); } - function _revokeTxIfExpired(bytes calldata txTag) internal { - Transaction storage txn = transactions[txTag]; + 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(txTag); + emit TransactionExpired(txid); } } - // encode address as byte array with 32 bytes, and pad with zeros from right - function addressToExocoreBytes(address addr) internal pure returns (bytes memory) { - return abi.encodePacked(bytes32(bytes20(addr))); + function _registerAddress(bytes memory depositor, address exocoreAddress) internal { + require(depositor.length > 0 && exocoreAddress != address(0), "Invalid address"); + require(btcToExocoreAddress[depositor] != address(0), "Depositor address already registered"); + require(exocoreToBtcAddress[exocoreAddress].length == 0, "Exocore address already registered"); + + btcToExocoreAddress[depositor] = exocoreAddress; + exocoreToBtcAddress[exocoreAddress] = depositor; + + emit AddressRegistered(depositor, exocoreAddress); + } + + function _processStakeMsg(StakeMsg memory _msg) internal { + // increment inbound nonce for the client chain and mark the tx as processed + inboundNonce[_msg.chainId]++; + processedBtcTxs[_msg.txTag] = TxInfo(true, block.timestamp); + + // register address if not already registered + if (btcToExocoreAddress[_msg.srcAddress] == address(0) && exocoreToBtcAddress[_msg.exocoreAddress].length == 0) { + if (_msg.exocoreAddress == address(0)) { + revert Errors.ZeroAddress(); + } + _registerAddress(_msg.srcAddress, _msg.exocoreAddress); + } + + address stakerExoAddr = btcToExocoreAddress[_msg.srcAddress]; + uint256 fee = _msg.amount * bridgeFee; + 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.chainId, _msg.srcAddress, 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.chainId, stakerExoAddr, _msg.operator, amountAfterFee); + if (!success) { + emit DelegationFailedForStake(_msg.chainId, stakerExoAddr, _msg.operator, amountAfterFee); + } else { + emit DelegationCompleted(_msg.chainId, stakerExoAddr, _msg.operator, amountAfterFee); + } + } } } diff --git a/src/libraries/Errors.sol b/src/libraries/Errors.sol index 00eff708..f3029dc7 100644 --- a/src/libraries/Errors.sol +++ b/src/libraries/Errors.sol @@ -318,4 +318,10 @@ library Errors { /// @dev ExocoreBtcGateway: witness has already submitted proof error WitnessAlreadySubmittedProof(); + /// @dev ExocoreBtcGateway: invalid stake message + error InvalidStakeMessage(); + + /// @dev ExocoreBtcGateway: transaction tag has already been processed + error TxTagAlreadyProcessed(); + } diff --git a/src/storage/ExocoreBtcGatewayStorage.sol b/src/storage/ExocoreBtcGatewayStorage.sol index 7be1deeb..ab8aecce 100644 --- a/src/storage/ExocoreBtcGatewayStorage.sol +++ b/src/storage/ExocoreBtcGatewayStorage.sol @@ -12,15 +12,17 @@ contract ExocoreBtcGatewayStorage { * @dev Each field should be matched with the corresponding field of ClientChainID */ enum Token { - BTC + 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 TokenType + * @dev Each field should be matched with the corresponding field of Token */ - enum ClientChain { - Bitcoin + enum ClientChainID { + None, // 0: Invalid/uninitialized chain + Bitcoin // 1: Bitcoin chain, matches with Token.BTC } /** @@ -29,8 +31,7 @@ contract ExocoreBtcGatewayStorage { enum TxStatus { NotStarted, // 0: Default state - transaction hasn't started collecting proofs Pending, // 1: Currently collecting witness proofs - Processed, // 2: Successfully processed - Expired // 3: Failed due to timeout, but can be retried + Expired // 2: Failed due to timeout, but can be retried } /** @@ -54,7 +55,7 @@ contract ExocoreBtcGatewayStorage { * @dev Struct to store interchain message information */ struct StakeMsg { - ClientChain clientChain; + ClientChainID chainId; bytes srcAddress; // the address of the depositor on the source chain address exocoreAddress; // the address of the depositor on the Exocore chain string operator; // the operator to delegate to, would only deposit to exocore address if operator is empty @@ -78,25 +79,22 @@ contract ExocoreBtcGatewayStorage { */ struct Transaction { TxStatus status; - ClientChain clientChain; - uint256 amount; - bytes srcAddress; - address recipient; - uint256 expiryTime; uint256 proofCount; + uint256 expiryTime; mapping(address => uint256) witnessTime; + StakeMsg stakeMsg; } /** * @dev Struct for peg-out requests */ struct PegOutRequest { - ClientChain clientChain; + ClientChainID chainId; + uint64 nonce; address requester; bytes clientChainAddress; uint256 amount; WithdrawType withdrawType; - TxStatus status; uint256 timestamp; } @@ -123,16 +121,10 @@ contract ExocoreBtcGatewayStorage { 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 + * @dev Mapping to store transaction information, key is the message hash */ - mapping(bytes => Transaction) public transactions; + mapping(bytes32 => Transaction) public transactions; /** * @dev Mapping to store processed Bitcoin transactions @@ -140,9 +132,9 @@ contract ExocoreBtcGatewayStorage { mapping(bytes => TxInfo) public processedBtcTxs; /** - * @dev Mapping to store peg-out requests + * @dev Mapping to store peg-out requests, key is the nonce */ - mapping(bytes32 => PegOutRequest) public pegOutRequests; + mapping(uint64 => PegOutRequest) public pegOutRequests; /** * @dev Mapping to store authorized witnesses @@ -160,16 +152,26 @@ contract ExocoreBtcGatewayStorage { mapping(address => bytes) public exocoreToBtcAddress; /** - * @dev Mapping to store inbound bytes nonce for each chain and sender + * @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(uint32 => mapping(bytes => uint64)) public inboundBytesNonce; + mapping(ClientChainID => uint64) public pegOutNonce; /** * @notice Mapping to store delegation nonce for each chain and delegator * @dev The nonce is incremented for each delegate/undelegate operation * @dev The nonce is provided to the precompile as operation id */ - mapping(uint32 => mapping(address => uint64)) public delegationNonce; + mapping(ClientChainID => mapping(address => uint64)) public delegationNonce; uint256[40] private __gap; @@ -184,7 +186,7 @@ contract ExocoreBtcGatewayStorage { * @param updatedBalance The updated balance after deposit */ event DepositCompleted( - uint32 indexed srcChainId, + ClientChainID indexed srcChainId, bytes txTag, address indexed depositorExoAddr, bytes depositorClientChainAddr, @@ -202,8 +204,8 @@ contract ExocoreBtcGatewayStorage { * @param updatedBalance The updated balance after withdrawal request */ event WithdrawPrincipalRequested( - uint32 indexed srcChainId, - bytes32 indexed requestId, + ClientChainID indexed srcChainId, + uint64 indexed requestId, address indexed withdrawerExoAddr, bytes withdrawerClientChainAddr, uint256 amount, @@ -220,8 +222,8 @@ contract ExocoreBtcGatewayStorage { * @param updatedBalance The updated balance after withdrawal request */ event WithdrawRewardRequested( - uint32 indexed srcChainId, - bytes32 indexed requestId, + ClientChainID indexed srcChainId, + uint64 indexed requestId, address indexed withdrawerExoAddr, bytes withdrawerClientChainAddr, uint256 amount, @@ -238,7 +240,7 @@ contract ExocoreBtcGatewayStorage { * @param updatedBalance The updated balance after withdrawal */ event WithdrawPrincipalCompleted( - uint32 indexed srcChainId, + ClientChainID indexed srcChainId, bytes32 indexed requestId, address indexed withdrawerExoAddr, bytes withdrawerClientChainAddr, @@ -256,7 +258,7 @@ contract ExocoreBtcGatewayStorage { * @param updatedBalance The updated balance after withdrawal */ event WithdrawRewardCompleted( - uint32 indexed srcChainId, + ClientChainID indexed srcChainId, bytes32 indexed requestId, address indexed withdrawerExoAddr, bytes withdrawerClientChainAddr, @@ -271,26 +273,25 @@ contract ExocoreBtcGatewayStorage { * @param operator The operator's address * @param amount The amount delegated */ - event DelegationCompleted(uint32 clientChainId, address exoDelegator, string operator, uint256 amount); + event DelegationCompleted(ClientChainID indexed clientChainId, address indexed exoDelegator, string operator, uint256 amount); /** - * @dev Emitted when an undelegation is completed + * @dev Emitted when a delegation fails for a stake message * @param clientChainId The LayerZero chain ID of the client chain * @param exoDelegator The delegator's Exocore address * @param operator The operator's address - * @param amount The amount undelegated + * @param amount The amount delegated */ - event UndelegationCompleted(uint32 clientChainId, address exoDelegator, string operator, uint256 amount); + event DelegationFailedForStake(ClientChainID indexed clientChainId, address indexed exoDelegator, string operator, uint256 amount); /** - * @dev Emitted when a deposit and delegation is completed + * @dev Emitted when an undelegation is completed * @param clientChainId The LayerZero chain ID of the client chain - * @param exoDepositor The depositor's Exocore address + * @param exoDelegator The delegator's Exocore address * @param operator The operator's address - * @param amount The amount deposited and delegated - * @param updatedBalance The updated balance after the operation + * @param amount The amount undelegated */ - event DepositAndDelegationCompleted(uint32 clientChainId, address exoDepositor, string operator, uint256 amount, uint256 updatedBalance); + event UndelegationCompleted(ClientChainID indexed clientChainId, address indexed exoDelegator, string operator, uint256 amount); /** * @dev Emitted when an address is registered @@ -313,11 +314,11 @@ contract ExocoreBtcGatewayStorage { /** * @dev Emitted when a proof is submitted - * @param txTag The txid + vout-index + * @param messageHash The hash of the stake message * @param witness The address of the witness submitting the proof - * @param message The interchain message associated with the proof + * @param message The stake message associated with the proof */ - event ProofSubmitted(bytes txTag, address indexed witness, StakeMsg message); + event ProofSubmitted(bytes32 indexed messageHash, address indexed witness, StakeMsg message); /** * @dev Emitted when a deposit is processed @@ -329,9 +330,9 @@ contract ExocoreBtcGatewayStorage { /** * @dev Emitted when a transaction expires - * @param txTag The txid + vout-index of the expired transaction + * @param txid The message hash of the expired transaction */ - event TransactionExpired(bytes txTag); + event TransactionExpired(bytes32 txid); /** * @dev Emitted when a peg-out transaction expires @@ -360,9 +361,8 @@ contract ExocoreBtcGatewayStorage { /** * @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); + event PegOutProcessed(uint64 indexed requestId); /** * @dev Emitted when a peg-out request status is updated @@ -426,13 +426,13 @@ contract ExocoreBtcGatewayStorage { * @dev Thrown when the requested peg-out does not exist * @param requestId The ID of the non-existent request */ - error RequestNotFound(bytes32 requestId); + error RequestNotFound(uint64 requestId); /** * @dev Thrown when attempting to create a request that already exists * @param requestId The ID of the existing request */ - error RequestAlreadyExists(bytes32 requestId); + error RequestAlreadyExists(uint64 requestId); /** * @dev Thrown when a deposit operation fails @@ -451,7 +451,7 @@ contract ExocoreBtcGatewayStorage { error WithdrawRewardFailed(); /** - * @dev Thrown when a delegation operation fails + * @dev Thrown when a delegation operation fails, not when processing a stake message */ error DelegationFailed(); @@ -492,22 +492,11 @@ contract ExocoreBtcGatewayStorage { /** * @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); + function _verifyInboundNonce(ClientChainID srcChainId, uint64 nonce) internal view { + if (nonce != inboundNonce[srcChainId] + 1) { + revert UnexpectedInboundNonce(inboundNonce[srcChainId] + 1, nonce); } - inboundBytesNonce[srcChainId][srcAddress] = nonce; - } - - function getChainIdByToken(Token token) public pure returns (uint32) { - return uint32(uint8(token)) + 1; - } - - function getChainId(ClientChain clientChain) public pure returns (uint32) { - return uint32(uint8(clientChain)) + 1; } } From 6e2885b4ef85cd99d52b1f3e86408147addf8b08 Mon Sep 17 00:00:00 2001 From: adu Date: Fri, 8 Nov 2024 10:21:26 +0800 Subject: [PATCH 04/11] refactor: add modifiers --- src/core/ExocoreBtcGateway.sol | 159 +++++++++++++---------- src/libraries/ExocoreBytes.sol | 2 + src/storage/ExocoreBtcGatewayStorage.sol | 68 +++++++--- 3 files changed, 138 insertions(+), 91 deletions(-) diff --git a/src/core/ExocoreBtcGateway.sol b/src/core/ExocoreBtcGateway.sol index 20546327..2355d2de 100644 --- a/src/core/ExocoreBtcGateway.sol +++ b/src/core/ExocoreBtcGateway.sol @@ -27,6 +27,7 @@ contract ExocoreBtcGateway is ReentrancyGuardUpgradeable, ExocoreBtcGatewayStorage { + using ExocoreBytes for address; /** @@ -79,20 +80,9 @@ contract ExocoreBtcGateway is function activateStakingForClientChain(ClientChainID clientChain_) external { if (clientChain_ == ClientChainID.Bitcoin) { _registerOrUpdateClientChain( - clientChain_, - STAKER_ACCOUNT_LENGTH, - BITCOIN_NAME, - BITCOIN_METADATA, - BITCOIN_SIGNATURE_SCHEME - ); - _registerOrUpdateToken( - clientChain_, - VIRTUAL_TOKEN, - BTC_DECIMALS, - BTC_NAME, - BTC_METADATA, - BTC_ORACLE_INFO + clientChain_, STAKER_ACCOUNT_LENGTH, BITCOIN_NAME, BITCOIN_METADATA, BITCOIN_SIGNATURE_SCHEME ); + _registerOrUpdateToken(clientChain_, VIRTUAL_TOKEN, BTC_DECIMALS, BTC_NAME, BTC_METADATA, BTC_ORACLE_INFO); } else { revert InvalidTokenType(); } @@ -134,16 +124,6 @@ contract ExocoreBtcGateway is emit BridgeFeeUpdated(_newFee); } - /** - * @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, address exocoreAddress) external onlyAuthorizedWitness { - _registerAddress(depositor, exocoreAddress); - } - /** * @notice Submits a proof for a stake message. * @notice The submitted message would be processed after collecting enough proofs from withnesses. @@ -163,7 +143,8 @@ contract ExocoreBtcGateway is 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 the witness has already submitted proof at or after the start of the proof window, they cannot submit + // again if (txn.witnessTime[msg.sender] >= txn.expiryTime - PROOF_TIMEOUT) { revert Errors.WitnessAlreadySubmittedProof(); } @@ -215,8 +196,11 @@ contract ExocoreBtcGateway is nonReentrant whenNotPaused isValidAmount(amount) + isValidToken(token) + isRegistered(token, msg.sender) { ClientChainID chainId = ClientChainID(uint8(token)); + bool success = _delegate(chainId, msg.sender, operator, amount); if (!success) { revert DelegationFailed(); @@ -236,10 +220,15 @@ contract ExocoreBtcGateway is nonReentrant whenNotPaused isValidAmount(amount) + isValidToken(token) + isRegistered(token, msg.sender) { ClientChainID chainId = ClientChainID(uint8(token)); - uint64 nonce = ++delegationNonce[chainId][msg.sender]; - bool success = DELEGATION_CONTRACT.undelegate(uint32(uint8(chainId)), nonce, VIRTUAL_TOKEN, msg.sender.toExocoreBytes(), bytes(operator), amount); + + uint64 nonce = ++delegationNonce[chainId]; + bool success = DELEGATION_CONTRACT.undelegate( + uint32(uint8(chainId)), nonce, VIRTUAL_TOKEN, msg.sender.toExocoreBytes(), bytes(operator), amount + ); if (!success) { revert UndelegationFailed(); } @@ -256,8 +245,11 @@ contract ExocoreBtcGateway is nonReentrant whenNotPaused isValidAmount(amount) + isValidToken(token) + isRegistered(token, msg.sender) { ClientChainID chainId = ClientChainID(uint8(token)); + (bool success, uint256 updatedBalance) = ASSETS_CONTRACT.withdrawLST(uint32(uint8(chainId)), VIRTUAL_TOKEN, msg.sender.toExocoreBytes(), amount); if (!success) { @@ -279,8 +271,11 @@ contract ExocoreBtcGateway is nonReentrant whenNotPaused isValidAmount(amount) + isValidToken(token) + isRegistered(token, msg.sender) { ClientChainID chainId = ClientChainID(uint8(token)); + (bool success, uint256 updatedBalance) = REWARD_CONTRACT.claimReward(uint32(uint8(chainId)), VIRTUAL_TOKEN, msg.sender.toExocoreBytes(), amount); if (!success) { @@ -321,12 +316,17 @@ contract ExocoreBtcGateway is } /** - * @notice Gets the BTC address corresponding to an Exocore address. - * @param exocoreAddress The Exocore address. - * @return The corresponding BTC address. + * @notice Gets the client chain address for a given Exocore address + * @param chainId The client chain ID + * @param exocoreAddress The Exocore address + * @return The client chain address */ - function getBtcAddress(address exocoreAddress) external view returns (bytes memory) { - return exocoreToBtcAddress[exocoreAddress]; + function getClientChainAddress(ClientChainID chainId, address exocoreAddress) + external + view + returns (bytes memory) + { + return outboundRegistry[chainId][exocoreAddress]; } /** @@ -335,7 +335,7 @@ contract ExocoreBtcGateway is * @return The current nonce. */ function nextInboundNonce(ClientChainID srcChainId) external view returns (uint64) { - return inboundNonce[srcChainId]+1; + return inboundNonce[srcChainId] + 1; } /** @@ -371,7 +371,13 @@ contract ExocoreBtcGateway is /** * @notice Registers or updates the Bitcoin chain with the Exocore system. */ - function _registerOrUpdateClientChain(ClientChainID chainId, uint8 stakerAccountLength, string memory name, string memory metadata, string memory signatureScheme) internal { + function _registerOrUpdateClientChain( + ClientChainID chainId, + uint8 stakerAccountLength, + string memory name, + string memory metadata, + string memory signatureScheme + ) internal { uint32 chainIdUint32 = uint32(uint8(chainId)); (bool success, bool updated) = ASSETS_CONTRACT.registerOrUpdateClientChain( chainIdUint32, stakerAccountLength, name, metadata, signatureScheme @@ -386,7 +392,14 @@ contract ExocoreBtcGateway is } } - function _registerOrUpdateToken(ClientChainID chainId, bytes memory token, uint8 decimals, string memory name, string memory metadata, string memory oracleInfo) internal { + function _registerOrUpdateToken( + ClientChainID chainId, + bytes memory token, + uint8 decimals, + string memory name, + string memory metadata, + string memory oracleInfo + ) internal { uint32 chainIdUint32 = uint32(uint8(chainId)); bool registered = ASSETS_CONTRACT.registerToken(chainIdUint32, token, decimals, name, metadata, oracleInfo); if (!registered) { @@ -405,15 +418,14 @@ contract ExocoreBtcGateway is * @param _msg The interchain message. * @param signature The signature to verify. */ - function _verifySignature(StakeMsg calldata _msg, bytes memory signature) internal view returns (bytes32 messageHash) { + function _verifySignature(StakeMsg calldata _msg, bytes memory signature) + internal + view + returns (bytes32 messageHash) + { // StakeMsg, EIP721 is preferred next step. bytes memory encodeMsg = abi.encode( - _msg.chainId, - _msg.srcAddress, - _msg.operator, - _msg.amount, - _msg.nonce, - _msg.txTag + _msg.chainId, _msg.srcAddress, _msg.exocoreAddress, _msg.operator, _msg.amount, _msg.nonce, _msg.txTag ); messageHash = keccak256(encodeMsg); @@ -426,13 +438,12 @@ contract ExocoreBtcGateway is */ function _verifyStakeMsgFields(StakeMsg calldata _msg) internal pure { // Combine all non-zero checks into a single value - uint256 validityCheck = uint8(_msg.chainId) - | _msg.srcAddress.length - | _msg.amount - | _msg.nonce - | _msg.txTag.length; - - if (validityCheck == 0) revert Errors.InvalidStakeMessage(); + uint256 validityCheck = + uint8(_msg.chainId) | _msg.srcAddress.length | _msg.amount | _msg.nonce | _msg.txTag.length; + + if (validityCheck == 0) { + revert Errors.InvalidStakeMessage(); + } } function _verifyTxTagNotProcessed(bytes calldata txTag) internal view { @@ -479,12 +490,11 @@ contract ExocoreBtcGateway is function _initiatePegOut(ClientChainID clientChain, uint256 _amount, address withdrawer, WithdrawType _withdrawType) internal returns (uint64 requestId, bytes memory clientChainAddress) - { - + { // 1. Check client c address - clientChainAddress = exocoreToBtcAddress[withdrawer]; + clientChainAddress = outboundRegistry[clientChain][withdrawer]; if (clientChainAddress.length == 0) { - revert BtcAddressNotRegistered(); + revert AddressNotRegistered(); } // 2. increase the peg-out nonce for the client chain and return as requestId @@ -520,8 +530,9 @@ contract ExocoreBtcGateway is uint256 amount, bytes memory txTag ) internal { - (bool success, uint256 updatedBalance) = - ASSETS_CONTRACT.depositLST(uint32(uint8(clientChainId)), VIRTUAL_TOKEN, depositorExoAddr.toExocoreBytes(), amount); + (bool success, uint256 updatedBalance) = ASSETS_CONTRACT.depositLST( + uint32(uint8(clientChainId)), VIRTUAL_TOKEN, depositorExoAddr.toExocoreBytes(), amount + ); if (!success) { revert DepositFailed(txTag); } @@ -538,14 +549,14 @@ contract ExocoreBtcGateway is * @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][delegator]; - success = DELEGATION_CONTRACT.delegate(uint32(uint8(clientChainId)), nonce, VIRTUAL_TOKEN, delegator.toExocoreBytes(), bytes(operator), amount); + 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 { @@ -556,15 +567,15 @@ contract ExocoreBtcGateway is } } - function _registerAddress(bytes memory depositor, address exocoreAddress) internal { + function _registerAddress(ClientChainID chainId, bytes memory depositor, address exocoreAddress) internal { require(depositor.length > 0 && exocoreAddress != address(0), "Invalid address"); - require(btcToExocoreAddress[depositor] != address(0), "Depositor address already registered"); - require(exocoreToBtcAddress[exocoreAddress].length == 0, "Exocore address already registered"); + require(inboundRegistry[chainId][depositor] != address(0), "Depositor address already registered"); + require(outboundRegistry[chainId][exocoreAddress].length == 0, "Exocore address already registered"); - btcToExocoreAddress[depositor] = exocoreAddress; - exocoreToBtcAddress[exocoreAddress] = depositor; + inboundRegistry[chainId][depositor] = exocoreAddress; + outboundRegistry[chainId][exocoreAddress] = depositor; - emit AddressRegistered(depositor, exocoreAddress); + emit AddressRegistered(chainId, depositor, exocoreAddress); } function _processStakeMsg(StakeMsg memory _msg) internal { @@ -573,14 +584,17 @@ contract ExocoreBtcGateway is processedBtcTxs[_msg.txTag] = TxInfo(true, block.timestamp); // register address if not already registered - if (btcToExocoreAddress[_msg.srcAddress] == address(0) && exocoreToBtcAddress[_msg.exocoreAddress].length == 0) { + if ( + inboundRegistry[_msg.chainId][_msg.srcAddress] == address(0) + && outboundRegistry[_msg.chainId][_msg.exocoreAddress].length == 0 + ) { if (_msg.exocoreAddress == address(0)) { revert Errors.ZeroAddress(); } - _registerAddress(_msg.srcAddress, _msg.exocoreAddress); + _registerAddress(_msg.chainId, _msg.srcAddress, _msg.exocoreAddress); } - address stakerExoAddr = btcToExocoreAddress[_msg.srcAddress]; + address stakerExoAddr = inboundRegistry[_msg.chainId][_msg.srcAddress]; uint256 fee = _msg.amount * bridgeFee; uint256 amountAfterFee = _msg.amount - fee; @@ -588,7 +602,8 @@ contract ExocoreBtcGateway is // this should always succeed and never revert, otherwise something is wrong. _deposit(_msg.chainId, _msg.srcAddress, 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 + // 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.chainId, stakerExoAddr, _msg.operator, amountAfterFee); if (!success) { diff --git a/src/libraries/ExocoreBytes.sol b/src/libraries/ExocoreBytes.sol index 78e328b6..7d5ee374 100644 --- a/src/libraries/ExocoreBytes.sol +++ b/src/libraries/ExocoreBytes.sol @@ -2,10 +2,12 @@ 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 index ab8aecce..54c8e318 100644 --- a/src/storage/ExocoreBtcGatewayStorage.sol +++ b/src/storage/ExocoreBtcGatewayStorage.sol @@ -12,8 +12,9 @@ contract ExocoreBtcGatewayStorage { * @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 + None, // 0: Invalid/uninitialized token + BTC // 1: Bitcoin token, matches with ClientChainID.Bitcoin + } /** @@ -21,17 +22,19 @@ contract ExocoreBtcGatewayStorage { * @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 + None, // 0: Invalid/uninitialized chain + Bitcoin // 1: Bitcoin chain, matches with Token.BTC + } /** * @dev Enum to represent the status of a transaction */ enum TxStatus { - NotStarted, // 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 + NotStarted, // 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 + } /** @@ -142,14 +145,20 @@ contract ExocoreBtcGatewayStorage { mapping(address => bool) public authorizedWitnesses; /** - * @dev Mapping to store Bitcoin to Exocore address mappings + * @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(bytes => address) public btcToExocoreAddress; + mapping(ClientChainID => mapping(bytes => address)) public inboundRegistry; /** - * @dev Mapping to store Exocore to Bitcoin address mappings + * @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(address => bytes) public exocoreToBtcAddress; + mapping(ClientChainID => mapping(address => bytes)) public outboundRegistry; /** * @dev Mapping to store inbound nonce for each chain @@ -167,11 +176,11 @@ contract ExocoreBtcGatewayStorage { mapping(ClientChainID => uint64) public pegOutNonce; /** - * @notice Mapping to store delegation nonce for each chain and delegator + * @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 => mapping(address => uint64)) public delegationNonce; + mapping(ClientChainID => uint64) public delegationNonce; uint256[40] private __gap; @@ -273,7 +282,9 @@ contract ExocoreBtcGatewayStorage { * @param operator The operator's address * @param amount The amount delegated */ - event DelegationCompleted(ClientChainID indexed clientChainId, address indexed exoDelegator, string operator, uint256 amount); + event DelegationCompleted( + ClientChainID indexed clientChainId, address indexed exoDelegator, string operator, uint256 amount + ); /** * @dev Emitted when a delegation fails for a stake message @@ -282,7 +293,9 @@ contract ExocoreBtcGatewayStorage { * @param operator The operator's address * @param amount The amount delegated */ - event DelegationFailedForStake(ClientChainID indexed clientChainId, address indexed exoDelegator, string operator, uint256 amount); + event DelegationFailedForStake( + ClientChainID indexed clientChainId, address indexed exoDelegator, string operator, uint256 amount + ); /** * @dev Emitted when an undelegation is completed @@ -291,14 +304,17 @@ contract ExocoreBtcGatewayStorage { * @param operator The operator's address * @param amount The amount undelegated */ - event UndelegationCompleted(ClientChainID indexed clientChainId, address indexed exoDelegator, string operator, uint256 amount); + event UndelegationCompleted( + ClientChainID indexed clientChainId, address indexed exoDelegator, string operator, uint256 amount + ); /** * @dev Emitted when an address is registered + * @param chainId The LayerZero chain ID of the client chain * @param depositor The depositor's address * @param exocoreAddress The corresponding Exocore address */ - event AddressRegistered(bytes depositor, address indexed exocoreAddress); + event AddressRegistered(ClientChainID indexed chainId, bytes depositor, address indexed exocoreAddress); /** * @dev Emitted when a new witness is added @@ -412,9 +428,9 @@ contract ExocoreBtcGatewayStorage { error BtcTxAlreadyProcessed(); /** - * @dev Thrown when a Bitcoin address is not registered + * @dev Thrown when an address is not registered */ - error BtcAddressNotRegistered(); + error AddressNotRegistered(); /** * @dev Thrown when trying to process a request with an invalid status @@ -489,6 +505,19 @@ contract ExocoreBtcGatewayStorage { _; } + modifier isValidToken(Token token) { + require(token != Token.None, "ExocoreBtcGatewayStorage: Invalid token"); + _; + } + + modifier isRegistered(Token token, address exocoreAddress) { + require( + outboundRegistry[ClientChainID(uint8(token))][exocoreAddress].length > 0, + "ExocoreBtcGatewayStorage: Address not registered" + ); + _; + } + /** * @dev Internal function to verify and update the inbound bytes nonce * @param srcChainId The source chain ID @@ -499,4 +528,5 @@ contract ExocoreBtcGatewayStorage { revert UnexpectedInboundNonce(inboundNonce[srcChainId] + 1, nonce); } } + } From ee91e9131905b5277ca2ef5a637c107212c54da2 Mon Sep 17 00:00:00 2001 From: adu Date: Mon, 18 Nov 2024 18:15:23 +0800 Subject: [PATCH 05/11] wip: unit tests(unfinished) --- src/core/ExocoreBtcGateway.sol | 189 ++- src/libraries/Errors.sol | 24 + src/storage/ExocoreBtcGatewayStorage.sol | 72 +- test/foundry/unit/ExocoreBtcGateway.t.sol | 1376 +++++++++++++++------ 4 files changed, 1228 insertions(+), 433 deletions(-) diff --git a/src/core/ExocoreBtcGateway.sol b/src/core/ExocoreBtcGateway.sol index 2355d2de..2af4c394 100644 --- a/src/core/ExocoreBtcGateway.sol +++ b/src/core/ExocoreBtcGateway.sol @@ -62,29 +62,38 @@ contract ExocoreBtcGateway is */ constructor() { authorizedWitnesses[EXOCORE_WITNESS] = true; + authorizedWitnessCount = 1; _disableInitializers(); } /** - * @notice Initializes the contract with the Exocore witness address. - * @param _witness The address of the Exocore witness . + * @notice Initializes the contract with the Exocore witness address and owner address. + * @param owner_ The address of the owner. + * @param witnesses The addresses of the witnesses. */ - function initialize(address _witness) external initializer { - addWitness(_witness); + function initialize(address owner_, address[] calldata witnesses) external initializer { + if (owner_ == address(0) || witnesses.length == 0) { + revert Errors.ZeroAddress(); + } + for (uint256 i = 0; i < witnesses.length; i++) { + _addWitness(witnesses[i]); + } __Pausable_init_unchained(); + __ReentrancyGuard_init_unchained(); + _transferOwnership(owner_); } /** * @notice Activates token staking by registering or updating the chain and token with the Exocore system. */ - function activateStakingForClientChain(ClientChainID clientChain_) external { + function activateStakingForClientChain(ClientChainID clientChain_) external onlyOwner whenNotPaused { if (clientChain_ == ClientChainID.Bitcoin) { _registerOrUpdateClientChain( clientChain_, STAKER_ACCOUNT_LENGTH, BITCOIN_NAME, BITCOIN_METADATA, BITCOIN_SIGNATURE_SCHEME ); _registerOrUpdateToken(clientChain_, VIRTUAL_TOKEN, BTC_DECIMALS, BTC_NAME, BTC_METADATA, BTC_ORACLE_INFO); } else { - revert InvalidTokenType(); + revert Errors.InvalidClientChain(); } } @@ -93,49 +102,56 @@ contract ExocoreBtcGateway is * @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); + function addWitness(address _witness) external onlyOwner whenNotPaused { + _addWitness(_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. + * @custom:throws CannotRemoveLastWitness if the last witness is being removed */ - function removeWitness(address _witness) external onlyOwner { - require(authorizedWitnesses[_witness], "Witness not authorized"); + function removeWitness(address _witness) external onlyOwner whenNotPaused { + if (authorizedWitnessCount <= 1) { + revert Errors.CannotRemoveLastWitness(); + } + if (!authorizedWitnesses[_witness]) { + revert Errors.WitnessNotAuthorized(_witness); + } authorizedWitnesses[_witness] = false; + authorizedWitnessCount--; emit WitnessRemoved(_witness); } /** - * @notice Updates the bridge fee. - * @param _newFee The new fee to be set (in basis points, max 1000 or 10%). + * @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 updateBridgeFee(uint256 _newFee) external onlyOwner { - require(_newFee <= 1000, "Fee cannot exceed 10%"); // Max fee of 10% - bridgeFee = _newFee; - emit BridgeFeeUpdated(_newFee); + 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. - * @param _message The interchain message. + * @param witness The witness address that signed the message. + * @param _message The stake message. * @param _signature The signature of the message. */ - function submitProofForStakeMsg(StakeMsg calldata _message, bytes calldata _signature) + function submitProofForStakeMsg(address witness, StakeMsg calldata _message, bytes calldata _signature) external nonReentrant whenNotPaused { - bytes32 messageHash = _verifyStakeMessage(_message, _signature); + 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); @@ -162,6 +178,7 @@ contract ExocoreBtcGateway is // Check for consensus if (txn.proofCount >= REQUIRED_PROOFS) { + processedTransactions[messageHash] = true; _processStakeMsg(txn.stakeMsg); delete transactions[messageHash]; } @@ -169,18 +186,19 @@ contract ExocoreBtcGateway is /** * @notice Deposits BTC to the Exocore system. - * @param _msg The interchain message containing the deposit details. - * @param signature The signature to verify. + * @param witness The witness address that signed the message. + * @param _msg The stake message. + * @param signature The signature of the message. */ - function processStakeMessage(StakeMsg calldata _msg, bytes calldata signature) + function processStakeMessage(address witness, StakeMsg calldata _msg, bytes calldata signature) external nonReentrant whenNotPaused - isValidAmount(_msg.amount) - onlyAuthorizedWitness { - require(authorizedWitnesses[msg.sender], "Not an authorized witness"); - _verifyStakeMessage(_msg, signature); + if (!_isAuthorizedWitness(witness)) { + revert Errors.WitnessNotAuthorized(witness); + } + _verifyStakeMessage(witness, _msg, signature); _processStakeMsg(_msg); } @@ -196,14 +214,17 @@ contract ExocoreBtcGateway is nonReentrant whenNotPaused isValidAmount(amount) - isValidToken(token) isRegistered(token, msg.sender) { + if (!isValidOperatorAddress(operator)) { + revert Errors.InvalidOperator(); + } + ClientChainID chainId = ClientChainID(uint8(token)); bool success = _delegate(chainId, msg.sender, operator, amount); if (!success) { - revert DelegationFailed(); + revert Errors.DelegationFailed(); } emit DelegationCompleted(chainId, msg.sender, operator, amount); @@ -215,14 +236,17 @@ contract ExocoreBtcGateway is * @param operator The operator's exocore address. * @param amount The amount to undelegate. */ - function undelegateFrom(Token token, string memory operator, uint256 amount) + function undelegateFrom(Token token, string calldata operator, uint256 amount) external nonReentrant whenNotPaused isValidAmount(amount) - isValidToken(token) isRegistered(token, msg.sender) { + if (!isValidOperatorAddress(operator)) { + revert Errors.InvalidOperator(); + } + ClientChainID chainId = ClientChainID(uint8(token)); uint64 nonce = ++delegationNonce[chainId]; @@ -245,7 +269,6 @@ contract ExocoreBtcGateway is nonReentrant whenNotPaused isValidAmount(amount) - isValidToken(token) isRegistered(token, msg.sender) { ClientChainID chainId = ClientChainID(uint8(token)); @@ -271,7 +294,6 @@ contract ExocoreBtcGateway is nonReentrant whenNotPaused isValidAmount(amount) - isValidToken(token) isRegistered(token, msg.sender) { ClientChainID chainId = ClientChainID(uint8(token)); @@ -347,25 +369,62 @@ contract ExocoreBtcGateway is return pegOutRequests[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]; + } + /** * @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); + function _addWitness(address _witness) internal { + if (_witness == address(0)) { + revert Errors.ZeroAddress(); + } + if (_isAuthorizedWitness(_witness)) { + revert Errors.WitnessAlreadyAuthorized(_witness); + } + authorizedWitnesses[_witness] = true; + authorizedWitnessCount++; + emit WitnessAdded(_witness); } /** @@ -414,13 +473,14 @@ contract ExocoreBtcGateway is } /** - * @notice Verifies the signature of an interchain message. - * @param _msg The interchain message. + * @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(StakeMsg calldata _msg, bytes memory signature) + function _verifySignature(address signer, StakeMsg calldata _msg, bytes memory signature) internal - view + pure returns (bytes32 messageHash) { // StakeMsg, EIP721 is preferred next step. @@ -429,7 +489,7 @@ contract ExocoreBtcGateway is ); messageHash = keccak256(encodeMsg); - SignatureVerifier.verifyMsgSig(msg.sender, messageHash, signature); + SignatureVerifier.verifyMsgSig(signer, messageHash, signature); } /** @@ -438,26 +498,31 @@ contract ExocoreBtcGateway is */ function _verifyStakeMsgFields(StakeMsg calldata _msg) internal pure { // Combine all non-zero checks into a single value - uint256 validityCheck = + uint256 nonZeroCheck = uint8(_msg.chainId) | _msg.srcAddress.length | _msg.amount | _msg.nonce | _msg.txTag.length; - if (validityCheck == 0) { + if (nonZeroCheck == 0) { revert Errors.InvalidStakeMessage(); } + + if (bytes(_msg.operator).length > 0 && !isValidOperatorAddress(_msg.operator)) { + revert Errors.InvalidOperator(); + } } - function _verifyTxTagNotProcessed(bytes calldata txTag) internal view { - if (processedBtcTxs[txTag].processed) { + function _verifyTxTagNotProcessed(ClientChainID chainId, bytes calldata txTag) internal view { + if (processedClientChainTxs[chainId][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(StakeMsg calldata _msg, bytes calldata signature) + function _verifyStakeMessage(address witness, StakeMsg calldata _msg, bytes calldata signature) internal view returns (bytes32 messageHash) @@ -469,10 +534,10 @@ contract ExocoreBtcGateway is _verifyInboundNonce(_msg.chainId, _msg.nonce); // Verify that the txTag has not been processed - _verifyTxTagNotProcessed(_msg.txTag); + _verifyTxTagNotProcessed(_msg.chainId, _msg.txTag); // Verify signature - messageHash = _verifySignature(_msg, signature); + messageHash = _verifySignature(witness, _msg, signature); } /** @@ -534,7 +599,7 @@ contract ExocoreBtcGateway is uint32(uint8(clientChainId)), VIRTUAL_TOKEN, depositorExoAddr.toExocoreBytes(), amount ); if (!success) { - revert DepositFailed(txTag); + revert Errors.DepositFailed(txTag); } emit DepositCompleted(clientChainId, txTag, depositorExoAddr, srcAddress, amount, updatedBalance); @@ -581,7 +646,7 @@ contract ExocoreBtcGateway is function _processStakeMsg(StakeMsg memory _msg) internal { // increment inbound nonce for the client chain and mark the tx as processed inboundNonce[_msg.chainId]++; - processedBtcTxs[_msg.txTag] = TxInfo(true, block.timestamp); + processedClientChainTxs[_msg.chainId][_msg.txTag] = true; // register address if not already registered if ( @@ -595,7 +660,7 @@ contract ExocoreBtcGateway is } address stakerExoAddr = inboundRegistry[_msg.chainId][_msg.srcAddress]; - uint256 fee = _msg.amount * bridgeFee; + uint256 fee = _msg.amount * bridgeFeeRate / BASIS_POINTS; uint256 amountAfterFee = _msg.amount - fee; // we use registered exocore address as the depositor diff --git a/src/libraries/Errors.sol b/src/libraries/Errors.sol index f3029dc7..1835caa2 100644 --- a/src/libraries/Errors.sol +++ b/src/libraries/Errors.sol @@ -324,4 +324,28 @@ library Errors { /// @dev ExocoreBtcGateway: transaction tag has already been processed error TxTagAlreadyProcessed(); + /// @dev ExocoreBtcGateway: invalid operator address + error InvalidOperator(); + + /// @dev ExocoreBtcGateway: witness has already been authorized + error WitnessAlreadyAuthorized(address witness); + + /// @dev ExocoreBtcGateway: witness has not been authorized + error WitnessNotAuthorized(address witness); + + /// @dev ExocoreBtcGateway: cannot remove the last witness + error CannotRemoveLastWitness(); + + /// @dev ExocoreBtcGateway: invalid client chain + error InvalidClientChain(); + + /// @dev ExocoreBtcGateway: deposit failed + error DepositFailed(bytes txTag); + + /// @dev ExocoreBtcGateway: address not registered + error AddressNotRegistered(); + + /// @dev ExocoreBtcGateway: delegation failed + error DelegationFailed(); + } diff --git a/src/storage/ExocoreBtcGatewayStorage.sol b/src/storage/ExocoreBtcGatewayStorage.sol index 54c8e318..437c893c 100644 --- a/src/storage/ExocoreBtcGatewayStorage.sol +++ b/src/storage/ExocoreBtcGatewayStorage.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.19; +import {Errors} from "../libraries/Errors.sol"; + /** * @title ExocoreBtcGatewayStorage * @dev This contract manages the storage for the Exocore-Bitcoin gateway @@ -31,7 +33,7 @@ contract ExocoreBtcGatewayStorage { * @dev Enum to represent the status of a transaction */ enum TxStatus { - NotStarted, // 0: Default state - transaction hasn't started collecting proofs + 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 @@ -46,14 +48,6 @@ contract ExocoreBtcGatewayStorage { WithdrawReward } - /** - * @dev Struct to store transaction information - */ - struct TxInfo { - bool processed; - uint256 timestamp; - } - /** * @dev Struct to store interchain message information */ @@ -104,6 +98,9 @@ contract ExocoreBtcGatewayStorage { /* -------------------------------------------------------------------------- */ /* Constants */ /* -------------------------------------------------------------------------- */ + /// @notice the human readable prefix for Exocore bech32 encoded address. + bytes public constant EXO_ADDRESS_PREFIX = bytes("exo1"); + // 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"; @@ -122,7 +119,12 @@ contract ExocoreBtcGatewayStorage { 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%) + 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% + + /// @notice The count of authorized witnesses + uint256 public authorizedWitnessCount; /** * @dev Mapping to store transaction information, key is the message hash @@ -130,9 +132,14 @@ contract ExocoreBtcGatewayStorage { mapping(bytes32 => Transaction) public transactions; /** - * @dev Mapping to store processed Bitcoin transactions + * @dev Mapping to store processed transactions + */ + mapping(bytes32 => bool) public processedTransactions; + + /** + * @dev Mapping to store processed ClientChain transactions */ - mapping(bytes => TxInfo) public processedBtcTxs; + mapping(ClientChainID => mapping(bytes => bool)) public processedClientChainTxs; /** * @dev Mapping to store peg-out requests, key is the nonce @@ -357,10 +364,10 @@ contract ExocoreBtcGatewayStorage { event PegOutTransactionExpired(bytes32 requestId); /** - * @dev Emitted when the bridge fee is updated - * @param newFee The new bridge fee + * @dev Emitted when the bridge rate is updated + * @param newRate The new bridge rate */ - event BridgeFeeUpdated(uint256 newFee); + event BridgeFeeRateUpdated(uint256 newRate); /** * @dev Emitted when the deposit limit is updated @@ -501,21 +508,36 @@ contract ExocoreBtcGatewayStorage { * @param amount The amount to check */ modifier isValidAmount(uint256 amount) { - require(amount > 0, "ExocoreBtcGatewayStorage: amount should be greater than zero"); + if (amount == 0) { + revert Errors.ZeroAmount(); + } _; } - modifier isValidToken(Token token) { - require(token != Token.None, "ExocoreBtcGatewayStorage: Invalid token"); + modifier isRegistered(Token token, address exocoreAddress) { + if (outboundRegistry[ClientChainID(uint8(token))][exocoreAddress].length == 0) { + revert Errors.AddressNotRegistered(); + } _; } - modifier isRegistered(Token token, address exocoreAddress) { - require( - outboundRegistry[ClientChainID(uint8(token))][exocoreAddress].length > 0, - "ExocoreBtcGatewayStorage: Address not registered" - ); - _; + /// @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; } /** @@ -525,7 +547,7 @@ contract ExocoreBtcGatewayStorage { */ function _verifyInboundNonce(ClientChainID srcChainId, uint64 nonce) internal view { if (nonce != inboundNonce[srcChainId] + 1) { - revert UnexpectedInboundNonce(inboundNonce[srcChainId] + 1, nonce); + revert Errors.UnexpectedInboundNonce(inboundNonce[srcChainId] + 1, nonce); } } diff --git a/test/foundry/unit/ExocoreBtcGateway.t.sol b/test/foundry/unit/ExocoreBtcGateway.t.sol index 0b3915ff..9ea0b782 100644 --- a/test/foundry/unit/ExocoreBtcGateway.t.sol +++ b/test/foundry/unit/ExocoreBtcGateway.t.sol @@ -1,419 +1,1103 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.19; -import "src/core/ExocoreBtcGateway.sol"; -import "src/interfaces/precompiles/IAssets.sol"; +import "forge-std/Test.sol"; +import {ExocoreBtcGateway} from "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 {Errors} from "src/libraries/Errors.sol"; +import {SignatureVerifier} from "src/libraries/SignatureVerifier.sol"; +import {ExocoreBtcGatewayStorage} from "src/storage/ExocoreBtcGatewayStorage.sol"; +import "test/mocks/AssetsMock.sol"; +import "test/mocks/DelegationMock.sol"; +import "test/mocks/RewardMock.sol"; -import "forge-std/Test.sol"; +import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; -contract ExocoreBtcGatewayTest is ExocoreBtcGatewayStorage, Test { +contract ExocoreBtcGatewayTest is Test { - ExocoreBtcGateway internal exocoreBtcGateway; + using stdStorage for StdStorage; - uint32 internal exocoreChainId = 2; - uint32 internal clientBtcChainId = 111; + struct Player { + uint256 privateKey; + address addr; + } - address internal validator = address(0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266); - address internal btcToken = address(0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599); - address internal delegatorAddr = address(0x70997970C51812dc3A010C7d01b50e0d17dc79C8); - bytes internal BTC_TOKEN = abi.encodePacked(bytes32(bytes20(btcToken))); + ExocoreBtcGateway gateway; + ExocoreBtcGateway gatewayLogic; + address owner; + address user; + address relayer; + Player[3] witnesses; + bytes btcAddress; + string operator; + ExocoreBtcGatewayStorage.Transaction txn; - using stdStorage for StdStorage; + address public constant EXOCORE_WITNESS = address(0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266); - // Mock contracts - IDelegation internal mockDelegation; - IAssets internal mockAssets; - IReward internal mockClaimReward; + // 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; - function setUp() public { - // Deploy mock contracts - _bindPrecompileMocks(); + // 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))); - // Deploy the main contract - exocoreBtcGateway = new ExocoreBtcGateway(); + 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"; - // Whitelist the btcToken - // Calculate the storage slot for the mapping - bytes32 whitelistedSlot = bytes32( - stdstore.target(address(exocoreBtcGateway)).sig("isWhitelistedToken(address)").with_key(btcToken).find() - ); + uint256 public constant REQUIRED_PROOFS = 2; + uint256 public constant PROOF_TIMEOUT = 1 days; - // Set the storage value to true (1) - vm.store(address(exocoreBtcGateway), whitelistedSlot, bytes32(uint256(1))); - } + event WitnessAdded(address indexed witness); + event WitnessRemoved(address indexed witness); + event AddressRegistered( + ExocoreBtcGatewayStorage.ClientChainID indexed chainId, bytes depositor, address exocoreAddress + ); + event DepositCompleted( + ExocoreBtcGatewayStorage.ClientChainID indexed chainId, + bytes txTag, + address indexed exocoreAddress, + bytes srcAddress, + uint256 amount, + uint256 updatedBalance + ); + event DelegationCompleted( + ExocoreBtcGatewayStorage.ClientChainID indexed chainId, + address indexed delegator, + string operator, + uint256 amount + ); + event ProofSubmitted(bytes32 indexed txId, address indexed witness); + event StakeMsgExecuted(bytes32 indexed txId); + event BridgeFeeRateUpdated(uint256 newRate); - function _bindPrecompileMocks() internal { - // bind precompile mock contracts code to constant precompile address so that local simulation could pass + event ClientChainRegistered(uint32 clientChainId); + event ClientChainUpdated(uint32 clientChainId); + event WhitelistTokenAdded(uint32 clientChainId, address indexed token); + event WhitelistTokenUpdated(uint32 clientChainId, address indexed token); + event DelegationFailedForStake( + ExocoreBtcGatewayStorage.ClientChainID indexed clientChainId, + address indexed exoDelegator, + string operator, + uint256 amount + ); + + 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 mock contracts and bind to precompile addresses 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); + bytes memory RewardMockCode = vm.getDeployedCode("RewardMock.sol"); + vm.etch(REWARD_PRECOMPILE_ADDRESS, RewardMockCode); + + // Deploy and initialize gateway + gatewayLogic = new ExocoreBtcGateway(); + gateway = ExocoreBtcGateway(address(new TransparentUpgradeableProxy(address(gatewayLogic), address(0), ""))); + address[] memory initialWitnesses = new address[](1); + initialWitnesses[0] = witnesses[0].addr; + gateway.initialize(owner, initialWitnesses); + } + + function test_initialize() public { + assertEq(gateway.owner(), owner); + assertTrue(gateway.authorizedWitnesses(witnesses[0].addr)); + assertEq(gateway.authorizedWitnessCount(), 2); + } + + function test_AddWitness() public { + vm.prank(owner); + + vm.expectEmit(true, false, false, false); + emit WitnessAdded(witnesses[1].addr); + + gateway.addWitness(witnesses[1].addr); + assertTrue(gateway.authorizedWitnesses(witnesses[1].addr)); + assertEq(gateway.authorizedWitnessCount(), 3); + } + + function test_AddWitness_RevertNotOwner() public { + vm.prank(user); + vm.expectRevert("Ownable: caller is not the owner"); + gateway.addWitness(witnesses[1].addr); + } + + function test_AddWitness_RevertZeroAddress() public { + vm.prank(owner); + vm.expectRevert(Errors.ZeroAddress.selector); + gateway.addWitness(address(0)); + } + + function test_AddWitness_RevertAlreadyAuthorized() public { + // First add a witness + vm.startPrank(owner); + gateway.addWitness(witnesses[1].addr); + + // Try to add the same witness again + vm.expectRevert("Witness already authorized"); + gateway.addWitness(witnesses[1].addr); + vm.stopPrank(); + } + + function test_AddWitness_RevertWhenPaused() public { + vm.startPrank(owner); + gateway.pause(); + + vm.expectRevert("Pausable: paused"); + gateway.addWitness(witnesses[1].addr); + vm.stopPrank(); + } + + function test_RemoveWitness() public { + vm.prank(owner); + + vm.expectEmit(true, false, false, false); + emit WitnessRemoved(witnesses[0].addr); + + gateway.removeWitness(witnesses[0].addr); + assertFalse(gateway.authorizedWitnesses(witnesses[0].addr)); + assertEq(gateway.authorizedWitnessCount(), 1); + } + + function test_RemoveWitness_RevertNotOwner() public { + vm.prank(user); + vm.expectRevert("Ownable: caller is not the owner"); + gateway.removeWitness(witnesses[0].addr); + } + + function test_RemoveWitness_RevertWitnessNotAuthorized() public { + vm.prank(owner); + vm.expectRevert(abi.encodeWithSelector(Errors.WitnessNotAuthorized.selector, witnesses[1].addr)); + gateway.removeWitness(witnesses[1].addr); + } + + function test_RemoveWitness_RevertWhenPaused() public { + vm.startPrank(owner); + gateway.pause(); + + vm.expectRevert("Pausable: paused"); + gateway.removeWitness(witnesses[0].addr); + vm.stopPrank(); + } + + function test_RemoveWitness_CannotRemoveLastWitness() public { + // First remove all witnesses except one + vm.startPrank(owner); + for (uint256 i = 0; i < witnesses.length; i++) { + if (gateway.authorizedWitnesses(witnesses[i].addr)) { + gateway.removeWitness(witnesses[i].addr); + } + } + + // Try to remove the hardcoded witness + vm.expectRevert(Errors.CannotRemoveLastWitness.selector); + gateway.removeWitness(EXOCORE_WITNESS); + vm.stopPrank(); + } + + function test_RemoveWitness_MultipleRemovals() public { + vm.startPrank(owner); + + // First add another witness + gateway.addWitness(witnesses[1].addr); + assertTrue(gateway.authorizedWitnesses(witnesses[1].addr)); + assertEq(gateway.authorizedWitnessCount(), 3); + + // Remove first witness + gateway.removeWitness(witnesses[0].addr); + assertFalse(gateway.authorizedWitnesses(witnesses[0].addr)); + assertTrue(gateway.authorizedWitnesses(witnesses[1].addr)); + assertEq(gateway.authorizedWitnessCount(), 2); + + // Remove second witness + gateway.removeWitness(witnesses[1].addr); + assertFalse(gateway.authorizedWitnesses(witnesses[1].addr)); + assertEq(gateway.authorizedWitnessCount(), 1); + + 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 10%"); + 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() public { + vm.startPrank(owner); + + // Mock successful chain registration + bytes memory chainRegisterCall = abi.encodeWithSelector( + IAssets.registerOrUpdateClientChain.selector, + uint32(uint8(ExocoreBtcGatewayStorage.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(ExocoreBtcGatewayStorage.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(uint32(uint8(ExocoreBtcGatewayStorage.ClientChainID.Bitcoin))); + vm.expectEmit(true, false, false, false); + emit WhitelistTokenAdded(uint32(uint8(ExocoreBtcGatewayStorage.ClientChainID.Bitcoin)), VIRTUAL_TOKEN_ADDRESS); + + gateway.activateStakingForClientChain(ExocoreBtcGatewayStorage.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(ExocoreBtcGatewayStorage.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(ExocoreBtcGatewayStorage.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(ExocoreBtcGatewayStorage.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(uint32(uint8(ExocoreBtcGatewayStorage.ClientChainID.Bitcoin))); + vm.expectEmit(true, false, false, false); + emit WhitelistTokenUpdated(uint32(uint8(ExocoreBtcGatewayStorage.ClientChainID.Bitcoin)), VIRTUAL_TOKEN_ADDRESS); + + gateway.activateStakingForClientChain(ExocoreBtcGatewayStorage.ClientChainID.Bitcoin); + vm.stopPrank(); } - /** - * @notice Test the depositTo function with the first InterchainMsg. - */ - function testDepositToWithFirstMessage() public { - assertTrue(exocoreBtcGateway.isWhitelistedToken(btcToken)); + function test_ActivateStakingForClientChain_RevertChainRegistrationFailed() public { + vm.startPrank(owner); - bytes memory btcAddress = _stringToBytes("tb1pdwf5ar0kxr2sdhxw28wqhjwzynzlkdrqlgx8ju3sr02hkldqmlfspm0mmh"); - bytes memory exocoreAddress = _addressToBytes(delegatorAddr); - console.logBytes(btcAddress); + // Mock failed chain registration + bytes memory chainRegisterCall = abi.encodeWithSelector( + IAssets.registerOrUpdateClientChain.selector, + uint32(uint8(ExocoreBtcGatewayStorage.ClientChainID.Bitcoin)), + STAKER_ACCOUNT_LENGTH, + BITCOIN_NAME, + BITCOIN_METADATA, + BITCOIN_SIGNATURE_SCHEME + ); + vm.mockCall( + ASSETS_PRECOMPILE_ADDRESS, + chainRegisterCall, + abi.encode(false, false) // registration failed + ); - // Get the inboundBytesNonce - uint256 nonce = exocoreBtcGateway.inboundBytesNonce(clientBtcChainId, btcAddress) + 1; - assertEq(nonce, 1, "Nonce should be 1"); + vm.expectRevert( + abi.encodeWithSelector( + Errors.RegisterClientChainToExocoreFailed.selector, + uint32(uint8(ExocoreBtcGatewayStorage.ClientChainID.Bitcoin)) + ) + ); + gateway.activateStakingForClientChain(ExocoreBtcGatewayStorage.ClientChainID.Bitcoin); + vm.stopPrank(); + } - // register address. - vm.prank(validator); - exocoreBtcGateway.registerAddress(btcAddress, exocoreAddress); - InterchainMsg memory _msg = InterchainMsg({ - srcChainID: clientBtcChainId, - dstChainID: exocoreChainId, + function test_ActivateStakingForClientChain_RevertTokenRegistrationAndUpdateFailed() public { + vm.startPrank(owner); + + // Mock successful chain registration + bytes memory chainRegisterCall = abi.encodeWithSelector( + IAssets.registerOrUpdateClientChain.selector, + uint32(uint8(ExocoreBtcGatewayStorage.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(ExocoreBtcGatewayStorage.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(ExocoreBtcGatewayStorage.ClientChainID.Bitcoin)), + VIRTUAL_TOKEN, + BTC_METADATA + ); + vm.mockCall(ASSETS_PRECOMPILE_ADDRESS, tokenUpdateCall, abi.encode(false)); + + vm.expectRevert( + abi.encodeWithSelector( + Errors.AddWhitelistTokenFailed.selector, + uint32(uint8(ExocoreBtcGatewayStorage.ClientChainID.Bitcoin)), + bytes32(VIRTUAL_TOKEN) + ) + ); + gateway.activateStakingForClientChain(ExocoreBtcGatewayStorage.ClientChainID.Bitcoin); + vm.stopPrank(); + } + + function test_ActivateStakingForClientChain_RevertInvalidChain() public { + vm.prank(owner); + vm.expectRevert(Errors.InvalidClientChain.selector); + gateway.activateStakingForClientChain(ExocoreBtcGatewayStorage.ClientChainID.None); + } + + function test_ActivateStakingForClientChain_RevertNotOwner() public { + vm.prank(user); + vm.expectRevert("Ownable: caller is not the owner"); + gateway.activateStakingForClientChain(ExocoreBtcGatewayStorage.ClientChainID.Bitcoin); + } + + function test_ActivateStakingForClientChain_RevertWhenPaused() public { + vm.startPrank(owner); + gateway.pause(); + + vm.expectRevert("Pausable: paused"); + gateway.activateStakingForClientChain(ExocoreBtcGatewayStorage.ClientChainID.Bitcoin); + vm.stopPrank(); + } + + function test_SubmitProofForStakeMsg() public { + _addAllWitnesses(); + + // Create stake message + ExocoreBtcGatewayStorage.StakeMsg memory stakeMsg = ExocoreBtcGatewayStorage.StakeMsg({ + chainId: ExocoreBtcGatewayStorage.ClientChainID.Bitcoin, srcAddress: btcAddress, - dstAddress: _stringToBytes("tb1qqytgqkzvg48p700s46n57wfgaf04h7ca5m03qcschaawv9qqw2vsp67ku4"), - token: btcToken, - amount: 39_900_000_000_000, + exocoreAddress: user, + operator: operator, + amount: 1 ether, nonce: 1, - txTag: _stringToBytes("b2c4366e29da536bd1ca5ac1790ba1d3a5e706a2b5e2674dee2678a669432ffc-3"), - payload: "0x" + txTag: bytes("tx1-0") }); - bytes memory signature = - hex"aa70b655593f96d19dca3ef0bfc6602b6597a3b6253de2b709b81306a09d46867f857e8a44e64f0c1be6f4ec90a66e28401e007b7efb6fd344164af8316e1f571b"; + bytes32 txId = _getMessageHash(stakeMsg); + bytes memory signature = _generateSignature(stakeMsg, witnesses[0].privateKey); - // 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); + // 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); + + // mock Assets precompile deposit success and Delegation precompile delegate success + vm.mockCall(ASSETS_PRECOMPILE_ADDRESS, abi.encodeWithSelector(IAssets.depositLST.selector), abi.encode(true)); + vm.mockCall( + DELEGATION_PRECOMPILE_ADDRESS, abi.encodeWithSelector(IDelegation.delegate.selector), abi.encode(true) + ); + + // Submit proof from second witness + signature = _generateSignature(stakeMsg, witnesses[1].privateKey); + vm.prank(witnesses[1].addr); + vm.expectEmit(true, true, false, true); + emit ProofSubmitted(txId, witnesses[1].addr); + + // This should trigger message execution as we have enough proofs + vm.expectEmit(true, false, false, false); + emit StakeMsgExecuted(txId); + gateway.submitProofForStakeMsg(witnesses[1].addr, stakeMsg, signature); - // Simulate the validator calling the depositTo function - vm.prank(validator); - exocoreBtcGateway.depositTo(_msg, signature); + // Verify message was processed + assertTrue(gateway.processedClientChainTxs(stakeMsg.chainId, stakeMsg.txTag)); + assertTrue(gateway.processedTransactions(txId)); } - /** - * @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)); + function test_SubmitProofForStakeMsg_RevertInvalidSignature() public { + _addAllWitnesses(); - console.logBytes(btcAddress); + ExocoreBtcGatewayStorage.StakeMsg memory stakeMsg = ExocoreBtcGatewayStorage.StakeMsg({ + chainId: ExocoreBtcGatewayStorage.ClientChainID.Bitcoin, + srcAddress: btcAddress, + exocoreAddress: user, + operator: operator, + amount: 1 ether, + nonce: 1, + txTag: bytes("tx1-0") + }); - // Get the inboundBytesNonce - uint256 nonce = exocoreBtcGateway.inboundBytesNonce(clientBtcChainId, btcAddress) + 1; - assertEq(nonce, 1, "Nonce should be 1"); + bytes memory invalidSignature = bytes("invalid"); + + vm.prank(relayer); + vm.expectRevert(SignatureVerifier.InvalidSignature.selector); + gateway.submitProofForStakeMsg(witnesses[0].addr, stakeMsg, invalidSignature); + } - // register address. - vm.prank(validator); - exocoreBtcGateway.registerAddress(btcAddress, exocoreAddress); + function test_SubmitProofForStakeMsg_RevertUnauthorizedWitness() public { + _addAllWitnesses(); - InterchainMsg memory _msg = InterchainMsg({ - srcChainID: clientBtcChainId, - dstChainID: exocoreChainId, + ExocoreBtcGatewayStorage.StakeMsg memory stakeMsg = ExocoreBtcGatewayStorage.StakeMsg({ + chainId: ExocoreBtcGatewayStorage.ClientChainID.Bitcoin, srcAddress: btcAddress, - dstAddress: _stringToBytes("tb1qqytgqkzvg48p700s46n57wfgaf04h7ca5m03qcschaawv9qqw2vsp67ku4"), - token: btcToken, - amount: 49_000_000_000_000, // 0.000049 BTC + exocoreAddress: user, + operator: operator, + amount: 1 ether, nonce: 1, - txTag: _stringToBytes("102f5578c65f78cda5b1c4b35b58281b66c27a4929bb4f938fd15fa8f2d1c58b-1"), - payload: "0x" + txTag: bytes("tx1") }); - // 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"); - } + + 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(); + + ExocoreBtcGatewayStorage.StakeMsg memory stakeMsg = ExocoreBtcGatewayStorage.StakeMsg({ + chainId: ExocoreBtcGatewayStorage.ClientChainID.Bitcoin, + srcAddress: btcAddress, + exocoreAddress: user, + operator: operator, + amount: 1 ether, + nonce: 1, + txTag: bytes("tx1") + }); + + // Submit proofs from REQUIRED_PROOFS - 1 witnesses + for (uint256 i = 0; i < REQUIRED_PROOFS - 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[REQUIRED_PROOFS - 1].privateKey); + vm.prank(relayer); + gateway.submitProofForStakeMsg(witnesses[REQUIRED_PROOFS - 1].addr, stakeMsg, lastSignature); + + // Verify transaction is restarted owing to expired and not processed + bytes32 messageHash = _getMessageHash(stakeMsg); + assertEq(uint8(gateway.getTransactionStatus(messageHash)), uint8(ExocoreBtcGatewayStorage.TxStatus.Pending)); + assertEq(gateway.getTransactionProofCount(messageHash), 1); + assertFalse(gateway.processedClientChainTxs(stakeMsg.chainId, stakeMsg.txTag)); } - /** - * @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); + function test_SubmitProofForStakeMsg_RestartExpiredTransaction() public { + _addAllWitnesses(); + + ExocoreBtcGatewayStorage.StakeMsg memory stakeMsg = ExocoreBtcGatewayStorage.StakeMsg({ + chainId: ExocoreBtcGatewayStorage.ClientChainID.Bitcoin, + srcAddress: 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(ExocoreBtcGatewayStorage.TxStatus.Pending)); + assertEq(gateway.getTransactionProofCount(messageHash), 1); + assertTrue(gateway.getTransactionWitnessTime(messageHash, witnesses[0].addr) > 0); + assertFalse(gateway.processedTransactions(messageHash)); } - /** - * @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 + function test_SubmitProofForStakeMsg_JoinRestartedTransaction() public { + _addAllWitnesses(); - // 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); + ExocoreBtcGatewayStorage.StakeMsg memory stakeMsg = ExocoreBtcGatewayStorage.StakeMsg({ + chainId: ExocoreBtcGatewayStorage.ClientChainID.Bitcoin, + srcAddress: btcAddress, + exocoreAddress: user, + operator: operator, + amount: 1 ether, + nonce: 1, + txTag: bytes("tx1") + }); - 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 - ) + // 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); + + // as PROOFS_REQUIRED is 2, the transaction should be processed after another witness submits proof + + // mock Assets precompile deposit success and Delegation precompile delegate success + vm.mockCall(ASSETS_PRECOMPILE_ADDRESS, abi.encodeWithSelector(IAssets.depositLST.selector), abi.encode(true)); + vm.mockCall( + DELEGATION_PRECOMPILE_ADDRESS, abi.encodeWithSelector(IDelegation.delegate.selector), abi.encode(true) + ); + + // First witness can submit proof again in new round + 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(ExocoreBtcGatewayStorage.TxStatus.NotStartedOrProcessed) ); - 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)); + assertTrue(gateway.processedTransactions(messageHash)); + assertTrue(gateway.processedClientChainTxs(stakeMsg.chainId, stakeMsg.txTag)); + assertTrue(gateway.getTransactionWitnessTime(messageHash, witnesses[0].addr) > 0); // mapping can not be deleted + // even if we delete txn after processing + assertTrue(gateway.getTransactionWitnessTime(messageHash, witnesses[1].addr) > 0); // mapping can not be deleted + // even if we delete txn after processing + } + + function test_SubmitProofForStakeMsg_RevertDuplicateProofInSameRound() public { + _addAllWitnesses(); + + ExocoreBtcGatewayStorage.StakeMsg memory stakeMsg = ExocoreBtcGatewayStorage.StakeMsg({ + chainId: ExocoreBtcGatewayStorage.ClientChainID.Bitcoin, + srcAddress: 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_RegisterNewAddress() public { + ExocoreBtcGatewayStorage.StakeMsg memory stakeMsg = ExocoreBtcGatewayStorage.StakeMsg({ + chainId: ExocoreBtcGatewayStorage.ClientChainID.Bitcoin, + srcAddress: 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)); + 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 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, + emit AddressRegistered(ExocoreBtcGatewayStorage.ClientChainID.Bitcoin, btcAddress, user); + gateway.processStakeMessage(witnesses[0].addr, stakeMsg, signature); + + // Verify address registration + assertEq(gateway.getClientChainAddress(ExocoreBtcGatewayStorage.ClientChainID.Bitcoin, user), btcAddress); + } + + function test_ProcessStakeMessage_WithBridgeFee() public { + ExocoreBtcGatewayStorage.StakeMsg memory stakeMsg = ExocoreBtcGatewayStorage.StakeMsg({ + chainId: ExocoreBtcGatewayStorage.ClientChainID.Bitcoin, srcAddress: btcAddress, - dstAddress: _stringToBytes("tb1qqytgqkzvg48p700s46n57wfgaf04h7ca5m03qcschaawv9qqw2vsp67ku4"), - token: btcToken, - amount: 39_900_000_000_000, + exocoreAddress: user, + operator: operator, + amount: 1 ether, nonce: 1, - txTag: _stringToBytes("b2c4366e29da536bd1ca5ac1790ba1d3a5e706a2b5e2674dee2678a669432ffc-3"), - payload: "0x" + txTag: bytes("tx1") }); - bytes memory signature = - hex"aa70b655593f96d19dca3ef0bfc6602b6597a3b6253de2b709b81306a09d46867f857e8a44e64f0c1be6f4ec90a66e28401e007b7efb6fd344164af8316e1f571b"; + + // 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); + + // mock Assets precompile deposit success and Delegation precompile delegate success + vm.mockCall(ASSETS_PRECOMPILE_ADDRESS, abi.encodeWithSelector(IAssets.depositLST.selector), abi.encode(true)); + vm.mockCall( + DELEGATION_PRECOMPILE_ADDRESS, abi.encodeWithSelector(IDelegation.delegate.selector), abi.encode(true) + ); + uint256 amountAfterFee = 1 ether - 1 ether * 100 / 10_000; + + vm.expectEmit(true, true, true, true, address(gateway)); + emit DepositCompleted( + ExocoreBtcGatewayStorage.ClientChainID.Bitcoin, + stakeMsg.txTag, + user, + stakeMsg.srcAddress, + amountAfterFee, + stakeMsg.amount + ); + + vm.expectEmit(true, true, true, true, address(gateway)); + emit DelegationCompleted(ExocoreBtcGatewayStorage.ClientChainID.Bitcoin, user, operator, amountAfterFee); + + vm.prank(relayer); + gateway.processStakeMessage(witnesses[0].addr, stakeMsg, signature); + } + + function test_ProcessStakeMessage_WithDelegation() public { + ExocoreBtcGatewayStorage.StakeMsg memory stakeMsg = ExocoreBtcGatewayStorage.StakeMsg({ + chainId: ExocoreBtcGatewayStorage.ClientChainID.Bitcoin, + srcAddress: 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.encodeWithSignature(IAssets.deposit.selector), abi.encode(true)); + vm.mockCall( + DELEGATION_PRECOMPILE_ADDRESS, abi.encodeWithSignature(IDelegation.delegate.selector), abi.encode(true) + ); + + bytes memory signature = _generateSignature(stakeMsg, witnesses[0].privateKey); + + vm.prank(relayer); 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); + emit DelegationCompleted(ExocoreBtcGatewayStorage.ClientChainID.Bitcoin, user, operator, 1 ether); + gateway.processStakeMessage(witnesses[0], stakeMsg, signature); } - /** - * @notice Test registerAddress function - */ - function testRegisterAddress() public { - bytes memory btcAddress = _stringToBytes("tb1pdwf5ar0kxr2sdhxw28wqhjwzynzlkdrqlgx8ju3sr02hkldqmlfspm0mmh"); - bytes memory exocoreAddress = _stringToBytes("0x70997970C51812dc3A010C7d01b50e0d17dc79C8"); + function test_ProcessStakeMessage_DelegationFailureNotRevert() public { + ExocoreBtcGatewayStorage.StakeMsg memory stakeMsg = ExocoreBtcGatewayStorage.StakeMsg({ + chainId: ExocoreBtcGatewayStorage.ClientChainID.Bitcoin, + srcAddress: 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.encodeWithSignature(IAssets.deposit.selector), abi.encode(true)); + vm.mockCall( + DELEGATION_PRECOMPILE_ADDRESS, abi.encodeWithSignature(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 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)); + emit DepositCompleted(ExocoreBtcGatewayStorage.ClientChainID.Bitcoin, user, 1 ether); + + // delegation should fail 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); + emit DelegationFailedForStake(ExocoreBtcGatewayStorage.ClientChainID.Bitcoin, user, operator, 1 ether); + + gateway.processStakeMessage(witnesses[0], stakeMsg, signature); + } + + function test_ProcessStakeMessage_RevertOnDepositFailure() public { + ExocoreBtcGatewayStorage.StakeMsg memory stakeMsg = ExocoreBtcGatewayStorage.StakeMsg({ + chainId: ExocoreBtcGatewayStorage.ClientChainID.Bitcoin, + srcAddress: btcAddress, + exocoreAddress: user, + operator: "", + amount: 1 ether, + nonce: 1, + txTag: bytes("tx1") + }); + + // mock Assets precompile deposit failure + vm.mockCall(ASSETS_PRECOMPILE_ADDRESS, abi.encodeWithSignature(IAssets.deposit.selector), abi.encode(false)); + + bytes memory signature = _generateSignature(stakeMsg, witnesses[0].privateKey); + + vm.prank(relayer); + vm.expectRevert(abi.encodeWithSelector(Errors.DepositFailed.selector, bytes("tx1"))); + gateway.processStakeMessage(witnesses[0], stakeMsg, signature); + } + + function test_ProcessStakeMessage_RevertWhenPaused() public { + ExocoreBtcGatewayStorage.StakeMsg memory stakeMsg = ExocoreBtcGatewayStorage.StakeMsg({ + chainId: ExocoreBtcGatewayStorage.ClientChainID.Bitcoin, + srcAddress: 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(witnesses[0]); + vm.expectRevert("Pausable: paused"); + gateway.processStakeMessage(witnesses[0], stakeMsg, signature); + } + + function test_ProcessStakeMessage_RevertUnauthorizedWitness() public { + ExocoreBtcGatewayStorage.StakeMsg memory stakeMsg = ExocoreBtcGatewayStorage.StakeMsg({ + chainId: ExocoreBtcGatewayStorage.ClientChainID.Bitcoin, + srcAddress: 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); + vm.expectRevert(abi.encodeWithSelector(Errors.WitnessNotAuthorized.selector, unauthorizedWitness)); + gateway.processStakeMessage(unauthorizedWitness, stakeMsg, signature); + } + + function test_ProcessStakeMessage_RevertInvalidStakeMessage() public { + // Create invalid message with all zero values + ExocoreBtcGatewayStorage.StakeMsg memory stakeMsg = ExocoreBtcGatewayStorage.StakeMsg({ + chainId: ExocoreBtcGatewayStorage.ClientChainID.None, + srcAddress: 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], stakeMsg, signature); + } + + function test_ProcessStakeMessage_RevertZeroExocoreAddressBeforeRegistration() public { + ExocoreBtcGatewayStorage.StakeMsg memory stakeMsg = ExocoreBtcGatewayStorage.StakeMsg({ + chainId: ExocoreBtcGatewayStorage.ClientChainID.Bitcoin, + srcAddress: 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], stakeMsg, signature); + } + + function test_ProcessStakeMessage_RevertInvalidNonce() public { + ExocoreBtcGatewayStorage.StakeMsg memory stakeMsg = ExocoreBtcGatewayStorage.StakeMsg({ + chainId: ExocoreBtcGatewayStorage.ClientChainID.Bitcoin, + srcAddress: btcAddress, + exocoreAddress: user, + operator: "", + amount: 1 ether, + nonce: gateway.nextInboundNonce() + 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.nonce) + ); + gateway.processStakeMessage(witnesses[0], stakeMsg, signature); + } + + function test_DelegateTo() public { + // Setup: Register user's client chain address first + _mockRegisterAddress(user, btcAddress); + + // mock delegation precompile delegate success + vm.mockCall( + DELEGATION_PRECOMPILE_ADDRESS, abi.encodeWithSignature(IDelegation.delegate.selector), abi.encode(true) + ); + + vm.prank(user); vm.expectEmit(true, true, true, true); - emit PegOutRequestStatusUpdated(requestId, ExocoreBtcGatewayStorage.TxStatus.Processed); - exocoreBtcGateway.setPegOutRequestStatus(requestId, ExocoreBtcGatewayStorage.TxStatus.Processed); + emit DelegationCompleted(ExocoreBtcGatewayStorage.ClientChainID.Bitcoin, user, operator, 1 ether); - ExocoreBtcGatewayStorage.PegOutRequest memory request = exocoreBtcGateway.getPegOutRequest(requestId); - assertEq( - uint256(request.status), - uint256(ExocoreBtcGatewayStorage.TxStatus.Processed), - "Status was not updated correctly" + gateway.delegateTo(ExocoreBtcGatewayStorage.Token.BTC, operator, 1 ether); + + // Verify nonce increment + assertEq(gateway.delegationNonce(ExocoreBtcGatewayStorage.ClientChainID.Bitcoin), 1); + } + + function test_DelegateTo_RevertZeroAmount() public { + _mockRegisterAddress(user, btcAddress); + + vm.prank(user); + vm.expectRevert(Errors.ZeroAmount.selector); + gateway.delegateTo(ExocoreBtcGatewayStorage.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(ExocoreBtcGatewayStorage.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(ExocoreBtcGatewayStorage.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(ExocoreBtcGatewayStorage.Token.BTC, invalidOperator, 1 ether); + } + + function test_DelegateTo_RevertDelegationFailed() public { + _mockRegisterAddress(user, btcAddress); + + // Mock delegation failure + vm.mockCall( + DELEGATION_PRECOMPILE_ADDRESS, abi.encodeWithSignature(IDelegation.delegate.selector), abi.encode(false) ); + + vm.prank(user); + vm.expectRevert(abi.encodeWithSelector(Errors.DelegationFailed.selector)); + gateway.delegateTo(ExocoreBtcGatewayStorage.Token.BTC, operator, 1 ether); } - /** - * @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 functions + function _mockRegisterAddress(address exocoreAddr, bytes memory btcAddr) internal { + stdstore.target(address(gateway)).sig("inboundRegistry(ClientChainID, bytes)").with_key( + ExocoreBtcGatewayStorage.ClientChainID.Bitcoin, btcAddr + ).checked_write(exocoreAddr); + stdstore.target(address(gateway)).sig("outboundRegistry(ClientChainID, address)").with_key( + ExocoreBtcGatewayStorage.ClientChainID.Bitcoin, exocoreAddr + ).checked_write(btcAddr); + assertEq(gateway.inboundRegistry(ExocoreBtcGatewayStorage.ClientChainID.Bitcoin, btcAddr), exocoreAddr); + assertEq(gateway.outboundRegistry(ExocoreBtcGatewayStorage.ClientChainID.Bitcoin, exocoreAddr), btcAddr); } - // Helper function to convert string to bytes - function _stringToBytes(string memory source) internal pure returns (bytes memory) { - return abi.encodePacked(source); + function _addAllWitnesses() internal { + for (uint256 i = 0; i < witnesses.length; i++) { + if (!gateway.authorizedWitnesses(witnesses[i].addr)) { + vm.prank(owner); + gateway.addWitness(witnesses[i].addr); + } + } } - function _addressToBytes(address _addr) internal pure returns (bytes memory) { - return abi.encodePacked(bytes32(bytes20(_addr))); + function _getMessageHash(ExocoreBtcGatewayStorage.StakeMsg memory msg_) internal pure returns (bytes32) { + return keccak256( + abi.encode( + msg_.chainId, // ClientChainID + msg_.srcAddress, // bytes - Bitcoin address + msg_.exocoreAddress, // address + msg_.operator, // string + msg_.amount, // uint256 + msg_.nonce, // uint64 + msg_.txTag // bytes + ) + ); + } + + function _generateSignature(ExocoreBtcGatewayStorage.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); + + // Return the signature in the format expected by the contract + return abi.encodePacked(r, s, v); } } From 8e680d66a5a79f67d0840dc82a8290476d401721 Mon Sep 17 00:00:00 2001 From: adu Date: Wed, 20 Nov 2024 10:21:12 +0800 Subject: [PATCH 06/11] fix: unit tests --- src/core/ExocoreBtcGateway.sol | 93 ++-- src/libraries/Errors.sol | 21 + src/storage/ExocoreBtcGatewayStorage.sol | 110 +---- test/foundry/unit/ExocoreBtcGateway.t.sol | 526 +++++++++++++++++++--- 4 files changed, 544 insertions(+), 206 deletions(-) diff --git a/src/core/ExocoreBtcGateway.sol b/src/core/ExocoreBtcGateway.sol index 2af4c394..c5fba8b5 100644 --- a/src/core/ExocoreBtcGateway.sol +++ b/src/core/ExocoreBtcGateway.sol @@ -29,13 +29,14 @@ contract ExocoreBtcGateway is { using ExocoreBytes for address; + using SignatureVerifier for bytes32; /** * @dev Modifier to restrict access to authorized witnesses only. */ modifier onlyAuthorizedWitness() { if (!_isAuthorizedWitness(msg.sender)) { - revert UnauthorizedWitness(); + revert Errors.UnauthorizedWitness(); } _; } @@ -61,8 +62,6 @@ contract ExocoreBtcGateway is * @dev Sets up initial configuration for testing purposes. */ constructor() { - authorizedWitnesses[EXOCORE_WITNESS] = true; - authorizedWitnessCount = 1; _disableInitializers(); } @@ -161,26 +160,28 @@ contract ExocoreBtcGateway is 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[msg.sender] >= txn.expiryTime - PROOF_TIMEOUT) { + if (txn.witnessTime[witness] >= txn.expiryTime - PROOF_TIMEOUT) { revert Errors.WitnessAlreadySubmittedProof(); } - txn.witnessTime[msg.sender] = block.timestamp; + txn.witnessTime[witness] = block.timestamp; txn.proofCount++; } else { txn.status = TxStatus.Pending; txn.expiryTime = block.timestamp + PROOF_TIMEOUT; txn.proofCount = 1; - txn.witnessTime[msg.sender] = block.timestamp; + txn.witnessTime[witness] = block.timestamp; txn.stakeMsg = _message; } - emit ProofSubmitted(messageHash, msg.sender, _message); + emit ProofSubmitted(messageHash, witness); // Check for consensus if (txn.proofCount >= REQUIRED_PROOFS) { processedTransactions[messageHash] = true; _processStakeMsg(txn.stakeMsg); delete transactions[messageHash]; + + emit TransactionProcessed(messageHash); } } @@ -254,7 +255,7 @@ contract ExocoreBtcGateway is uint32(uint8(chainId)), nonce, VIRTUAL_TOKEN, msg.sender.toExocoreBytes(), bytes(operator), amount ); if (!success) { - revert UndelegationFailed(); + revert Errors.UndelegationFailed(); } emit UndelegationCompleted(chainId, msg.sender, operator, amount); } @@ -264,23 +265,22 @@ contract ExocoreBtcGateway is * @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) - isRegistered(token, msg.sender) - { + function withdrawPrincipal(Token token, uint256 amount) external nonReentrant whenNotPaused isValidAmount(amount) { ClientChainID chainId = ClientChainID(uint8(token)); + bytes memory clientChainAddress = outboundRegistry[chainId][msg.sender]; + if (clientChainAddress.length == 0) { + revert Errors.AddressNotRegistered(); + } + (bool success, uint256 updatedBalance) = ASSETS_CONTRACT.withdrawLST(uint32(uint8(chainId)), VIRTUAL_TOKEN, msg.sender.toExocoreBytes(), amount); if (!success) { - revert WithdrawPrincipalFailed(); + revert Errors.WithdrawPrincipalFailed(); } - (uint64 requestId, bytes memory clientChainAddress) = - _initiatePegOut(chainId, amount, msg.sender, WithdrawType.WithdrawPrincipal); + uint64 requestId = + _initiatePegOut(chainId, amount, msg.sender, clientChainAddress, WithdrawType.WithdrawPrincipal); emit WithdrawPrincipalRequested(chainId, requestId, msg.sender, clientChainAddress, amount, updatedBalance); } @@ -289,23 +289,20 @@ contract ExocoreBtcGateway is * @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) - isRegistered(token, msg.sender) - { + function withdrawReward(Token token, uint256 amount) external nonReentrant whenNotPaused isValidAmount(amount) { ClientChainID chainId = ClientChainID(uint8(token)); + bytes memory clientChainAddress = outboundRegistry[chainId][msg.sender]; + if (clientChainAddress.length == 0) { + revert Errors.AddressNotRegistered(); + } (bool success, uint256 updatedBalance) = REWARD_CONTRACT.claimReward(uint32(uint8(chainId)), VIRTUAL_TOKEN, msg.sender.toExocoreBytes(), amount); if (!success) { - revert WithdrawRewardFailed(); + revert Errors.WithdrawRewardFailed(); } - (uint64 requestId, bytes memory clientChainAddress) = - _initiatePegOut(chainId, amount, msg.sender, WithdrawType.WithdrawReward); + uint64 requestId = _initiatePegOut(chainId, amount, msg.sender, clientChainAddress, WithdrawType.WithdrawReward); emit WithdrawRewardRequested(chainId, requestId, msg.sender, clientChainAddress, amount, updatedBalance); } @@ -327,7 +324,7 @@ contract ExocoreBtcGateway is // Check if the request exists if (request.requester == address(0)) { - revert RequestNotFound(requestId); + revert Errors.RequestNotFound(requestId); } // delete the request @@ -351,6 +348,20 @@ contract ExocoreBtcGateway is return outboundRegistry[chainId][exocoreAddress]; } + /** + * @notice Gets the Exocore address for a given client chain address + * @param chainId The client chain ID + * @param clientChainAddress The client chain address + * @return The Exocore address + */ + function getExocoreAddress(ClientChainID chainId, bytes calldata clientChainAddress) + external + view + returns (address) + { + return inboundRegistry[chainId][clientChainAddress]; + } + /** * @notice Gets the current nonce for a given BTC address. * @param srcChainId The source chain ID. @@ -548,27 +559,23 @@ contract ExocoreBtcGateway is * @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 clientChainAddress The client chain 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(ClientChainID clientChain, uint256 _amount, address withdrawer, WithdrawType _withdrawType) - internal - returns (uint64 requestId, bytes memory clientChainAddress) - { - // 1. Check client c address - clientChainAddress = outboundRegistry[clientChain][withdrawer]; - if (clientChainAddress.length == 0) { - revert AddressNotRegistered(); - } - + function _initiatePegOut( + ClientChainID clientChain, + uint256 _amount, + address withdrawer, + bytes memory clientChainAddress, + WithdrawType _withdrawType + ) internal returns (uint64 requestId) { // 2. increase the peg-out nonce for the client chain and return as requestId requestId = ++pegOutNonce[clientChain]; // 3. Check if request already exists PegOutRequest storage request = pegOutRequests[requestId]; if (request.requester != address(0)) { - revert RequestAlreadyExists(requestId); + revert Errors.RequestAlreadyExists(requestId); } // 4. Create new PegOutRequest @@ -634,7 +641,7 @@ contract ExocoreBtcGateway is function _registerAddress(ClientChainID chainId, bytes memory depositor, address exocoreAddress) internal { require(depositor.length > 0 && exocoreAddress != address(0), "Invalid address"); - require(inboundRegistry[chainId][depositor] != address(0), "Depositor address already registered"); + require(inboundRegistry[chainId][depositor] == address(0), "Depositor address already registered"); require(outboundRegistry[chainId][exocoreAddress].length == 0, "Exocore address already registered"); inboundRegistry[chainId][depositor] = exocoreAddress; @@ -677,6 +684,8 @@ contract ExocoreBtcGateway is emit DelegationCompleted(_msg.chainId, stakerExoAddr, _msg.operator, amountAfterFee); } } + + emit StakeMsgExecuted(_msg.chainId, _msg.nonce, stakerExoAddr, amountAfterFee); } } diff --git a/src/libraries/Errors.sol b/src/libraries/Errors.sol index 1835caa2..7cf47fa5 100644 --- a/src/libraries/Errors.sol +++ b/src/libraries/Errors.sol @@ -327,6 +327,9 @@ library Errors { /// @dev ExocoreBtcGateway: invalid operator address error InvalidOperator(); + /// @dev ExocoreBtcGateway: invalid token + error InvalidToken(); + /// @dev ExocoreBtcGateway: witness has already been authorized error WitnessAlreadyAuthorized(address witness); @@ -348,4 +351,22 @@ library Errors { /// @dev ExocoreBtcGateway: delegation failed error DelegationFailed(); + /// @dev ExocoreBtcGateway: withdraw principal failed + error WithdrawPrincipalFailed(); + + /// @dev ExocoreBtcGateway: undelegation failed + error UndelegationFailed(); + + /// @dev ExocoreBtcGateway: withdraw reward failed + error WithdrawRewardFailed(); + + /// @dev ExocoreBtcGateway: request not found + error RequestNotFound(uint64 requestId); + + /// @dev ExocoreBtcGateway: request already exists + error RequestAlreadyExists(uint64 requestId); + + /// @dev ExocoreBtcGateway: witness not authorized + error UnauthorizedWitness(); + } diff --git a/src/storage/ExocoreBtcGatewayStorage.sol b/src/storage/ExocoreBtcGatewayStorage.sol index 437c893c..fad83bb3 100644 --- a/src/storage/ExocoreBtcGatewayStorage.sol +++ b/src/storage/ExocoreBtcGatewayStorage.sol @@ -192,6 +192,22 @@ contract ExocoreBtcGatewayStorage { uint256[40] private __gap; // Events + + /** + * @dev Emitted when a stake message is executed + * @param chainId The LayerZero chain ID of the client chain + * @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 chainId, 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 srcChainId The source chain ID @@ -339,9 +355,8 @@ contract ExocoreBtcGatewayStorage { * @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 - * @param message The stake message associated with the proof */ - event ProofSubmitted(bytes32 indexed messageHash, address indexed witness, StakeMsg message); + event ProofSubmitted(bytes32 indexed messageHash, address indexed witness); /** * @dev Emitted when a deposit is processed @@ -412,97 +427,6 @@ contract ExocoreBtcGatewayStorage { /// @param token The address of the token. event WhitelistTokenUpdated(uint32 clientChainId, address indexed token); - // 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 an address is not registered - */ - error AddressNotRegistered(); - - /** - * @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(uint64 requestId); - - /** - * @dev Thrown when attempting to create a request that already exists - * @param requestId The ID of the existing request - */ - error RequestAlreadyExists(uint64 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, not when processing a stake message - */ - 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); - - error InvalidTokenType(); - error InvalidClientChainId(); - /** * @dev Modifier to check if an amount is valid * @param amount The amount to check diff --git a/test/foundry/unit/ExocoreBtcGateway.t.sol b/test/foundry/unit/ExocoreBtcGateway.t.sol index 9ea0b782..1c8e6e08 100644 --- a/test/foundry/unit/ExocoreBtcGateway.t.sol +++ b/test/foundry/unit/ExocoreBtcGateway.t.sol @@ -8,6 +8,8 @@ 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 {ExocoreBtcGatewayStorage} from "src/storage/ExocoreBtcGatewayStorage.sol"; import "test/mocks/AssetsMock.sol"; @@ -19,6 +21,8 @@ import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.so contract ExocoreBtcGatewayTest is Test { using stdStorage for StdStorage; + using SignatureVerifier for bytes32; + using ExocoreBytes for address; struct Player { uint256 privateKey; @@ -58,7 +62,7 @@ contract ExocoreBtcGatewayTest is Test { event WitnessAdded(address indexed witness); event WitnessRemoved(address indexed witness); event AddressRegistered( - ExocoreBtcGatewayStorage.ClientChainID indexed chainId, bytes depositor, address exocoreAddress + ExocoreBtcGatewayStorage.ClientChainID indexed chainId, bytes depositor, address indexed exocoreAddress ); event DepositCompleted( ExocoreBtcGatewayStorage.ClientChainID indexed chainId, @@ -74,7 +78,13 @@ contract ExocoreBtcGatewayTest is Test { string operator, uint256 amount ); - event ProofSubmitted(bytes32 indexed txId, address indexed witness); + event UndelegationCompleted( + ExocoreBtcGatewayStorage.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); @@ -88,6 +98,30 @@ contract ExocoreBtcGatewayTest is Test { string operator, uint256 amount ); + event StakeMsgExecuted( + ExocoreBtcGatewayStorage.ClientChainID indexed chainId, + uint64 nonce, + address indexed exocoreAddress, + uint256 amount + ); + event TransactionProcessed(bytes32 indexed txId); + + event WithdrawPrincipalRequested( + ExocoreBtcGatewayStorage.ClientChainID indexed srcChainId, + uint64 indexed requestId, + address indexed withdrawerExoAddr, + bytes withdrawerClientChainAddr, + uint256 amount, + uint256 updatedBalance + ); + event WithdrawRewardRequested( + ExocoreBtcGatewayStorage.ClientChainID indexed srcChainId, + uint64 indexed requestId, + address indexed withdrawerExoAddr, + bytes withdrawerClientChainAddr, + uint256 amount, + uint256 updatedBalance + ); function setUp() public { owner = address(1); @@ -100,19 +134,9 @@ contract ExocoreBtcGatewayTest is Test { btcAddress = bytes("1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa"); operator = "exo13hasr43vvq8v44xpzh0l6yuym4kca98f87j7ac"; - // Deploy mock contracts and bind to precompile addresses - 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 RewardMockCode = vm.getDeployedCode("RewardMock.sol"); - vm.etch(REWARD_PRECOMPILE_ADDRESS, RewardMockCode); - // Deploy and initialize gateway gatewayLogic = new ExocoreBtcGateway(); - gateway = ExocoreBtcGateway(address(new TransparentUpgradeableProxy(address(gatewayLogic), address(0), ""))); + gateway = ExocoreBtcGateway(address(new TransparentUpgradeableProxy(address(gatewayLogic), address(0xab), ""))); address[] memory initialWitnesses = new address[](1); initialWitnesses[0] = witnesses[0].addr; gateway.initialize(owner, initialWitnesses); @@ -121,10 +145,10 @@ contract ExocoreBtcGatewayTest is Test { function test_initialize() public { assertEq(gateway.owner(), owner); assertTrue(gateway.authorizedWitnesses(witnesses[0].addr)); - assertEq(gateway.authorizedWitnessCount(), 2); + assertEq(gateway.authorizedWitnessCount(), 1); } - function test_AddWitness() public { + function test_AddWitness_Success() public { vm.prank(owner); vm.expectEmit(true, false, false, false); @@ -132,7 +156,7 @@ contract ExocoreBtcGatewayTest is Test { gateway.addWitness(witnesses[1].addr); assertTrue(gateway.authorizedWitnesses(witnesses[1].addr)); - assertEq(gateway.authorizedWitnessCount(), 3); + assertEq(gateway.authorizedWitnessCount(), 2); } function test_AddWitness_RevertNotOwner() public { @@ -153,7 +177,7 @@ contract ExocoreBtcGatewayTest is Test { gateway.addWitness(witnesses[1].addr); // Try to add the same witness again - vm.expectRevert("Witness already authorized"); + vm.expectRevert(abi.encodeWithSelector(Errors.WitnessAlreadyAuthorized.selector, witnesses[1].addr)); gateway.addWitness(witnesses[1].addr); vm.stopPrank(); } @@ -168,7 +192,12 @@ contract ExocoreBtcGatewayTest is Test { } function test_RemoveWitness() public { - vm.prank(owner); + vm.startPrank(owner); + + // we need to add a witness before removing the first witness, since we cannot remove the last witness + gateway.addWitness(witnesses[1].addr); + assertTrue(gateway.authorizedWitnesses(witnesses[1].addr)); + assertEq(gateway.authorizedWitnessCount(), 2); vm.expectEmit(true, false, false, false); emit WitnessRemoved(witnesses[0].addr); @@ -185,9 +214,14 @@ contract ExocoreBtcGatewayTest is Test { } function test_RemoveWitness_RevertWitnessNotAuthorized() public { - vm.prank(owner); - vm.expectRevert(abi.encodeWithSelector(Errors.WitnessNotAuthorized.selector, witnesses[1].addr)); - gateway.removeWitness(witnesses[1].addr); + // first add another witness to make total witnesses count 2 + vm.startPrank(owner); + gateway.addWitness(witnesses[1].addr); + + // try to remove the unauthorized one + vm.expectRevert(abi.encodeWithSelector(Errors.WitnessNotAuthorized.selector, witnesses[2].addr)); + gateway.removeWitness(witnesses[2].addr); + vm.stopPrank(); } function test_RemoveWitness_RevertWhenPaused() public { @@ -200,17 +234,13 @@ contract ExocoreBtcGatewayTest is Test { } function test_RemoveWitness_CannotRemoveLastWitness() public { - // First remove all witnesses except one - vm.startPrank(owner); - for (uint256 i = 0; i < witnesses.length; i++) { - if (gateway.authorizedWitnesses(witnesses[i].addr)) { - gateway.removeWitness(witnesses[i].addr); - } - } + // there should be only one witness added + assertEq(gateway.authorizedWitnessCount(), 1); + vm.startPrank(owner); // Try to remove the hardcoded witness vm.expectRevert(Errors.CannotRemoveLastWitness.selector); - gateway.removeWitness(EXOCORE_WITNESS); + gateway.removeWitness(witnesses[0].addr); vm.stopPrank(); } @@ -220,6 +250,11 @@ contract ExocoreBtcGatewayTest is Test { // First add another witness gateway.addWitness(witnesses[1].addr); assertTrue(gateway.authorizedWitnesses(witnesses[1].addr)); + assertEq(gateway.authorizedWitnessCount(), 2); + + // And add another witness + gateway.addWitness(witnesses[2].addr); + assertTrue(gateway.authorizedWitnesses(witnesses[2].addr)); assertEq(gateway.authorizedWitnessCount(), 3); // Remove first witness @@ -269,7 +304,7 @@ contract ExocoreBtcGatewayTest is Test { function test_UpdateBridgeFee_RevertExceedMax() public { vm.prank(owner); - vm.expectRevert("Fee cannot exceed 10%"); + vm.expectRevert("Fee cannot exceed max bridge fee rate"); gateway.updateBridgeFeeRate(1001); // 10.01% } @@ -308,7 +343,7 @@ contract ExocoreBtcGatewayTest is Test { vm.stopPrank(); } - function test_ActivateStakingForClientChain() public { + function test_ActivateStakingForClientChain_Success() public { vm.startPrank(owner); // Mock successful chain registration @@ -502,7 +537,7 @@ contract ExocoreBtcGatewayTest is Test { vm.stopPrank(); } - function test_SubmitProofForStakeMsg() public { + function test_SubmitProofForStakeMsg_Success() public { _addAllWitnesses(); // Create stake message @@ -526,20 +561,26 @@ contract ExocoreBtcGatewayTest is Test { gateway.submitProofForStakeMsg(witnesses[0].addr, stakeMsg, signature); // mock Assets precompile deposit success and Delegation precompile delegate success - vm.mockCall(ASSETS_PRECOMPILE_ADDRESS, abi.encodeWithSelector(IAssets.depositLST.selector), abi.encode(true)); + 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) ); // Submit proof from second witness signature = _generateSignature(stakeMsg, witnesses[1].privateKey); - vm.prank(witnesses[1].addr); + vm.prank(relayer); vm.expectEmit(true, true, false, true); emit ProofSubmitted(txId, witnesses[1].addr); // This should trigger message execution as we have enough proofs vm.expectEmit(true, false, false, false); - emit StakeMsgExecuted(txId); + emit StakeMsgExecuted(stakeMsg.chainId, stakeMsg.nonce, stakeMsg.exocoreAddress, stakeMsg.amount); + vm.expectEmit(true, false, false, false); + emit TransactionProcessed(txId); gateway.submitProofForStakeMsg(witnesses[1].addr, stakeMsg, signature); // Verify message was processed @@ -687,7 +728,11 @@ contract ExocoreBtcGatewayTest is Test { // as PROOFS_REQUIRED is 2, the transaction should be processed after another witness submits proof // mock Assets precompile deposit success and Delegation precompile delegate success - vm.mockCall(ASSETS_PRECOMPILE_ADDRESS, abi.encodeWithSelector(IAssets.depositLST.selector), abi.encode(true)); + 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) ); @@ -749,7 +794,11 @@ contract ExocoreBtcGatewayTest is Test { }); // mock Assets precompile deposit success and Delegation precompile delegate success - vm.mockCall(ASSETS_PRECOMPILE_ADDRESS, abi.encodeWithSelector(IAssets.depositLST.selector), abi.encode(true)); + 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) ); @@ -759,10 +808,13 @@ contract ExocoreBtcGatewayTest is Test { vm.prank(relayer); vm.expectEmit(true, true, true, true); emit AddressRegistered(ExocoreBtcGatewayStorage.ClientChainID.Bitcoin, btcAddress, user); + vm.expectEmit(true, true, true, true); + emit StakeMsgExecuted(ExocoreBtcGatewayStorage.ClientChainID.Bitcoin, stakeMsg.nonce, user, stakeMsg.amount); gateway.processStakeMessage(witnesses[0].addr, stakeMsg, signature); // Verify address registration assertEq(gateway.getClientChainAddress(ExocoreBtcGatewayStorage.ClientChainID.Bitcoin, user), btcAddress); + assertEq(gateway.getExocoreAddress(ExocoreBtcGatewayStorage.ClientChainID.Bitcoin, btcAddress), user); } function test_ProcessStakeMessage_WithBridgeFee() public { @@ -782,13 +834,17 @@ contract ExocoreBtcGatewayTest is Test { // 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)); + 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) ); - uint256 amountAfterFee = 1 ether - 1 ether * 100 / 10_000; vm.expectEmit(true, true, true, true, address(gateway)); emit DepositCompleted( @@ -797,7 +853,7 @@ contract ExocoreBtcGatewayTest is Test { user, stakeMsg.srcAddress, amountAfterFee, - stakeMsg.amount + amountAfterFee ); vm.expectEmit(true, true, true, true, address(gateway)); @@ -819,9 +875,13 @@ contract ExocoreBtcGatewayTest is Test { }); // mock Assets precompile deposit success and Delegation precompile delegate success - vm.mockCall(ASSETS_PRECOMPILE_ADDRESS, abi.encodeWithSignature(IAssets.deposit.selector), abi.encode(true)); vm.mockCall( - DELEGATION_PRECOMPILE_ADDRESS, abi.encodeWithSignature(IDelegation.delegate.selector), abi.encode(true) + 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); @@ -829,7 +889,7 @@ contract ExocoreBtcGatewayTest is Test { vm.prank(relayer); vm.expectEmit(true, true, true, true); emit DelegationCompleted(ExocoreBtcGatewayStorage.ClientChainID.Bitcoin, user, operator, 1 ether); - gateway.processStakeMessage(witnesses[0], stakeMsg, signature); + gateway.processStakeMessage(witnesses[0].addr, stakeMsg, signature); } function test_ProcessStakeMessage_DelegationFailureNotRevert() public { @@ -844,9 +904,13 @@ contract ExocoreBtcGatewayTest is Test { }); // mock Assets precompile deposit success and Delegation precompile delegate failure - vm.mockCall(ASSETS_PRECOMPILE_ADDRESS, abi.encodeWithSignature(IAssets.deposit.selector), abi.encode(true)); vm.mockCall( - DELEGATION_PRECOMPILE_ADDRESS, abi.encodeWithSignature(IDelegation.delegate.selector), abi.encode(false) + 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); @@ -854,13 +918,20 @@ contract ExocoreBtcGatewayTest is Test { vm.prank(relayer); // deposit should be successful vm.expectEmit(true, true, true, true); - emit DepositCompleted(ExocoreBtcGatewayStorage.ClientChainID.Bitcoin, user, 1 ether); + emit DepositCompleted( + ExocoreBtcGatewayStorage.ClientChainID.Bitcoin, + stakeMsg.txTag, + user, + stakeMsg.srcAddress, + 1 ether, + stakeMsg.amount + ); // delegation should fail vm.expectEmit(true, true, true, true); emit DelegationFailedForStake(ExocoreBtcGatewayStorage.ClientChainID.Bitcoin, user, operator, 1 ether); - gateway.processStakeMessage(witnesses[0], stakeMsg, signature); + gateway.processStakeMessage(witnesses[0].addr, stakeMsg, signature); } function test_ProcessStakeMessage_RevertOnDepositFailure() public { @@ -875,13 +946,15 @@ contract ExocoreBtcGatewayTest is Test { }); // mock Assets precompile deposit failure - vm.mockCall(ASSETS_PRECOMPILE_ADDRESS, abi.encodeWithSignature(IAssets.deposit.selector), abi.encode(false)); + 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], stakeMsg, signature); + gateway.processStakeMessage(witnesses[0].addr, stakeMsg, signature); } function test_ProcessStakeMessage_RevertWhenPaused() public { @@ -900,9 +973,9 @@ contract ExocoreBtcGatewayTest is Test { vm.prank(owner); gateway.pause(); - vm.prank(witnesses[0]); + vm.prank(relayer); vm.expectRevert("Pausable: paused"); - gateway.processStakeMessage(witnesses[0], stakeMsg, signature); + gateway.processStakeMessage(witnesses[0].addr, stakeMsg, signature); } function test_ProcessStakeMessage_RevertUnauthorizedWitness() public { @@ -919,9 +992,9 @@ contract ExocoreBtcGatewayTest is Test { Player memory unauthorizedWitness = Player({addr: vm.addr(0x999), privateKey: 0x999}); bytes memory signature = _generateSignature(stakeMsg, unauthorizedWitness.privateKey); - vm.prank(unauthorizedWitness); - vm.expectRevert(abi.encodeWithSelector(Errors.WitnessNotAuthorized.selector, unauthorizedWitness)); - gateway.processStakeMessage(unauthorizedWitness, stakeMsg, signature); + vm.prank(unauthorizedWitness.addr); + vm.expectRevert(abi.encodeWithSelector(Errors.WitnessNotAuthorized.selector, unauthorizedWitness.addr)); + gateway.processStakeMessage(unauthorizedWitness.addr, stakeMsg, signature); } function test_ProcessStakeMessage_RevertInvalidStakeMessage() public { @@ -940,7 +1013,7 @@ contract ExocoreBtcGatewayTest is Test { vm.prank(relayer); vm.expectRevert(Errors.InvalidStakeMessage.selector); - gateway.processStakeMessage(witnesses[0], stakeMsg, signature); + gateway.processStakeMessage(witnesses[0].addr, stakeMsg, signature); } function test_ProcessStakeMessage_RevertZeroExocoreAddressBeforeRegistration() public { @@ -958,7 +1031,7 @@ contract ExocoreBtcGatewayTest is Test { vm.prank(relayer); vm.expectRevert(Errors.ZeroAddress.selector); - gateway.processStakeMessage(witnesses[0], stakeMsg, signature); + gateway.processStakeMessage(witnesses[0].addr, stakeMsg, signature); } function test_ProcessStakeMessage_RevertInvalidNonce() public { @@ -968,7 +1041,7 @@ contract ExocoreBtcGatewayTest is Test { exocoreAddress: user, operator: "", amount: 1 ether, - nonce: gateway.nextInboundNonce() + 1, + nonce: gateway.nextInboundNonce(ExocoreBtcGatewayStorage.ClientChainID.Bitcoin) + 1, txTag: bytes("tx1") }); @@ -976,18 +1049,20 @@ contract ExocoreBtcGatewayTest is Test { vm.prank(relayer); vm.expectRevert( - abi.encodeWithSelector(Errors.UnexpectedInboundNonce.selector, gateway.nextInboundNonce(), stakeMsg.nonce) + abi.encodeWithSelector( + Errors.UnexpectedInboundNonce.selector, gateway.nextInboundNonce(stakeMsg.chainId), stakeMsg.nonce + ) ); - gateway.processStakeMessage(witnesses[0], stakeMsg, signature); + gateway.processStakeMessage(witnesses[0].addr, stakeMsg, signature); } - function test_DelegateTo() public { + 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.encodeWithSignature(IDelegation.delegate.selector), abi.encode(true) + DELEGATION_PRECOMPILE_ADDRESS, abi.encodeWithSelector(IDelegation.delegate.selector), abi.encode(true) ); vm.prank(user); @@ -1042,7 +1117,7 @@ contract ExocoreBtcGatewayTest is Test { // Mock delegation failure vm.mockCall( - DELEGATION_PRECOMPILE_ADDRESS, abi.encodeWithSignature(IDelegation.delegate.selector), abi.encode(false) + DELEGATION_PRECOMPILE_ADDRESS, abi.encodeWithSelector(IDelegation.delegate.selector), abi.encode(false) ); vm.prank(user); @@ -1050,16 +1125,325 @@ contract ExocoreBtcGatewayTest is Test { gateway.delegateTo(ExocoreBtcGatewayStorage.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(ExocoreBtcGatewayStorage.ClientChainID.Bitcoin, user, operator, 1 ether); + + gateway.undelegateFrom(ExocoreBtcGatewayStorage.Token.BTC, operator, 1 ether); + + // Verify nonce increment + assertEq(gateway.delegationNonce(ExocoreBtcGatewayStorage.ClientChainID.Bitcoin), 1); + } + + function test_UndelegateFrom_RevertZeroAmount() public { + _mockRegisterAddress(user, btcAddress); + + vm.prank(user); + vm.expectRevert(Errors.ZeroAmount.selector); + gateway.undelegateFrom(ExocoreBtcGatewayStorage.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(ExocoreBtcGatewayStorage.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(ExocoreBtcGatewayStorage.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(ExocoreBtcGatewayStorage.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(ExocoreBtcGatewayStorage.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( + ExocoreBtcGatewayStorage.ClientChainID.Bitcoin, + 1, // first request ID + user, + btcAddress, + 1 ether, + 2 ether + ); + + gateway.withdrawPrincipal(ExocoreBtcGatewayStorage.Token.BTC, 1 ether); + + // Verify pegOutNonce increment + assertEq(gateway.pegOutNonce(ExocoreBtcGatewayStorage.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(ExocoreBtcGatewayStorage.Token.BTC, 1 ether); + } + + function test_WithdrawPrincipal_RevertZeroAmount() public { + _mockRegisterAddress(user, btcAddress); + + vm.prank(user); + vm.expectRevert(Errors.ZeroAmount.selector); + gateway.withdrawPrincipal(ExocoreBtcGatewayStorage.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(ExocoreBtcGatewayStorage.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(ExocoreBtcGatewayStorage.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(ExocoreBtcGatewayStorage.Token.BTC, 1 ether); + + // Verify peg-out request details + ExocoreBtcGatewayStorage.PegOutRequest memory request = gateway.getPegOutRequest(1); + assertEq(uint8(request.chainId), uint8(ExocoreBtcGatewayStorage.ClientChainID.Bitcoin)); + assertEq(request.requester, user); + assertEq(request.clientChainAddress, btcAddress); + assertEq(request.amount, 1 ether); + assertEq(uint8(request.withdrawType), uint8(ExocoreBtcGatewayStorage.WithdrawType.WithdrawPrincipal)); + assertTrue(request.timestamp > 0); + } + + 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( + ExocoreBtcGatewayStorage.ClientChainID.Bitcoin, + 1, // first request ID + user, + btcAddress, + 1 ether, + 2 ether + ); + + gateway.withdrawReward(ExocoreBtcGatewayStorage.Token.BTC, 1 ether); + + // Verify pegOutNonce increment + assertEq(gateway.pegOutNonce(ExocoreBtcGatewayStorage.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(ExocoreBtcGatewayStorage.Token.BTC, 1 ether); + } + + function test_WithdrawReward_RevertZeroAmount() public { + _mockRegisterAddress(user, btcAddress); + + vm.prank(user); + vm.expectRevert(Errors.ZeroAmount.selector); + gateway.withdrawReward(ExocoreBtcGatewayStorage.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(ExocoreBtcGatewayStorage.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(ExocoreBtcGatewayStorage.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(ExocoreBtcGatewayStorage.Token.BTC, 1 ether); + + // Verify peg-out request details + ExocoreBtcGatewayStorage.PegOutRequest memory request = gateway.getPegOutRequest(1); + assertEq(uint8(request.chainId), uint8(ExocoreBtcGatewayStorage.ClientChainID.Bitcoin)); + assertEq(request.requester, user); + assertEq(request.clientChainAddress, btcAddress); + assertEq(request.amount, 1 ether); + assertEq(uint8(request.withdrawType), uint8(ExocoreBtcGatewayStorage.WithdrawType.WithdrawReward)); + assertTrue(request.timestamp > 0); + } + + function test_WithdrawReward_MultipleRequests() public { + _mockRegisterAddress(user, btcAddress); + + // Mock successful claimReward + bytes memory claimCall1 = abi.encodeWithSelector( + IReward.claimReward.selector, + uint32(uint8(ExocoreBtcGatewayStorage.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(ExocoreBtcGatewayStorage.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(ExocoreBtcGatewayStorage.Token.BTC, 1 ether); + + // Second withdrawal + gateway.withdrawReward(ExocoreBtcGatewayStorage.Token.BTC, 0.5 ether); + + vm.stopPrank(); + + // Verify both requests exist with correct details + ExocoreBtcGatewayStorage.PegOutRequest memory request1 = gateway.getPegOutRequest(1); + assertEq(request1.amount, 1 ether); + + ExocoreBtcGatewayStorage.PegOutRequest memory request2 = gateway.getPegOutRequest(2); + assertEq(request2.amount, 0.5 ether); + + // Verify nonce increment + assertEq(gateway.pegOutNonce(ExocoreBtcGatewayStorage.ClientChainID.Bitcoin), 2); + } + // Helper functions function _mockRegisterAddress(address exocoreAddr, bytes memory btcAddr) internal { - stdstore.target(address(gateway)).sig("inboundRegistry(ClientChainID, bytes)").with_key( - ExocoreBtcGatewayStorage.ClientChainID.Bitcoin, btcAddr - ).checked_write(exocoreAddr); - stdstore.target(address(gateway)).sig("outboundRegistry(ClientChainID, address)").with_key( - ExocoreBtcGatewayStorage.ClientChainID.Bitcoin, exocoreAddr - ).checked_write(btcAddr); - assertEq(gateway.inboundRegistry(ExocoreBtcGatewayStorage.ClientChainID.Bitcoin, btcAddr), exocoreAddr); - assertEq(gateway.outboundRegistry(ExocoreBtcGatewayStorage.ClientChainID.Bitcoin, exocoreAddr), btcAddr); + ExocoreBtcGatewayStorage.StakeMsg memory stakeMsg = ExocoreBtcGatewayStorage.StakeMsg({ + chainId: ExocoreBtcGatewayStorage.ClientChainID.Bitcoin, + srcAddress: btcAddr, + exocoreAddress: exocoreAddr, + operator: "", + amount: 1 ether, + nonce: gateway.nextInboundNonce(ExocoreBtcGatewayStorage.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(ExocoreBtcGatewayStorage.ClientChainID.Bitcoin, btcAddr, exocoreAddr); + + vm.prank(relayer); + gateway.processStakeMessage(witnesses[0].addr, stakeMsg, signature); + + // Verify address registration + assertEq(gateway.getClientChainAddress(ExocoreBtcGatewayStorage.ClientChainID.Bitcoin, exocoreAddr), btcAddr); + assertEq(gateway.getExocoreAddress(ExocoreBtcGatewayStorage.ClientChainID.Bitcoin, btcAddr), exocoreAddr); } function _addAllWitnesses() internal { @@ -1094,7 +1478,7 @@ contract ExocoreBtcGatewayTest is Test { bytes32 messageHash = _getMessageHash(msg_); // Sign the encoded message hash - (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, messageHash); + (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); From fc377af4bb0d1a5f28f1340966d621ad375544d6 Mon Sep 17 00:00:00 2001 From: adu Date: Wed, 20 Nov 2024 15:32:09 +0800 Subject: [PATCH 07/11] fix: slither and comments --- slither.config.json | 2 +- ...{ExocoreBtcGateway.sol => UTXOGateway.sol} | 70 +-- src/libraries/Errors.sol | 2 +- ...ewayStorage.sol => UTXOGatewayStorage.sol} | 56 ++- ...coreBtcGateway.t.sol => UTXOGateway.t.sol} | 413 +++++++++++------- 5 files changed, 342 insertions(+), 201 deletions(-) rename src/core/{ExocoreBtcGateway.sol => UTXOGateway.sol} (91%) rename src/storage/{ExocoreBtcGatewayStorage.sol => UTXOGatewayStorage.sol} (90%) rename test/foundry/unit/{ExocoreBtcGateway.t.sol => UTXOGateway.t.sol} (74%) 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/UTXOGateway.sol similarity index 91% rename from src/core/ExocoreBtcGateway.sol rename to src/core/UTXOGateway.sol index c5fba8b5..4433a3a2 100644 --- a/src/core/ExocoreBtcGateway.sol +++ b/src/core/UTXOGateway.sol @@ -8,24 +8,23 @@ 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 {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"; -// 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. + * @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 ExocoreBtcGateway is +contract UTXOGateway is Initializable, PausableUpgradeable, OwnableUpgradeable, ReentrancyGuardUpgradeable, - ExocoreBtcGatewayStorage + UTXOGatewayStorage { using ExocoreBytes for address; @@ -137,10 +136,13 @@ contract ExocoreBtcGateway is /** * @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 @@ -179,6 +181,8 @@ contract ExocoreBtcGateway is if (txn.proofCount >= REQUIRED_PROOFS) { 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); @@ -186,7 +190,7 @@ contract ExocoreBtcGateway is } /** - * @notice Deposits BTC to the Exocore system. + * @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. @@ -205,7 +209,7 @@ contract ExocoreBtcGateway is } /** - * @notice Delegates BTC to an operator. + * @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. @@ -232,7 +236,7 @@ contract ExocoreBtcGateway is } /** - * @notice Undelegates BTC from an operator. + * @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. @@ -261,7 +265,7 @@ contract ExocoreBtcGateway is } /** - * @notice Withdraws the principal BTC. + * @notice Withdraws the principal BTC like tokens. * @param token The value of the token enum. * @param amount The amount to withdraw. */ @@ -285,7 +289,7 @@ contract ExocoreBtcGateway is } /** - * @notice Withdraws the reward BTC. + * @notice Withdraws the reward BTC like tokens. * @param token The value of the token enum. * @param amount The amount to withdraw. */ @@ -309,6 +313,9 @@ contract ExocoreBtcGateway is /** * @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 clientChain 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 */ @@ -317,21 +324,28 @@ contract ExocoreBtcGateway is onlyAuthorizedWitness nonReentrant whenNotPaused - returns (uint64 requestId) + returns (PegOutRequest memory nextRequest) { - requestId = ++outboundNonce[clientChain]; - PegOutRequest storage request = pegOutRequests[requestId]; + uint64 requestId = ++outboundNonce[clientChain]; + nextRequest = pegOutRequests[clientChain][requestId]; // Check if the request exists - if (request.requester == address(0)) { + if (nextRequest.requester == address(0)) { revert Errors.RequestNotFound(requestId); } // delete the request - delete pegOutRequests[requestId]; + delete pegOutRequests[clientChain][requestId]; // Emit event - emit PegOutProcessed(requestId); + emit PegOutProcessed( + uint8(nextRequest.withdrawType), + clientChain, + requestId, + nextRequest.requester, + nextRequest.clientChainAddress, + nextRequest.amount + ); } /** @@ -363,21 +377,22 @@ contract ExocoreBtcGateway is } /** - * @notice Gets the current nonce for a given BTC address. + * @notice Gets the next inbound nonce for a given source chain ID. * @param srcChainId The source chain ID. - * @return The current nonce. + * @return The next inbound nonce. */ function nextInboundNonce(ClientChainID srcChainId) external view returns (uint64) { return inboundNonce[srcChainId] + 1; } /** - * @notice Retrieves a PegOutRequest by its requestId. + * @notice Retrieves a PegOutRequest by client chain id and request id + * @param clientChain The client chain ID * @param requestId The unique identifier of the request. * @return The PegOutRequest struct associated with the given requestId. */ - function getPegOutRequest(uint64 requestId) public view returns (PegOutRequest memory) { - return pegOutRequests[requestId]; + function getPegOutRequest(ClientChainID clientChain, uint64 requestId) public view returns (PegOutRequest memory) { + return pegOutRequests[clientChain][requestId]; } /** @@ -559,7 +574,6 @@ contract ExocoreBtcGateway is * @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 - * @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( @@ -573,18 +587,18 @@ contract ExocoreBtcGateway is requestId = ++pegOutNonce[clientChain]; // 3. Check if request already exists - PegOutRequest storage request = pegOutRequests[requestId]; + PegOutRequest storage request = pegOutRequests[clientChain][requestId]; if (request.requester != address(0)) { - revert Errors.RequestAlreadyExists(requestId); + revert Errors.RequestAlreadyExists(uint32(uint8(clientChain)), requestId); } // 4. Create new PegOutRequest request.chainId = clientChain; + request.nonce = requestId; request.requester = withdrawer; request.clientChainAddress = clientChainAddress; request.amount = _amount; request.withdrawType = _withdrawType; - request.timestamp = block.timestamp; } /** diff --git a/src/libraries/Errors.sol b/src/libraries/Errors.sol index 7cf47fa5..3645b329 100644 --- a/src/libraries/Errors.sol +++ b/src/libraries/Errors.sol @@ -364,7 +364,7 @@ library Errors { error RequestNotFound(uint64 requestId); /// @dev ExocoreBtcGateway: request already exists - error RequestAlreadyExists(uint64 requestId); + error RequestAlreadyExists(uint32 clientChain, uint64 requestId); /// @dev ExocoreBtcGateway: witness not authorized error UnauthorizedWitness(); diff --git a/src/storage/ExocoreBtcGatewayStorage.sol b/src/storage/UTXOGatewayStorage.sol similarity index 90% rename from src/storage/ExocoreBtcGatewayStorage.sol rename to src/storage/UTXOGatewayStorage.sol index fad83bb3..01c60b82 100644 --- a/src/storage/ExocoreBtcGatewayStorage.sol +++ b/src/storage/UTXOGatewayStorage.sol @@ -4,10 +4,10 @@ pragma solidity ^0.8.19; import {Errors} from "../libraries/Errors.sol"; /** - * @title ExocoreBtcGatewayStorage - * @dev This contract manages the storage for the Exocore-Bitcoin gateway + * @title UTXOGatewayStorage + * @dev This contract manages the storage for the UTXO gateway */ -contract ExocoreBtcGatewayStorage { +contract UTXOGatewayStorage { /** * @notice Enum to represent the type of supported token @@ -92,7 +92,6 @@ contract ExocoreBtcGatewayStorage { bytes clientChainAddress; uint256 amount; WithdrawType withdrawType; - uint256 timestamp; } /* -------------------------------------------------------------------------- */ @@ -101,7 +100,7 @@ contract ExocoreBtcGatewayStorage { /// @notice the human readable prefix for Exocore bech32 encoded address. bytes public constant EXO_ADDRESS_PREFIX = bytes("exo1"); - // chain id from layerzero, virtual for bitcoin since it's not yet a layerzero chain + // 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"; @@ -142,9 +141,12 @@ contract ExocoreBtcGatewayStorage { mapping(ClientChainID => mapping(bytes => bool)) public processedClientChainTxs; /** - * @dev Mapping to store peg-out requests, key is the nonce + * @dev Mapping to store peg-out requests + * @dev Key1: ClientChainID + * @dev Key2: nonce + * @dev Value: PegOutRequest */ - mapping(uint64 => PegOutRequest) public pegOutRequests; + mapping(ClientChainID => mapping(uint64 => PegOutRequest)) public pegOutRequests; /** * @dev Mapping to store authorized witnesses @@ -195,7 +197,7 @@ contract ExocoreBtcGatewayStorage { /** * @dev Emitted when a stake message is executed - * @param chainId The LayerZero chain ID of the client chain + * @param chainId 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) @@ -300,7 +302,7 @@ contract ExocoreBtcGatewayStorage { /** * @dev Emitted when a delegation is completed - * @param clientChainId The LayerZero chain ID of the client chain + * @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 @@ -311,7 +313,7 @@ contract ExocoreBtcGatewayStorage { /** * @dev Emitted when a delegation fails for a stake message - * @param clientChainId The LayerZero chain ID of the client chain + * @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 @@ -322,7 +324,7 @@ contract ExocoreBtcGatewayStorage { /** * @dev Emitted when an undelegation is completed - * @param clientChainId The LayerZero chain ID of the client chain + * @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 @@ -333,7 +335,7 @@ contract ExocoreBtcGatewayStorage { /** * @dev Emitted when an address is registered - * @param chainId The LayerZero chain ID of the client chain + * @param chainId The chain ID of the client chain, should not violate the layerzero chain id * @param depositor The depositor's address * @param exocoreAddress The corresponding Exocore address */ @@ -372,12 +374,6 @@ contract ExocoreBtcGatewayStorage { */ event TransactionExpired(bytes32 txid); - /** - * @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 rate is updated * @param newRate The new bridge rate @@ -398,9 +394,21 @@ contract ExocoreBtcGatewayStorage { /** * @dev Emitted when a peg-out is processed - * @param requestId The unique identifier of the processed peg-out request + * @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(uint64 indexed requestId); + 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 @@ -410,20 +418,20 @@ contract ExocoreBtcGatewayStorage { event PegOutRequestStatusUpdated(bytes32 indexed requestId, TxStatus newStatus); /// @notice Emitted upon the registration of a new client chain. - /// @param clientChainId The LayerZero chain ID of the client chain. + /// @param clientChainId The chain ID of the client chain. event ClientChainRegistered(uint32 clientChainId); /// @notice Emitted upon the update of a client chain. - /// @param clientChainId The LayerZero chain ID of the client chain. + /// @param clientChainId The chain ID of the client chain. event ClientChainUpdated(uint32 clientChainId); /// @notice Emitted when a token is added to the whitelist. - /// @param clientChainId The LayerZero chain ID of the client chain. + /// @param clientChainId The chain ID of the client chain. /// @param token The address of the token. event WhitelistTokenAdded(uint32 clientChainId, address indexed token); /// @notice Emitted when a token is updated in the whitelist. - /// @param clientChainId The LayerZero chain ID of the client chain. + /// @param clientChainId The chain ID of the client chain. /// @param token The address of the token. event WhitelistTokenUpdated(uint32 clientChainId, address indexed token); diff --git a/test/foundry/unit/ExocoreBtcGateway.t.sol b/test/foundry/unit/UTXOGateway.t.sol similarity index 74% rename from test/foundry/unit/ExocoreBtcGateway.t.sol rename to test/foundry/unit/UTXOGateway.t.sol index 1c8e6e08..ec96d5ea 100644 --- a/test/foundry/unit/ExocoreBtcGateway.t.sol +++ b/test/foundry/unit/UTXOGateway.t.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.19; import "forge-std/Test.sol"; -import {ExocoreBtcGateway} from "src/core/ExocoreBtcGateway.sol"; +import {UTXOGateway} from "src/core/UTXOGateway.sol"; import "src/interfaces/precompiles/IAssets.sol"; import "src/interfaces/precompiles/IDelegation.sol"; @@ -11,14 +11,14 @@ import {Errors} from "src/libraries/Errors.sol"; import {ExocoreBytes} from "src/libraries/ExocoreBytes.sol"; import {SignatureVerifier} from "src/libraries/SignatureVerifier.sol"; -import {ExocoreBtcGatewayStorage} from "src/storage/ExocoreBtcGatewayStorage.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 ExocoreBtcGatewayTest is Test { +contract UTXOGatewayTest is Test { using stdStorage for StdStorage; using SignatureVerifier for bytes32; @@ -29,15 +29,14 @@ contract ExocoreBtcGatewayTest is Test { address addr; } - ExocoreBtcGateway gateway; - ExocoreBtcGateway gatewayLogic; + UTXOGateway gateway; + UTXOGateway gatewayLogic; address owner; address user; address relayer; Player[3] witnesses; bytes btcAddress; string operator; - ExocoreBtcGatewayStorage.Transaction txn; address public constant EXOCORE_WITNESS = address(0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266); @@ -62,10 +61,10 @@ contract ExocoreBtcGatewayTest is Test { event WitnessAdded(address indexed witness); event WitnessRemoved(address indexed witness); event AddressRegistered( - ExocoreBtcGatewayStorage.ClientChainID indexed chainId, bytes depositor, address indexed exocoreAddress + UTXOGatewayStorage.ClientChainID indexed chainId, bytes depositor, address indexed exocoreAddress ); event DepositCompleted( - ExocoreBtcGatewayStorage.ClientChainID indexed chainId, + UTXOGatewayStorage.ClientChainID indexed chainId, bytes txTag, address indexed exocoreAddress, bytes srcAddress, @@ -73,13 +72,10 @@ contract ExocoreBtcGatewayTest is Test { uint256 updatedBalance ); event DelegationCompleted( - ExocoreBtcGatewayStorage.ClientChainID indexed chainId, - address indexed delegator, - string operator, - uint256 amount + UTXOGatewayStorage.ClientChainID indexed chainId, address indexed delegator, string operator, uint256 amount ); event UndelegationCompleted( - ExocoreBtcGatewayStorage.ClientChainID indexed clientChainId, + UTXOGatewayStorage.ClientChainID indexed clientChainId, address indexed exoDelegator, string operator, uint256 amount @@ -93,21 +89,18 @@ contract ExocoreBtcGatewayTest is Test { event WhitelistTokenAdded(uint32 clientChainId, address indexed token); event WhitelistTokenUpdated(uint32 clientChainId, address indexed token); event DelegationFailedForStake( - ExocoreBtcGatewayStorage.ClientChainID indexed clientChainId, + UTXOGatewayStorage.ClientChainID indexed clientChainId, address indexed exoDelegator, string operator, uint256 amount ); event StakeMsgExecuted( - ExocoreBtcGatewayStorage.ClientChainID indexed chainId, - uint64 nonce, - address indexed exocoreAddress, - uint256 amount + UTXOGatewayStorage.ClientChainID indexed chainId, uint64 nonce, address indexed exocoreAddress, uint256 amount ); event TransactionProcessed(bytes32 indexed txId); event WithdrawPrincipalRequested( - ExocoreBtcGatewayStorage.ClientChainID indexed srcChainId, + UTXOGatewayStorage.ClientChainID indexed srcChainId, uint64 indexed requestId, address indexed withdrawerExoAddr, bytes withdrawerClientChainAddr, @@ -115,13 +108,21 @@ contract ExocoreBtcGatewayTest is Test { uint256 updatedBalance ); event WithdrawRewardRequested( - ExocoreBtcGatewayStorage.ClientChainID indexed srcChainId, + 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 + ); function setUp() public { owner = address(1); @@ -135,8 +136,8 @@ contract ExocoreBtcGatewayTest is Test { operator = "exo13hasr43vvq8v44xpzh0l6yuym4kca98f87j7ac"; // Deploy and initialize gateway - gatewayLogic = new ExocoreBtcGateway(); - gateway = ExocoreBtcGateway(address(new TransparentUpgradeableProxy(address(gatewayLogic), address(0xab), ""))); + 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); @@ -349,7 +350,7 @@ contract ExocoreBtcGatewayTest is Test { // Mock successful chain registration bytes memory chainRegisterCall = abi.encodeWithSelector( IAssets.registerOrUpdateClientChain.selector, - uint32(uint8(ExocoreBtcGatewayStorage.ClientChainID.Bitcoin)), + uint32(uint8(UTXOGatewayStorage.ClientChainID.Bitcoin)), STAKER_ACCOUNT_LENGTH, BITCOIN_NAME, BITCOIN_METADATA, @@ -364,7 +365,7 @@ contract ExocoreBtcGatewayTest is Test { // Mock successful token registration bytes memory tokenRegisterCall = abi.encodeWithSelector( IAssets.registerToken.selector, - uint32(uint8(ExocoreBtcGatewayStorage.ClientChainID.Bitcoin)), + uint32(uint8(UTXOGatewayStorage.ClientChainID.Bitcoin)), VIRTUAL_TOKEN, BTC_DECIMALS, BTC_NAME, @@ -378,11 +379,11 @@ contract ExocoreBtcGatewayTest is Test { ); vm.expectEmit(true, false, false, false); - emit ClientChainRegistered(uint32(uint8(ExocoreBtcGatewayStorage.ClientChainID.Bitcoin))); + emit ClientChainRegistered(uint32(uint8(UTXOGatewayStorage.ClientChainID.Bitcoin))); vm.expectEmit(true, false, false, false); - emit WhitelistTokenAdded(uint32(uint8(ExocoreBtcGatewayStorage.ClientChainID.Bitcoin)), VIRTUAL_TOKEN_ADDRESS); + emit WhitelistTokenAdded(uint32(uint8(UTXOGatewayStorage.ClientChainID.Bitcoin)), VIRTUAL_TOKEN_ADDRESS); - gateway.activateStakingForClientChain(ExocoreBtcGatewayStorage.ClientChainID.Bitcoin); + gateway.activateStakingForClientChain(UTXOGatewayStorage.ClientChainID.Bitcoin); vm.stopPrank(); } @@ -392,7 +393,7 @@ contract ExocoreBtcGatewayTest is Test { // Mock chain update bytes memory chainRegisterCall = abi.encodeWithSelector( IAssets.registerOrUpdateClientChain.selector, - uint32(uint8(ExocoreBtcGatewayStorage.ClientChainID.Bitcoin)), + uint32(uint8(UTXOGatewayStorage.ClientChainID.Bitcoin)), STAKER_ACCOUNT_LENGTH, BITCOIN_NAME, BITCOIN_METADATA, @@ -407,7 +408,7 @@ contract ExocoreBtcGatewayTest is Test { // Mock token update bytes memory tokenRegisterCall = abi.encodeWithSelector( IAssets.registerToken.selector, - uint32(uint8(ExocoreBtcGatewayStorage.ClientChainID.Bitcoin)), + uint32(uint8(UTXOGatewayStorage.ClientChainID.Bitcoin)), VIRTUAL_TOKEN, BTC_DECIMALS, BTC_NAME, @@ -423,7 +424,7 @@ contract ExocoreBtcGatewayTest is Test { // Mock token update call bytes memory tokenUpdateCall = abi.encodeWithSelector( IAssets.updateToken.selector, - uint32(uint8(ExocoreBtcGatewayStorage.ClientChainID.Bitcoin)), + uint32(uint8(UTXOGatewayStorage.ClientChainID.Bitcoin)), VIRTUAL_TOKEN, BTC_METADATA ); @@ -434,11 +435,11 @@ contract ExocoreBtcGatewayTest is Test { ); vm.expectEmit(true, false, false, false); - emit ClientChainUpdated(uint32(uint8(ExocoreBtcGatewayStorage.ClientChainID.Bitcoin))); + emit ClientChainUpdated(uint32(uint8(UTXOGatewayStorage.ClientChainID.Bitcoin))); vm.expectEmit(true, false, false, false); - emit WhitelistTokenUpdated(uint32(uint8(ExocoreBtcGatewayStorage.ClientChainID.Bitcoin)), VIRTUAL_TOKEN_ADDRESS); + emit WhitelistTokenUpdated(uint32(uint8(UTXOGatewayStorage.ClientChainID.Bitcoin)), VIRTUAL_TOKEN_ADDRESS); - gateway.activateStakingForClientChain(ExocoreBtcGatewayStorage.ClientChainID.Bitcoin); + gateway.activateStakingForClientChain(UTXOGatewayStorage.ClientChainID.Bitcoin); vm.stopPrank(); } @@ -448,7 +449,7 @@ contract ExocoreBtcGatewayTest is Test { // Mock failed chain registration bytes memory chainRegisterCall = abi.encodeWithSelector( IAssets.registerOrUpdateClientChain.selector, - uint32(uint8(ExocoreBtcGatewayStorage.ClientChainID.Bitcoin)), + uint32(uint8(UTXOGatewayStorage.ClientChainID.Bitcoin)), STAKER_ACCOUNT_LENGTH, BITCOIN_NAME, BITCOIN_METADATA, @@ -463,10 +464,10 @@ contract ExocoreBtcGatewayTest is Test { vm.expectRevert( abi.encodeWithSelector( Errors.RegisterClientChainToExocoreFailed.selector, - uint32(uint8(ExocoreBtcGatewayStorage.ClientChainID.Bitcoin)) + uint32(uint8(UTXOGatewayStorage.ClientChainID.Bitcoin)) ) ); - gateway.activateStakingForClientChain(ExocoreBtcGatewayStorage.ClientChainID.Bitcoin); + gateway.activateStakingForClientChain(UTXOGatewayStorage.ClientChainID.Bitcoin); vm.stopPrank(); } @@ -476,7 +477,7 @@ contract ExocoreBtcGatewayTest is Test { // Mock successful chain registration bytes memory chainRegisterCall = abi.encodeWithSelector( IAssets.registerOrUpdateClientChain.selector, - uint32(uint8(ExocoreBtcGatewayStorage.ClientChainID.Bitcoin)), + uint32(uint8(UTXOGatewayStorage.ClientChainID.Bitcoin)), STAKER_ACCOUNT_LENGTH, BITCOIN_NAME, BITCOIN_METADATA, @@ -487,7 +488,7 @@ contract ExocoreBtcGatewayTest is Test { // Mock failed token registration bytes memory tokenRegisterCall = abi.encodeWithSelector( IAssets.registerToken.selector, - uint32(uint8(ExocoreBtcGatewayStorage.ClientChainID.Bitcoin)), + uint32(uint8(UTXOGatewayStorage.ClientChainID.Bitcoin)), VIRTUAL_TOKEN, BTC_DECIMALS, BTC_NAME, @@ -499,7 +500,7 @@ contract ExocoreBtcGatewayTest is Test { // Mock failed token update bytes memory tokenUpdateCall = abi.encodeWithSelector( IAssets.updateToken.selector, - uint32(uint8(ExocoreBtcGatewayStorage.ClientChainID.Bitcoin)), + uint32(uint8(UTXOGatewayStorage.ClientChainID.Bitcoin)), VIRTUAL_TOKEN, BTC_METADATA ); @@ -508,24 +509,24 @@ contract ExocoreBtcGatewayTest is Test { vm.expectRevert( abi.encodeWithSelector( Errors.AddWhitelistTokenFailed.selector, - uint32(uint8(ExocoreBtcGatewayStorage.ClientChainID.Bitcoin)), + uint32(uint8(UTXOGatewayStorage.ClientChainID.Bitcoin)), bytes32(VIRTUAL_TOKEN) ) ); - gateway.activateStakingForClientChain(ExocoreBtcGatewayStorage.ClientChainID.Bitcoin); + gateway.activateStakingForClientChain(UTXOGatewayStorage.ClientChainID.Bitcoin); vm.stopPrank(); } function test_ActivateStakingForClientChain_RevertInvalidChain() public { vm.prank(owner); vm.expectRevert(Errors.InvalidClientChain.selector); - gateway.activateStakingForClientChain(ExocoreBtcGatewayStorage.ClientChainID.None); + gateway.activateStakingForClientChain(UTXOGatewayStorage.ClientChainID.None); } function test_ActivateStakingForClientChain_RevertNotOwner() public { vm.prank(user); vm.expectRevert("Ownable: caller is not the owner"); - gateway.activateStakingForClientChain(ExocoreBtcGatewayStorage.ClientChainID.Bitcoin); + gateway.activateStakingForClientChain(UTXOGatewayStorage.ClientChainID.Bitcoin); } function test_ActivateStakingForClientChain_RevertWhenPaused() public { @@ -533,7 +534,7 @@ contract ExocoreBtcGatewayTest is Test { gateway.pause(); vm.expectRevert("Pausable: paused"); - gateway.activateStakingForClientChain(ExocoreBtcGatewayStorage.ClientChainID.Bitcoin); + gateway.activateStakingForClientChain(UTXOGatewayStorage.ClientChainID.Bitcoin); vm.stopPrank(); } @@ -541,8 +542,8 @@ contract ExocoreBtcGatewayTest is Test { _addAllWitnesses(); // Create stake message - ExocoreBtcGatewayStorage.StakeMsg memory stakeMsg = ExocoreBtcGatewayStorage.StakeMsg({ - chainId: ExocoreBtcGatewayStorage.ClientChainID.Bitcoin, + UTXOGatewayStorage.StakeMsg memory stakeMsg = UTXOGatewayStorage.StakeMsg({ + chainId: UTXOGatewayStorage.ClientChainID.Bitcoin, srcAddress: btcAddress, exocoreAddress: user, operator: operator, @@ -591,8 +592,8 @@ contract ExocoreBtcGatewayTest is Test { function test_SubmitProofForStakeMsg_RevertInvalidSignature() public { _addAllWitnesses(); - ExocoreBtcGatewayStorage.StakeMsg memory stakeMsg = ExocoreBtcGatewayStorage.StakeMsg({ - chainId: ExocoreBtcGatewayStorage.ClientChainID.Bitcoin, + UTXOGatewayStorage.StakeMsg memory stakeMsg = UTXOGatewayStorage.StakeMsg({ + chainId: UTXOGatewayStorage.ClientChainID.Bitcoin, srcAddress: btcAddress, exocoreAddress: user, operator: operator, @@ -611,8 +612,8 @@ contract ExocoreBtcGatewayTest is Test { function test_SubmitProofForStakeMsg_RevertUnauthorizedWitness() public { _addAllWitnesses(); - ExocoreBtcGatewayStorage.StakeMsg memory stakeMsg = ExocoreBtcGatewayStorage.StakeMsg({ - chainId: ExocoreBtcGatewayStorage.ClientChainID.Bitcoin, + UTXOGatewayStorage.StakeMsg memory stakeMsg = UTXOGatewayStorage.StakeMsg({ + chainId: UTXOGatewayStorage.ClientChainID.Bitcoin, srcAddress: btcAddress, exocoreAddress: user, operator: operator, @@ -632,8 +633,8 @@ contract ExocoreBtcGatewayTest is Test { function test_SubmitProofForStakeMsg_ExpiredBeforeConsensus() public { _addAllWitnesses(); - ExocoreBtcGatewayStorage.StakeMsg memory stakeMsg = ExocoreBtcGatewayStorage.StakeMsg({ - chainId: ExocoreBtcGatewayStorage.ClientChainID.Bitcoin, + UTXOGatewayStorage.StakeMsg memory stakeMsg = UTXOGatewayStorage.StakeMsg({ + chainId: UTXOGatewayStorage.ClientChainID.Bitcoin, srcAddress: btcAddress, exocoreAddress: user, operator: operator, @@ -659,7 +660,7 @@ contract ExocoreBtcGatewayTest is Test { // Verify transaction is restarted owing to expired and not processed bytes32 messageHash = _getMessageHash(stakeMsg); - assertEq(uint8(gateway.getTransactionStatus(messageHash)), uint8(ExocoreBtcGatewayStorage.TxStatus.Pending)); + assertEq(uint8(gateway.getTransactionStatus(messageHash)), uint8(UTXOGatewayStorage.TxStatus.Pending)); assertEq(gateway.getTransactionProofCount(messageHash), 1); assertFalse(gateway.processedClientChainTxs(stakeMsg.chainId, stakeMsg.txTag)); } @@ -667,8 +668,8 @@ contract ExocoreBtcGatewayTest is Test { function test_SubmitProofForStakeMsg_RestartExpiredTransaction() public { _addAllWitnesses(); - ExocoreBtcGatewayStorage.StakeMsg memory stakeMsg = ExocoreBtcGatewayStorage.StakeMsg({ - chainId: ExocoreBtcGatewayStorage.ClientChainID.Bitcoin, + UTXOGatewayStorage.StakeMsg memory stakeMsg = UTXOGatewayStorage.StakeMsg({ + chainId: UTXOGatewayStorage.ClientChainID.Bitcoin, srcAddress: btcAddress, exocoreAddress: user, operator: operator, @@ -693,7 +694,7 @@ contract ExocoreBtcGatewayTest is Test { bytes32 messageHash = _getMessageHash(stakeMsg); // Verify transaction is restarted - assertEq(uint8(gateway.getTransactionStatus(messageHash)), uint8(ExocoreBtcGatewayStorage.TxStatus.Pending)); + 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)); @@ -702,8 +703,8 @@ contract ExocoreBtcGatewayTest is Test { function test_SubmitProofForStakeMsg_JoinRestartedTransaction() public { _addAllWitnesses(); - ExocoreBtcGatewayStorage.StakeMsg memory stakeMsg = ExocoreBtcGatewayStorage.StakeMsg({ - chainId: ExocoreBtcGatewayStorage.ClientChainID.Bitcoin, + UTXOGatewayStorage.StakeMsg memory stakeMsg = UTXOGatewayStorage.StakeMsg({ + chainId: UTXOGatewayStorage.ClientChainID.Bitcoin, srcAddress: btcAddress, exocoreAddress: user, operator: operator, @@ -746,8 +747,7 @@ contract ExocoreBtcGatewayTest is Test { // Verify both witnesses' proofs are counted assertEq( - uint8(gateway.getTransactionStatus(messageHash)), - uint8(ExocoreBtcGatewayStorage.TxStatus.NotStartedOrProcessed) + uint8(gateway.getTransactionStatus(messageHash)), uint8(UTXOGatewayStorage.TxStatus.NotStartedOrProcessed) ); assertTrue(gateway.processedTransactions(messageHash)); assertTrue(gateway.processedClientChainTxs(stakeMsg.chainId, stakeMsg.txTag)); @@ -760,8 +760,8 @@ contract ExocoreBtcGatewayTest is Test { function test_SubmitProofForStakeMsg_RevertDuplicateProofInSameRound() public { _addAllWitnesses(); - ExocoreBtcGatewayStorage.StakeMsg memory stakeMsg = ExocoreBtcGatewayStorage.StakeMsg({ - chainId: ExocoreBtcGatewayStorage.ClientChainID.Bitcoin, + UTXOGatewayStorage.StakeMsg memory stakeMsg = UTXOGatewayStorage.StakeMsg({ + chainId: UTXOGatewayStorage.ClientChainID.Bitcoin, srcAddress: btcAddress, exocoreAddress: user, operator: operator, @@ -783,8 +783,8 @@ contract ExocoreBtcGatewayTest is Test { } function test_ProcessStakeMessage_RegisterNewAddress() public { - ExocoreBtcGatewayStorage.StakeMsg memory stakeMsg = ExocoreBtcGatewayStorage.StakeMsg({ - chainId: ExocoreBtcGatewayStorage.ClientChainID.Bitcoin, + UTXOGatewayStorage.StakeMsg memory stakeMsg = UTXOGatewayStorage.StakeMsg({ + chainId: UTXOGatewayStorage.ClientChainID.Bitcoin, srcAddress: btcAddress, exocoreAddress: user, operator: "", @@ -807,19 +807,19 @@ contract ExocoreBtcGatewayTest is Test { vm.prank(relayer); vm.expectEmit(true, true, true, true); - emit AddressRegistered(ExocoreBtcGatewayStorage.ClientChainID.Bitcoin, btcAddress, user); + emit AddressRegistered(UTXOGatewayStorage.ClientChainID.Bitcoin, btcAddress, user); vm.expectEmit(true, true, true, true); - emit StakeMsgExecuted(ExocoreBtcGatewayStorage.ClientChainID.Bitcoin, stakeMsg.nonce, user, stakeMsg.amount); + emit StakeMsgExecuted(UTXOGatewayStorage.ClientChainID.Bitcoin, stakeMsg.nonce, user, stakeMsg.amount); gateway.processStakeMessage(witnesses[0].addr, stakeMsg, signature); // Verify address registration - assertEq(gateway.getClientChainAddress(ExocoreBtcGatewayStorage.ClientChainID.Bitcoin, user), btcAddress); - assertEq(gateway.getExocoreAddress(ExocoreBtcGatewayStorage.ClientChainID.Bitcoin, btcAddress), user); + assertEq(gateway.getClientChainAddress(UTXOGatewayStorage.ClientChainID.Bitcoin, user), btcAddress); + assertEq(gateway.getExocoreAddress(UTXOGatewayStorage.ClientChainID.Bitcoin, btcAddress), user); } function test_ProcessStakeMessage_WithBridgeFee() public { - ExocoreBtcGatewayStorage.StakeMsg memory stakeMsg = ExocoreBtcGatewayStorage.StakeMsg({ - chainId: ExocoreBtcGatewayStorage.ClientChainID.Bitcoin, + UTXOGatewayStorage.StakeMsg memory stakeMsg = UTXOGatewayStorage.StakeMsg({ + chainId: UTXOGatewayStorage.ClientChainID.Bitcoin, srcAddress: btcAddress, exocoreAddress: user, operator: operator, @@ -848,7 +848,7 @@ contract ExocoreBtcGatewayTest is Test { vm.expectEmit(true, true, true, true, address(gateway)); emit DepositCompleted( - ExocoreBtcGatewayStorage.ClientChainID.Bitcoin, + UTXOGatewayStorage.ClientChainID.Bitcoin, stakeMsg.txTag, user, stakeMsg.srcAddress, @@ -857,15 +857,15 @@ contract ExocoreBtcGatewayTest is Test { ); vm.expectEmit(true, true, true, true, address(gateway)); - emit DelegationCompleted(ExocoreBtcGatewayStorage.ClientChainID.Bitcoin, user, operator, amountAfterFee); + emit DelegationCompleted(UTXOGatewayStorage.ClientChainID.Bitcoin, user, operator, amountAfterFee); vm.prank(relayer); gateway.processStakeMessage(witnesses[0].addr, stakeMsg, signature); } function test_ProcessStakeMessage_WithDelegation() public { - ExocoreBtcGatewayStorage.StakeMsg memory stakeMsg = ExocoreBtcGatewayStorage.StakeMsg({ - chainId: ExocoreBtcGatewayStorage.ClientChainID.Bitcoin, + UTXOGatewayStorage.StakeMsg memory stakeMsg = UTXOGatewayStorage.StakeMsg({ + chainId: UTXOGatewayStorage.ClientChainID.Bitcoin, srcAddress: btcAddress, exocoreAddress: user, operator: operator, @@ -888,13 +888,13 @@ contract ExocoreBtcGatewayTest is Test { vm.prank(relayer); vm.expectEmit(true, true, true, true); - emit DelegationCompleted(ExocoreBtcGatewayStorage.ClientChainID.Bitcoin, user, operator, 1 ether); + emit DelegationCompleted(UTXOGatewayStorage.ClientChainID.Bitcoin, user, operator, 1 ether); gateway.processStakeMessage(witnesses[0].addr, stakeMsg, signature); } function test_ProcessStakeMessage_DelegationFailureNotRevert() public { - ExocoreBtcGatewayStorage.StakeMsg memory stakeMsg = ExocoreBtcGatewayStorage.StakeMsg({ - chainId: ExocoreBtcGatewayStorage.ClientChainID.Bitcoin, + UTXOGatewayStorage.StakeMsg memory stakeMsg = UTXOGatewayStorage.StakeMsg({ + chainId: UTXOGatewayStorage.ClientChainID.Bitcoin, srcAddress: btcAddress, exocoreAddress: user, operator: operator, @@ -919,7 +919,7 @@ contract ExocoreBtcGatewayTest is Test { // deposit should be successful vm.expectEmit(true, true, true, true); emit DepositCompleted( - ExocoreBtcGatewayStorage.ClientChainID.Bitcoin, + UTXOGatewayStorage.ClientChainID.Bitcoin, stakeMsg.txTag, user, stakeMsg.srcAddress, @@ -929,14 +929,14 @@ contract ExocoreBtcGatewayTest is Test { // delegation should fail vm.expectEmit(true, true, true, true); - emit DelegationFailedForStake(ExocoreBtcGatewayStorage.ClientChainID.Bitcoin, user, operator, 1 ether); + emit DelegationFailedForStake(UTXOGatewayStorage.ClientChainID.Bitcoin, user, operator, 1 ether); gateway.processStakeMessage(witnesses[0].addr, stakeMsg, signature); } function test_ProcessStakeMessage_RevertOnDepositFailure() public { - ExocoreBtcGatewayStorage.StakeMsg memory stakeMsg = ExocoreBtcGatewayStorage.StakeMsg({ - chainId: ExocoreBtcGatewayStorage.ClientChainID.Bitcoin, + UTXOGatewayStorage.StakeMsg memory stakeMsg = UTXOGatewayStorage.StakeMsg({ + chainId: UTXOGatewayStorage.ClientChainID.Bitcoin, srcAddress: btcAddress, exocoreAddress: user, operator: "", @@ -958,8 +958,8 @@ contract ExocoreBtcGatewayTest is Test { } function test_ProcessStakeMessage_RevertWhenPaused() public { - ExocoreBtcGatewayStorage.StakeMsg memory stakeMsg = ExocoreBtcGatewayStorage.StakeMsg({ - chainId: ExocoreBtcGatewayStorage.ClientChainID.Bitcoin, + UTXOGatewayStorage.StakeMsg memory stakeMsg = UTXOGatewayStorage.StakeMsg({ + chainId: UTXOGatewayStorage.ClientChainID.Bitcoin, srcAddress: btcAddress, exocoreAddress: user, operator: "", @@ -979,8 +979,8 @@ contract ExocoreBtcGatewayTest is Test { } function test_ProcessStakeMessage_RevertUnauthorizedWitness() public { - ExocoreBtcGatewayStorage.StakeMsg memory stakeMsg = ExocoreBtcGatewayStorage.StakeMsg({ - chainId: ExocoreBtcGatewayStorage.ClientChainID.Bitcoin, + UTXOGatewayStorage.StakeMsg memory stakeMsg = UTXOGatewayStorage.StakeMsg({ + chainId: UTXOGatewayStorage.ClientChainID.Bitcoin, srcAddress: btcAddress, exocoreAddress: user, operator: "", @@ -999,8 +999,8 @@ contract ExocoreBtcGatewayTest is Test { function test_ProcessStakeMessage_RevertInvalidStakeMessage() public { // Create invalid message with all zero values - ExocoreBtcGatewayStorage.StakeMsg memory stakeMsg = ExocoreBtcGatewayStorage.StakeMsg({ - chainId: ExocoreBtcGatewayStorage.ClientChainID.None, + UTXOGatewayStorage.StakeMsg memory stakeMsg = UTXOGatewayStorage.StakeMsg({ + chainId: UTXOGatewayStorage.ClientChainID.None, srcAddress: bytes(""), exocoreAddress: address(0), operator: "", @@ -1017,8 +1017,8 @@ contract ExocoreBtcGatewayTest is Test { } function test_ProcessStakeMessage_RevertZeroExocoreAddressBeforeRegistration() public { - ExocoreBtcGatewayStorage.StakeMsg memory stakeMsg = ExocoreBtcGatewayStorage.StakeMsg({ - chainId: ExocoreBtcGatewayStorage.ClientChainID.Bitcoin, + UTXOGatewayStorage.StakeMsg memory stakeMsg = UTXOGatewayStorage.StakeMsg({ + chainId: UTXOGatewayStorage.ClientChainID.Bitcoin, srcAddress: btcAddress, exocoreAddress: address(0), // Zero address operator: "", @@ -1035,13 +1035,13 @@ contract ExocoreBtcGatewayTest is Test { } function test_ProcessStakeMessage_RevertInvalidNonce() public { - ExocoreBtcGatewayStorage.StakeMsg memory stakeMsg = ExocoreBtcGatewayStorage.StakeMsg({ - chainId: ExocoreBtcGatewayStorage.ClientChainID.Bitcoin, + UTXOGatewayStorage.StakeMsg memory stakeMsg = UTXOGatewayStorage.StakeMsg({ + chainId: UTXOGatewayStorage.ClientChainID.Bitcoin, srcAddress: btcAddress, exocoreAddress: user, operator: "", amount: 1 ether, - nonce: gateway.nextInboundNonce(ExocoreBtcGatewayStorage.ClientChainID.Bitcoin) + 1, + nonce: gateway.nextInboundNonce(UTXOGatewayStorage.ClientChainID.Bitcoin) + 1, txTag: bytes("tx1") }); @@ -1067,12 +1067,12 @@ contract ExocoreBtcGatewayTest is Test { vm.prank(user); vm.expectEmit(true, true, true, true); - emit DelegationCompleted(ExocoreBtcGatewayStorage.ClientChainID.Bitcoin, user, operator, 1 ether); + emit DelegationCompleted(UTXOGatewayStorage.ClientChainID.Bitcoin, user, operator, 1 ether); - gateway.delegateTo(ExocoreBtcGatewayStorage.Token.BTC, operator, 1 ether); + gateway.delegateTo(UTXOGatewayStorage.Token.BTC, operator, 1 ether); // Verify nonce increment - assertEq(gateway.delegationNonce(ExocoreBtcGatewayStorage.ClientChainID.Bitcoin), 1); + assertEq(gateway.delegationNonce(UTXOGatewayStorage.ClientChainID.Bitcoin), 1); } function test_DelegateTo_RevertZeroAmount() public { @@ -1080,7 +1080,7 @@ contract ExocoreBtcGatewayTest is Test { vm.prank(user); vm.expectRevert(Errors.ZeroAmount.selector); - gateway.delegateTo(ExocoreBtcGatewayStorage.Token.BTC, operator, 0); + gateway.delegateTo(UTXOGatewayStorage.Token.BTC, operator, 0); } function test_DelegateTo_RevertWhenPaused() public { @@ -1091,7 +1091,7 @@ contract ExocoreBtcGatewayTest is Test { vm.prank(user); vm.expectRevert("Pausable: paused"); - gateway.delegateTo(ExocoreBtcGatewayStorage.Token.BTC, operator, 1 ether); + gateway.delegateTo(UTXOGatewayStorage.Token.BTC, operator, 1 ether); } function test_DelegateTo_RevertNotRegistered() public { @@ -1099,7 +1099,7 @@ contract ExocoreBtcGatewayTest is Test { vm.prank(user); vm.expectRevert(Errors.AddressNotRegistered.selector); - gateway.delegateTo(ExocoreBtcGatewayStorage.Token.BTC, operator, 1 ether); + gateway.delegateTo(UTXOGatewayStorage.Token.BTC, operator, 1 ether); } function test_DelegateTo_RevertInvalidOperator() public { @@ -1109,7 +1109,7 @@ contract ExocoreBtcGatewayTest is Test { vm.prank(user); vm.expectRevert(Errors.InvalidOperator.selector); - gateway.delegateTo(ExocoreBtcGatewayStorage.Token.BTC, invalidOperator, 1 ether); + gateway.delegateTo(UTXOGatewayStorage.Token.BTC, invalidOperator, 1 ether); } function test_DelegateTo_RevertDelegationFailed() public { @@ -1122,7 +1122,7 @@ contract ExocoreBtcGatewayTest is Test { vm.prank(user); vm.expectRevert(abi.encodeWithSelector(Errors.DelegationFailed.selector)); - gateway.delegateTo(ExocoreBtcGatewayStorage.Token.BTC, operator, 1 ether); + gateway.delegateTo(UTXOGatewayStorage.Token.BTC, operator, 1 ether); } function test_UndelegateFrom_Success() public { @@ -1136,12 +1136,12 @@ contract ExocoreBtcGatewayTest is Test { vm.prank(user); vm.expectEmit(true, true, true, true); - emit UndelegationCompleted(ExocoreBtcGatewayStorage.ClientChainID.Bitcoin, user, operator, 1 ether); + emit UndelegationCompleted(UTXOGatewayStorage.ClientChainID.Bitcoin, user, operator, 1 ether); - gateway.undelegateFrom(ExocoreBtcGatewayStorage.Token.BTC, operator, 1 ether); + gateway.undelegateFrom(UTXOGatewayStorage.Token.BTC, operator, 1 ether); // Verify nonce increment - assertEq(gateway.delegationNonce(ExocoreBtcGatewayStorage.ClientChainID.Bitcoin), 1); + assertEq(gateway.delegationNonce(UTXOGatewayStorage.ClientChainID.Bitcoin), 1); } function test_UndelegateFrom_RevertZeroAmount() public { @@ -1149,7 +1149,7 @@ contract ExocoreBtcGatewayTest is Test { vm.prank(user); vm.expectRevert(Errors.ZeroAmount.selector); - gateway.undelegateFrom(ExocoreBtcGatewayStorage.Token.BTC, operator, 0); + gateway.undelegateFrom(UTXOGatewayStorage.Token.BTC, operator, 0); } function test_UndelegateFrom_RevertWhenPaused() public { @@ -1160,7 +1160,7 @@ contract ExocoreBtcGatewayTest is Test { vm.prank(user); vm.expectRevert("Pausable: paused"); - gateway.undelegateFrom(ExocoreBtcGatewayStorage.Token.BTC, operator, 1 ether); + gateway.undelegateFrom(UTXOGatewayStorage.Token.BTC, operator, 1 ether); } function test_UndelegateFrom_RevertNotRegistered() public { @@ -1168,7 +1168,7 @@ contract ExocoreBtcGatewayTest is Test { vm.prank(user); vm.expectRevert(Errors.AddressNotRegistered.selector); - gateway.undelegateFrom(ExocoreBtcGatewayStorage.Token.BTC, operator, 1 ether); + gateway.undelegateFrom(UTXOGatewayStorage.Token.BTC, operator, 1 ether); } function test_UndelegateFrom_RevertInvalidOperator() public { @@ -1178,7 +1178,7 @@ contract ExocoreBtcGatewayTest is Test { vm.prank(user); vm.expectRevert(Errors.InvalidOperator.selector); - gateway.undelegateFrom(ExocoreBtcGatewayStorage.Token.BTC, invalidOperator, 1 ether); + gateway.undelegateFrom(UTXOGatewayStorage.Token.BTC, invalidOperator, 1 ether); } function test_UndelegateFrom_RevertUndelegationFailed() public { @@ -1191,7 +1191,7 @@ contract ExocoreBtcGatewayTest is Test { vm.prank(user); vm.expectRevert(Errors.UndelegationFailed.selector); - gateway.undelegateFrom(ExocoreBtcGatewayStorage.Token.BTC, operator, 1 ether); + gateway.undelegateFrom(UTXOGatewayStorage.Token.BTC, operator, 1 ether); } function test_WithdrawPrincipal_Success() public { @@ -1206,7 +1206,7 @@ contract ExocoreBtcGatewayTest is Test { vm.prank(user); vm.expectEmit(true, true, true, true); emit WithdrawPrincipalRequested( - ExocoreBtcGatewayStorage.ClientChainID.Bitcoin, + UTXOGatewayStorage.ClientChainID.Bitcoin, 1, // first request ID user, btcAddress, @@ -1214,10 +1214,10 @@ contract ExocoreBtcGatewayTest is Test { 2 ether ); - gateway.withdrawPrincipal(ExocoreBtcGatewayStorage.Token.BTC, 1 ether); + gateway.withdrawPrincipal(UTXOGatewayStorage.Token.BTC, 1 ether); // Verify pegOutNonce increment - assertEq(gateway.pegOutNonce(ExocoreBtcGatewayStorage.ClientChainID.Bitcoin), 1); + assertEq(gateway.pegOutNonce(UTXOGatewayStorage.ClientChainID.Bitcoin), 1); } function test_WithdrawPrincipal_RevertWhenPaused() public { @@ -1228,7 +1228,7 @@ contract ExocoreBtcGatewayTest is Test { vm.prank(user); vm.expectRevert("Pausable: paused"); - gateway.withdrawPrincipal(ExocoreBtcGatewayStorage.Token.BTC, 1 ether); + gateway.withdrawPrincipal(UTXOGatewayStorage.Token.BTC, 1 ether); } function test_WithdrawPrincipal_RevertZeroAmount() public { @@ -1236,7 +1236,7 @@ contract ExocoreBtcGatewayTest is Test { vm.prank(user); vm.expectRevert(Errors.ZeroAmount.selector); - gateway.withdrawPrincipal(ExocoreBtcGatewayStorage.Token.BTC, 0); + gateway.withdrawPrincipal(UTXOGatewayStorage.Token.BTC, 0); } function test_WithdrawPrincipal_RevertWithdrawFailed() public { @@ -1249,7 +1249,7 @@ contract ExocoreBtcGatewayTest is Test { vm.prank(user); vm.expectRevert(Errors.WithdrawPrincipalFailed.selector); - gateway.withdrawPrincipal(ExocoreBtcGatewayStorage.Token.BTC, 1 ether); + gateway.withdrawPrincipal(UTXOGatewayStorage.Token.BTC, 1 ether); } function test_WithdrawPrincipal_RevertNotRegistered() public { @@ -1257,7 +1257,7 @@ contract ExocoreBtcGatewayTest is Test { vm.prank(user); vm.expectRevert(Errors.AddressNotRegistered.selector); - gateway.withdrawPrincipal(ExocoreBtcGatewayStorage.Token.BTC, 1 ether); + gateway.withdrawPrincipal(UTXOGatewayStorage.Token.BTC, 1 ether); } function test_WithdrawPrincipal_VerifyPegOutRequest() public { @@ -1269,16 +1269,17 @@ contract ExocoreBtcGatewayTest is Test { ); vm.prank(user); - gateway.withdrawPrincipal(ExocoreBtcGatewayStorage.Token.BTC, 1 ether); + gateway.withdrawPrincipal(UTXOGatewayStorage.Token.BTC, 1 ether); // Verify peg-out request details - ExocoreBtcGatewayStorage.PegOutRequest memory request = gateway.getPegOutRequest(1); - assertEq(uint8(request.chainId), uint8(ExocoreBtcGatewayStorage.ClientChainID.Bitcoin)); + UTXOGatewayStorage.PegOutRequest memory request = + gateway.getPegOutRequest(UTXOGatewayStorage.ClientChainID.Bitcoin, 1); + assertEq(uint8(request.chainId), uint8(UTXOGatewayStorage.ClientChainID.Bitcoin)); + assertEq(request.nonce, 1); assertEq(request.requester, user); assertEq(request.clientChainAddress, btcAddress); assertEq(request.amount, 1 ether); - assertEq(uint8(request.withdrawType), uint8(ExocoreBtcGatewayStorage.WithdrawType.WithdrawPrincipal)); - assertTrue(request.timestamp > 0); + assertEq(uint8(request.withdrawType), uint8(UTXOGatewayStorage.WithdrawType.WithdrawPrincipal)); } function test_WithdrawReward_Success() public { @@ -1293,7 +1294,7 @@ contract ExocoreBtcGatewayTest is Test { vm.prank(user); vm.expectEmit(true, true, true, true); emit WithdrawRewardRequested( - ExocoreBtcGatewayStorage.ClientChainID.Bitcoin, + UTXOGatewayStorage.ClientChainID.Bitcoin, 1, // first request ID user, btcAddress, @@ -1301,10 +1302,10 @@ contract ExocoreBtcGatewayTest is Test { 2 ether ); - gateway.withdrawReward(ExocoreBtcGatewayStorage.Token.BTC, 1 ether); + gateway.withdrawReward(UTXOGatewayStorage.Token.BTC, 1 ether); // Verify pegOutNonce increment - assertEq(gateway.pegOutNonce(ExocoreBtcGatewayStorage.ClientChainID.Bitcoin), 1); + assertEq(gateway.pegOutNonce(UTXOGatewayStorage.ClientChainID.Bitcoin), 1); } function test_WithdrawReward_RevertWhenPaused() public { @@ -1315,7 +1316,7 @@ contract ExocoreBtcGatewayTest is Test { vm.prank(user); vm.expectRevert("Pausable: paused"); - gateway.withdrawReward(ExocoreBtcGatewayStorage.Token.BTC, 1 ether); + gateway.withdrawReward(UTXOGatewayStorage.Token.BTC, 1 ether); } function test_WithdrawReward_RevertZeroAmount() public { @@ -1323,7 +1324,7 @@ contract ExocoreBtcGatewayTest is Test { vm.prank(user); vm.expectRevert(Errors.ZeroAmount.selector); - gateway.withdrawReward(ExocoreBtcGatewayStorage.Token.BTC, 0); + gateway.withdrawReward(UTXOGatewayStorage.Token.BTC, 0); } function test_WithdrawReward_RevertClaimFailed() public { @@ -1336,7 +1337,7 @@ contract ExocoreBtcGatewayTest is Test { vm.prank(user); vm.expectRevert(Errors.WithdrawRewardFailed.selector); - gateway.withdrawReward(ExocoreBtcGatewayStorage.Token.BTC, 1 ether); + gateway.withdrawReward(UTXOGatewayStorage.Token.BTC, 1 ether); } function test_WithdrawReward_RevertAddressNotRegistered() public { @@ -1344,7 +1345,7 @@ contract ExocoreBtcGatewayTest is Test { vm.prank(user); vm.expectRevert(Errors.AddressNotRegistered.selector); - gateway.withdrawReward(ExocoreBtcGatewayStorage.Token.BTC, 1 ether); + gateway.withdrawReward(UTXOGatewayStorage.Token.BTC, 1 ether); } function test_WithdrawReward_VerifyPegOutRequest() public { @@ -1356,16 +1357,17 @@ contract ExocoreBtcGatewayTest is Test { ); vm.prank(user); - gateway.withdrawReward(ExocoreBtcGatewayStorage.Token.BTC, 1 ether); + gateway.withdrawReward(UTXOGatewayStorage.Token.BTC, 1 ether); // Verify peg-out request details - ExocoreBtcGatewayStorage.PegOutRequest memory request = gateway.getPegOutRequest(1); - assertEq(uint8(request.chainId), uint8(ExocoreBtcGatewayStorage.ClientChainID.Bitcoin)); + UTXOGatewayStorage.PegOutRequest memory request = + gateway.getPegOutRequest(UTXOGatewayStorage.ClientChainID.Bitcoin, 1); + assertEq(uint8(request.chainId), uint8(UTXOGatewayStorage.ClientChainID.Bitcoin)); + assertEq(request.nonce, 1); assertEq(request.requester, user); assertEq(request.clientChainAddress, btcAddress); assertEq(request.amount, 1 ether); - assertEq(uint8(request.withdrawType), uint8(ExocoreBtcGatewayStorage.WithdrawType.WithdrawReward)); - assertTrue(request.timestamp > 0); + assertEq(uint8(request.withdrawType), uint8(UTXOGatewayStorage.WithdrawType.WithdrawReward)); } function test_WithdrawReward_MultipleRequests() public { @@ -1374,7 +1376,7 @@ contract ExocoreBtcGatewayTest is Test { // Mock successful claimReward bytes memory claimCall1 = abi.encodeWithSelector( IReward.claimReward.selector, - uint32(uint8(ExocoreBtcGatewayStorage.ClientChainID.Bitcoin)), + uint32(uint8(UTXOGatewayStorage.ClientChainID.Bitcoin)), VIRTUAL_TOKEN, user.toExocoreBytes(), 1 ether @@ -1383,7 +1385,7 @@ contract ExocoreBtcGatewayTest is Test { bytes memory claimCall2 = abi.encodeWithSelector( IReward.claimReward.selector, - uint32(uint8(ExocoreBtcGatewayStorage.ClientChainID.Bitcoin)), + uint32(uint8(UTXOGatewayStorage.ClientChainID.Bitcoin)), VIRTUAL_TOKEN, user.toExocoreBytes(), 0.5 ether @@ -1393,33 +1395,150 @@ contract ExocoreBtcGatewayTest is Test { vm.startPrank(user); // First withdrawal - gateway.withdrawReward(ExocoreBtcGatewayStorage.Token.BTC, 1 ether); + gateway.withdrawReward(UTXOGatewayStorage.Token.BTC, 1 ether); // Second withdrawal - gateway.withdrawReward(ExocoreBtcGatewayStorage.Token.BTC, 0.5 ether); + gateway.withdrawReward(UTXOGatewayStorage.Token.BTC, 0.5 ether); vm.stopPrank(); // Verify both requests exist with correct details - ExocoreBtcGatewayStorage.PegOutRequest memory request1 = gateway.getPegOutRequest(1); + UTXOGatewayStorage.PegOutRequest memory request1 = + gateway.getPegOutRequest(UTXOGatewayStorage.ClientChainID.Bitcoin, 1); assertEq(request1.amount, 1 ether); - ExocoreBtcGatewayStorage.PegOutRequest memory request2 = gateway.getPegOutRequest(2); + UTXOGatewayStorage.PegOutRequest memory request2 = + gateway.getPegOutRequest(UTXOGatewayStorage.ClientChainID.Bitcoin, 2); assertEq(request2.amount, 0.5 ether); // Verify nonce increment - assertEq(gateway.pegOutNonce(ExocoreBtcGatewayStorage.ClientChainID.Bitcoin), 2); + 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.chainId), uint8(UTXOGatewayStorage.ClientChainID.Bitcoin)); + assertEq(request.nonce, 1); + assertEq(request.requester, user); + assertEq(request.clientChainAddress, 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.clientChainAddress, ""); + + // 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.getClientChainAddress(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 { - ExocoreBtcGatewayStorage.StakeMsg memory stakeMsg = ExocoreBtcGatewayStorage.StakeMsg({ - chainId: ExocoreBtcGatewayStorage.ClientChainID.Bitcoin, + UTXOGatewayStorage.StakeMsg memory stakeMsg = UTXOGatewayStorage.StakeMsg({ + chainId: UTXOGatewayStorage.ClientChainID.Bitcoin, srcAddress: btcAddr, exocoreAddress: exocoreAddr, operator: "", amount: 1 ether, - nonce: gateway.nextInboundNonce(ExocoreBtcGatewayStorage.ClientChainID.Bitcoin), + nonce: gateway.nextInboundNonce(UTXOGatewayStorage.ClientChainID.Bitcoin), txTag: bytes("tx1") }); @@ -1436,14 +1555,14 @@ contract ExocoreBtcGatewayTest is Test { bytes memory signature = _generateSignature(stakeMsg, witnesses[0].privateKey); vm.expectEmit(true, true, true, true, address(gateway)); - emit AddressRegistered(ExocoreBtcGatewayStorage.ClientChainID.Bitcoin, btcAddr, exocoreAddr); + emit AddressRegistered(UTXOGatewayStorage.ClientChainID.Bitcoin, btcAddr, exocoreAddr); vm.prank(relayer); gateway.processStakeMessage(witnesses[0].addr, stakeMsg, signature); // Verify address registration - assertEq(gateway.getClientChainAddress(ExocoreBtcGatewayStorage.ClientChainID.Bitcoin, exocoreAddr), btcAddr); - assertEq(gateway.getExocoreAddress(ExocoreBtcGatewayStorage.ClientChainID.Bitcoin, btcAddr), exocoreAddr); + assertEq(gateway.getClientChainAddress(UTXOGatewayStorage.ClientChainID.Bitcoin, exocoreAddr), btcAddr); + assertEq(gateway.getExocoreAddress(UTXOGatewayStorage.ClientChainID.Bitcoin, btcAddr), exocoreAddr); } function _addAllWitnesses() internal { @@ -1455,7 +1574,7 @@ contract ExocoreBtcGatewayTest is Test { } } - function _getMessageHash(ExocoreBtcGatewayStorage.StakeMsg memory msg_) internal pure returns (bytes32) { + function _getMessageHash(UTXOGatewayStorage.StakeMsg memory msg_) internal pure returns (bytes32) { return keccak256( abi.encode( msg_.chainId, // ClientChainID @@ -1469,7 +1588,7 @@ contract ExocoreBtcGatewayTest is Test { ); } - function _generateSignature(ExocoreBtcGatewayStorage.StakeMsg memory msg_, uint256 privateKey) + function _generateSignature(UTXOGatewayStorage.StakeMsg memory msg_, uint256 privateKey) internal pure returns (bytes memory) From 02bb6e95c9b322d087bb0bf9e15c449a18b9ec4e Mon Sep 17 00:00:00 2001 From: adu Date: Mon, 25 Nov 2024 10:21:03 +0800 Subject: [PATCH 08/11] feat: consensus activate/deactivate --- src/core/UTXOGateway.sol | 120 ++++++++++---- src/libraries/Errors.sol | 47 +++--- src/storage/UTXOGatewayStorage.sol | 36 ++++- test/foundry/unit/UTXOGateway.t.sol | 238 ++++++++++++++++++++++++---- 4 files changed, 357 insertions(+), 84 deletions(-) diff --git a/src/core/UTXOGateway.sol b/src/core/UTXOGateway.sol index 4433a3a2..ff07b495 100644 --- a/src/core/UTXOGateway.sol +++ b/src/core/UTXOGateway.sol @@ -30,32 +30,6 @@ contract UTXOGateway is using ExocoreBytes for address; using SignatureVerifier for bytes32; - /** - * @dev Modifier to restrict access to authorized witnesses only. - */ - modifier onlyAuthorizedWitness() { - if (!_isAuthorizedWitness(msg.sender)) { - revert Errors.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. @@ -65,14 +39,23 @@ contract UTXOGateway is } /** - * @notice Initializes the contract with the Exocore witness address and owner address. + * @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) external initializer { + 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]); } @@ -81,6 +64,22 @@ contract UTXOGateway is _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. */ @@ -95,6 +94,33 @@ contract UTXOGateway is } } + /** + * @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 new authorized witness. * @param _witness The address of the witness to be added. @@ -117,9 +143,17 @@ contract UTXOGateway is 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); + } } /** @@ -148,6 +182,10 @@ contract UTXOGateway is nonReentrant whenNotPaused { + if (!_isConsensusRequired()) { + revert Errors.ConsensusNotRequired(); + } + if (!_isAuthorizedWitness(witness)) { revert Errors.WitnessNotAuthorized(witness); } @@ -178,7 +216,7 @@ contract UTXOGateway is emit ProofSubmitted(messageHash, witness); // Check for consensus - if (txn.proofCount >= REQUIRED_PROOFS) { + 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 @@ -200,6 +238,10 @@ contract UTXOGateway is nonReentrant whenNotPaused { + if (_isConsensusRequired()) { + revert Errors.ConsensusRequired(); + } + if (!_isAuthorizedWitness(witness)) { revert Errors.WitnessNotAuthorized(witness); } @@ -432,6 +474,18 @@ contract UTXOGateway is 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. @@ -448,9 +502,17 @@ contract UTXOGateway is 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); + } } /** diff --git a/src/libraries/Errors.sol b/src/libraries/Errors.sol index 3645b329..f79cdd84 100644 --- a/src/libraries/Errors.sol +++ b/src/libraries/Errors.sol @@ -312,61 +312,70 @@ library Errors { error InsufficientBalance(); /* -------------------------------------------------------------------------- */ - /* ExocoreBtcGateway Errors */ + /* UTXOGateway Errors */ /* -------------------------------------------------------------------------- */ - /// @dev ExocoreBtcGateway: witness has already submitted proof + /// @dev UTXOGateway: witness has already submitted proof error WitnessAlreadySubmittedProof(); - /// @dev ExocoreBtcGateway: invalid stake message + /// @dev UTXOGateway: invalid stake message error InvalidStakeMessage(); - /// @dev ExocoreBtcGateway: transaction tag has already been processed + /// @dev UTXOGateway: transaction tag has already been processed error TxTagAlreadyProcessed(); - /// @dev ExocoreBtcGateway: invalid operator address + /// @dev UTXOGateway: invalid operator address error InvalidOperator(); - /// @dev ExocoreBtcGateway: invalid token + /// @dev UTXOGateway: invalid token error InvalidToken(); - /// @dev ExocoreBtcGateway: witness has already been authorized + /// @dev UTXOGateway: witness has already been authorized error WitnessAlreadyAuthorized(address witness); - /// @dev ExocoreBtcGateway: witness has not been authorized + /// @dev UTXOGateway: witness has not been authorized error WitnessNotAuthorized(address witness); - /// @dev ExocoreBtcGateway: cannot remove the last witness + /// @dev UTXOGateway: cannot remove the last witness error CannotRemoveLastWitness(); - /// @dev ExocoreBtcGateway: invalid client chain + /// @dev UTXOGateway: invalid client chain error InvalidClientChain(); - /// @dev ExocoreBtcGateway: deposit failed + /// @dev UTXOGateway: deposit failed error DepositFailed(bytes txTag); - /// @dev ExocoreBtcGateway: address not registered + /// @dev UTXOGateway: address not registered error AddressNotRegistered(); - /// @dev ExocoreBtcGateway: delegation failed + /// @dev UTXOGateway: delegation failed error DelegationFailed(); - /// @dev ExocoreBtcGateway: withdraw principal failed + /// @dev UTXOGateway: withdraw principal failed error WithdrawPrincipalFailed(); - /// @dev ExocoreBtcGateway: undelegation failed + /// @dev UTXOGateway: undelegation failed error UndelegationFailed(); - /// @dev ExocoreBtcGateway: withdraw reward failed + /// @dev UTXOGateway: withdraw reward failed error WithdrawRewardFailed(); - /// @dev ExocoreBtcGateway: request not found + /// @dev UTXOGateway: request not found error RequestNotFound(uint64 requestId); - /// @dev ExocoreBtcGateway: request already exists + /// @dev UTXOGateway: request already exists error RequestAlreadyExists(uint32 clientChain, uint64 requestId); - /// @dev ExocoreBtcGateway: witness not authorized + /// @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/storage/UTXOGatewayStorage.sol b/src/storage/UTXOGatewayStorage.sol index 01c60b82..1e5b2457 100644 --- a/src/storage/UTXOGatewayStorage.sol +++ b/src/storage/UTXOGatewayStorage.sol @@ -115,13 +115,18 @@ contract UTXOGatewayStorage { string public constant BTC_METADATA = "BTC"; string public constant BTC_ORACLE_INFO = "BTC,BITCOIN,8"; - address public constant EXOCORE_WITNESS = address(0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266); - uint256 public constant REQUIRED_PROOFS = 2; 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; @@ -195,6 +200,13 @@ contract UTXOGatewayStorage { // 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 chainId The chain ID of the client chain, should not violate the layerzero chain id @@ -435,6 +447,16 @@ contract UTXOGatewayStorage { /// @param token The address of the token. event WhitelistTokenUpdated(uint32 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 @@ -453,6 +475,16 @@ contract UTXOGatewayStorage { _; } + /** + * @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. diff --git a/test/foundry/unit/UTXOGateway.t.sol b/test/foundry/unit/UTXOGateway.t.sol index ec96d5ea..914ec92a 100644 --- a/test/foundry/unit/UTXOGateway.t.sol +++ b/test/foundry/unit/UTXOGateway.t.sol @@ -55,7 +55,7 @@ contract UTXOGatewayTest is Test { string public constant BTC_METADATA = "BTC"; string public constant BTC_ORACLE_INFO = "BTC,BITCOIN,8"; - uint256 public constant REQUIRED_PROOFS = 2; + uint256 public initialRequiredProofs = 3; uint256 public constant PROOF_TIMEOUT = 1 days; event WitnessAdded(address indexed witness); @@ -124,6 +124,10 @@ contract UTXOGatewayTest is Test { 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); @@ -140,13 +144,68 @@ contract UTXOGatewayTest is Test { gateway = UTXOGateway(address(new TransparentUpgradeableProxy(address(gatewayLogic), address(0xab), ""))); address[] memory initialWitnesses = new address[](1); initialWitnesses[0] = witnesses[0].addr; - gateway.initialize(owner, initialWitnesses); + 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_AddWitness_Success() public { @@ -192,6 +251,27 @@ contract UTXOGatewayTest is Test { vm.stopPrank(); } + function test_AddWitness_ConsensusActivation() public { + // initially we have 1 witness, and required proofs is 3 + assertEq(gateway.authorizedWitnessCount(), 1); + assertEq(gateway.requiredProofs(), 3); + assertFalse(gateway.isConsensusRequired()); + + vm.startPrank(owner); + // Add second witness - no consensus event + gateway.addWitness(witnesses[1].addr); + + // Add third witness - should emit ConsensusActivated + vm.expectEmit(true, true, true, true); + emit ConsensusActivated(gateway.requiredProofs(), gateway.authorizedWitnessCount() + 1); + gateway.addWitness(witnesses[2].addr); + + // Add fourth witness - no consensus event + gateway.addWitness(address(0xaa)); + + vm.stopPrank(); + } + function test_RemoveWitness() public { vm.startPrank(owner); @@ -272,6 +352,24 @@ contract UTXOGatewayTest is Test { vm.stopPrank(); } + function test_RemoveWitness_ConsensusDeactivation() public { + // add total 3 witnesses + _addAllWitnesses(); + + // set + vm.startPrank(owner); + gateway.updateRequiredProofs(2); + + // Remove one witness - no consensus event + gateway.removeWitness(witnesses[2].addr); + + // Remove another witness - should emit ConsensusDeactivated + vm.expectEmit(true, true, true, true); + emit ConsensusDeactivated(gateway.requiredProofs(), gateway.authorizedWitnessCount() - 1); + gateway.removeWitness(witnesses[1].addr); + vm.stopPrank(); + } + function test_UpdateBridgeFee() public { uint256 newFee = 500; // 5% @@ -540,6 +638,7 @@ contract UTXOGatewayTest is Test { function test_SubmitProofForStakeMsg_Success() public { _addAllWitnesses(); + _activateConsensus(); // Create stake message UTXOGatewayStorage.StakeMsg memory stakeMsg = UTXOGatewayStorage.StakeMsg({ @@ -561,6 +660,14 @@ contract UTXOGatewayTest is Test { 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, @@ -571,26 +678,46 @@ contract UTXOGatewayTest is Test { DELEGATION_PRECOMPILE_ADDRESS, abi.encodeWithSelector(IDelegation.delegate.selector), abi.encode(true) ); - // Submit proof from second witness - signature = _generateSignature(stakeMsg, witnesses[1].privateKey); + signature = _generateSignature(stakeMsg, witnesses[2].privateKey); vm.prank(relayer); - vm.expectEmit(true, true, false, true); - emit ProofSubmitted(txId, witnesses[1].addr); - - // This should trigger message execution as we have enough proofs vm.expectEmit(true, false, false, false); emit StakeMsgExecuted(stakeMsg.chainId, stakeMsg.nonce, stakeMsg.exocoreAddress, stakeMsg.amount); vm.expectEmit(true, false, false, false); emit TransactionProcessed(txId); - gateway.submitProofForStakeMsg(witnesses[1].addr, stakeMsg, signature); + gateway.submitProofForStakeMsg(witnesses[2].addr, stakeMsg, signature); // Verify message was processed assertTrue(gateway.processedClientChainTxs(stakeMsg.chainId, 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({ + chainId: UTXOGatewayStorage.ClientChainID.Bitcoin, + srcAddress: 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({ chainId: UTXOGatewayStorage.ClientChainID.Bitcoin, @@ -611,6 +738,7 @@ contract UTXOGatewayTest is Test { function test_SubmitProofForStakeMsg_RevertUnauthorizedWitness() public { _addAllWitnesses(); + _activateConsensus(); UTXOGatewayStorage.StakeMsg memory stakeMsg = UTXOGatewayStorage.StakeMsg({ chainId: UTXOGatewayStorage.ClientChainID.Bitcoin, @@ -632,6 +760,7 @@ contract UTXOGatewayTest is Test { function test_SubmitProofForStakeMsg_ExpiredBeforeConsensus() public { _addAllWitnesses(); + _activateConsensus(); UTXOGatewayStorage.StakeMsg memory stakeMsg = UTXOGatewayStorage.StakeMsg({ chainId: UTXOGatewayStorage.ClientChainID.Bitcoin, @@ -643,8 +772,8 @@ contract UTXOGatewayTest is Test { txTag: bytes("tx1") }); - // Submit proofs from REQUIRED_PROOFS - 1 witnesses - for (uint256 i = 0; i < REQUIRED_PROOFS - 1; i++) { + // 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); @@ -654,9 +783,9 @@ contract UTXOGatewayTest is Test { vm.warp(block.timestamp + PROOF_TIMEOUT + 1); // Submit the last proof - bytes memory lastSignature = _generateSignature(stakeMsg, witnesses[REQUIRED_PROOFS - 1].privateKey); + bytes memory lastSignature = _generateSignature(stakeMsg, witnesses[gateway.requiredProofs() - 1].privateKey); vm.prank(relayer); - gateway.submitProofForStakeMsg(witnesses[REQUIRED_PROOFS - 1].addr, stakeMsg, lastSignature); + gateway.submitProofForStakeMsg(witnesses[gateway.requiredProofs() - 1].addr, stakeMsg, lastSignature); // Verify transaction is restarted owing to expired and not processed bytes32 messageHash = _getMessageHash(stakeMsg); @@ -667,6 +796,7 @@ contract UTXOGatewayTest is Test { function test_SubmitProofForStakeMsg_RestartExpiredTransaction() public { _addAllWitnesses(); + _activateConsensus(); UTXOGatewayStorage.StakeMsg memory stakeMsg = UTXOGatewayStorage.StakeMsg({ chainId: UTXOGatewayStorage.ClientChainID.Bitcoin, @@ -702,6 +832,9 @@ contract UTXOGatewayTest is Test { 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({ chainId: UTXOGatewayStorage.ClientChainID.Bitcoin, @@ -726,19 +859,8 @@ contract UTXOGatewayTest is Test { vm.prank(relayer); gateway.submitProofForStakeMsg(witnesses[1].addr, stakeMsg, signature1); - // as PROOFS_REQUIRED is 2, the transaction should be processed after another witness submits proof - - // 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) - ); - // 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); @@ -746,19 +868,17 @@ contract UTXOGatewayTest is Test { bytes32 messageHash = _getMessageHash(stakeMsg); // Verify both witnesses' proofs are counted - assertEq( - uint8(gateway.getTransactionStatus(messageHash)), uint8(UTXOGatewayStorage.TxStatus.NotStartedOrProcessed) - ); - assertTrue(gateway.processedTransactions(messageHash)); - assertTrue(gateway.processedClientChainTxs(stakeMsg.chainId, stakeMsg.txTag)); - assertTrue(gateway.getTransactionWitnessTime(messageHash, witnesses[0].addr) > 0); // mapping can not be deleted - // even if we delete txn after processing - assertTrue(gateway.getTransactionWitnessTime(messageHash, witnesses[1].addr) > 0); // mapping can not be deleted - // even if we delete txn after processing + assertEq(uint8(gateway.getTransactionStatus(messageHash)), uint8(UTXOGatewayStorage.TxStatus.Pending)); + assertEq(gateway.getTransactionProofCount(messageHash), 2); + assertFalse(gateway.processedTransactions(messageHash)); + assertFalse(gateway.processedClientChainTxs(stakeMsg.chainId, 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({ chainId: UTXOGatewayStorage.ClientChainID.Bitcoin, @@ -782,7 +902,29 @@ contract UTXOGatewayTest is Test { gateway.submitProofForStakeMsg(witnesses[0].addr, stakeMsg, signatureSecond); } + function test_ProcessStakeMessage_RevertConsensusActivated() public { + _activateConsensus(); + + UTXOGatewayStorage.StakeMsg memory stakeMsg = UTXOGatewayStorage.StakeMsg({ + chainId: UTXOGatewayStorage.ClientChainID.Bitcoin, + srcAddress: 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({ chainId: UTXOGatewayStorage.ClientChainID.Bitcoin, srcAddress: btcAddress, @@ -818,6 +960,8 @@ contract UTXOGatewayTest is Test { } function test_ProcessStakeMessage_WithBridgeFee() public { + _deactivateConsensus(); + UTXOGatewayStorage.StakeMsg memory stakeMsg = UTXOGatewayStorage.StakeMsg({ chainId: UTXOGatewayStorage.ClientChainID.Bitcoin, srcAddress: btcAddress, @@ -864,6 +1008,8 @@ contract UTXOGatewayTest is Test { } function test_ProcessStakeMessage_WithDelegation() public { + _deactivateConsensus(); + UTXOGatewayStorage.StakeMsg memory stakeMsg = UTXOGatewayStorage.StakeMsg({ chainId: UTXOGatewayStorage.ClientChainID.Bitcoin, srcAddress: btcAddress, @@ -935,6 +1081,8 @@ contract UTXOGatewayTest is Test { } function test_ProcessStakeMessage_RevertOnDepositFailure() public { + _deactivateConsensus(); + UTXOGatewayStorage.StakeMsg memory stakeMsg = UTXOGatewayStorage.StakeMsg({ chainId: UTXOGatewayStorage.ClientChainID.Bitcoin, srcAddress: btcAddress, @@ -958,6 +1106,8 @@ contract UTXOGatewayTest is Test { } function test_ProcessStakeMessage_RevertWhenPaused() public { + _deactivateConsensus(); + UTXOGatewayStorage.StakeMsg memory stakeMsg = UTXOGatewayStorage.StakeMsg({ chainId: UTXOGatewayStorage.ClientChainID.Bitcoin, srcAddress: btcAddress, @@ -979,6 +1129,8 @@ contract UTXOGatewayTest is Test { } function test_ProcessStakeMessage_RevertUnauthorizedWitness() public { + _deactivateConsensus(); + UTXOGatewayStorage.StakeMsg memory stakeMsg = UTXOGatewayStorage.StakeMsg({ chainId: UTXOGatewayStorage.ClientChainID.Bitcoin, srcAddress: btcAddress, @@ -998,6 +1150,8 @@ contract UTXOGatewayTest is Test { } function test_ProcessStakeMessage_RevertInvalidStakeMessage() public { + _deactivateConsensus(); + // Create invalid message with all zero values UTXOGatewayStorage.StakeMsg memory stakeMsg = UTXOGatewayStorage.StakeMsg({ chainId: UTXOGatewayStorage.ClientChainID.None, @@ -1017,6 +1171,8 @@ contract UTXOGatewayTest is Test { } function test_ProcessStakeMessage_RevertZeroExocoreAddressBeforeRegistration() public { + _deactivateConsensus(); + UTXOGatewayStorage.StakeMsg memory stakeMsg = UTXOGatewayStorage.StakeMsg({ chainId: UTXOGatewayStorage.ClientChainID.Bitcoin, srcAddress: btcAddress, @@ -1035,6 +1191,8 @@ contract UTXOGatewayTest is Test { } function test_ProcessStakeMessage_RevertInvalidNonce() public { + _deactivateConsensus(); + UTXOGatewayStorage.StakeMsg memory stakeMsg = UTXOGatewayStorage.StakeMsg({ chainId: UTXOGatewayStorage.ClientChainID.Bitcoin, srcAddress: btcAddress, @@ -1603,4 +1761,16 @@ contract UTXOGatewayTest is Test { 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(); + } + } From b2403ba4b30fa8abedb59b26b1908be25d393c25 Mon Sep 17 00:00:00 2001 From: adu Date: Tue, 26 Nov 2024 15:54:45 +0800 Subject: [PATCH 09/11] chore: unify naming convention chainId => clientChainId, srcAddress => clientAddress --- src/core/UTXOGateway.sol | 195 +++++++++++++++------------- src/storage/UTXOGatewayStorage.sol | 33 +++-- test/foundry/unit/UTXOGateway.t.sol | 134 +++++++++---------- 3 files changed, 191 insertions(+), 171 deletions(-) diff --git a/src/core/UTXOGateway.sol b/src/core/UTXOGateway.sol index ff07b495..cc0054b2 100644 --- a/src/core/UTXOGateway.sol +++ b/src/core/UTXOGateway.sol @@ -41,8 +41,7 @@ contract UTXOGateway is /** * @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. + * 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. @@ -83,12 +82,12 @@ contract UTXOGateway is /** * @notice Activates token staking by registering or updating the chain and token with the Exocore system. */ - function activateStakingForClientChain(ClientChainID clientChain_) external onlyOwner whenNotPaused { - if (clientChain_ == ClientChainID.Bitcoin) { + function activateStakingForClientChain(ClientChainID clientChainId) external onlyOwner whenNotPaused { + if (clientChainId == ClientChainID.Bitcoin) { _registerOrUpdateClientChain( - clientChain_, STAKER_ACCOUNT_LENGTH, BITCOIN_NAME, BITCOIN_METADATA, BITCOIN_SIGNATURE_SCHEME + clientChainId, STAKER_ACCOUNT_LENGTH, BITCOIN_NAME, BITCOIN_METADATA, BITCOIN_SIGNATURE_SCHEME ); - _registerOrUpdateToken(clientChain_, VIRTUAL_TOKEN, BTC_DECIMALS, BTC_NAME, BTC_METADATA, BTC_ORACLE_INFO); + _registerOrUpdateToken(clientChainId, VIRTUAL_TOKEN, BTC_DECIMALS, BTC_NAME, BTC_METADATA, BTC_ORACLE_INFO); } else { revert Errors.InvalidClientChain(); } @@ -267,14 +266,14 @@ contract UTXOGateway is revert Errors.InvalidOperator(); } - ClientChainID chainId = ClientChainID(uint8(token)); + ClientChainID clientChainId = ClientChainID(uint8(token)); - bool success = _delegate(chainId, msg.sender, operator, amount); + bool success = _delegate(clientChainId, msg.sender, operator, amount); if (!success) { revert Errors.DelegationFailed(); } - emit DelegationCompleted(chainId, msg.sender, operator, amount); + emit DelegationCompleted(clientChainId, msg.sender, operator, amount); } /** @@ -294,16 +293,16 @@ contract UTXOGateway is revert Errors.InvalidOperator(); } - ClientChainID chainId = ClientChainID(uint8(token)); + ClientChainID clientChainId = ClientChainID(uint8(token)); - uint64 nonce = ++delegationNonce[chainId]; + uint64 nonce = ++delegationNonce[clientChainId]; bool success = DELEGATION_CONTRACT.undelegate( - uint32(uint8(chainId)), nonce, VIRTUAL_TOKEN, msg.sender.toExocoreBytes(), bytes(operator), amount + uint32(uint8(clientChainId)), nonce, VIRTUAL_TOKEN, msg.sender.toExocoreBytes(), bytes(operator), amount ); if (!success) { revert Errors.UndelegationFailed(); } - emit UndelegationCompleted(chainId, msg.sender, operator, amount); + emit UndelegationCompleted(clientChainId, msg.sender, operator, amount); } /** @@ -312,22 +311,23 @@ contract UTXOGateway is * @param amount The amount to withdraw. */ function withdrawPrincipal(Token token, uint256 amount) external nonReentrant whenNotPaused isValidAmount(amount) { - ClientChainID chainId = ClientChainID(uint8(token)); + ClientChainID clientChainId = ClientChainID(uint8(token)); - bytes memory clientChainAddress = outboundRegistry[chainId][msg.sender]; - if (clientChainAddress.length == 0) { + bytes memory clientAddress = outboundRegistry[clientChainId][msg.sender]; + if (clientAddress.length == 0) { revert Errors.AddressNotRegistered(); } - (bool success, uint256 updatedBalance) = - ASSETS_CONTRACT.withdrawLST(uint32(uint8(chainId)), VIRTUAL_TOKEN, msg.sender.toExocoreBytes(), amount); + (bool success, uint256 updatedBalance) = ASSETS_CONTRACT.withdrawLST( + uint32(uint8(clientChainId)), VIRTUAL_TOKEN, msg.sender.toExocoreBytes(), amount + ); if (!success) { revert Errors.WithdrawPrincipalFailed(); } uint64 requestId = - _initiatePegOut(chainId, amount, msg.sender, clientChainAddress, WithdrawType.WithdrawPrincipal); - emit WithdrawPrincipalRequested(chainId, requestId, msg.sender, clientChainAddress, amount, updatedBalance); + _initiatePegOut(clientChainId, amount, msg.sender, clientAddress, WithdrawType.WithdrawPrincipal); + emit WithdrawPrincipalRequested(clientChainId, requestId, msg.sender, clientAddress, amount, updatedBalance); } /** @@ -336,40 +336,42 @@ contract UTXOGateway is * @param amount The amount to withdraw. */ function withdrawReward(Token token, uint256 amount) external nonReentrant whenNotPaused isValidAmount(amount) { - ClientChainID chainId = ClientChainID(uint8(token)); - bytes memory clientChainAddress = outboundRegistry[chainId][msg.sender]; - if (clientChainAddress.length == 0) { + 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(chainId)), VIRTUAL_TOKEN, msg.sender.toExocoreBytes(), amount); + (bool success, uint256 updatedBalance) = REWARD_CONTRACT.claimReward( + uint32(uint8(clientChainId)), VIRTUAL_TOKEN, msg.sender.toExocoreBytes(), amount + ); if (!success) { revert Errors.WithdrawRewardFailed(); } - uint64 requestId = _initiatePegOut(chainId, amount, msg.sender, clientChainAddress, WithdrawType.WithdrawReward); - emit WithdrawRewardRequested(chainId, requestId, msg.sender, clientChainAddress, amount, updatedBalance); + 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 clientChain The client chain ID + * @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 clientChain) + function processNextPegOut(ClientChainID clientChainId) external onlyAuthorizedWitness nonReentrant whenNotPaused returns (PegOutRequest memory nextRequest) { - uint64 requestId = ++outboundNonce[clientChain]; - nextRequest = pegOutRequests[clientChain][requestId]; + uint64 requestId = ++outboundNonce[clientChainId]; + nextRequest = pegOutRequests[clientChainId][requestId]; // Check if the request exists if (nextRequest.requester == address(0)) { @@ -377,64 +379,68 @@ contract UTXOGateway is } // delete the request - delete pegOutRequests[clientChain][requestId]; + delete pegOutRequests[clientChainId][requestId]; // Emit event emit PegOutProcessed( uint8(nextRequest.withdrawType), - clientChain, + clientChainId, requestId, nextRequest.requester, - nextRequest.clientChainAddress, + nextRequest.clientAddress, nextRequest.amount ); } /** * @notice Gets the client chain address for a given Exocore address - * @param chainId The client chain ID + * @param clientChainId The client chain ID * @param exocoreAddress The Exocore address * @return The client chain address */ - function getClientChainAddress(ClientChainID chainId, address exocoreAddress) + function getClientAddress(ClientChainID clientChainId, address exocoreAddress) external view returns (bytes memory) { - return outboundRegistry[chainId][exocoreAddress]; + return outboundRegistry[clientChainId][exocoreAddress]; } /** * @notice Gets the Exocore address for a given client chain address - * @param chainId The client chain ID - * @param clientChainAddress The client chain address + * @param clientChainId The client chain ID + * @param clientAddress The client chain address * @return The Exocore address */ - function getExocoreAddress(ClientChainID chainId, bytes calldata clientChainAddress) + function getExocoreAddress(ClientChainID clientChainId, bytes calldata clientAddress) external view returns (address) { - return inboundRegistry[chainId][clientChainAddress]; + return inboundRegistry[clientChainId][clientAddress]; } /** * @notice Gets the next inbound nonce for a given source chain ID. - * @param srcChainId The source chain ID. + * @param clientChainId The client chain ID. * @return The next inbound nonce. */ - function nextInboundNonce(ClientChainID srcChainId) external view returns (uint64) { - return inboundNonce[srcChainId] + 1; + function nextInboundNonce(ClientChainID clientChainId) external view returns (uint64) { + return inboundNonce[clientChainId] + 1; } /** * @notice Retrieves a PegOutRequest by client chain id and request id - * @param clientChain The client chain 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 clientChain, uint64 requestId) public view returns (PegOutRequest memory) { - return pegOutRequests[clientChain][requestId]; + function getPegOutRequest(ClientChainID clientChainId, uint64 requestId) + public + view + returns (PegOutRequest memory) + { + return pegOutRequests[clientChainId][requestId]; } /** @@ -519,44 +525,44 @@ contract UTXOGateway is * @notice Registers or updates the Bitcoin chain with the Exocore system. */ function _registerOrUpdateClientChain( - ClientChainID chainId, + ClientChainID clientChainId, uint8 stakerAccountLength, string memory name, string memory metadata, string memory signatureScheme ) internal { - uint32 chainIdUint32 = uint32(uint8(chainId)); (bool success, bool updated) = ASSETS_CONTRACT.registerOrUpdateClientChain( - chainIdUint32, stakerAccountLength, name, metadata, signatureScheme + uint32(uint8(clientChainId)), stakerAccountLength, name, metadata, signatureScheme ); if (!success) { - revert Errors.RegisterClientChainToExocoreFailed(chainIdUint32); + revert Errors.RegisterClientChainToExocoreFailed(uint32(uint8(clientChainId))); } if (updated) { - emit ClientChainUpdated(chainIdUint32); + emit ClientChainUpdated(clientChainId); } else { - emit ClientChainRegistered(chainIdUint32); + emit ClientChainRegistered(clientChainId); } } function _registerOrUpdateToken( - ClientChainID chainId, + ClientChainID clientChainId, bytes memory token, uint8 decimals, string memory name, string memory metadata, string memory oracleInfo ) internal { - uint32 chainIdUint32 = uint32(uint8(chainId)); - bool registered = ASSETS_CONTRACT.registerToken(chainIdUint32, token, decimals, name, metadata, oracleInfo); + uint32 clientChainIdUint32 = uint32(uint8(clientChainId)); + bool registered = + ASSETS_CONTRACT.registerToken(clientChainIdUint32, token, decimals, name, metadata, oracleInfo); if (!registered) { - bool updated = ASSETS_CONTRACT.updateToken(chainIdUint32, token, metadata); + bool updated = ASSETS_CONTRACT.updateToken(clientChainIdUint32, token, metadata); if (!updated) { - revert Errors.AddWhitelistTokenFailed(chainIdUint32, bytes32(token)); + revert Errors.AddWhitelistTokenFailed(clientChainIdUint32, bytes32(token)); } - emit WhitelistTokenUpdated(chainIdUint32, VIRTUAL_TOKEN_ADDRESS); + emit WhitelistTokenUpdated(clientChainId, VIRTUAL_TOKEN_ADDRESS); } else { - emit WhitelistTokenAdded(chainIdUint32, VIRTUAL_TOKEN_ADDRESS); + emit WhitelistTokenAdded(clientChainId, VIRTUAL_TOKEN_ADDRESS); } } @@ -573,7 +579,13 @@ contract UTXOGateway is { // StakeMsg, EIP721 is preferred next step. bytes memory encodeMsg = abi.encode( - _msg.chainId, _msg.srcAddress, _msg.exocoreAddress, _msg.operator, _msg.amount, _msg.nonce, _msg.txTag + _msg.clientChainId, + _msg.clientAddress, + _msg.exocoreAddress, + _msg.operator, + _msg.amount, + _msg.nonce, + _msg.txTag ); messageHash = keccak256(encodeMsg); @@ -587,7 +599,7 @@ contract UTXOGateway is function _verifyStakeMsgFields(StakeMsg calldata _msg) internal pure { // Combine all non-zero checks into a single value uint256 nonZeroCheck = - uint8(_msg.chainId) | _msg.srcAddress.length | _msg.amount | _msg.nonce | _msg.txTag.length; + uint8(_msg.clientChainId) | _msg.clientAddress.length | _msg.amount | _msg.nonce | _msg.txTag.length; if (nonZeroCheck == 0) { revert Errors.InvalidStakeMessage(); @@ -598,8 +610,8 @@ contract UTXOGateway is } } - function _verifyTxTagNotProcessed(ClientChainID chainId, bytes calldata txTag) internal view { - if (processedClientChainTxs[chainId][txTag]) { + function _verifyTxTagNotProcessed(ClientChainID clientChainId, bytes calldata txTag) internal view { + if (processedClientChainTxs[clientChainId][txTag]) { revert Errors.TxTagAlreadyProcessed(); } } @@ -619,10 +631,10 @@ contract UTXOGateway is _verifyStakeMsgFields(_msg); // Verify nonce - _verifyInboundNonce(_msg.chainId, _msg.nonce); + _verifyInboundNonce(_msg.clientChainId, _msg.nonce); // Verify that the txTag has not been processed - _verifyTxTagNotProcessed(_msg.chainId, _msg.txTag); + _verifyTxTagNotProcessed(_msg.clientChainId, _msg.txTag); // Verify signature messageHash = _verifySignature(witness, _msg, signature); @@ -631,34 +643,35 @@ contract UTXOGateway is /** * @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 clientChain The client chain to be pegged out + * @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 clientChain, + ClientChainID clientChainId, uint256 _amount, address withdrawer, - bytes memory clientChainAddress, + 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[clientChain]; + requestId = ++pegOutNonce[clientChainId]; // 3. Check if request already exists - PegOutRequest storage request = pegOutRequests[clientChain][requestId]; + PegOutRequest storage request = pegOutRequests[clientChainId][requestId]; if (request.requester != address(0)) { - revert Errors.RequestAlreadyExists(uint32(uint8(clientChain)), requestId); + revert Errors.RequestAlreadyExists(uint32(uint8(clientChainId)), requestId); } // 4. Create new PegOutRequest - request.chainId = clientChain; + request.clientChainId = clientChainId; request.nonce = requestId; request.requester = withdrawer; - request.clientChainAddress = clientChainAddress; + request.clientAddress = clientAddress; request.amount = _amount; request.withdrawType = _withdrawType; } @@ -715,53 +728,53 @@ contract UTXOGateway is } } - function _registerAddress(ClientChainID chainId, bytes memory depositor, address exocoreAddress) internal { + function _registerAddress(ClientChainID clientChainId, bytes memory depositor, address exocoreAddress) internal { require(depositor.length > 0 && exocoreAddress != address(0), "Invalid address"); - require(inboundRegistry[chainId][depositor] == address(0), "Depositor address already registered"); - require(outboundRegistry[chainId][exocoreAddress].length == 0, "Exocore address already registered"); + require(inboundRegistry[clientChainId][depositor] == address(0), "Depositor address already registered"); + require(outboundRegistry[clientChainId][exocoreAddress].length == 0, "Exocore address already registered"); - inboundRegistry[chainId][depositor] = exocoreAddress; - outboundRegistry[chainId][exocoreAddress] = depositor; + inboundRegistry[clientChainId][depositor] = exocoreAddress; + outboundRegistry[clientChainId][exocoreAddress] = depositor; - emit AddressRegistered(chainId, depositor, exocoreAddress); + 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.chainId]++; - processedClientChainTxs[_msg.chainId][_msg.txTag] = true; + inboundNonce[_msg.clientChainId]++; + processedClientChainTxs[_msg.clientChainId][_msg.txTag] = true; // register address if not already registered if ( - inboundRegistry[_msg.chainId][_msg.srcAddress] == address(0) - && outboundRegistry[_msg.chainId][_msg.exocoreAddress].length == 0 + inboundRegistry[_msg.clientChainId][_msg.clientAddress] == address(0) + && outboundRegistry[_msg.clientChainId][_msg.exocoreAddress].length == 0 ) { if (_msg.exocoreAddress == address(0)) { revert Errors.ZeroAddress(); } - _registerAddress(_msg.chainId, _msg.srcAddress, _msg.exocoreAddress); + _registerAddress(_msg.clientChainId, _msg.clientAddress, _msg.exocoreAddress); } - address stakerExoAddr = inboundRegistry[_msg.chainId][_msg.srcAddress]; + 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.chainId, _msg.srcAddress, stakerExoAddr, amountAfterFee, _msg.txTag); + _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.chainId, stakerExoAddr, _msg.operator, amountAfterFee); + bool success = _delegate(_msg.clientChainId, stakerExoAddr, _msg.operator, amountAfterFee); if (!success) { - emit DelegationFailedForStake(_msg.chainId, stakerExoAddr, _msg.operator, amountAfterFee); + emit DelegationFailedForStake(_msg.clientChainId, stakerExoAddr, _msg.operator, amountAfterFee); } else { - emit DelegationCompleted(_msg.chainId, stakerExoAddr, _msg.operator, amountAfterFee); + emit DelegationCompleted(_msg.clientChainId, stakerExoAddr, _msg.operator, amountAfterFee); } } - emit StakeMsgExecuted(_msg.chainId, _msg.nonce, stakerExoAddr, amountAfterFee); + emit StakeMsgExecuted(_msg.clientChainId, _msg.nonce, stakerExoAddr, amountAfterFee); } } diff --git a/src/storage/UTXOGatewayStorage.sol b/src/storage/UTXOGatewayStorage.sol index 1e5b2457..1d1174ae 100644 --- a/src/storage/UTXOGatewayStorage.sol +++ b/src/storage/UTXOGatewayStorage.sol @@ -49,16 +49,23 @@ contract UTXOGatewayStorage { } /** - * @dev Struct to store interchain message information + * @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 chainId; - bytes srcAddress; // the address of the depositor on the source chain - address exocoreAddress; // the address of the depositor on the Exocore chain - string operator; // the operator to delegate to, would only deposit to exocore address if operator is empty - uint256 amount; // deposit amount + ClientChainID clientChainId; + bytes clientAddress; + address exocoreAddress; + string operator; + uint256 amount; uint64 nonce; - bytes txTag; // lowercase(txid-vout) + bytes txTag; } /** @@ -86,10 +93,10 @@ contract UTXOGatewayStorage { * @dev Struct for peg-out requests */ struct PegOutRequest { - ClientChainID chainId; + ClientChainID clientChainId; uint64 nonce; address requester; - bytes clientChainAddress; + bytes clientAddress; uint256 amount; WithdrawType withdrawType; } @@ -431,21 +438,21 @@ contract UTXOGatewayStorage { /// @notice Emitted upon the registration of a new client chain. /// @param clientChainId The chain ID of the client chain. - event ClientChainRegistered(uint32 clientChainId); + event ClientChainRegistered(ClientChainID clientChainId); /// @notice Emitted upon the update of a client chain. /// @param clientChainId The chain ID of the client chain. - event ClientChainUpdated(uint32 clientChainId); + 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(uint32 clientChainId, address indexed 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(uint32 clientChainId, address indexed token); + event WhitelistTokenUpdated(ClientChainID clientChainId, address indexed token); /// @notice Emitted when consensus is activated /// @param requiredWitnessesCount The number of required witnesses diff --git a/test/foundry/unit/UTXOGateway.t.sol b/test/foundry/unit/UTXOGateway.t.sol index 914ec92a..094b24ee 100644 --- a/test/foundry/unit/UTXOGateway.t.sol +++ b/test/foundry/unit/UTXOGateway.t.sol @@ -84,10 +84,10 @@ contract UTXOGatewayTest is Test { event StakeMsgExecuted(bytes32 indexed txId); event BridgeFeeRateUpdated(uint256 newRate); - event ClientChainRegistered(uint32 clientChainId); - event ClientChainUpdated(uint32 clientChainId); - event WhitelistTokenAdded(uint32 clientChainId, address indexed token); - event WhitelistTokenUpdated(uint32 clientChainId, address indexed token); + 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, @@ -477,9 +477,9 @@ contract UTXOGatewayTest is Test { ); vm.expectEmit(true, false, false, false); - emit ClientChainRegistered(uint32(uint8(UTXOGatewayStorage.ClientChainID.Bitcoin))); + emit ClientChainRegistered(UTXOGatewayStorage.ClientChainID.Bitcoin); vm.expectEmit(true, false, false, false); - emit WhitelistTokenAdded(uint32(uint8(UTXOGatewayStorage.ClientChainID.Bitcoin)), VIRTUAL_TOKEN_ADDRESS); + emit WhitelistTokenAdded(UTXOGatewayStorage.ClientChainID.Bitcoin, VIRTUAL_TOKEN_ADDRESS); gateway.activateStakingForClientChain(UTXOGatewayStorage.ClientChainID.Bitcoin); vm.stopPrank(); @@ -533,9 +533,9 @@ contract UTXOGatewayTest is Test { ); vm.expectEmit(true, false, false, false); - emit ClientChainUpdated(uint32(uint8(UTXOGatewayStorage.ClientChainID.Bitcoin))); + emit ClientChainUpdated(UTXOGatewayStorage.ClientChainID.Bitcoin); vm.expectEmit(true, false, false, false); - emit WhitelistTokenUpdated(uint32(uint8(UTXOGatewayStorage.ClientChainID.Bitcoin)), VIRTUAL_TOKEN_ADDRESS); + emit WhitelistTokenUpdated(UTXOGatewayStorage.ClientChainID.Bitcoin, VIRTUAL_TOKEN_ADDRESS); gateway.activateStakingForClientChain(UTXOGatewayStorage.ClientChainID.Bitcoin); vm.stopPrank(); @@ -642,8 +642,8 @@ contract UTXOGatewayTest is Test { // Create stake message UTXOGatewayStorage.StakeMsg memory stakeMsg = UTXOGatewayStorage.StakeMsg({ - chainId: UTXOGatewayStorage.ClientChainID.Bitcoin, - srcAddress: btcAddress, + clientChainId: UTXOGatewayStorage.ClientChainID.Bitcoin, + clientAddress: btcAddress, exocoreAddress: user, operator: operator, amount: 1 ether, @@ -681,13 +681,13 @@ contract UTXOGatewayTest is Test { signature = _generateSignature(stakeMsg, witnesses[2].privateKey); vm.prank(relayer); vm.expectEmit(true, false, false, false); - emit StakeMsgExecuted(stakeMsg.chainId, stakeMsg.nonce, stakeMsg.exocoreAddress, stakeMsg.amount); + 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.chainId, stakeMsg.txTag)); + assertTrue(gateway.processedClientChainTxs(stakeMsg.clientChainId, stakeMsg.txTag)); assertTrue(gateway.processedTransactions(txId)); } @@ -698,8 +698,8 @@ contract UTXOGatewayTest is Test { _deactivateConsensus(); UTXOGatewayStorage.StakeMsg memory stakeMsg = UTXOGatewayStorage.StakeMsg({ - chainId: UTXOGatewayStorage.ClientChainID.Bitcoin, - srcAddress: btcAddress, + clientChainId: UTXOGatewayStorage.ClientChainID.Bitcoin, + clientAddress: btcAddress, exocoreAddress: user, operator: operator, amount: 1 ether, @@ -720,8 +720,8 @@ contract UTXOGatewayTest is Test { _activateConsensus(); UTXOGatewayStorage.StakeMsg memory stakeMsg = UTXOGatewayStorage.StakeMsg({ - chainId: UTXOGatewayStorage.ClientChainID.Bitcoin, - srcAddress: btcAddress, + clientChainId: UTXOGatewayStorage.ClientChainID.Bitcoin, + clientAddress: btcAddress, exocoreAddress: user, operator: operator, amount: 1 ether, @@ -741,8 +741,8 @@ contract UTXOGatewayTest is Test { _activateConsensus(); UTXOGatewayStorage.StakeMsg memory stakeMsg = UTXOGatewayStorage.StakeMsg({ - chainId: UTXOGatewayStorage.ClientChainID.Bitcoin, - srcAddress: btcAddress, + clientChainId: UTXOGatewayStorage.ClientChainID.Bitcoin, + clientAddress: btcAddress, exocoreAddress: user, operator: operator, amount: 1 ether, @@ -763,8 +763,8 @@ contract UTXOGatewayTest is Test { _activateConsensus(); UTXOGatewayStorage.StakeMsg memory stakeMsg = UTXOGatewayStorage.StakeMsg({ - chainId: UTXOGatewayStorage.ClientChainID.Bitcoin, - srcAddress: btcAddress, + clientChainId: UTXOGatewayStorage.ClientChainID.Bitcoin, + clientAddress: btcAddress, exocoreAddress: user, operator: operator, amount: 1 ether, @@ -791,7 +791,7 @@ contract UTXOGatewayTest is Test { bytes32 messageHash = _getMessageHash(stakeMsg); assertEq(uint8(gateway.getTransactionStatus(messageHash)), uint8(UTXOGatewayStorage.TxStatus.Pending)); assertEq(gateway.getTransactionProofCount(messageHash), 1); - assertFalse(gateway.processedClientChainTxs(stakeMsg.chainId, stakeMsg.txTag)); + assertFalse(gateway.processedClientChainTxs(stakeMsg.clientChainId, stakeMsg.txTag)); } function test_SubmitProofForStakeMsg_RestartExpiredTransaction() public { @@ -799,8 +799,8 @@ contract UTXOGatewayTest is Test { _activateConsensus(); UTXOGatewayStorage.StakeMsg memory stakeMsg = UTXOGatewayStorage.StakeMsg({ - chainId: UTXOGatewayStorage.ClientChainID.Bitcoin, - srcAddress: btcAddress, + clientChainId: UTXOGatewayStorage.ClientChainID.Bitcoin, + clientAddress: btcAddress, exocoreAddress: user, operator: operator, amount: 1 ether, @@ -837,8 +837,8 @@ contract UTXOGatewayTest is Test { assertEq(gateway.requiredProofs(), 3); UTXOGatewayStorage.StakeMsg memory stakeMsg = UTXOGatewayStorage.StakeMsg({ - chainId: UTXOGatewayStorage.ClientChainID.Bitcoin, - srcAddress: btcAddress, + clientChainId: UTXOGatewayStorage.ClientChainID.Bitcoin, + clientAddress: btcAddress, exocoreAddress: user, operator: operator, amount: 1 ether, @@ -871,7 +871,7 @@ contract UTXOGatewayTest is Test { assertEq(uint8(gateway.getTransactionStatus(messageHash)), uint8(UTXOGatewayStorage.TxStatus.Pending)); assertEq(gateway.getTransactionProofCount(messageHash), 2); assertFalse(gateway.processedTransactions(messageHash)); - assertFalse(gateway.processedClientChainTxs(stakeMsg.chainId, stakeMsg.txTag)); + assertFalse(gateway.processedClientChainTxs(stakeMsg.clientChainId, stakeMsg.txTag)); assertTrue(gateway.getTransactionWitnessTime(messageHash, witnesses[0].addr) > 0); assertTrue(gateway.getTransactionWitnessTime(messageHash, witnesses[1].addr) > 0); } @@ -881,8 +881,8 @@ contract UTXOGatewayTest is Test { _activateConsensus(); UTXOGatewayStorage.StakeMsg memory stakeMsg = UTXOGatewayStorage.StakeMsg({ - chainId: UTXOGatewayStorage.ClientChainID.Bitcoin, - srcAddress: btcAddress, + clientChainId: UTXOGatewayStorage.ClientChainID.Bitcoin, + clientAddress: btcAddress, exocoreAddress: user, operator: operator, amount: 1 ether, @@ -906,8 +906,8 @@ contract UTXOGatewayTest is Test { _activateConsensus(); UTXOGatewayStorage.StakeMsg memory stakeMsg = UTXOGatewayStorage.StakeMsg({ - chainId: UTXOGatewayStorage.ClientChainID.Bitcoin, - srcAddress: btcAddress, + clientChainId: UTXOGatewayStorage.ClientChainID.Bitcoin, + clientAddress: btcAddress, exocoreAddress: user, operator: operator, amount: 1 ether, @@ -926,8 +926,8 @@ contract UTXOGatewayTest is Test { _deactivateConsensus(); UTXOGatewayStorage.StakeMsg memory stakeMsg = UTXOGatewayStorage.StakeMsg({ - chainId: UTXOGatewayStorage.ClientChainID.Bitcoin, - srcAddress: btcAddress, + clientChainId: UTXOGatewayStorage.ClientChainID.Bitcoin, + clientAddress: btcAddress, exocoreAddress: user, operator: "", amount: 1 ether, @@ -955,7 +955,7 @@ contract UTXOGatewayTest is Test { gateway.processStakeMessage(witnesses[0].addr, stakeMsg, signature); // Verify address registration - assertEq(gateway.getClientChainAddress(UTXOGatewayStorage.ClientChainID.Bitcoin, user), btcAddress); + assertEq(gateway.getClientAddress(UTXOGatewayStorage.ClientChainID.Bitcoin, user), btcAddress); assertEq(gateway.getExocoreAddress(UTXOGatewayStorage.ClientChainID.Bitcoin, btcAddress), user); } @@ -963,8 +963,8 @@ contract UTXOGatewayTest is Test { _deactivateConsensus(); UTXOGatewayStorage.StakeMsg memory stakeMsg = UTXOGatewayStorage.StakeMsg({ - chainId: UTXOGatewayStorage.ClientChainID.Bitcoin, - srcAddress: btcAddress, + clientChainId: UTXOGatewayStorage.ClientChainID.Bitcoin, + clientAddress: btcAddress, exocoreAddress: user, operator: operator, amount: 1 ether, @@ -995,7 +995,7 @@ contract UTXOGatewayTest is Test { UTXOGatewayStorage.ClientChainID.Bitcoin, stakeMsg.txTag, user, - stakeMsg.srcAddress, + stakeMsg.clientAddress, amountAfterFee, amountAfterFee ); @@ -1011,8 +1011,8 @@ contract UTXOGatewayTest is Test { _deactivateConsensus(); UTXOGatewayStorage.StakeMsg memory stakeMsg = UTXOGatewayStorage.StakeMsg({ - chainId: UTXOGatewayStorage.ClientChainID.Bitcoin, - srcAddress: btcAddress, + clientChainId: UTXOGatewayStorage.ClientChainID.Bitcoin, + clientAddress: btcAddress, exocoreAddress: user, operator: operator, amount: 1 ether, @@ -1040,8 +1040,8 @@ contract UTXOGatewayTest is Test { function test_ProcessStakeMessage_DelegationFailureNotRevert() public { UTXOGatewayStorage.StakeMsg memory stakeMsg = UTXOGatewayStorage.StakeMsg({ - chainId: UTXOGatewayStorage.ClientChainID.Bitcoin, - srcAddress: btcAddress, + clientChainId: UTXOGatewayStorage.ClientChainID.Bitcoin, + clientAddress: btcAddress, exocoreAddress: user, operator: operator, amount: 1 ether, @@ -1068,7 +1068,7 @@ contract UTXOGatewayTest is Test { UTXOGatewayStorage.ClientChainID.Bitcoin, stakeMsg.txTag, user, - stakeMsg.srcAddress, + stakeMsg.clientAddress, 1 ether, stakeMsg.amount ); @@ -1084,8 +1084,8 @@ contract UTXOGatewayTest is Test { _deactivateConsensus(); UTXOGatewayStorage.StakeMsg memory stakeMsg = UTXOGatewayStorage.StakeMsg({ - chainId: UTXOGatewayStorage.ClientChainID.Bitcoin, - srcAddress: btcAddress, + clientChainId: UTXOGatewayStorage.ClientChainID.Bitcoin, + clientAddress: btcAddress, exocoreAddress: user, operator: "", amount: 1 ether, @@ -1109,8 +1109,8 @@ contract UTXOGatewayTest is Test { _deactivateConsensus(); UTXOGatewayStorage.StakeMsg memory stakeMsg = UTXOGatewayStorage.StakeMsg({ - chainId: UTXOGatewayStorage.ClientChainID.Bitcoin, - srcAddress: btcAddress, + clientChainId: UTXOGatewayStorage.ClientChainID.Bitcoin, + clientAddress: btcAddress, exocoreAddress: user, operator: "", amount: 1 ether, @@ -1132,8 +1132,8 @@ contract UTXOGatewayTest is Test { _deactivateConsensus(); UTXOGatewayStorage.StakeMsg memory stakeMsg = UTXOGatewayStorage.StakeMsg({ - chainId: UTXOGatewayStorage.ClientChainID.Bitcoin, - srcAddress: btcAddress, + clientChainId: UTXOGatewayStorage.ClientChainID.Bitcoin, + clientAddress: btcAddress, exocoreAddress: user, operator: "", amount: 1 ether, @@ -1154,8 +1154,8 @@ contract UTXOGatewayTest is Test { // Create invalid message with all zero values UTXOGatewayStorage.StakeMsg memory stakeMsg = UTXOGatewayStorage.StakeMsg({ - chainId: UTXOGatewayStorage.ClientChainID.None, - srcAddress: bytes(""), + clientChainId: UTXOGatewayStorage.ClientChainID.None, + clientAddress: bytes(""), exocoreAddress: address(0), operator: "", amount: 0, @@ -1174,8 +1174,8 @@ contract UTXOGatewayTest is Test { _deactivateConsensus(); UTXOGatewayStorage.StakeMsg memory stakeMsg = UTXOGatewayStorage.StakeMsg({ - chainId: UTXOGatewayStorage.ClientChainID.Bitcoin, - srcAddress: btcAddress, + clientChainId: UTXOGatewayStorage.ClientChainID.Bitcoin, + clientAddress: btcAddress, exocoreAddress: address(0), // Zero address operator: "", amount: 1 ether, @@ -1194,8 +1194,8 @@ contract UTXOGatewayTest is Test { _deactivateConsensus(); UTXOGatewayStorage.StakeMsg memory stakeMsg = UTXOGatewayStorage.StakeMsg({ - chainId: UTXOGatewayStorage.ClientChainID.Bitcoin, - srcAddress: btcAddress, + clientChainId: UTXOGatewayStorage.ClientChainID.Bitcoin, + clientAddress: btcAddress, exocoreAddress: user, operator: "", amount: 1 ether, @@ -1208,7 +1208,7 @@ contract UTXOGatewayTest is Test { vm.prank(relayer); vm.expectRevert( abi.encodeWithSelector( - Errors.UnexpectedInboundNonce.selector, gateway.nextInboundNonce(stakeMsg.chainId), stakeMsg.nonce + Errors.UnexpectedInboundNonce.selector, gateway.nextInboundNonce(stakeMsg.clientChainId), stakeMsg.nonce ) ); gateway.processStakeMessage(witnesses[0].addr, stakeMsg, signature); @@ -1432,10 +1432,10 @@ contract UTXOGatewayTest is Test { // Verify peg-out request details UTXOGatewayStorage.PegOutRequest memory request = gateway.getPegOutRequest(UTXOGatewayStorage.ClientChainID.Bitcoin, 1); - assertEq(uint8(request.chainId), uint8(UTXOGatewayStorage.ClientChainID.Bitcoin)); + assertEq(uint8(request.clientChainId), uint8(UTXOGatewayStorage.ClientChainID.Bitcoin)); assertEq(request.nonce, 1); assertEq(request.requester, user); - assertEq(request.clientChainAddress, btcAddress); + assertEq(request.clientAddress, btcAddress); assertEq(request.amount, 1 ether); assertEq(uint8(request.withdrawType), uint8(UTXOGatewayStorage.WithdrawType.WithdrawPrincipal)); } @@ -1520,10 +1520,10 @@ contract UTXOGatewayTest is Test { // Verify peg-out request details UTXOGatewayStorage.PegOutRequest memory request = gateway.getPegOutRequest(UTXOGatewayStorage.ClientChainID.Bitcoin, 1); - assertEq(uint8(request.chainId), uint8(UTXOGatewayStorage.ClientChainID.Bitcoin)); + assertEq(uint8(request.clientChainId), uint8(UTXOGatewayStorage.ClientChainID.Bitcoin)); assertEq(request.nonce, 1); assertEq(request.requester, user); - assertEq(request.clientChainAddress, btcAddress); + assertEq(request.clientAddress, btcAddress); assertEq(request.amount, 1 ether); assertEq(uint8(request.withdrawType), uint8(UTXOGatewayStorage.WithdrawType.WithdrawReward)); } @@ -1593,10 +1593,10 @@ contract UTXOGatewayTest is Test { gateway.processNextPegOut(UTXOGatewayStorage.ClientChainID.Bitcoin); // Verify returned request contents - assertEq(uint8(request.chainId), uint8(UTXOGatewayStorage.ClientChainID.Bitcoin)); + assertEq(uint8(request.clientChainId), uint8(UTXOGatewayStorage.ClientChainID.Bitcoin)); assertEq(request.nonce, 1); assertEq(request.requester, user); - assertEq(request.clientChainAddress, btcAddress); + assertEq(request.clientAddress, btcAddress); assertEq(request.amount, 1 ether); assertEq(uint8(request.withdrawType), uint8(UTXOGatewayStorage.WithdrawType.WithdrawPrincipal)); @@ -1605,7 +1605,7 @@ contract UTXOGatewayTest is Test { gateway.getPegOutRequest(UTXOGatewayStorage.ClientChainID.Bitcoin, 1); assertEq(deletedRequest.requester, address(0)); assertEq(deletedRequest.amount, 0); - assertEq(deletedRequest.clientChainAddress, ""); + assertEq(deletedRequest.clientAddress, ""); // Verify outbound nonce increment assertEq(gateway.outboundNonce(UTXOGatewayStorage.ClientChainID.Bitcoin), 1); @@ -1674,7 +1674,7 @@ contract UTXOGatewayTest is Test { // Helper function to setup a peg-out request function _setupPegOutRequest() internal { - if (gateway.getClientChainAddress(UTXOGatewayStorage.ClientChainID.Bitcoin, user).length == 0) { + if (gateway.getClientAddress(UTXOGatewayStorage.ClientChainID.Bitcoin, user).length == 0) { _mockRegisterAddress(user, btcAddress); } @@ -1691,8 +1691,8 @@ contract UTXOGatewayTest is Test { // Helper functions function _mockRegisterAddress(address exocoreAddr, bytes memory btcAddr) internal { UTXOGatewayStorage.StakeMsg memory stakeMsg = UTXOGatewayStorage.StakeMsg({ - chainId: UTXOGatewayStorage.ClientChainID.Bitcoin, - srcAddress: btcAddr, + clientChainId: UTXOGatewayStorage.ClientChainID.Bitcoin, + clientAddress: btcAddr, exocoreAddress: exocoreAddr, operator: "", amount: 1 ether, @@ -1719,7 +1719,7 @@ contract UTXOGatewayTest is Test { gateway.processStakeMessage(witnesses[0].addr, stakeMsg, signature); // Verify address registration - assertEq(gateway.getClientChainAddress(UTXOGatewayStorage.ClientChainID.Bitcoin, exocoreAddr), btcAddr); + assertEq(gateway.getClientAddress(UTXOGatewayStorage.ClientChainID.Bitcoin, exocoreAddr), btcAddr); assertEq(gateway.getExocoreAddress(UTXOGatewayStorage.ClientChainID.Bitcoin, btcAddr), exocoreAddr); } @@ -1735,8 +1735,8 @@ contract UTXOGatewayTest is Test { function _getMessageHash(UTXOGatewayStorage.StakeMsg memory msg_) internal pure returns (bytes32) { return keccak256( abi.encode( - msg_.chainId, // ClientChainID - msg_.srcAddress, // bytes - Bitcoin address + msg_.clientChainId, // ClientChainID + msg_.clientAddress, // bytes - Bitcoin address msg_.exocoreAddress, // address msg_.operator, // string msg_.amount, // uint256 From 8a6c0be9584fcebe3390c14645a210133b20b67c Mon Sep 17 00:00:00 2001 From: adu Date: Tue, 26 Nov 2024 17:02:01 +0800 Subject: [PATCH 10/11] refactor: addWitness => addWitnesses, removeWitness => removeWitnesses --- src/core/UTXOGateway.sol | 59 +++++++------ test/foundry/unit/UTXOGateway.t.sol | 126 ++++++++++++++++++---------- 2 files changed, 119 insertions(+), 66 deletions(-) diff --git a/src/core/UTXOGateway.sol b/src/core/UTXOGateway.sol index cc0054b2..bf2aada5 100644 --- a/src/core/UTXOGateway.sol +++ b/src/core/UTXOGateway.sol @@ -121,37 +121,28 @@ contract UTXOGateway is } /** - * @notice Adds a new authorized witness. - * @param _witness The address of the witness to be added. + * @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 addWitness(address _witness) external onlyOwner whenNotPaused { - _addWitness(_witness); + function addWitnesses(address[] calldata witnesses) external onlyOwner whenNotPaused { + for (uint256 i = 0; i < witnesses.length; i++) { + _addWitness(witnesses[i]); + } } /** - * @notice Removes an authorized witness. - * @param _witness The address of the witness to be removed. + * @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. - * @custom:throws CannotRemoveLastWitness if the last witness is being removed */ - function removeWitness(address _witness) external onlyOwner whenNotPaused { - 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); + function removeWitnesses(address[] calldata witnesses) external onlyOwner whenNotPaused { + for (uint256 i = 0; i < witnesses.length; i++) { + _removeWitness(witnesses[i]); } } @@ -521,6 +512,26 @@ contract UTXOGateway is } } + 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. */ diff --git a/test/foundry/unit/UTXOGateway.t.sol b/test/foundry/unit/UTXOGateway.t.sol index 094b24ee..0523c1d2 100644 --- a/test/foundry/unit/UTXOGateway.t.sol +++ b/test/foundry/unit/UTXOGateway.t.sol @@ -208,165 +208,205 @@ contract UTXOGatewayTest is Test { vm.stopPrank(); } - function test_AddWitness_Success() public { + function test_AddWitnesses_Success() public { vm.prank(owner); vm.expectEmit(true, false, false, false); emit WitnessAdded(witnesses[1].addr); - gateway.addWitness(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_AddWitness_RevertNotOwner() public { + 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.addWitness(witnesses[1].addr); + gateway.addWitnesses(witnessesToAdd); } - function test_AddWitness_RevertZeroAddress() public { + function test_AddWitnesses_RevertZeroAddress() public { + address[] memory witnessesToAdd = new address[](1); + witnessesToAdd[0] = address(0); + vm.prank(owner); vm.expectRevert(Errors.ZeroAddress.selector); - gateway.addWitness(address(0)); + gateway.addWitnesses(witnessesToAdd); } - function test_AddWitness_RevertAlreadyAuthorized() public { - // First add a witness + function test_AddWitnesses_RevertAlreadyAuthorized() public { + address[] memory witnessesToAdd = new address[](1); + witnessesToAdd[0] = witnesses[1].addr; + vm.startPrank(owner); - gateway.addWitness(witnesses[1].addr); + gateway.addWitnesses(witnessesToAdd); // Try to add the same witness again vm.expectRevert(abi.encodeWithSelector(Errors.WitnessAlreadyAuthorized.selector, witnesses[1].addr)); - gateway.addWitness(witnesses[1].addr); + gateway.addWitnesses(witnessesToAdd); vm.stopPrank(); } - function test_AddWitness_RevertWhenPaused() public { + 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.addWitness(witnesses[1].addr); + gateway.addWitnesses(witnessesToAdd); vm.stopPrank(); } - function test_AddWitness_ConsensusActivation() public { + 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 - gateway.addWitness(witnesses[1].addr); + 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.addWitness(witnesses[2].addr); + gateway.addWitnesses(witnessesToAdd); // Add fourth witness - no consensus event - gateway.addWitness(address(0xaa)); + witnessesToAdd[0] = address(0xaa); + gateway.addWitnesses(witnessesToAdd); vm.stopPrank(); } - function test_RemoveWitness() public { + 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 - gateway.addWitness(witnesses[1].addr); + 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); - gateway.removeWitness(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_RemoveWitness_RevertNotOwner() public { + 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.removeWitness(witnesses[0].addr); + gateway.removeWitnesses(witnessesToRemove); } - function test_RemoveWitness_RevertWitnessNotAuthorized() public { + 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.addWitness(witnesses[1].addr); + 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.removeWitness(witnesses[2].addr); + gateway.removeWitnesses(witnessesToRemove); vm.stopPrank(); } - function test_RemoveWitness_RevertWhenPaused() public { + 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.removeWitness(witnesses[0].addr); + gateway.removeWitnesses(witnessesToRemove); vm.stopPrank(); } - function test_RemoveWitness_CannotRemoveLastWitness() public { + 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.removeWitness(witnesses[0].addr); + gateway.removeWitnesses(witnessesToRemove); vm.stopPrank(); } - function test_RemoveWitness_MultipleRemovals() public { + function test_RemoveWitnesses_MultipleRemovals() public { vm.startPrank(owner); - // First add another witness - gateway.addWitness(witnesses[1].addr); + // 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)); - assertEq(gateway.authorizedWitnessCount(), 2); - - // And add another witness - gateway.addWitness(witnesses[2].addr); assertTrue(gateway.authorizedWitnesses(witnesses[2].addr)); assertEq(gateway.authorizedWitnessCount(), 3); // Remove first witness - gateway.removeWitness(witnesses[0].addr); + 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 - gateway.removeWitness(witnesses[1].addr); + witnessesToRemove[0] = witnesses[1].addr; + gateway.removeWitnesses(witnessesToRemove); assertFalse(gateway.authorizedWitnesses(witnesses[1].addr)); assertEq(gateway.authorizedWitnessCount(), 1); vm.stopPrank(); } - function test_RemoveWitness_ConsensusDeactivation() public { + function test_RemoveWitnesses_ConsensusDeactivation() public { // add total 3 witnesses _addAllWitnesses(); - // set + // set required proofs to 2 vm.startPrank(owner); gateway.updateRequiredProofs(2); // Remove one witness - no consensus event - gateway.removeWitness(witnesses[2].addr); + 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); - gateway.removeWitness(witnesses[1].addr); + witnessesToRemove[0] = witnesses[1].addr; + gateway.removeWitnesses(witnessesToRemove); vm.stopPrank(); } @@ -1724,10 +1764,12 @@ contract UTXOGatewayTest is Test { } 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.addWitness(witnesses[i].addr); + gateway.addWitnesses(witnessesToAdd); } } } From d9122158e18b5c200c5c0e7e367e25672ab358d8 Mon Sep 17 00:00:00 2001 From: adu Date: Tue, 3 Dec 2024 10:44:43 +0800 Subject: [PATCH 11/11] chore: chainid => clientChainId --- src/storage/UTXOGatewayStorage.sol | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/src/storage/UTXOGatewayStorage.sol b/src/storage/UTXOGatewayStorage.sol index 1d1174ae..2531f975 100644 --- a/src/storage/UTXOGatewayStorage.sol +++ b/src/storage/UTXOGatewayStorage.sol @@ -216,12 +216,14 @@ contract UTXOGatewayStorage { /** * @dev Emitted when a stake message is executed - * @param chainId The chain ID of the client chain, should not violate the layerzero chain id + * @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 chainId, uint64 nonce, address indexed exocoreAddress, uint256 amount); + event StakeMsgExecuted( + ClientChainID indexed clientChainId, uint64 nonce, address indexed exocoreAddress, uint256 amount + ); /** * @dev Emitted when a transaction is processed @@ -231,7 +233,7 @@ contract UTXOGatewayStorage { /** * @dev Emitted when a deposit is completed - * @param srcChainId The source chain ID + * @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 @@ -239,7 +241,7 @@ contract UTXOGatewayStorage { * @param updatedBalance The updated balance after deposit */ event DepositCompleted( - ClientChainID indexed srcChainId, + ClientChainID indexed clientChainId, bytes txTag, address indexed depositorExoAddr, bytes depositorClientChainAddr, @@ -250,14 +252,14 @@ contract UTXOGatewayStorage { /** * @dev Emitted when a principal withdrawal is requested * @param requestId The unique identifier for the withdrawal request - * @param srcChainId The source chain ID + * @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 srcChainId, + ClientChainID indexed clientChainId, uint64 indexed requestId, address indexed withdrawerExoAddr, bytes withdrawerClientChainAddr, @@ -268,14 +270,14 @@ contract UTXOGatewayStorage { /** * @dev Emitted when a reward withdrawal is requested * @param requestId The unique identifier for the withdrawal request - * @param srcChainId The source chain ID + * @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 srcChainId, + ClientChainID indexed clientChainId, uint64 indexed requestId, address indexed withdrawerExoAddr, bytes withdrawerClientChainAddr, @@ -285,7 +287,7 @@ contract UTXOGatewayStorage { /** * @dev Emitted when a principal withdrawal is completed - * @param srcChainId The source chain ID + * @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 @@ -293,7 +295,7 @@ contract UTXOGatewayStorage { * @param updatedBalance The updated balance after withdrawal */ event WithdrawPrincipalCompleted( - ClientChainID indexed srcChainId, + ClientChainID indexed clientChainId, bytes32 indexed requestId, address indexed withdrawerExoAddr, bytes withdrawerClientChainAddr, @@ -303,7 +305,7 @@ contract UTXOGatewayStorage { /** * @dev Emitted when a reward withdrawal is completed - * @param srcChainId The source chain ID + * @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 @@ -311,7 +313,7 @@ contract UTXOGatewayStorage { * @param updatedBalance The updated balance after withdrawal */ event WithdrawRewardCompleted( - ClientChainID indexed srcChainId, + ClientChainID indexed clientChainId, bytes32 indexed requestId, address indexed withdrawerExoAddr, bytes withdrawerClientChainAddr, @@ -354,11 +356,11 @@ contract UTXOGatewayStorage { /** * @dev Emitted when an address is registered - * @param chainId The chain ID of the client chain, should not violate the layerzero chain id + * @param clientChainId The client chain ID * @param depositor The depositor's address * @param exocoreAddress The corresponding Exocore address */ - event AddressRegistered(ClientChainID indexed chainId, bytes depositor, address indexed exocoreAddress); + event AddressRegistered(ClientChainID indexed clientChainId, bytes depositor, address indexed exocoreAddress); /** * @dev Emitted when a new witness is added