Feature/GBI-2808 - PegOut Contract implementation#349
Conversation
| function claimContribution() external onlyOwner nonReentrant { | ||
| DaoContributorStorage storage $ = _getContributorStorage(); | ||
| uint256 amount = $.currentContribution; | ||
| $.currentContribution = 0; | ||
| address feeCollector = $.feeCollector; | ||
| if (amount == 0) revert NoFees(); | ||
| if (feeCollector == address(0)) revert FeeCollectorUnset(); | ||
| if (amount > address(this).balance) revert Flyover.NoBalance(amount, address(this).balance); | ||
| emit DaoFeesClaimed(msg.sender, feeCollector, amount); | ||
| (bool sent, bytes memory reason) = feeCollector.call{value: amount}(""); | ||
| if (!sent) revert Flyover.PaymentFailed(feeCollector, amount, reason); | ||
| } |
Check warning
Code scanning / Slither
Low-level calls Warning
| function __OwnableDaoContributor_init( | ||
| address owner, | ||
| uint256 feePercentage, | ||
| address payable feeCollector | ||
| ) internal onlyInitializing { | ||
| __ReentrancyGuard_init_unchained(); | ||
| __Ownable_init_unchained(owner); | ||
| DaoContributorStorage storage $ = _getContributorStorage(); | ||
| $.feePercentage = feePercentage; | ||
| $.feeCollector = feeCollector; | ||
| } |
Check warning
Code scanning / Slither
Conformance to Solidity naming conventions Warning
| function _getContributorStorage() private pure returns (DaoContributorStorage storage $) { | ||
| assembly { | ||
| $.slot := _CONTRIBUTOR_STORAGE_LOCATION | ||
| } | ||
| } |
Check warning
Code scanning / Slither
Assembly usage Warning
| function depositPegOut( | ||
| Quotes.PegOutQuote calldata quote, | ||
| bytes calldata signature | ||
| ) external payable nonReentrant override { | ||
| if(!_collateralManagement.isRegistered(_PEG_TYPE, quote.lpRskAddress)) { | ||
| revert Flyover.ProviderNotRegistered(quote.lpRskAddress); | ||
| } | ||
| uint256 requiredAmount = quote.value + quote.callFee + quote.productFeeAmount + quote.gasFee; | ||
| if (msg.value < requiredAmount) { | ||
| revert InsufficientAmount(msg.value, requiredAmount); | ||
| } | ||
| if (quote.depositDateLimit < block.timestamp || quote.expireDate < block.timestamp) { | ||
| revert QuoteExpiredByTime(quote.depositDateLimit, quote.expireDate); | ||
| } | ||
| if (quote.expireBlock < block.number) { | ||
| revert QuoteExpiredByBlocks(quote.expireBlock); | ||
| } | ||
|
|
||
| bytes32 quoteHash = _hashPegOutQuote(quote); | ||
| if (!SignatureValidator.verify(quote.lpRskAddress, quoteHash, signature)) { | ||
| revert SignatureValidator.IncorrectSignature(quote.lpRskAddress, quoteHash, signature); | ||
| } | ||
|
|
||
| Quotes.PegOutQuote storage registeredQuote = _pegOutQuotes[quoteHash]; | ||
|
|
||
| if (_isQuoteCompleted(quoteHash)) { | ||
| revert QuoteAlreadyCompleted(quoteHash); | ||
| } | ||
| if (registeredQuote.lbcAddress != address(0)) { | ||
| revert QuoteAlreadyRegistered(quoteHash); | ||
| } | ||
|
|
||
| _pegOutQuotes[quoteHash] = quote; | ||
| _pegOutRegistry[quoteHash].depositTimestamp = block.timestamp; | ||
|
|
||
| emit PegOutDeposit(quoteHash, msg.sender, msg.value, block.timestamp); | ||
|
|
||
| if (dustThreshold > msg.value - requiredAmount) { | ||
| return; | ||
| } | ||
|
|
||
| uint256 change = msg.value - requiredAmount; | ||
| emit PegOutChangePaid(quoteHash, quote.rskRefundAddress, change); | ||
| (bool sent, bytes memory reason) = quote.rskRefundAddress.call{value: change}(""); | ||
| if (!sent) { | ||
| revert Flyover.PaymentFailed(quote.rskRefundAddress, change, reason); | ||
| } | ||
| } |
Check notice
Code scanning / Slither
Block timestamp Low
| function depositPegOut( | ||
| Quotes.PegOutQuote calldata quote, | ||
| bytes calldata signature | ||
| ) external payable nonReentrant override { | ||
| if(!_collateralManagement.isRegistered(_PEG_TYPE, quote.lpRskAddress)) { | ||
| revert Flyover.ProviderNotRegistered(quote.lpRskAddress); | ||
| } | ||
| uint256 requiredAmount = quote.value + quote.callFee + quote.productFeeAmount + quote.gasFee; | ||
| if (msg.value < requiredAmount) { | ||
| revert InsufficientAmount(msg.value, requiredAmount); | ||
| } | ||
| if (quote.depositDateLimit < block.timestamp || quote.expireDate < block.timestamp) { | ||
| revert QuoteExpiredByTime(quote.depositDateLimit, quote.expireDate); | ||
| } | ||
| if (quote.expireBlock < block.number) { | ||
| revert QuoteExpiredByBlocks(quote.expireBlock); | ||
| } | ||
|
|
||
| bytes32 quoteHash = _hashPegOutQuote(quote); | ||
| if (!SignatureValidator.verify(quote.lpRskAddress, quoteHash, signature)) { | ||
| revert SignatureValidator.IncorrectSignature(quote.lpRskAddress, quoteHash, signature); | ||
| } | ||
|
|
||
| Quotes.PegOutQuote storage registeredQuote = _pegOutQuotes[quoteHash]; | ||
|
|
||
| if (_isQuoteCompleted(quoteHash)) { | ||
| revert QuoteAlreadyCompleted(quoteHash); | ||
| } | ||
| if (registeredQuote.lbcAddress != address(0)) { | ||
| revert QuoteAlreadyRegistered(quoteHash); | ||
| } | ||
|
|
||
| _pegOutQuotes[quoteHash] = quote; | ||
| _pegOutRegistry[quoteHash].depositTimestamp = block.timestamp; | ||
|
|
||
| emit PegOutDeposit(quoteHash, msg.sender, msg.value, block.timestamp); | ||
|
|
||
| if (dustThreshold > msg.value - requiredAmount) { | ||
| return; | ||
| } | ||
|
|
||
| uint256 change = msg.value - requiredAmount; | ||
| emit PegOutChangePaid(quoteHash, quote.rskRefundAddress, change); | ||
| (bool sent, bytes memory reason) = quote.rskRefundAddress.call{value: change}(""); | ||
| if (!sent) { | ||
| revert Flyover.PaymentFailed(quote.rskRefundAddress, change, reason); | ||
| } | ||
| } |
Check warning
Code scanning / Slither
Low-level calls Warning
| function refundUserPegOut(bytes32 quoteHash) external nonReentrant override { | ||
| Quotes.PegOutQuote memory quote = _pegOutQuotes[quoteHash]; | ||
|
|
||
| if (quote.lbcAddress == address(0)) revert Flyover.QuoteNotFound(quoteHash); | ||
| // solhint-disable-next-line gas-strict-inequalities | ||
| if (quote.expireDate >= block.timestamp || quote.expireBlock >= block.number) revert QuoteNotExpired(quoteHash); | ||
|
|
||
| uint256 valueToTransfer = quote.value + quote.callFee + quote.productFeeAmount + quote.gasFee; | ||
| address addressToTransfer = quote.rskRefundAddress; | ||
|
|
||
| delete _pegOutQuotes[quoteHash]; | ||
| _pegOutRegistry[quoteHash].completed = true; | ||
|
|
||
| emit PegOutUserRefunded(quoteHash, addressToTransfer, valueToTransfer); | ||
| _collateralManagement.slashPegOutCollateral(quote, quoteHash); | ||
|
|
||
| (bool sent, bytes memory reason) = addressToTransfer.call{value: valueToTransfer}(""); | ||
| if (!sent) { | ||
| revert Flyover.PaymentFailed(addressToTransfer, valueToTransfer, reason); | ||
| } | ||
| } |
Check notice
Code scanning / Slither
Block timestamp Low
| function refundUserPegOut(bytes32 quoteHash) external nonReentrant override { | ||
| Quotes.PegOutQuote memory quote = _pegOutQuotes[quoteHash]; | ||
|
|
||
| if (quote.lbcAddress == address(0)) revert Flyover.QuoteNotFound(quoteHash); | ||
| // solhint-disable-next-line gas-strict-inequalities | ||
| if (quote.expireDate >= block.timestamp || quote.expireBlock >= block.number) revert QuoteNotExpired(quoteHash); | ||
|
|
||
| uint256 valueToTransfer = quote.value + quote.callFee + quote.productFeeAmount + quote.gasFee; | ||
| address addressToTransfer = quote.rskRefundAddress; | ||
|
|
||
| delete _pegOutQuotes[quoteHash]; | ||
| _pegOutRegistry[quoteHash].completed = true; | ||
|
|
||
| emit PegOutUserRefunded(quoteHash, addressToTransfer, valueToTransfer); | ||
| _collateralManagement.slashPegOutCollateral(quote, quoteHash); | ||
|
|
||
| (bool sent, bytes memory reason) = addressToTransfer.call{value: valueToTransfer}(""); | ||
| if (!sent) { | ||
| revert Flyover.PaymentFailed(addressToTransfer, valueToTransfer, reason); | ||
| } | ||
| } |
Check warning
Code scanning / Slither
Low-level calls Warning
| function _shouldPenalize( | ||
| Quotes.PegOutQuote memory quote, | ||
| bytes32 quoteHash, | ||
| bytes32 blockHash | ||
| ) private view returns (bool) { | ||
| bytes memory firstConfirmationHeader = _bridge.getBtcBlockchainBlockHeaderByHash(blockHash); | ||
| if(firstConfirmationHeader.length < 1) revert Flyover.EmptyBlockHeader(blockHash); | ||
|
|
||
| uint256 firstConfirmationTimestamp = BtcUtils.getBtcBlockTimestamp(firstConfirmationHeader); | ||
| uint256 expectedConfirmationTime = _pegOutRegistry[quoteHash].depositTimestamp + | ||
| quote.transferTime + | ||
| btcBlockTime; | ||
|
|
||
| // penalize if the transfer was not made on time | ||
| if (firstConfirmationTimestamp > expectedConfirmationTime) { | ||
| return true; | ||
| } | ||
|
|
||
| // penalize if LP is refunding after expiration | ||
| if (block.timestamp > quote.expireDate || block.number > quote.expireBlock) { | ||
| return true; | ||
| } | ||
|
|
||
| return false; | ||
| } |
Check notice
Code scanning / Slither
Block timestamp Low
Dependency Review✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.OpenSSF Scorecard
Scanned Files |
There was a problem hiding this comment.
Pull Request Overview
This PR implements the PegOutContract functionality for the Flyover protocol, adding comprehensive peg-out capabilities with proper testing and documentation. The implementation includes new contract structures, comprehensive test coverage, and standardized DAO contribution mechanisms.
- Adds
PegOutContractimplementation with deposit, refund, and user refund functionality - Implements
OwnableDaoContributorUpgradeablefor standardized DAO fee collection - Adds comprehensive test suite covering all contract functions and edge cases
Reviewed Changes
Copilot reviewed 25 out of 25 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| contracts/PegOutContract.sol | Main contract implementing peg-out functionality with Bitcoin transaction validation |
| contracts/DaoContributor.sol | Base contract for standardized DAO fee collection and contribution management |
| contracts/interfaces/PegOut.sol | Interface definition for peg-out operations with comprehensive error handling |
| test/pegout/*.test.ts | Comprehensive test suite covering deposit, refund, configuration, and hashing functionality |
| test/utils/*.ts | Enhanced utility functions supporting both legacy and new contract testing |
| contracts/test-contracts/*.sol | Mock contracts for testing payment failures and reentrancy scenarios |
| function _validateBtcTxNullData(BtcUtils.TxRawOutput[] memory outputs, bytes32 quoteHash) private pure { | ||
| bytes memory scriptContent = BtcUtils.parseNullDataScript(outputs[_QUOTE_HASH_OUTPUT].pkScript); | ||
| uint256 scriptLength = scriptContent.length; | ||
|
|
||
| if (scriptLength != _QUOTE_HASH_SIZE + 1 || uint8(scriptContent[0]) != _QUOTE_HASH_SIZE) { | ||
| revert MalformedTransaction(scriptContent); | ||
| } | ||
|
|
||
| bytes32 txQuoteHash; | ||
| assembly { | ||
| txQuoteHash := mload(add(scriptContent, 33)) // 32 bytes after the first byte | ||
| } | ||
| if (quoteHash != txQuoteHash) revert InvalidQuoteHash(quoteHash, txQuoteHash); | ||
| } |
Check warning
Code scanning / Slither
Assembly usage Warning
What
PegOutContractimplementationOwnableDaoContributorUpgradeableto standardise the way the new contracts contribute to the DAOTask
https://rsklabs.atlassian.net/browse/GBI-2808