-
Notifications
You must be signed in to change notification settings - Fork 9
Expand file tree
/
Copy pathPegOutContract.sol
More file actions
377 lines (329 loc) · 16.9 KB
/
PegOutContract.sol
File metadata and controls
377 lines (329 loc) · 16.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
// SPDX-License-Identifier: MIT
pragma solidity 0.8.25;
import {BtcUtils} from "@rsksmart/btc-transaction-solidity-helper/contracts/BtcUtils.sol";
import {OwnableDaoContributorUpgradeable} from "./DaoContributor.sol";
import {IBridge} from "./interfaces/IBridge.sol";
import {ICollateralManagement, CollateralManagementSet} from "./interfaces/ICollateralManagement.sol";
import {IPegOut} from "./interfaces/IPegOut.sol";
import {Flyover} from "./libraries/Flyover.sol";
import {Quotes} from "./libraries/Quotes.sol";
import {SignatureValidator} from "./libraries/SignatureValidator.sol";
/// @title PegOutContract
/// @notice This contract is used to handle the peg out of the RSK network to the Bitcoin network
/// @author Rootstock Labs
contract PegOutContract is
OwnableDaoContributorUpgradeable,
IPegOut
{
/// @notice This struct is used to store the information of a peg out
/// @param completed whether the peg out has been completed or not,
/// completed means the peg out was paid and refunded (to any party)
/// @param depositTimestamp the timestamp of the deposit
struct PegOutRecord {
bool completed;
uint256 depositTimestamp;
}
/// @notice The version of the contract
string constant public VERSION = "1.0.0";
Flyover.ProviderType constant private _PEG_TYPE = Flyover.ProviderType.PegOut;
uint256 constant private _PAY_TO_ADDRESS_OUTPUT = 0;
uint256 constant private _QUOTE_HASH_OUTPUT = 1;
uint256 constant private _SAT_TO_WEI_CONVERSION = 10**10;
uint256 constant private _QUOTE_HASH_SIZE = 32;
IBridge private _bridge;
ICollateralManagement private _collateralManagement;
mapping(bytes32 => Quotes.PegOutQuote) private _pegOutQuotes;
mapping(bytes32 => PegOutRecord) private _pegOutRegistry;
/// @notice The dust threshold for the peg out. If the difference between the amount paid and the amount required
/// is more than this value, the difference goes back to the user's wallet
uint256 public dustThreshold;
/// @notice Average Bitcoin block time in seconds, used to calculate the expected confirmation time
uint256 public btcBlockTime;
bool private _mainnet;
/// @notice This event is emitted when the dust threshold is set
/// @param oldThreshold the old dust threshold
/// @param newThreshold the new dust threshold
event DustThresholdSet(uint256 indexed oldThreshold, uint256 indexed newThreshold);
/// @notice This event is emitted when the Bitcoin block time is set
/// @param oldTime the old Bitcoin block time
/// @param newTime the new Bitcoin block time
event BtcBlockTimeSet(uint256 indexed oldTime, uint256 indexed newTime);
// solhint-disable-next-line comprehensive-interface
receive() external payable {
revert Flyover.PaymentNotAllowed();
}
/// @inheritdoc IPegOut
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 Flyover.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, block.timestamp, msg.value);
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);
}
}
/// @notice This function is used to initialize the contract
/// @param owner the owner of the contract
/// @param bridge the address of the Rootstock bridge
/// @param dustThreshold_ the dust threshold for the peg out
/// @param collateralManagement the address of the Collateral Management contract
/// @param mainnet whether the contract is on the mainnet or not
/// @param btcBlockTime_ the average Bitcoin block time in seconds
/// @param daoFeePercentage the percentage of the peg out amount that goes to the DAO.
/// Use zero to disable the DAO integration feature
/// @param daoFeeCollector the address of the DAO fee collector
// solhint-disable-next-line comprehensive-interface
function initialize(
address owner,
address payable bridge,
uint256 dustThreshold_,
address collateralManagement,
bool mainnet,
uint256 btcBlockTime_,
uint256 daoFeePercentage,
address payable daoFeeCollector
) external initializer {
if (collateralManagement.code.length == 0) revert Flyover.NoContract(collateralManagement);
__OwnableDaoContributor_init(owner, daoFeePercentage, daoFeeCollector);
_bridge = IBridge(bridge);
_collateralManagement = ICollateralManagement(collateralManagement);
_mainnet = mainnet;
dustThreshold = dustThreshold_;
btcBlockTime = btcBlockTime_;
}
/// @notice This function is used to set the collateral management contract
/// @param collateralManagement the address of the Collateral Management contract
/// @dev This function is only callable by the owner of the contract
// solhint-disable-next-line comprehensive-interface
function setCollateralManagement(address collateralManagement) external onlyOwner {
if (collateralManagement.code.length == 0) revert Flyover.NoContract(collateralManagement);
emit CollateralManagementSet(address(_collateralManagement), collateralManagement);
_collateralManagement = ICollateralManagement(collateralManagement);
}
/// @notice This function is used to set the dust threshold
/// @param threshold the new dust threshold
/// @dev This function is only callable by the owner of the contract
// solhint-disable-next-line comprehensive-interface
function setDustThreshold(uint256 threshold) external onlyOwner {
emit DustThresholdSet(dustThreshold, threshold);
dustThreshold = threshold;
}
/// @notice This function is used to set the average Bitcoin block time
/// @param blockTime the new average Bitcoin block time in seconds
/// @dev This function is only callable by the owner of the contract
// solhint-disable-next-line comprehensive-interface
function setBtcBlockTime(uint256 blockTime) external onlyOwner {
emit BtcBlockTimeSet(btcBlockTime, blockTime);
btcBlockTime = blockTime;
}
/// @inheritdoc IPegOut
function refundPegOut(
bytes32 quoteHash,
bytes calldata btcTx,
bytes32 btcBlockHeaderHash,
uint256 merkleBranchPath,
bytes32[] calldata merkleBranchHashes
) external nonReentrant override {
if(!_collateralManagement.isRegistered(_PEG_TYPE, msg.sender)) {
revert Flyover.ProviderNotRegistered(msg.sender);
}
if (_isQuoteCompleted(quoteHash)) revert QuoteAlreadyCompleted(quoteHash);
Quotes.PegOutQuote memory quote = _pegOutQuotes[quoteHash];
if (quote.lbcAddress == address(0)) revert Flyover.QuoteNotFound(quoteHash);
if (quote.lpRskAddress != msg.sender) revert Flyover.InvalidSender(quote.lpRskAddress, msg.sender);
BtcUtils.TxRawOutput[] memory outputs = BtcUtils.getOutputs(btcTx);
_validateBtcTxNullData(outputs, quoteHash);
_validateBtcTxConfirmations(quote, btcTx, btcBlockHeaderHash, merkleBranchPath, merkleBranchHashes);
_validateBtcTxAmount(outputs, quote);
_validateBtcTxDestination(outputs, quote);
delete _pegOutQuotes[quoteHash];
_pegOutRegistry[quoteHash].completed = true;
emit PegOutRefunded(quoteHash);
_addDaoContribution(quote.lpRskAddress, quote.productFeeAmount);
if (_shouldPenalize(quote, quoteHash, btcBlockHeaderHash)) {
_collateralManagement.slashPegOutCollateral(msg.sender, quote, quoteHash);
}
uint256 refundAmount = quote.value + quote.callFee + quote.gasFee;
(bool sent, bytes memory reason) = quote.lpRskAddress.call{value: refundAmount}("");
if (!sent) {
revert Flyover.PaymentFailed(quote.lpRskAddress, refundAmount, reason);
}
}
/// @inheritdoc IPegOut
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(msg.sender, quote, quoteHash);
(bool sent, bytes memory reason) = addressToTransfer.call{value: valueToTransfer}("");
if (!sent) {
revert Flyover.PaymentFailed(addressToTransfer, valueToTransfer, reason);
}
}
/// @inheritdoc IPegOut
function hashPegOutQuote(
Quotes.PegOutQuote calldata quote
) external view override returns (bytes32) {
return _hashPegOutQuote(quote);
}
/// @inheritdoc IPegOut
function isQuoteCompleted(bytes32 quoteHash) external view override returns (bool) {
return _isQuoteCompleted(quoteHash);
}
/// @notice This function is used to hash a peg out quote
/// @dev The function also validates the quote belongs to this contract
/// @param quote the peg out quote to hash
/// @return quoteHash the hash of the peg out quote
function _hashPegOutQuote(
Quotes.PegOutQuote calldata quote
) private view returns (bytes32) {
if (address(this) != quote.lbcAddress) {
revert Flyover.IncorrectContract(address(this), quote.lbcAddress);
}
return keccak256(Quotes.encodePegOutQuote(quote));
}
/// @notice This function is used to check if a quote has been completed (refunded by any party)
/// @param quoteHash the hash of the quote to check
/// @return completed whether the quote has been completed or not
function _isQuoteCompleted(bytes32 quoteHash) private view returns (bool) {
return _pegOutRegistry[quoteHash].completed;
}
/// @notice This function is used to check if a liquidity provider should be penalized
/// according to the following rules:
/// - If the transfer was not made on time, the liquidity provider should be penalized
/// - If the liquidity provider is refunding after expiration, the liquidity provider should be penalized
/// @param quote the peg out quote
/// @param quoteHash the hash of the quote
/// @param blockHash the hash of the block that contains the first confirmation of the peg out transaction
/// @return shouldPenalize whether the liquidity provider should be penalized or not
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;
}
/// @notice This function is used to validate the number of confirmations of the Bitcoin transaction.
/// The function interacts with the Rootstock bridge to get the number of confirmations
/// @param quote the peg out quote
/// @param btcTx the Bitcoin transaction
/// @param btcBlockHeaderHash the hash of the block that contains the first confirmation of the peg out transaction
/// @param merkleBranchPath the path of the merkle branch
/// @param merkleBranchHashes the hashes of the merkle branch
function _validateBtcTxConfirmations(
Quotes.PegOutQuote memory quote,
bytes calldata btcTx,
bytes32 btcBlockHeaderHash,
uint256 merkleBranchPath,
bytes32[] calldata merkleBranchHashes
) private view {
int256 confirmations = _bridge.getBtcTransactionConfirmations(
BtcUtils.hashBtcTx(btcTx),
btcBlockHeaderHash,
merkleBranchPath,
merkleBranchHashes
);
if (confirmations < 0) {
revert UnableToGetConfirmations(confirmations);
} else if (confirmations < int(uint256(quote.transferConfirmations))) {
revert NotEnoughConfirmations(int(uint256(quote.transferConfirmations)), confirmations);
}
}
/// @notice This function is used to validate the destination of the Bitcoin transaction
/// @param outputs the outputs of the Bitcoin transaction
/// @param quote the peg out quote
function _validateBtcTxDestination(
BtcUtils.TxRawOutput[] memory outputs,
Quotes.PegOutQuote memory quote
) private view {
bytes memory btcTxDestination = BtcUtils.outputScriptToAddress(
outputs[_PAY_TO_ADDRESS_OUTPUT].pkScript,
_mainnet
);
if (keccak256(quote.depositAddress) != keccak256(btcTxDestination)) {
revert InvalidDestination(quote.depositAddress, btcTxDestination);
}
}
/// @notice This function is used to validate the amount of the Bitcoin transaction
/// @param outputs the outputs of the Bitcoin transaction
/// @param quote the peg out quote
function _validateBtcTxAmount(
BtcUtils.TxRawOutput[] memory outputs,
Quotes.PegOutQuote memory quote
) private pure {
uint256 requiredAmount = quote.value;
if (quote.value > _SAT_TO_WEI_CONVERSION && (quote.value % _SAT_TO_WEI_CONVERSION) != 0) {
requiredAmount = quote.value - (quote.value % _SAT_TO_WEI_CONVERSION);
}
uint256 paidAmount = outputs[_PAY_TO_ADDRESS_OUTPUT].value * _SAT_TO_WEI_CONVERSION;
if (paidAmount < requiredAmount) revert Flyover.InsufficientAmount(paidAmount, requiredAmount);
}
/// @notice This function is used to validate the null data of the Bitcoin transaction. The null data
/// is used to store the hash of the peg out quote in the Bitcoin transaction
/// @param outputs the outputs of the Bitcoin transaction
/// @param quoteHash the hash of the peg out quote
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);
}
}