Skip to content

Feature/GBI-2808 - PegOut Contract implementation#349

Merged
Luisfc68 merged 13 commits intolbc-splitfrom
feature/GBI-2808
Aug 13, 2025
Merged

Feature/GBI-2808 - PegOut Contract implementation#349
Luisfc68 merged 13 commits intolbc-splitfrom
feature/GBI-2808

Conversation

@Luisfc68
Copy link
Copy Markdown
Collaborator

@Luisfc68 Luisfc68 commented Aug 4, 2025

What

  • Add PegOutContract implementation
  • Add unit tests for the PegOut contract
  • Add OwnableDaoContributorUpgradeable to standardise the way the new contracts contribute to the DAO
  • Add natspec documentation for the new contracts
  • Run test in every PR

Task

https://rsklabs.atlassian.net/browse/GBI-2808

Comment on lines +59 to +70
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

Comment on lines +110 to +120
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

Comment on lines +137 to +141
function _getContributorStorage() private pure returns (DaoContributorStorage storage $) {
assembly {
$.slot := _CONTRIBUTOR_STORAGE_LOCATION
}
}

Check warning

Code scanning / Slither

Assembly usage Warning

Comment on lines +60 to +107
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

Comment on lines +60 to +107
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

Comment on lines +208 to +228
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

Comment on lines +208 to +228
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

Comment on lines +270 to +294
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

@github-actions
Copy link
Copy Markdown

github-actions bot commented Aug 4, 2025

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

OpenSSF Scorecard

PackageVersionScoreDetails

Scanned Files

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 PegOutContract implementation with deposit, refund, and user refund functionality
  • Implements OwnableDaoContributorUpgradeable for 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

Base automatically changed from split-preparations to lbc-split August 5, 2025 18:45
Comment on lines +358 to +371
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

@Luisfc68 Luisfc68 marked this pull request as ready for review August 5, 2025 19:10
Copy link
Copy Markdown
Collaborator

@Hakob23 Hakob23 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@Luisfc68 Luisfc68 merged commit 8212fb2 into lbc-split Aug 13, 2025
5 checks passed
@Luisfc68 Luisfc68 deleted the feature/GBI-2808 branch August 13, 2025 17:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants