Skip to content

Commit e5a248c

Browse files
committed
feat: add PegOutContract initial version
1 parent 4199658 commit e5a248c

File tree

8 files changed

+494
-0
lines changed

8 files changed

+494
-0
lines changed

contracts/DaoContributor.sol

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity 0.8.25;
3+
4+
import {
5+
OwnableUpgradeable
6+
} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
7+
import {
8+
ReentrancyGuardUpgradeable
9+
} from "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol";
10+
import {Flyover} from "./libraries/Flyover.sol";
11+
12+
abstract contract OwnableDaoContributorUpgradeable is
13+
ReentrancyGuardUpgradeable,
14+
OwnableUpgradeable {
15+
16+
// @custom:storage-location erc7201:rsk.dao.contributor
17+
struct DaoContributorStorage {
18+
uint256 feePercentage;
19+
uint256 currentContribution;
20+
address payable feeCollector;
21+
}
22+
23+
// keccak256(abi.encode(uint256(keccak256(bytes("rsk.dao.contributor"))) - 1)) & ~bytes32(uint256(0xff));
24+
bytes32 private constant _CONTRIBUTOR_STORAGE_LOCATION =
25+
0xb7e513d124139aa68259a99d4c2c344f3ba61e36716330d77f7fa887d0048e00;
26+
27+
event DaoContribution(address indexed contributor, uint256 indexed amount);
28+
event DaoFeesClaimed(address indexed claimer, address indexed receiver, uint256 indexed amount);
29+
event ContributionsConfigured(address indexed feeCollector, uint256 indexed feePercentage);
30+
31+
error NoFees();
32+
error FeeCollectorUnset();
33+
34+
function claimContribution() external onlyOwner nonReentrant {
35+
DaoContributorStorage storage $ = _getContributorStorage();
36+
uint256 amount = $.currentContribution;
37+
$.currentContribution = 0;
38+
address feeCollector = $.feeCollector;
39+
if (amount == 0) revert NoFees();
40+
if (feeCollector == address(0)) revert FeeCollectorUnset();
41+
if (amount > address(this).balance) revert Flyover.NoBalance(amount, address(this).balance);
42+
emit DaoFeesClaimed(msg.sender, feeCollector, amount);
43+
(bool sent, bytes memory reason) = feeCollector.call{value: amount}("");
44+
if (!sent) revert Flyover.PaymentFailed(feeCollector, amount, reason);
45+
}
46+
47+
function configureContributions(
48+
address payable feeCollector,
49+
uint256 feePercentage
50+
) external onlyOwner {
51+
DaoContributorStorage storage $ = _getContributorStorage();
52+
$.feeCollector = feeCollector;
53+
$.feePercentage = feePercentage;
54+
emit ContributionsConfigured(feeCollector, feePercentage);
55+
}
56+
57+
function getFeePercentage() external view returns (uint256) {
58+
return _getContributorStorage().feePercentage;
59+
}
60+
61+
function getCurrentContribution() external view returns (uint256) {
62+
return _getContributorStorage().currentContribution;
63+
}
64+
65+
function getFeeCollector() external view returns (address) {
66+
return _getContributorStorage().feeCollector;
67+
}
68+
69+
// solhint-disable-next-line func-name-mixedcase
70+
function __OwnableDaoContributor_init(
71+
address owner,
72+
uint256 feePercentage,
73+
address payable feeCollector
74+
) internal onlyInitializing {
75+
__Ownable_init(owner);
76+
__ReentrancyGuard_init();
77+
DaoContributorStorage storage $ = _getContributorStorage();
78+
$.feePercentage = feePercentage;
79+
$.feeCollector = feeCollector;
80+
}
81+
82+
function _addDaoContribution(address contributor, uint256 amount) internal {
83+
if (amount < 1) return;
84+
DaoContributorStorage storage $ = _getContributorStorage();
85+
$.currentContribution += amount;
86+
emit DaoContribution(contributor, amount);
87+
}
88+
89+
function _getContributorStorage() private pure returns (DaoContributorStorage storage $) {
90+
assembly {
91+
$.slot := _CONTRIBUTOR_STORAGE_LOCATION
92+
}
93+
}
94+
}

contracts/PegOutContract.sol

Lines changed: 274 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,274 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity 0.8.25;
3+
4+
import {BtcUtils} from "@rsksmart/btc-transaction-solidity-helper/contracts/BtcUtils.sol";
5+
import {OwnableDaoContributorUpgradeable} from "./DaoContributor.sol";
6+
import {IBridge} from "./interfaces/Bridge.sol";
7+
import {ICollateralManagement} from "./interfaces/CollateralManagement.sol";
8+
import {IPegOut} from "./interfaces/PegOut.sol";
9+
import {Flyover} from "./libraries/Flyover.sol";
10+
import {Quotes} from "./libraries/Quotes.sol";
11+
import {SignatureValidator} from "./libraries/SignatureValidator.sol";
12+
13+
contract PegOutContract is
14+
OwnableDaoContributorUpgradeable,
15+
IPegOut
16+
{
17+
struct PegOutRecord {
18+
bool completed;
19+
uint256 depositTimestamp;
20+
}
21+
22+
string constant public VERSION = "1.0.0";
23+
Flyover.ProviderType constant private _PEG_TYPE = Flyover.ProviderType.PegOut;
24+
uint256 constant private _PAY_TO_ADDRESS_OUTPUT = 0;
25+
uint256 constant private _QUOTE_HASH_OUTPUT = 1;
26+
uint256 constant private _SAT_TO_WEI_CONVERSION = 10**10;
27+
uint256 constant private _QUOTE_HASH_SIZE = 32;
28+
29+
IBridge private _bridge;
30+
ICollateralManagement private _collateralManagement;
31+
32+
mapping(bytes32 => Quotes.PegOutQuote) private _pegOutQuotes;
33+
mapping(bytes32 => PegOutRecord) private _pegOutRegistry;
34+
35+
uint256 public dustThreshold;
36+
bool private _mainnet;
37+
uint256 private _btcBlockTime;
38+
39+
function depositPegOut(
40+
Quotes.PegOutQuote calldata quote,
41+
bytes calldata signature
42+
) external payable nonReentrant override {
43+
if(!_collateralManagement.isRegistered(_PEG_TYPE, quote.lpRskAddress)) {
44+
revert Flyover.ProviderNotRegistered(quote.lpRskAddress);
45+
}
46+
uint256 requiredAmount = quote.value + quote.callFee + quote.productFeeAmount + quote.gasFee;
47+
if (msg.value < requiredAmount) {
48+
revert InsufficientAmount(msg.value, requiredAmount);
49+
}
50+
if (quote.depositDateLimit > block.timestamp || quote.expireDate > block.timestamp) {
51+
revert QuoteExpiredByTime(quote.depositDateLimit, quote.expireDate);
52+
}
53+
if (quote.expireBlock > block.number) {
54+
revert QuoteExpiredByBlocks(quote.expireBlock);
55+
}
56+
57+
bytes32 quoteHash = _hashPegOutQuote(quote);
58+
if (!SignatureValidator.verify(quote.lpRskAddress, quoteHash, signature)) {
59+
revert SignatureValidator.IncorrectSignature(quote.lpRskAddress, quoteHash, signature);
60+
}
61+
62+
Quotes.PegOutQuote storage registeredQuote = _pegOutQuotes[quoteHash];
63+
64+
if (_isQuoteCompleted(quoteHash)) {
65+
revert QuoteAlreadyCompleted(quoteHash);
66+
}
67+
if (registeredQuote.lbcAddress != address(0)) {
68+
revert QuoteAlreadyRegistered(quoteHash);
69+
}
70+
71+
_pegOutQuotes[quoteHash] = quote;
72+
_pegOutRegistry[quoteHash].depositTimestamp = block.timestamp;
73+
74+
emit PegOutDeposit(quoteHash, msg.sender, msg.value, block.timestamp);
75+
76+
if (dustThreshold < requiredAmount - msg.value) {
77+
return;
78+
}
79+
80+
uint256 change = requiredAmount - msg.value;
81+
emit PegOutChangePaid(quoteHash, msg.sender, change);
82+
(bool sent, bytes memory reason) = quote.lpRskAddress.call{value: change}("");
83+
if (!sent) {
84+
revert Flyover.PaymentFailed(quote.lpRskAddress, change, reason);
85+
}
86+
}
87+
88+
// solhint-disable-next-line comprehensive-interface
89+
function initialize(
90+
address owner,
91+
address payable bridge,
92+
uint256 dustThreshold_,
93+
address collateralManagement,
94+
bool mainnet,
95+
uint256 daoFeePercentage,
96+
address payable daoFeeCollector
97+
) external initializer {
98+
__OwnableDaoContributor_init(owner, daoFeePercentage, daoFeeCollector);
99+
_bridge = IBridge(bridge);
100+
_collateralManagement = ICollateralManagement(collateralManagement);
101+
_mainnet = mainnet;
102+
dustThreshold = dustThreshold_;
103+
}
104+
105+
function refundPegOut(
106+
bytes32 quoteHash,
107+
bytes calldata btcTx,
108+
bytes32 btcBlockHeaderHash,
109+
uint256 partialMerkleTree,
110+
bytes32[] calldata merkleBranchHashes
111+
) external nonReentrant override {
112+
if(!_collateralManagement.isRegistered(_PEG_TYPE, msg.sender)) {
113+
revert Flyover.ProviderNotRegistered(msg.sender);
114+
}
115+
if (_isQuoteCompleted(quoteHash)) revert QuoteAlreadyCompleted(quoteHash);
116+
117+
Quotes.PegOutQuote storage quote = _pegOutQuotes[quoteHash];
118+
if (quote.lbcAddress == address(0)) revert Flyover.QuoteNotFound(quoteHash);
119+
if (quote.lpRskAddress != msg.sender) revert InvalidSender(quote.lpRskAddress, msg.sender);
120+
121+
BtcUtils.TxRawOutput[] memory outputs = BtcUtils.getOutputs(btcTx);
122+
_validateBtcTxNullData(outputs, quoteHash);
123+
_validateBtcTxConfirmations(quote, btcTx, btcBlockHeaderHash, partialMerkleTree, merkleBranchHashes);
124+
_validateBtcTxAmount(outputs, quote);
125+
_validateBtcTxDestination(outputs, quote);
126+
127+
if (_shouldPenalize(quote, quoteHash, btcBlockHeaderHash)) {
128+
_collateralManagement.slashPegOutCollateral(quote, quoteHash);
129+
}
130+
131+
delete _pegOutQuotes[quoteHash];
132+
_pegOutRegistry[quoteHash].completed = true;
133+
emit PegOutRefunded(quoteHash);
134+
135+
uint256 refundAmount = quote.value + quote.callFee + quote.gasFee;
136+
(bool sent, bytes memory reason) = quote.lpRskAddress.call{value: refundAmount}("");
137+
if (!sent) {
138+
revert Flyover.PaymentFailed(quote.lpRskAddress, refundAmount, reason);
139+
}
140+
_addDaoContribution(quote.lpRskAddress, quote.productFeeAmount);
141+
}
142+
143+
function refundUserPegOut(bytes32 quoteHash) external nonReentrant override {
144+
Quotes.PegOutQuote storage quote = _pegOutQuotes[quoteHash];
145+
146+
if (quote.lbcAddress == address(0)) revert Flyover.QuoteNotFound(quoteHash);
147+
// solhint-disable-next-line gas-strict-inequalities
148+
if (quote.expireDate >= block.timestamp || quote.expireBlock >= block.number) revert QuoteNotExpired(quoteHash);
149+
150+
uint256 valueToTransfer = quote.value + quote.callFee + quote.productFeeAmount + quote.gasFee;
151+
address addressToTransfer = quote.rskRefundAddress;
152+
153+
emit PegOutUserRefunded(quoteHash, quote.rskRefundAddress, valueToTransfer);
154+
_collateralManagement.slashPegOutCollateral(quote, quoteHash);
155+
156+
delete _pegOutQuotes[quoteHash];
157+
_pegOutRegistry[quoteHash].completed = true;
158+
159+
(bool sent, bytes memory reason) = addressToTransfer.call{value: valueToTransfer}("");
160+
if (!sent) {
161+
revert Flyover.PaymentFailed(addressToTransfer, valueToTransfer, reason);
162+
}
163+
}
164+
165+
function hashPegOutQuote(
166+
Quotes.PegOutQuote calldata quote
167+
) external view override returns (bytes32) {
168+
return _hashPegOutQuote(quote);
169+
}
170+
171+
function isQuoteCompleted(bytes32 quoteHash) external view override returns (bool) {
172+
return _isQuoteCompleted(quoteHash);
173+
}
174+
175+
function _hashPegOutQuote(
176+
Quotes.PegOutQuote calldata quote
177+
) private view returns (bytes32) {
178+
if (address(this) != quote.lbcAddress) {
179+
revert Flyover.IncorrectContract(address(this), quote.lbcAddress);
180+
}
181+
return keccak256(Quotes.encodePegOutQuote(quote));
182+
}
183+
184+
function _isQuoteCompleted(bytes32 quoteHash) private view returns (bool) {
185+
return _pegOutRegistry[quoteHash].completed;
186+
}
187+
188+
function _shouldPenalize(
189+
Quotes.PegOutQuote storage quote,
190+
bytes32 quoteHash,
191+
bytes32 blockHash
192+
) private view returns (bool) {
193+
bytes memory firstConfirmationHeader = _bridge.getBtcBlockchainBlockHeaderByHash(blockHash);
194+
if(firstConfirmationHeader.length < 1) revert Flyover.InvalidBlockHeader(firstConfirmationHeader);
195+
196+
uint256 firstConfirmationTimestamp = BtcUtils.getBtcBlockTimestamp(firstConfirmationHeader);
197+
uint256 expectedConfirmationTime = _pegOutRegistry[quoteHash].depositTimestamp +
198+
quote.transferTime +
199+
_btcBlockTime;
200+
201+
// penalize if the transfer was not made on time
202+
if (firstConfirmationTimestamp > expectedConfirmationTime) {
203+
return true;
204+
}
205+
206+
// penalize if LP is refunding after expiration
207+
if (block.timestamp > quote.expireDate || block.number > quote.expireBlock) {
208+
return true;
209+
}
210+
211+
return false;
212+
}
213+
214+
function _validateBtcTxConfirmations(
215+
Quotes.PegOutQuote storage quote,
216+
bytes calldata btcTx,
217+
bytes32 btcBlockHeaderHash,
218+
uint256 partialMerkleTree,
219+
bytes32[] calldata merkleBranchHashes
220+
) private view {
221+
int256 confirmations = _bridge.getBtcTransactionConfirmations(
222+
BtcUtils.hashBtcTx(btcTx),
223+
btcBlockHeaderHash,
224+
partialMerkleTree,
225+
merkleBranchHashes
226+
);
227+
if (confirmations < 0) {
228+
revert UnableToGetConfirmations(confirmations);
229+
} else if (confirmations < int(uint256(quote.transferConfirmations))) {
230+
revert NotEnoughConfirmations(int(uint256(quote.transferConfirmations)), confirmations);
231+
}
232+
}
233+
234+
function _validateBtcTxAmount(
235+
BtcUtils.TxRawOutput[] memory outputs,
236+
Quotes.PegOutQuote storage quote
237+
) private view {
238+
uint256 requiredAmount = quote.value;
239+
if (quote.value > _SAT_TO_WEI_CONVERSION && (quote.value % _SAT_TO_WEI_CONVERSION) != 0) {
240+
requiredAmount = quote.value - (quote.value % _SAT_TO_WEI_CONVERSION);
241+
}
242+
uint256 paidAmount = outputs[_PAY_TO_ADDRESS_OUTPUT].value * _SAT_TO_WEI_CONVERSION;
243+
if (paidAmount < requiredAmount) revert InsufficientAmount(requiredAmount, paidAmount);
244+
}
245+
246+
function _validateBtcTxDestination(
247+
BtcUtils.TxRawOutput[] memory outputs,
248+
Quotes.PegOutQuote storage quote
249+
) private view {
250+
bytes memory btcTxDestination = BtcUtils.outputScriptToAddress(
251+
outputs[_PAY_TO_ADDRESS_OUTPUT].pkScript,
252+
_mainnet
253+
);
254+
if (keccak256(quote.depositAddress) != keccak256(btcTxDestination)) {
255+
revert InvalidDestination(quote.depositAddress, btcTxDestination);
256+
}
257+
}
258+
259+
function _validateBtcTxNullData(BtcUtils.TxRawOutput[] memory outputs, bytes32 quoteHash) private pure {
260+
bytes memory scriptContent = BtcUtils.parseNullDataScript(outputs[_QUOTE_HASH_OUTPUT].pkScript);
261+
uint256 scriptLength = scriptContent.length;
262+
263+
if (scriptLength != _QUOTE_HASH_SIZE + 1 || uint8(scriptContent[0]) != _QUOTE_HASH_SIZE) {
264+
revert MalformedTransaction(scriptContent);
265+
}
266+
267+
// shift the array to remove the first byte (the size)
268+
for (uint8 i = 0 ; i < scriptLength - 1; ++i) {
269+
scriptContent[i] = scriptContent[i + 1];
270+
}
271+
bytes32 txQuoteHash = abi.decode(scriptContent, (bytes32));
272+
if (quoteHash != txQuoteHash) revert InvalidQuoteHash(quoteHash, txQuoteHash);
273+
}
274+
}

0 commit comments

Comments
 (0)