@keep-network/tbtc-v2 0.1.1-dev.22 → 0.1.1-dev.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.adoc +12 -0
- package/artifacts/TBTC.json +11 -10
- package/artifacts/TBTCToken.json +11 -10
- package/artifacts/VendingMachine.json +12 -11
- package/artifacts/solcInputs/7823c0ea8f2f46d2393b3380efff7ecb.json +191 -0
- package/build/contracts/GovernanceUtils.sol/GovernanceUtils.dbg.json +1 -1
- package/build/contracts/bank/Bank.sol/Bank.dbg.json +1 -1
- package/build/contracts/bridge/BitcoinTx.sol/BitcoinTx.dbg.json +1 -1
- package/build/contracts/bridge/Bridge.sol/Bridge.dbg.json +1 -1
- package/build/contracts/bridge/Bridge.sol/Bridge.json +440 -27
- package/build/contracts/bridge/Bridge.sol/IRelay.dbg.json +1 -1
- package/build/contracts/bridge/EcdsaLib.sol/EcdsaLib.dbg.json +4 -0
- package/build/contracts/bridge/EcdsaLib.sol/EcdsaLib.json +10 -0
- package/build/contracts/bridge/VendingMachine.sol/VendingMachine.dbg.json +1 -1
- package/build/contracts/bridge/Wallets.sol/Wallets.dbg.json +4 -0
- package/build/contracts/bridge/Wallets.sol/Wallets.json +138 -0
- package/build/contracts/token/TBTC.sol/TBTC.dbg.json +1 -1
- package/build/contracts/vault/IVault.sol/IVault.dbg.json +1 -1
- package/build/contracts/vault/TBTCVault.sol/TBTCVault.dbg.json +1 -1
- package/contracts/bridge/Bridge.sol +236 -158
- package/contracts/bridge/EcdsaLib.sol +30 -0
- package/contracts/bridge/Wallets.sol +520 -0
- package/package.json +20 -17
- package/artifacts/solcInputs/e2b5f983e9c69369a4f41eb2f0688e3c.json +0 -140
|
@@ -19,9 +19,12 @@ import "@openzeppelin/contracts/access/Ownable.sol";
|
|
|
19
19
|
|
|
20
20
|
import {BTCUtils} from "@keep-network/bitcoin-spv-sol/contracts/BTCUtils.sol";
|
|
21
21
|
import {BytesLib} from "@keep-network/bitcoin-spv-sol/contracts/BytesLib.sol";
|
|
22
|
+
import {IWalletOwner as EcdsaWalletOwner} from "@keep-network/ecdsa/contracts/api/IWalletOwner.sol";
|
|
22
23
|
|
|
23
24
|
import "../bank/Bank.sol";
|
|
24
25
|
import "./BitcoinTx.sol";
|
|
26
|
+
import "./EcdsaLib.sol";
|
|
27
|
+
import "./Wallets.sol";
|
|
25
28
|
|
|
26
29
|
/// @title Interface for the Bitcoin relay
|
|
27
30
|
/// @notice Contains only the methods needed by tBTC v2. The Bitcoin relay
|
|
@@ -54,10 +57,11 @@ interface IRelay {
|
|
|
54
57
|
/// wallet informs the Bridge about the sweep increasing appropriate
|
|
55
58
|
/// balances in the Bank.
|
|
56
59
|
/// @dev Bridge is an upgradeable component of the Bank.
|
|
57
|
-
contract Bridge is Ownable {
|
|
60
|
+
contract Bridge is Ownable, EcdsaWalletOwner {
|
|
58
61
|
using BTCUtils for bytes;
|
|
59
62
|
using BTCUtils for uint256;
|
|
60
63
|
using BytesLib for bytes;
|
|
64
|
+
using Wallets for Wallets.Data;
|
|
61
65
|
|
|
62
66
|
/// @notice Represents data which must be revealed by the depositor during
|
|
63
67
|
/// deposit reveal.
|
|
@@ -165,36 +169,6 @@ contract Bridge is Ownable {
|
|
|
165
169
|
uint64 changeValue;
|
|
166
170
|
}
|
|
167
171
|
|
|
168
|
-
/// @notice Represents wallet state:
|
|
169
|
-
enum WalletState {
|
|
170
|
-
/// @dev The wallet is unknown to the Bridge.
|
|
171
|
-
Unknown,
|
|
172
|
-
/// @dev The wallet can sweep deposits and accept redemption requests.
|
|
173
|
-
Active,
|
|
174
|
-
/// @dev The wallet was deemed unhealthy and is expected to move their
|
|
175
|
-
/// outstanding funds to another wallet. The wallet can still
|
|
176
|
-
/// fulfill their pending redemption requests although new
|
|
177
|
-
/// redemption requests and new deposit reveals are not accepted.
|
|
178
|
-
MovingFunds,
|
|
179
|
-
/// @dev The wallet moved or redeemed all their funds and cannot
|
|
180
|
-
/// perform any action.
|
|
181
|
-
Closed,
|
|
182
|
-
/// @dev The wallet committed a fraud that was reported. The wallet is
|
|
183
|
-
/// blocked and can not perform any actions in the Bridge.
|
|
184
|
-
/// Off-chain coordination with the wallet operators is needed to
|
|
185
|
-
/// recover funds.
|
|
186
|
-
Terminated
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
/// @notice Holds information about a wallet.
|
|
190
|
-
struct Wallet {
|
|
191
|
-
// Current state of the wallet.
|
|
192
|
-
WalletState state;
|
|
193
|
-
// The total redeemable value of pending redemption requests targeting
|
|
194
|
-
// that wallet.
|
|
195
|
-
uint64 pendingRedemptionsValue;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
172
|
/// @notice The number of confirmations on the Bitcoin chain required to
|
|
199
173
|
/// successfully evaluate an SPV proof.
|
|
200
174
|
uint256 public immutable txProofDifficultyFactor;
|
|
@@ -212,13 +186,21 @@ contract Bridge is Ownable {
|
|
|
212
186
|
/// Treasury takes part in the operators rewarding process.
|
|
213
187
|
address public immutable treasury;
|
|
214
188
|
|
|
189
|
+
/// TODO: Make it governable.
|
|
190
|
+
/// @notice The minimal amount that can be requested for deposit.
|
|
191
|
+
/// Value of this parameter must take into account the value of
|
|
192
|
+
/// `depositTreasuryFeeDivisor` and `depositTxMaxFee`
|
|
193
|
+
/// parameters in order to make requests that can incur the
|
|
194
|
+
/// treasury and transaction fee and still satisfy the depositor.
|
|
195
|
+
uint64 public depositDustThreshold;
|
|
196
|
+
|
|
215
197
|
/// TODO: Make it governable.
|
|
216
198
|
/// @notice Divisor used to compute the treasury fee taken from each
|
|
217
199
|
/// deposit and transferred to the treasury upon sweep proof
|
|
218
200
|
/// submission. That fee is computed as follows:
|
|
219
201
|
/// `treasuryFee = depositedAmount / depositTreasuryFeeDivisor`
|
|
220
202
|
/// For example, if the treasury fee needs to be 2% of each deposit,
|
|
221
|
-
/// the `
|
|
203
|
+
/// the `depositTreasuryFeeDivisor` should be set to `50`
|
|
222
204
|
/// because `1/50 = 0.02 = 2%`.
|
|
223
205
|
uint64 public depositTreasuryFeeDivisor;
|
|
224
206
|
|
|
@@ -279,15 +261,6 @@ contract Bridge is Ownable {
|
|
|
279
261
|
/// validating them before attempting to execute a sweep.
|
|
280
262
|
mapping(uint256 => DepositRequest) public deposits;
|
|
281
263
|
|
|
282
|
-
/// @notice Maps the 20-byte wallet public key hash (computed using
|
|
283
|
-
/// Bitcoin HASH160 over the compressed ECDSA public key) to
|
|
284
|
-
/// the latest wallet's main UTXO computed as
|
|
285
|
-
/// keccak256(txHash | txOutputIndex | txOutputValue). The `tx`
|
|
286
|
-
/// prefix refers to the transaction which created that main UTXO.
|
|
287
|
-
/// The txHash is bytes32 (ordered as in Bitcoin internally),
|
|
288
|
-
/// txOutputIndex an uint32, and txOutputValue an uint64 value.
|
|
289
|
-
mapping(bytes20 => bytes32) public mainUtxos;
|
|
290
|
-
|
|
291
264
|
/// @notice Collection of all pending redemption requests indexed by
|
|
292
265
|
/// redemption key built as
|
|
293
266
|
/// keccak256(walletPubKeyHash | redeemerOutputScript). The
|
|
@@ -302,8 +275,6 @@ contract Bridge is Ownable {
|
|
|
302
275
|
/// successfully
|
|
303
276
|
/// - `notifyRedemptionTimeout` in case the request was reported
|
|
304
277
|
/// to be timed out
|
|
305
|
-
/// - `submitRedemptionFraudProof` in case the request was handled
|
|
306
|
-
/// in an fraudulent way amount-wise.
|
|
307
278
|
mapping(uint256 => RedemptionRequest) public pendingRedemptions;
|
|
308
279
|
|
|
309
280
|
/// @notice Collection of all timed out redemptions requests indexed by
|
|
@@ -324,14 +295,39 @@ contract Bridge is Ownable {
|
|
|
324
295
|
// slither-disable-next-line uninitialized-state
|
|
325
296
|
mapping(uint256 => RedemptionRequest) public timedOutRedemptions;
|
|
326
297
|
|
|
327
|
-
/// @notice
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
298
|
+
/// @notice State related with wallets.
|
|
299
|
+
Wallets.Data internal wallets;
|
|
300
|
+
|
|
301
|
+
event WalletCreationPeriodUpdated(uint32 newCreationPeriod);
|
|
302
|
+
|
|
303
|
+
event WalletBtcBalanceRangeUpdated(
|
|
304
|
+
uint64 newMinBtcBalance,
|
|
305
|
+
uint64 newMaxBtcBalance
|
|
306
|
+
);
|
|
307
|
+
|
|
308
|
+
event WalletMaxAgeUpdated(uint32 newMaxAge);
|
|
309
|
+
|
|
310
|
+
event NewWalletRequested();
|
|
311
|
+
|
|
312
|
+
event NewWalletRegistered(
|
|
313
|
+
bytes32 indexed ecdsaWalletID,
|
|
314
|
+
bytes20 indexed walletPubKeyHash
|
|
315
|
+
);
|
|
316
|
+
|
|
317
|
+
event WalletMovingFunds(
|
|
318
|
+
bytes32 indexed ecdsaWalletID,
|
|
319
|
+
bytes20 indexed walletPubKeyHash
|
|
320
|
+
);
|
|
321
|
+
|
|
322
|
+
event WalletClosed(
|
|
323
|
+
bytes32 indexed ecdsaWalletID,
|
|
324
|
+
bytes20 indexed walletPubKeyHash
|
|
325
|
+
);
|
|
326
|
+
|
|
327
|
+
event WalletTerminated(
|
|
328
|
+
bytes32 indexed ecdsaWalletID,
|
|
329
|
+
bytes20 indexed walletPubKeyHash
|
|
330
|
+
);
|
|
335
331
|
|
|
336
332
|
event VaultStatusUpdated(address indexed vault, bool isTrusted);
|
|
337
333
|
|
|
@@ -367,6 +363,7 @@ contract Bridge is Ownable {
|
|
|
367
363
|
address _bank,
|
|
368
364
|
address _relay,
|
|
369
365
|
address _treasury,
|
|
366
|
+
address _ecdsaWalletRegistry,
|
|
370
367
|
uint256 _txProofDifficultyFactor
|
|
371
368
|
) {
|
|
372
369
|
require(_bank != address(0), "Bank address cannot be zero");
|
|
@@ -381,16 +378,62 @@ contract Bridge is Ownable {
|
|
|
381
378
|
txProofDifficultyFactor = _txProofDifficultyFactor;
|
|
382
379
|
|
|
383
380
|
// TODO: Revisit initial values.
|
|
381
|
+
depositDustThreshold = 1000000; // 1000000 satoshi = 0.01 BTC
|
|
384
382
|
depositTxMaxFee = 1000; // 1000 satoshi
|
|
385
383
|
depositTreasuryFeeDivisor = 2000; // 1/2000 == 5bps == 0.05% == 0.0005
|
|
386
384
|
redemptionDustThreshold = 1000000; // 1000000 satoshi = 0.01 BTC
|
|
387
385
|
redemptionTreasuryFeeDivisor = 2000; // 1/2000 == 5bps == 0.05% == 0.0005
|
|
388
386
|
redemptionTxMaxFee = 1000; // 1000 satoshi
|
|
389
387
|
redemptionTimeout = 172800; // 48 hours
|
|
388
|
+
|
|
389
|
+
// TODO: Revisit initial values.
|
|
390
|
+
wallets.init(_ecdsaWalletRegistry);
|
|
391
|
+
wallets.setCreationPeriod(1 weeks);
|
|
392
|
+
wallets.setBtcBalanceRange(1 * 1e8, 10 * 1e8); // [1 BTC, 10 BTC]
|
|
393
|
+
wallets.setMaxAge(26 weeks); // ~6 months
|
|
390
394
|
}
|
|
391
395
|
|
|
392
|
-
|
|
393
|
-
|
|
396
|
+
/// @notice Updates parameters used by the `Wallets` library.
|
|
397
|
+
/// @param creationPeriod New value of the wallet creation period
|
|
398
|
+
/// @param minBtcBalance New value of the minimum BTC balance
|
|
399
|
+
/// @param maxBtcBalance New value of the maximum BTC balance
|
|
400
|
+
/// @param maxAge New value of the wallet maximum age
|
|
401
|
+
/// @dev Requirements:
|
|
402
|
+
/// - Caller must be the contract owner.
|
|
403
|
+
/// - Minimum BTC balance must be greater than zero
|
|
404
|
+
/// - Maximum BTC balance must be greater than minimum BTC balance
|
|
405
|
+
function updateWalletsParameters(
|
|
406
|
+
uint32 creationPeriod,
|
|
407
|
+
uint64 minBtcBalance,
|
|
408
|
+
uint64 maxBtcBalance,
|
|
409
|
+
uint32 maxAge
|
|
410
|
+
) external onlyOwner {
|
|
411
|
+
wallets.setCreationPeriod(creationPeriod);
|
|
412
|
+
wallets.setBtcBalanceRange(minBtcBalance, maxBtcBalance);
|
|
413
|
+
wallets.setMaxAge(maxAge);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/// @return creationPeriod Value of the wallet creation period
|
|
417
|
+
/// @return minBtcBalance Value of the minimum BTC balance
|
|
418
|
+
/// @return maxBtcBalance Value of the maximum BTC balance
|
|
419
|
+
/// @return maxAge Value of the wallet max age
|
|
420
|
+
function getWalletsParameters()
|
|
421
|
+
external
|
|
422
|
+
view
|
|
423
|
+
returns (
|
|
424
|
+
uint32 creationPeriod,
|
|
425
|
+
uint64 minBtcBalance,
|
|
426
|
+
uint64 maxBtcBalance,
|
|
427
|
+
uint32 maxAge
|
|
428
|
+
)
|
|
429
|
+
{
|
|
430
|
+
creationPeriod = wallets.creationPeriod;
|
|
431
|
+
minBtcBalance = wallets.minBtcBalance;
|
|
432
|
+
maxBtcBalance = wallets.maxBtcBalance;
|
|
433
|
+
maxAge = wallets.maxAge;
|
|
434
|
+
|
|
435
|
+
return (creationPeriod, minBtcBalance, maxBtcBalance, maxAge);
|
|
436
|
+
}
|
|
394
437
|
|
|
395
438
|
/// @notice Allows the Governance to mark the given vault address as trusted
|
|
396
439
|
/// or no longer trusted. Vaults are not trusted by default.
|
|
@@ -410,6 +453,105 @@ contract Bridge is Ownable {
|
|
|
410
453
|
emit VaultStatusUpdated(vault, isTrusted);
|
|
411
454
|
}
|
|
412
455
|
|
|
456
|
+
/// @notice Requests creation of a new wallet. This function just
|
|
457
|
+
/// forms a request and the creation process is performed
|
|
458
|
+
/// asynchronously. Once a wallet is created, the ECDSA Wallet
|
|
459
|
+
/// Registry will notify this contract by calling the
|
|
460
|
+
/// `__ecdsaWalletCreatedCallback` function.
|
|
461
|
+
/// @param activeWalletMainUtxo Data of the active wallet's main UTXO, as
|
|
462
|
+
/// currently known on the Ethereum chain.
|
|
463
|
+
/// @dev Requirements:
|
|
464
|
+
/// - `activeWalletMainUtxo` components must point to the recent main
|
|
465
|
+
/// UTXO of the given active wallet, as currently known on the
|
|
466
|
+
/// Ethereum chain. If there is no active wallet at the moment, or
|
|
467
|
+
/// the active wallet has no main UTXO, this parameter can be
|
|
468
|
+
/// empty as it is ignored.
|
|
469
|
+
/// - Wallet creation must not be in progress
|
|
470
|
+
/// - If the active wallet is set, one of the following
|
|
471
|
+
/// conditions must be true:
|
|
472
|
+
/// - The active wallet BTC balance is above the minimum threshold
|
|
473
|
+
/// and the active wallet is old enough, i.e. the creation period
|
|
474
|
+
/// was elapsed since its creation time
|
|
475
|
+
/// - The active wallet BTC balance is above the maximum threshold
|
|
476
|
+
function requestNewWallet(BitcoinTx.UTXO calldata activeWalletMainUtxo)
|
|
477
|
+
external
|
|
478
|
+
{
|
|
479
|
+
wallets.requestNewWallet(activeWalletMainUtxo);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/// @notice A callback function that is called by the ECDSA Wallet Registry
|
|
483
|
+
/// once a new ECDSA wallet is created.
|
|
484
|
+
/// @param ecdsaWalletID Wallet's unique identifier.
|
|
485
|
+
/// @param publicKeyX Wallet's public key's X coordinate.
|
|
486
|
+
/// @param publicKeyY Wallet's public key's Y coordinate.
|
|
487
|
+
/// @dev Requirements:
|
|
488
|
+
/// - The only caller authorized to call this function is `registry`
|
|
489
|
+
/// - Given wallet data must not belong to an already registered wallet
|
|
490
|
+
function __ecdsaWalletCreatedCallback(
|
|
491
|
+
bytes32 ecdsaWalletID,
|
|
492
|
+
bytes32 publicKeyX,
|
|
493
|
+
bytes32 publicKeyY
|
|
494
|
+
) external override {
|
|
495
|
+
wallets.registerNewWallet(ecdsaWalletID, publicKeyX, publicKeyY);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/// @notice A callback function that is called by the ECDSA Wallet Registry
|
|
499
|
+
/// once a wallet heartbeat failure is detected.
|
|
500
|
+
/// @param publicKeyX Wallet's public key's X coordinate
|
|
501
|
+
/// @param publicKeyY Wallet's public key's Y coordinate
|
|
502
|
+
/// @dev Requirements:
|
|
503
|
+
/// - The only caller authorized to call this function is `registry`
|
|
504
|
+
/// - Wallet must be in Live state
|
|
505
|
+
function __ecdsaWalletHeartbeatFailedCallback(
|
|
506
|
+
bytes32,
|
|
507
|
+
bytes32 publicKeyX,
|
|
508
|
+
bytes32 publicKeyY
|
|
509
|
+
) external override {
|
|
510
|
+
wallets.notifyWalletHeartbeatFailed(publicKeyX, publicKeyY);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/// @notice Notifies that the wallet is either old enough or has too few
|
|
514
|
+
/// satoshis left and qualifies to be closed.
|
|
515
|
+
/// @param walletPubKeyHash 20-byte public key hash of the wallet
|
|
516
|
+
/// @param walletMainUtxo Data of the wallet's main UTXO, as currently
|
|
517
|
+
/// known on the Ethereum chain.
|
|
518
|
+
/// @dev Requirements:
|
|
519
|
+
/// - Wallet must not be set as the current active wallet
|
|
520
|
+
/// - Wallet must exceed the wallet maximum age OR the wallet BTC
|
|
521
|
+
/// balance must be lesser than the minimum threshold. If the latter
|
|
522
|
+
/// case is true, the `walletMainUtxo` components must point to the
|
|
523
|
+
/// recent main UTXO of the given wallet, as currently known on the
|
|
524
|
+
/// Ethereum chain. If the wallet has no main UTXO, this parameter
|
|
525
|
+
/// can be empty as it is ignored since the wallet balance is
|
|
526
|
+
/// assumed to be zero.
|
|
527
|
+
/// - Wallet must be in Live state
|
|
528
|
+
function notifyCloseableWallet(
|
|
529
|
+
bytes20 walletPubKeyHash,
|
|
530
|
+
BitcoinTx.UTXO calldata walletMainUtxo
|
|
531
|
+
) external {
|
|
532
|
+
wallets.notifyCloseableWallet(walletPubKeyHash, walletMainUtxo);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/// @notice Gets details about a registered wallet.
|
|
536
|
+
/// @param walletPubKeyHash The 20-byte wallet public key hash (computed
|
|
537
|
+
/// using Bitcoin HASH160 over the compressed ECDSA public key)
|
|
538
|
+
/// @return Wallet details.
|
|
539
|
+
function getWallet(bytes20 walletPubKeyHash)
|
|
540
|
+
external
|
|
541
|
+
view
|
|
542
|
+
returns (Wallets.Wallet memory)
|
|
543
|
+
{
|
|
544
|
+
return wallets.registeredWallets[walletPubKeyHash];
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/// @notice Gets the public key hash of the active wallet.
|
|
548
|
+
/// @return The 20-byte public key hash (computed using Bitcoin HASH160
|
|
549
|
+
/// over the compressed ECDSA public key) of the active wallet.
|
|
550
|
+
/// Returns bytes20(0) if there is no active wallet at the moment.
|
|
551
|
+
function getActiveWalletPubKeyHash() external view returns (bytes20) {
|
|
552
|
+
return wallets.activeWalletPubKeyHash;
|
|
553
|
+
}
|
|
554
|
+
|
|
413
555
|
/// @notice Determines the current Bitcoin SPV proof difficulty context.
|
|
414
556
|
/// @return proofDifficulty Bitcoin proof difficulty context.
|
|
415
557
|
function proofDifficultyContext()
|
|
@@ -468,7 +610,7 @@ contract Bridge is Ownable {
|
|
|
468
610
|
"Vault is not trusted"
|
|
469
611
|
);
|
|
470
612
|
|
|
471
|
-
// TODO: Validate if `walletPubKeyHash` is a known and
|
|
613
|
+
// TODO: Validate if `walletPubKeyHash` is a known and live wallet.
|
|
472
614
|
// TODO: Should we enforce a specific locktime at contract level?
|
|
473
615
|
|
|
474
616
|
bytes memory expectedScript = abi.encodePacked(
|
|
@@ -549,7 +691,10 @@ contract Bridge is Ownable {
|
|
|
549
691
|
|
|
550
692
|
uint64 fundingOutputAmount = fundingOutput.extractValue();
|
|
551
693
|
|
|
552
|
-
|
|
694
|
+
require(
|
|
695
|
+
fundingOutputAmount >= depositDustThreshold,
|
|
696
|
+
"Deposit amount too small"
|
|
697
|
+
);
|
|
553
698
|
|
|
554
699
|
deposit.amount = fundingOutputAmount;
|
|
555
700
|
deposit.depositor = reveal.depositor;
|
|
@@ -634,7 +779,11 @@ contract Bridge is Ownable {
|
|
|
634
779
|
uint64 sweepTxOutputValue
|
|
635
780
|
) = processSweepTxOutput(sweepTx.outputVector);
|
|
636
781
|
|
|
637
|
-
|
|
782
|
+
Wallets.Wallet storage wallet = wallets.registeredWallets[
|
|
783
|
+
walletPubKeyHash
|
|
784
|
+
];
|
|
785
|
+
|
|
786
|
+
// TODO: Validate if `walletPubKeyHash` is a known and live wallet.
|
|
638
787
|
|
|
639
788
|
// Check if the main UTXO for given wallet exists. If so, validate
|
|
640
789
|
// passed main UTXO data against the stored hash and use them for
|
|
@@ -644,7 +793,7 @@ contract Bridge is Ownable {
|
|
|
644
793
|
0,
|
|
645
794
|
0
|
|
646
795
|
);
|
|
647
|
-
bytes32 mainUtxoHash =
|
|
796
|
+
bytes32 mainUtxoHash = wallet.mainUtxoHash;
|
|
648
797
|
if (mainUtxoHash != bytes32(0)) {
|
|
649
798
|
require(
|
|
650
799
|
keccak256(
|
|
@@ -712,7 +861,7 @@ contract Bridge is Ownable {
|
|
|
712
861
|
// Record this sweep data and assign them to the wallet public key hash
|
|
713
862
|
// as new main UTXO. Transaction output index is always 0 as sweep
|
|
714
863
|
// transaction always contains only one output.
|
|
715
|
-
|
|
864
|
+
wallet.mainUtxoHash = keccak256(
|
|
716
865
|
abi.encodePacked(sweepTxHash, uint32(0), sweepTxOutputValue)
|
|
717
866
|
);
|
|
718
867
|
|
|
@@ -983,7 +1132,7 @@ contract Bridge is Ownable {
|
|
|
983
1132
|
/// @notice Requests redemption of the given amount from the specified
|
|
984
1133
|
/// wallet to the redeemer Bitcoin output script.
|
|
985
1134
|
/// @param walletPubKeyHash The 20-byte wallet public key hash (computed
|
|
986
|
-
|
|
1135
|
+
/// using Bitcoin HASH160 over the compressed ECDSA public key)
|
|
987
1136
|
/// @param mainUtxo Data of the wallet's main UTXO, as currently known on
|
|
988
1137
|
/// the Ethereum chain
|
|
989
1138
|
/// @param redeemerOutputScript The redeemer's length-prefixed output
|
|
@@ -998,7 +1147,7 @@ contract Bridge is Ownable {
|
|
|
998
1147
|
/// `amount - (amount / redemptionTreasuryFeeDivisor) - redemptionTxMaxFee`.
|
|
999
1148
|
/// Fees values are taken at the moment of request creation.
|
|
1000
1149
|
/// @dev Requirements:
|
|
1001
|
-
/// - Wallet behind `walletPubKeyHash` must be
|
|
1150
|
+
/// - Wallet behind `walletPubKeyHash` must be live
|
|
1002
1151
|
/// - `mainUtxo` components must point to the recent main UTXO
|
|
1003
1152
|
/// of the given wallet, as currently known on the Ethereum chain.
|
|
1004
1153
|
/// - `redeemerOutputScript` must be a proper Bitcoin script
|
|
@@ -1015,12 +1164,16 @@ contract Bridge is Ownable {
|
|
|
1015
1164
|
bytes calldata redeemerOutputScript,
|
|
1016
1165
|
uint64 amount
|
|
1017
1166
|
) external {
|
|
1167
|
+
Wallets.Wallet storage wallet = wallets.registeredWallets[
|
|
1168
|
+
walletPubKeyHash
|
|
1169
|
+
];
|
|
1170
|
+
|
|
1018
1171
|
require(
|
|
1019
|
-
|
|
1020
|
-
"Wallet must be in
|
|
1172
|
+
wallet.state == Wallets.WalletState.Live,
|
|
1173
|
+
"Wallet must be in Live state"
|
|
1021
1174
|
);
|
|
1022
1175
|
|
|
1023
|
-
bytes32 mainUtxoHash =
|
|
1176
|
+
bytes32 mainUtxoHash = wallet.mainUtxoHash;
|
|
1024
1177
|
require(
|
|
1025
1178
|
mainUtxoHash != bytes32(0),
|
|
1026
1179
|
"No main UTXO for the given wallet"
|
|
@@ -1082,7 +1235,7 @@ contract Bridge is Ownable {
|
|
|
1082
1235
|
|
|
1083
1236
|
// Check if given redemption key is not used by a pending redemption.
|
|
1084
1237
|
// There is no need to check for existence in `timedOutRedemptions`
|
|
1085
|
-
// since the wallet's state is changed to other than
|
|
1238
|
+
// since the wallet's state is changed to other than Live after
|
|
1086
1239
|
// first time out is reported so making new requests is not possible.
|
|
1087
1240
|
// slither-disable-next-line incorrect-equality
|
|
1088
1241
|
require(
|
|
@@ -1103,12 +1256,9 @@ contract Bridge is Ownable {
|
|
|
1103
1256
|
// wallet we need to subtract the total value of all pending redemptions
|
|
1104
1257
|
// from that wallet's main UTXO value. Given that the treasury fee is
|
|
1105
1258
|
// not redeemed from the wallet, we are subtracting it.
|
|
1106
|
-
|
|
1107
|
-
amount -
|
|
1108
|
-
treasuryFee;
|
|
1259
|
+
wallet.pendingRedemptionsValue += amount - treasuryFee;
|
|
1109
1260
|
require(
|
|
1110
|
-
mainUtxo.txOutputValue >=
|
|
1111
|
-
wallets[walletPubKeyHash].pendingRedemptionsValue,
|
|
1261
|
+
mainUtxo.txOutputValue >= wallet.pendingRedemptionsValue,
|
|
1112
1262
|
"Insufficient wallet funds"
|
|
1113
1263
|
);
|
|
1114
1264
|
|
|
@@ -1205,11 +1355,15 @@ contract Bridge is Ownable {
|
|
|
1205
1355
|
walletPubKeyHash
|
|
1206
1356
|
);
|
|
1207
1357
|
|
|
1208
|
-
|
|
1358
|
+
Wallets.Wallet storage wallet = wallets.registeredWallets[
|
|
1359
|
+
walletPubKeyHash
|
|
1360
|
+
];
|
|
1361
|
+
|
|
1362
|
+
Wallets.WalletState walletState = wallet.state;
|
|
1209
1363
|
require(
|
|
1210
|
-
walletState == WalletState.
|
|
1211
|
-
walletState == WalletState.MovingFunds,
|
|
1212
|
-
"Wallet must be in
|
|
1364
|
+
walletState == Wallets.WalletState.Live ||
|
|
1365
|
+
walletState == Wallets.WalletState.MovingFunds,
|
|
1366
|
+
"Wallet must be in Live or MovingFuds state"
|
|
1213
1367
|
);
|
|
1214
1368
|
|
|
1215
1369
|
// Process redemption transaction outputs to extract some info required
|
|
@@ -1222,7 +1376,7 @@ contract Bridge is Ownable {
|
|
|
1222
1376
|
if (outputsInfo.changeValue > 0) {
|
|
1223
1377
|
// If the change value is grater than zero, it means the change
|
|
1224
1378
|
// output exists and can be used as new wallet's main UTXO.
|
|
1225
|
-
|
|
1379
|
+
wallet.mainUtxoHash = keccak256(
|
|
1226
1380
|
abi.encodePacked(
|
|
1227
1381
|
redemptionTxHash,
|
|
1228
1382
|
outputsInfo.changeIndex,
|
|
@@ -1233,11 +1387,10 @@ contract Bridge is Ownable {
|
|
|
1233
1387
|
// If the change value is zero, it means the change output doesn't
|
|
1234
1388
|
// exists and no funds left on the wallet. Delete the main UTXO
|
|
1235
1389
|
// for that wallet to represent that state in a proper way.
|
|
1236
|
-
delete
|
|
1390
|
+
delete wallet.mainUtxoHash;
|
|
1237
1391
|
}
|
|
1238
1392
|
|
|
1239
|
-
|
|
1240
|
-
.totalBurnableValue;
|
|
1393
|
+
wallet.pendingRedemptionsValue -= outputsInfo.totalBurnableValue;
|
|
1241
1394
|
|
|
1242
1395
|
emit RedemptionsCompleted(walletPubKeyHash, redemptionTxHash);
|
|
1243
1396
|
|
|
@@ -1263,7 +1416,9 @@ contract Bridge is Ownable {
|
|
|
1263
1416
|
bytes20 walletPubKeyHash
|
|
1264
1417
|
) internal view {
|
|
1265
1418
|
// Assert that main UTXO for passed wallet exists in storage.
|
|
1266
|
-
bytes32 mainUtxoHash =
|
|
1419
|
+
bytes32 mainUtxoHash = wallets
|
|
1420
|
+
.registeredWallets[walletPubKeyHash]
|
|
1421
|
+
.mainUtxoHash;
|
|
1267
1422
|
require(mainUtxoHash != bytes32(0), "No main UTXO for given wallet");
|
|
1268
1423
|
|
|
1269
1424
|
// Assert that passed main UTXO parameter is the same as in storage and
|
|
@@ -1526,7 +1681,7 @@ contract Bridge is Ownable {
|
|
|
1526
1681
|
// by copying the entire `RedemptionRequest` struct there. No need
|
|
1527
1682
|
// to check if `timedOutRedemptions` mapping already contains
|
|
1528
1683
|
// that key because `requestRedemption` blocks requests targeting
|
|
1529
|
-
// non-
|
|
1684
|
+
// non-live wallets. Because `notifyRedemptionTimeout` changes
|
|
1530
1685
|
// wallet state after first call (point 9), there is no possibility
|
|
1531
1686
|
// that the given redemption key could be reported as timed out
|
|
1532
1687
|
// multiple times. At the same time, if the given redemption key
|
|
@@ -1537,83 +1692,6 @@ contract Bridge is Ownable {
|
|
|
1537
1692
|
// 7. Reduce the `pendingRedemptionsValue` (`wallets` mapping) for
|
|
1538
1693
|
// given wallet by request's redeemable amount computed as
|
|
1539
1694
|
// `requestedAmount - treasuryFee`.
|
|
1540
|
-
// 8.
|
|
1541
|
-
//
|
|
1542
|
-
// order to prevent against new redemption requests hitting
|
|
1543
|
-
// that wallet.
|
|
1544
|
-
// 10. Expect the wallet to transfer its funds to another healthy
|
|
1545
|
-
// wallet (just as in case of failed heartbeat). The wallet is
|
|
1546
|
-
// expected to finish the already queued redemption requests
|
|
1547
|
-
// before moving funds but we are not going to enforce it on-chain.
|
|
1548
|
-
|
|
1549
|
-
// TODO: Function `submitRedemptionFraudProof`
|
|
1550
|
-
//
|
|
1551
|
-
// Deposit and redemption fraud proofs are challenging to implement
|
|
1552
|
-
// and it may happen we will have to rely on the coverage pool
|
|
1553
|
-
// (https://github.com/keep-network/coverage-pools) and DAO to
|
|
1554
|
-
// reimburse unlucky depositors and bring back the balance to the
|
|
1555
|
-
// system in case of a wallet fraud by liquidating a part of the
|
|
1556
|
-
// coverage pool manually.
|
|
1557
|
-
//
|
|
1558
|
-
// The probability of 51-of-100 wallet being fraudulent is negligible:
|
|
1559
|
-
// https://github.com/keep-network/tbtc-v2/blob/main/docs/rfc/rfc-2.adoc#111-group-size-and-threshold
|
|
1560
|
-
// and the coverage pool would be there to bring the balance back in
|
|
1561
|
-
// case we are unlucky and malicious wallet emerges.
|
|
1562
|
-
//
|
|
1563
|
-
// We do not want to slash for a misbehavior that is not provable
|
|
1564
|
-
// on-chain and it is possible to construct such a Bitcoin transaction
|
|
1565
|
-
// that is not provable on Ethereum, see
|
|
1566
|
-
// https://consensys.net/diligence/blog/2020/05/tbtc-navigating-the-cross-chain-conundrum
|
|
1567
|
-
//
|
|
1568
|
-
// The algorithm described below assumes we will be able to prove the
|
|
1569
|
-
// TX on Ethereum which may not always be the case. Consider the steps
|
|
1570
|
-
// below as an idea, and not necessarily how this function will be
|
|
1571
|
-
// implemented because it may happen this function will never be
|
|
1572
|
-
// implemented, given the Bitcoin transaction size problems.
|
|
1573
|
-
//
|
|
1574
|
-
// The algorithm:
|
|
1575
|
-
// 1. Take a `BitcoinTx.Info` and `BitcoinTx.Proof` of the
|
|
1576
|
-
// fraudulent transaction. It should also accept `walletPubKeyHash`
|
|
1577
|
-
// and index of fraudulent output. Probably index of fraudulent
|
|
1578
|
-
// input will be also required if the transaction is supposed
|
|
1579
|
-
// to have a bad input vector.
|
|
1580
|
-
// 2. Perform SPV proof to make sure it occurred on Bitcoin chain.
|
|
1581
|
-
// If not - revert.
|
|
1582
|
-
// 3. Check if wallet state is Active or MovingFunds. If not, revert.
|
|
1583
|
-
// 4. Validate the number of inputs. If there is one input and it
|
|
1584
|
-
// points to the wallet's main UTXO - move to point 5. If there
|
|
1585
|
-
// are multiple inputs and there is wallet's main UTXO in the set,
|
|
1586
|
-
// check if this is a sweep transaction. If it's not a sweep,
|
|
1587
|
-
// consider it as fraudulent and move to point 6.
|
|
1588
|
-
// In all other cases revert the call.
|
|
1589
|
-
// 5. Extract the output and analyze its type. The output is not
|
|
1590
|
-
// a fraud and the call should be reverted ONLY IF one of the
|
|
1591
|
-
// following conditions is true:
|
|
1592
|
-
// - Output is a requested redemption held by `pendingRedemptions`
|
|
1593
|
-
// and output value fulfills the request range. There is an
|
|
1594
|
-
// open question if a misfunded request should be removed
|
|
1595
|
-
// from `pendingRedemptions` (probably yes) and whether the
|
|
1596
|
-
// redeemer should be reimbursed in case of an underfund.
|
|
1597
|
-
// - Output is a timed out redemption held by `timedOutRedemptions`
|
|
1598
|
-
// and output value fulfills the request range.
|
|
1599
|
-
// - Output is a proper change i.e. a single output targeting
|
|
1600
|
-
// the wallet PKH back and having a non-zero value.
|
|
1601
|
-
// - Wallet is in MovingFunds state, the output points to the
|
|
1602
|
-
// expected target wallet, have non-zero value, and is a single
|
|
1603
|
-
// output in the vector.
|
|
1604
|
-
// In all other cases consider the transaction as fraud and
|
|
1605
|
-
// proceed to point 6.
|
|
1606
|
-
// 6. Punish the wallet, probably by severely slashing its operators.
|
|
1607
|
-
// 7. Change wallet's state in `wallets` mapping to `Terminated` in
|
|
1608
|
-
// order to prevent against new redemption requests hitting
|
|
1609
|
-
// that wallet. This also prevents against reporting a fraud
|
|
1610
|
-
// multiple times for one transaction (see point 3) and blocks
|
|
1611
|
-
// submission of sweep and redemption proofs. `Terminated` wallet
|
|
1612
|
-
// is blocked in the Bridge forever. If the fraud was a mistake
|
|
1613
|
-
// done by the wallet and the wallet is still honest deep in its
|
|
1614
|
-
// heart, the wallet can coordinate off-chain to recover the BTC
|
|
1615
|
-
// and donate it to another wallet. If they recover all of the
|
|
1616
|
-
// remaining BTC, DAO might decide to reward them with tokens so
|
|
1617
|
-
// that they can have at least some portion of their slashed
|
|
1618
|
-
// tokens back.
|
|
1695
|
+
// 8. Call `wallets.notifyRedemptionTimedOut` to propagate timeout
|
|
1696
|
+
// consequences to the wallet.
|
|
1619
1697
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
pragma solidity ^0.8.9;
|
|
2
|
+
|
|
3
|
+
import "@keep-network/bitcoin-spv-sol/contracts/BytesLib.sol";
|
|
4
|
+
|
|
5
|
+
library EcdsaLib {
|
|
6
|
+
using BytesLib for bytes;
|
|
7
|
+
|
|
8
|
+
/// @notice Converts public key X and Y coordinates (32-byte each) to a
|
|
9
|
+
/// compressed public key (33-byte). Compressed public key is X
|
|
10
|
+
/// coordinate prefixed with `02` or `03` based on the Y coordinate parity.
|
|
11
|
+
/// It is expected that the uncompressed public key is stripped
|
|
12
|
+
/// (i.e. it is not prefixed with `04`).
|
|
13
|
+
/// @param x Wallet's public key's X coordinate.
|
|
14
|
+
/// @param y Wallet's public key's Y coordinate.
|
|
15
|
+
/// @return Compressed public key (33-byte), prefixed with `02` or `03`.
|
|
16
|
+
function compressPublicKey(bytes32 x, bytes32 y)
|
|
17
|
+
internal
|
|
18
|
+
pure
|
|
19
|
+
returns (bytes memory)
|
|
20
|
+
{
|
|
21
|
+
bytes1 prefix;
|
|
22
|
+
if (uint256(y) % 2 == 0) {
|
|
23
|
+
prefix = hex"02";
|
|
24
|
+
} else {
|
|
25
|
+
prefix = hex"03";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return bytes.concat(prefix, x);
|
|
29
|
+
}
|
|
30
|
+
}
|