@keep-network/tbtc-v2 0.1.1-dev.10 → 0.1.1-dev.11

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.
@@ -19,9 +19,25 @@ 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 {
23
+ ValidateSPV
24
+ } from "@keep-network/bitcoin-spv-sol/contracts/ValidateSPV.sol";
22
25
 
26
+ import "../bank/Bank.sol";
23
27
  import "./BitcoinTx.sol";
24
28
 
29
+ /// @title Interface for the Bitcoin relay
30
+ /// @notice Contains only the methods needed by tBTC v2. The Bitcoin relay
31
+ /// provides the difficulty of the previous and current epoch. One
32
+ /// difficulty epoch spans 2016 blocks.
33
+ interface IRelay {
34
+ /// @notice Returns the difficulty of the current epoch.
35
+ function getCurrentEpochDifficulty() external view returns (uint256);
36
+
37
+ /// @notice Returns the difficulty of the previous epoch.
38
+ function getPrevEpochDifficulty() external view returns (uint256);
39
+ }
40
+
25
41
  /// @title Bitcoin Bridge
26
42
  /// @notice Bridge manages BTC deposit and redemption flow and is increasing and
27
43
  /// decreasing balances in the Bank as a result of BTC deposit and
@@ -43,13 +59,16 @@ import "./BitcoinTx.sol";
43
59
  /// @dev Bridge is an upgradeable component of the Bank.
44
60
  contract Bridge is Ownable {
45
61
  using BTCUtils for bytes;
62
+ using BTCUtils for uint256;
46
63
  using BytesLib for bytes;
64
+ using ValidateSPV for bytes;
65
+ using ValidateSPV for bytes32;
47
66
 
48
67
  /// @notice Represents data which must be revealed by the depositor during
49
68
  /// deposit reveal.
50
69
  struct RevealInfo {
51
70
  // Index of the funding output belonging to the funding transaction.
52
- uint8 fundingOutputIndex;
71
+ uint32 fundingOutputIndex;
53
72
  // Ethereum depositor address.
54
73
  address depositor;
55
74
  // The blinding factor as 8 bytes. Byte endianness doesn't matter
@@ -77,16 +96,31 @@ contract Bridge is Ownable {
77
96
  struct DepositInfo {
78
97
  // Ethereum depositor address.
79
98
  address depositor;
80
- // Deposit amount in satoshi (8-byte LE). For example:
81
- // 0.0001 BTC = 10000 satoshi = 0x1027000000000000
82
- bytes8 amount;
99
+ // Deposit amount in satoshi.
100
+ uint64 amount;
83
101
  // UNIX timestamp the deposit was revealed at.
84
102
  uint32 revealedAt;
85
103
  // Address of the Bank vault the deposit is routed to.
86
104
  // Optional, can be 0x0.
87
105
  address vault;
106
+ // UNIX timestamp the deposit was swept at. Note this is not the
107
+ // time when the deposit was swept on the Bitcoin chain but actually
108
+ // the time when the sweep proof was delivered to the Ethereum chain.
109
+ uint32 sweptAt;
88
110
  }
89
111
 
112
+ /// @notice The number of confirmations on the Bitcoin chain required to
113
+ /// successfully evaluate an SPV proof.
114
+ uint256 public immutable txProofDifficultyFactor;
115
+
116
+ // TODO: Revisit whether it should be updatable or not.
117
+ /// @notice Address of the Bank this Bridge belongs to.
118
+ Bank public immutable bank;
119
+
120
+ /// TODO: Make it updatable.
121
+ /// @notice Handle to the Bitcoin relay.
122
+ IRelay public immutable relay;
123
+
90
124
  /// @notice Indicates if the vault with the given address is trusted or not.
91
125
  /// Depositors can route their revealed deposits only to trusted
92
126
  /// vaults and have trusted vaults notified about new deposits as
@@ -96,20 +130,27 @@ contract Bridge is Ownable {
96
130
  /// address.
97
131
  mapping(address => bool) public isVaultTrusted;
98
132
 
99
- /// @notice Collection of all unswept deposits indexed by
133
+ /// @notice Collection of all revealed deposits indexed by
100
134
  /// keccak256(fundingTxHash | fundingOutputIndex).
101
- /// The fundingTxHash is LE bytes32 and fundingOutputIndex an uint8.
135
+ /// The fundingTxHash is LE bytes32 and fundingOutputIndex an uint32.
102
136
  /// This mapping may contain valid and invalid deposits and the
103
137
  /// wallet is responsible for validating them before attempting to
104
138
  /// execute a sweep.
105
- ///
106
- /// TODO: Explore the possibility of storing just a hash of DepositInfo.
107
- mapping(uint256 => DepositInfo) public unswept;
139
+ mapping(uint256 => DepositInfo) public deposits;
140
+
141
+ /// @notice Maps the wallet public key hash (computed using HASH160 opcode)
142
+ /// to the latest wallet's main UTXO computed as
143
+ /// keccak256(txHash | txOutputIndex | txOutputValue). The `tx`
144
+ /// prefix refers to the transaction which created that main UTXO.
145
+ mapping(bytes20 => bytes32) public mainUtxos;
146
+
147
+ event VaultStatusUpdated(address indexed vault, bool isTrusted);
108
148
 
109
149
  event DepositRevealed(
110
150
  bytes32 fundingTxHash,
111
- uint8 fundingOutputIndex,
151
+ uint32 fundingOutputIndex,
112
152
  address depositor,
153
+ uint64 amount,
113
154
  bytes8 blindingFactor,
114
155
  bytes20 walletPubKeyHash,
115
156
  bytes20 refundPubKeyHash,
@@ -117,7 +158,21 @@ contract Bridge is Ownable {
117
158
  address vault
118
159
  );
119
160
 
120
- event VaultStatusUpdated(address indexed vault, bool isTrusted);
161
+ event DepositsSwept(bytes20 walletPubKeyHash, bytes32 sweepTxHash);
162
+
163
+ constructor(
164
+ address _bank,
165
+ address _relay,
166
+ uint256 _txProofDifficultyFactor
167
+ ) {
168
+ require(_bank != address(0), "Bank address cannot be zero");
169
+ bank = Bank(_bank);
170
+
171
+ require(_relay != address(0), "Relay address cannot be zero");
172
+ relay = IRelay(_relay);
173
+
174
+ txProofDifficultyFactor = _txProofDifficultyFactor;
175
+ }
121
176
 
122
177
  /// @notice Allows the Governance to mark the given vault address as trusted
123
178
  /// or no longer trusted. Vaults are not trusted by default.
@@ -255,7 +310,7 @@ contract Bridge is Ownable {
255
310
  .hash256();
256
311
 
257
312
  DepositInfo storage deposit =
258
- unswept[
313
+ deposits[
259
314
  uint256(
260
315
  keccak256(
261
316
  abi.encodePacked(
@@ -267,15 +322,9 @@ contract Bridge is Ownable {
267
322
  ];
268
323
  require(deposit.revealedAt == 0, "Deposit already revealed");
269
324
 
270
- bytes8 fundingOutputAmount;
271
- /* solhint-disable-next-line no-inline-assembly */
272
- assembly {
273
- // First 8 bytes (little-endian) of the funding output represents
274
- // its value. To take the value, we need to jump over the first
275
- // word determining the array length, load the array, and trim it
276
- // by putting it to a bytes8.
277
- fundingOutputAmount := mload(add(fundingOutput, 32))
278
- }
325
+ uint64 fundingOutputAmount = fundingOutput.extractValue();
326
+
327
+ // TODO: Check the amount against the dust threshold.
279
328
 
280
329
  deposit.amount = fundingOutputAmount;
281
330
  deposit.depositor = reveal.depositor;
@@ -287,6 +336,7 @@ contract Bridge is Ownable {
287
336
  fundingTxHash,
288
337
  reveal.fundingOutputIndex,
289
338
  reveal.depositor,
339
+ fundingOutputAmount,
290
340
  reveal.blindingFactor,
291
341
  reveal.walletPubKeyHash,
292
342
  reveal.refundPubKeyHash,
@@ -306,35 +356,443 @@ contract Bridge is Ownable {
306
356
  /// during the reveal transaction, minus their fee share.
307
357
  ///
308
358
  /// It is possible to prove the given sweep only one time.
309
- /// @param sweepTx Bitcoin sweep transaction data.
310
- /// @param merkleProof The merkle proof of transaction inclusion in a block.
311
- /// @param txIndexInBlock Transaction index in the block (0-indexed).
312
- /// @param bitcoinHeaders Single bytestring of 80-byte bitcoin headers,
313
- /// lowest height first.
314
- function sweep(
359
+ /// @param sweepTx Bitcoin sweep transaction data
360
+ /// @param sweepProof Bitcoin sweep proof data
361
+ /// @param mainUtxo Data of the wallet's main UTXO, as currently known on
362
+ /// the Ethereum chain. If no main UTXO exists for the given wallet,
363
+ /// this parameter is ignored
364
+ /// @dev Requirements:
365
+ /// - `sweepTx` components must match the expected structure. See
366
+ /// `BitcoinTx.Info` docs for reference. Their values must exactly
367
+ /// correspond to appropriate Bitcoin transaction fields to produce
368
+ /// a provable transaction hash.
369
+ /// - The `sweepTx` should represent a Bitcoin transaction with 1..n
370
+ /// inputs. If the wallet has no main UTXO, all n inputs should
371
+ /// correspond to P2(W)SH revealed deposits UTXOs. If the wallet has
372
+ /// an existing main UTXO, one of the n inputs must point to that
373
+ /// main UTXO and remaining n-1 inputs should correspond to P2(W)SH
374
+ /// revealed deposits UTXOs. That transaction must have only
375
+ /// one P2(W)PKH output locking funds on the 20-byte wallet public
376
+ /// key hash.
377
+ /// - `sweepProof` components must match the expected structure. See
378
+ /// `BitcoinTx.Proof` docs for reference. The `bitcoinHeaders`
379
+ /// field must contain a valid number of block headers, not less
380
+ /// than the `txProofDifficultyFactor` contract constant.
381
+ /// - `mainUtxo` components must point to the recent main UTXO
382
+ /// of the given wallet, as currently known on the Ethereum chain.
383
+ /// If there is no main UTXO, this parameter is ignored.
384
+ function submitSweepProof(
315
385
  BitcoinTx.Info calldata sweepTx,
316
- bytes memory merkleProof,
317
- uint256 txIndexInBlock,
318
- bytes memory bitcoinHeaders
386
+ BitcoinTx.Proof calldata sweepProof,
387
+ BitcoinTx.UTXO calldata mainUtxo
319
388
  ) external {
320
- // TODO We need to read `fundingTxHash`, `fundingOutputIndex` from
321
- // `sweepTx.inputVector`. We then hash them to obtain deposit
322
- // identifier and read DepositInfo. From DepositInfo we know what
323
- // amount was inferred during deposit reveal transaction and we
324
- // use that amount to update their Bank balance, minus fee.
389
+ // TODO: Fail early if the function call gets frontrunned. See discussion:
390
+ // https://github.com/keep-network/tbtc-v2/pull/106#discussion_r801745204
391
+
392
+ // The actual transaction proof is performed here. After that point, we
393
+ // can assume the transaction happened on Bitcoin chain and has
394
+ // a sufficient number of confirmations as determined by
395
+ // `txProofDifficultyFactor` constant.
396
+ bytes32 sweepTxHash = validateSweepTxProof(sweepTx, sweepProof);
397
+
398
+ // Process sweep transaction output and extract its target wallet
399
+ // public key hash and value.
400
+ (bytes20 walletPubKeyHash, uint64 sweepTxOutputValue) =
401
+ processSweepTxOutput(sweepTx.outputVector);
402
+
403
+ // TODO: Validate if `walletPubKeyHash` is a known and active wallet.
404
+
405
+ // Check if the main UTXO for given wallet exists. If so, validate
406
+ // passed main UTXO data against the stored hash and use them for
407
+ // further processing. If no main UTXO exists, use empty data.
408
+ BitcoinTx.UTXO memory resolvedMainUtxo =
409
+ BitcoinTx.UTXO(bytes32(0), 0, 0);
410
+ bytes32 mainUtxoHash = mainUtxos[walletPubKeyHash];
411
+ if (mainUtxoHash != bytes32(0)) {
412
+ require(
413
+ keccak256(
414
+ abi.encodePacked(
415
+ mainUtxo.txHash,
416
+ mainUtxo.txOutputIndex,
417
+ mainUtxo.txOutputValue
418
+ )
419
+ ) == mainUtxoHash,
420
+ "Invalid main UTXO data"
421
+ );
422
+ resolvedMainUtxo = mainUtxo;
423
+ }
424
+
425
+ // Process sweep transaction inputs and extract their value sum and
426
+ // all information needed to perform deposit bookkeeping.
427
+ (
428
+ uint256 sweepTxInputsValue,
429
+ address[] memory depositors,
430
+ uint256[] memory depositedAmounts
431
+ ) = processSweepTxInputs(sweepTx.inputVector, resolvedMainUtxo);
432
+
433
+ // Compute the sweep transaction fee which is a difference between
434
+ // inputs amounts sum and the output amount.
435
+ // TODO: Check fee against max fee.
436
+ uint256 fee = sweepTxInputsValue - sweepTxOutputValue;
437
+ // Calculate fee share by dividing the total fee by deposits count.
438
+ // TODO: Deal with precision loss by having the last depositor pay
439
+ // the higher fee than others if there is a change, just like it has
440
+ // been proposed for the redemption flow. See:
441
+ // https://github.com/keep-network/tbtc-v2/pull/128#discussion_r800555359.
442
+ uint256 feeShare = fee / depositedAmounts.length;
443
+ // Reduce each deposit amount by fee share value.
444
+ for (uint256 i = 0; i < depositedAmounts.length; i++) {
445
+ // We don't have to check if `feeShare` is bigger than the amount
446
+ // since we have the dust threshold preventing against too small
447
+ // deposits amounts.
448
+ depositedAmounts[i] -= feeShare;
449
+ }
450
+
451
+ // Record this sweep data and assign them to the wallet public key hash
452
+ // as new main UTXO. Transaction output index is always 0 as sweep
453
+ // transaction always contains only one output.
454
+ mainUtxos[walletPubKeyHash] = keccak256(
455
+ abi.encodePacked(sweepTxHash, uint32(0), sweepTxOutputValue)
456
+ );
457
+
458
+ emit DepositsSwept(walletPubKeyHash, sweepTxHash);
459
+
460
+ // Update depositors balances in the Bank.
461
+ bank.increaseBalances(depositors, depositedAmounts);
462
+
463
+ // TODO: Handle deposits having `vault` set.
464
+ }
465
+
466
+ /// @notice Validates the SPV proof of the Bitcoin sweep transaction.
467
+ /// Reverts in case the validation or proof verification fail.
468
+ /// @param sweepTx Bitcoin sweep transaction data
469
+ /// @param sweepProof Bitcoin sweep proof data
470
+ /// @return sweepTxHash Proven 32-byte sweep transaction hash.
471
+ function validateSweepTxProof(
472
+ BitcoinTx.Info calldata sweepTx,
473
+ BitcoinTx.Proof calldata sweepProof
474
+ ) internal view returns (bytes32 sweepTxHash) {
475
+ require(
476
+ sweepTx.inputVector.validateVin(),
477
+ "Invalid input vector provided"
478
+ );
479
+ require(
480
+ sweepTx.outputVector.validateVout(),
481
+ "Invalid output vector provided"
482
+ );
483
+
484
+ sweepTxHash = abi
485
+ .encodePacked(
486
+ sweepTx
487
+ .version,
488
+ sweepTx
489
+ .inputVector,
490
+ sweepTx
491
+ .outputVector,
492
+ sweepTx
493
+ .locktime
494
+ )
495
+ .hash256();
496
+
497
+ checkProofFromTxHash(sweepTxHash, sweepProof);
498
+
499
+ return sweepTxHash;
500
+ }
501
+
502
+ /// @notice Checks the given Bitcoin transaction hash against the SPV proof.
503
+ /// Reverts in case the check fails.
504
+ /// @param txHash 32-byte hash of the checked Bitcoin transaction
505
+ /// @param proof Bitcoin proof data
506
+ function checkProofFromTxHash(
507
+ bytes32 txHash,
508
+ BitcoinTx.Proof calldata proof
509
+ ) internal view {
510
+ require(
511
+ txHash.prove(
512
+ proof.bitcoinHeaders.extractMerkleRootLE(),
513
+ proof.merkleProof,
514
+ proof.txIndexInBlock
515
+ ),
516
+ "Tx merkle proof is not valid for provided header and tx hash"
517
+ );
518
+
519
+ evaluateProofDifficulty(proof.bitcoinHeaders);
520
+ }
521
+
522
+ /// @notice Evaluates the given Bitcoin proof difficulty against the actual
523
+ /// Bitcoin chain difficulty provided by the relay oracle.
524
+ /// Reverts in case the evaluation fails.
525
+ /// @param bitcoinHeaders Bitcoin headers chain being part of the SPV
526
+ /// proof. Used to extract the observed proof difficulty
527
+ function evaluateProofDifficulty(bytes memory bitcoinHeaders)
528
+ internal
529
+ view
530
+ {
531
+ uint256 requestedDiff = 0;
532
+ uint256 currentDiff = relay.getCurrentEpochDifficulty();
533
+ uint256 previousDiff = relay.getPrevEpochDifficulty();
534
+ uint256 firstHeaderDiff =
535
+ bitcoinHeaders.extractTarget().calculateDifficulty();
536
+
537
+ if (firstHeaderDiff == currentDiff) {
538
+ requestedDiff = currentDiff;
539
+ } else if (firstHeaderDiff == previousDiff) {
540
+ requestedDiff = previousDiff;
541
+ } else {
542
+ revert("Not at current or previous difficulty");
543
+ }
544
+
545
+ uint256 observedDiff = bitcoinHeaders.validateHeaderChain();
546
+
547
+ require(
548
+ observedDiff != ValidateSPV.getErrBadLength(),
549
+ "Invalid length of the headers chain"
550
+ );
551
+ require(
552
+ observedDiff != ValidateSPV.getErrInvalidChain(),
553
+ "Invalid headers chain"
554
+ );
555
+ require(
556
+ observedDiff != ValidateSPV.getErrLowWork(),
557
+ "Insufficient work in a header"
558
+ );
559
+
560
+ require(
561
+ observedDiff >= requestedDiff * txProofDifficultyFactor,
562
+ "Insufficient accumulated difficulty in header chain"
563
+ );
564
+ }
565
+
566
+ /// @notice Processes the Bitcoin sweep transaction output vector by
567
+ /// extracting the single output and using it to gain additional
568
+ /// information required for further processing (e.g. value and
569
+ /// wallet public key hash).
570
+ /// @param sweepTxOutputVector Bitcoin sweep transaction output vector.
571
+ /// This function assumes vector's structure is valid so it must be
572
+ /// validated using e.g. `BTCUtils.validateVout` function before
573
+ /// it is passed here
574
+ /// @return walletPubKeyHash 20-byte wallet public key hash.
575
+ /// @return value 8-byte sweep transaction output value.
576
+ function processSweepTxOutput(bytes memory sweepTxOutputVector)
577
+ internal
578
+ pure
579
+ returns (bytes20 walletPubKeyHash, uint64 value)
580
+ {
581
+ // To determine the total number of sweep transaction outputs, we need to
582
+ // parse the compactSize uint (VarInt) the output vector is prepended by.
583
+ // That compactSize uint encodes the number of vector elements using the
584
+ // format presented in:
585
+ // https://developer.bitcoin.org/reference/transactions.html#compactsize-unsigned-integers
586
+ // We don't need asserting the compactSize uint is parseable since it
587
+ // was already checked during `validateVout` validation.
588
+ // See `BitcoinTx.outputVector` docs for more details.
589
+ (, uint256 outputsCount) = sweepTxOutputVector.parseVarInt();
590
+ require(
591
+ outputsCount == 1,
592
+ "Sweep transaction must have a single output"
593
+ );
594
+
595
+ bytes memory output = sweepTxOutputVector.extractOutputAtIndex(0);
596
+ value = output.extractValue();
597
+ bytes memory walletPubKeyHashBytes = output.extractHash();
598
+ // The sweep transaction output should always be P2PKH or P2WPKH.
599
+ // In both cases, the wallet public key hash should be 20 bytes length.
600
+ require(
601
+ walletPubKeyHashBytes.length == 20,
602
+ "Wallet public key hash should have 20 bytes"
603
+ );
604
+ /* solhint-disable-next-line no-inline-assembly */
605
+ assembly {
606
+ walletPubKeyHash := mload(add(walletPubKeyHashBytes, 32))
607
+ }
608
+
609
+ return (walletPubKeyHash, value);
610
+ }
611
+
612
+ /// @notice Processes the Bitcoin sweep transaction input vector. It
613
+ /// extracts each input and tries to obtain associated deposit or
614
+ /// main UTXO data, depending on the input type. Reverts
615
+ /// if one of the inputs cannot be recognized as a pointer to a
616
+ /// revealed deposit or expected main UTXO.
617
+ /// This function also marks each processed deposit as swept.
618
+ /// @param sweepTxInputVector Bitcoin sweep transaction input vector.
619
+ /// This function assumes vector's structure is valid so it must be
620
+ /// validated using e.g. `BTCUtils.validateVin` function before
621
+ /// it is passed here
622
+ /// @param mainUtxo Data of the wallet's main UTXO. If no main UTXO
623
+ /// exists for the given the wallet, this parameter's fields should
624
+ /// be zeroed to bypass the main UTXO validation
625
+ /// @return inputsTotalValue Sum of all inputs values i.e. all deposits and
626
+ /// main UTXO value, if present.
627
+ /// @return depositors Addresses of depositors who performed processed
628
+ /// deposits. Ordered in the same order as deposits inputs in the
629
+ /// input vector. Size of this array is either equal to the
630
+ /// number of inputs (main UTXO doesn't exist) or less by one
631
+ /// (main UTXO exists and is pointed by one of the inputs).
632
+ /// @return depositedAmounts Amounts of deposits corresponding to processed
633
+ /// deposits. Ordered in the same order as deposits inputs in the
634
+ /// input vector. Size of this array is either equal to the
635
+ /// number of inputs (main UTXO doesn't exist) or less by one
636
+ /// (main UTXO exists and is pointed by one of the inputs).
637
+ function processSweepTxInputs(
638
+ bytes memory sweepTxInputVector,
639
+ BitcoinTx.UTXO memory mainUtxo
640
+ )
641
+ internal
642
+ returns (
643
+ uint256 inputsTotalValue,
644
+ address[] memory depositors,
645
+ uint256[] memory depositedAmounts
646
+ )
647
+ {
648
+ // If the passed `mainUtxo` parameter's values are zeroed, the main UTXO
649
+ // for the given wallet doesn't exist and it is not expected to be
650
+ // included in the sweep transaction input vector.
651
+ bool mainUtxoExpected = mainUtxo.txHash != bytes32(0);
652
+ bool mainUtxoFound = false;
653
+
654
+ // Determining the total number of sweep transaction inputs in the same
655
+ // way as for number of outputs. See `BitcoinTx.inputVector` docs for
656
+ // more details.
657
+ (uint256 inputsCompactSizeUintLength, uint256 inputsCount) =
658
+ sweepTxInputVector.parseVarInt();
659
+
660
+ // To determine the first input starting index, we must jump over
661
+ // the compactSize uint which prepends the input vector. One byte
662
+ // must be added because `BtcUtils.parseVarInt` does not include
663
+ // compactSize uint tag in the returned length.
325
664
  //
326
- // TODO We need to validate if the sum in the output minus the
327
- // amount from the previous wallet balance input minus fees is
328
- // equal to the amount by which Bank balances were increased.
665
+ // For >= 0 && <= 252, `BTCUtils.determineVarIntDataLengthAt`
666
+ // returns `0`, so we jump over one byte of compactSize uint.
329
667
  //
330
- // TODO We need to validate `sweepTx.outputVector` to see if the balance
331
- // was not transferred away from the wallet before increasing
332
- // balances in the bank.
668
+ // For >= 253 && <= 0xffff there is `0xfd` tag,
669
+ // `BTCUtils.determineVarIntDataLengthAt` returns `2` (no
670
+ // tag byte included) so we need to jump over 1+2 bytes of
671
+ // compactSize uint.
333
672
  //
334
- // TODO Delete deposit from unswept mapping or mark it as swept
335
- // depending on the gas costs. Alternatively, do not allow to
336
- // use the same TX input vector twice. Sweep should be provable
337
- // only one time.
673
+ // Please refer `BTCUtils` library and compactSize uint
674
+ // docs in `BitcoinTx` library for more details.
675
+ uint256 inputStartingIndex = 1 + inputsCompactSizeUintLength;
676
+
677
+ // Determine the swept deposits count. If main UTXO is NOT expected,
678
+ // all inputs should be deposits. If main UTXO is expected, one input
679
+ // should point to that main UTXO.
680
+ depositors = new address[](
681
+ !mainUtxoExpected ? inputsCount : inputsCount - 1
682
+ );
683
+ depositedAmounts = new uint256[](depositors.length);
684
+
685
+ // Initialize helper variables.
686
+ uint256 processedDepositsCount = 0;
687
+
688
+ // Inputs processing loop.
689
+ for (uint256 i = 0; i < inputsCount; i++) {
690
+ // Check if we are at the end of the input vector.
691
+ if (inputStartingIndex >= sweepTxInputVector.length) {
692
+ break;
693
+ }
694
+
695
+ (bytes32 inputTxHash, uint32 inputTxIndex, uint256 inputLength) =
696
+ parseTxInputAt(sweepTxInputVector, inputStartingIndex);
697
+
698
+ DepositInfo storage deposit =
699
+ deposits[
700
+ uint256(
701
+ keccak256(abi.encodePacked(inputTxHash, inputTxIndex))
702
+ )
703
+ ];
704
+
705
+ if (deposit.revealedAt != 0) {
706
+ // If we entered here, that means the input was identified as
707
+ // a revealed deposit.
708
+ require(deposit.sweptAt == 0, "Deposit already swept");
709
+
710
+ if (processedDepositsCount == depositors.length) {
711
+ // If this condition is true, that means a deposit input
712
+ // took place of an expected main UTXO input.
713
+ // In other words, there is no expected main UTXO
714
+ // input and all inputs come from valid, revealed deposits.
715
+ revert(
716
+ "Expected main UTXO not present in sweep transaction inputs"
717
+ );
718
+ }
719
+
720
+ /* solhint-disable-next-line not-rely-on-time */
721
+ deposit.sweptAt = uint32(block.timestamp);
722
+
723
+ depositors[processedDepositsCount] = deposit.depositor;
724
+ depositedAmounts[processedDepositsCount] = deposit.amount;
725
+ inputsTotalValue += depositedAmounts[processedDepositsCount];
726
+
727
+ processedDepositsCount++;
728
+ } else if (
729
+ mainUtxoExpected != mainUtxoFound &&
730
+ mainUtxo.txHash == inputTxHash
731
+ ) {
732
+ // If we entered here, that means the input was identified as
733
+ // the expected main UTXO.
734
+ inputsTotalValue += mainUtxo.txOutputValue;
735
+ mainUtxoFound = true;
736
+ } else {
737
+ revert("Unknown input type");
738
+ }
739
+
740
+ // Make the `inputStartingIndex` pointing to the next input by
741
+ // increasing it by current input's length.
742
+ inputStartingIndex += inputLength;
743
+ }
744
+
745
+ // Construction of the input processing loop guarantees that:
746
+ // `processedDepositsCount == depositors.length == depositedAmounts.length`
747
+ // is always true at this point. We just use the first variable
748
+ // to assert the total count of swept deposit is bigger than zero.
749
+ require(
750
+ processedDepositsCount > 0,
751
+ "Sweep transaction must process at least one deposit"
752
+ );
753
+
754
+ // Assert the main UTXO was used as one of current sweep's inputs if
755
+ // it was actually expected.
756
+ require(
757
+ mainUtxoExpected == mainUtxoFound,
758
+ "Expected main UTXO not present in sweep transaction inputs"
759
+ );
760
+
761
+ return (inputsTotalValue, depositors, depositedAmounts);
762
+ }
763
+
764
+ /// @notice Parses a Bitcoin transaction input starting at the given index.
765
+ /// @param inputVector Bitcoin transaction input vector
766
+ /// @param inputStartingIndex Index the given input starts at
767
+ /// @return inputTxHash 32-byte hash of the Bitcoin transaction which is
768
+ /// pointed in the given input's outpoint.
769
+ /// @return inputTxIndex 4-byte index of the Bitcoin transaction output
770
+ /// which is pointed in the given input's outpoint.
771
+ /// @return inputLength Byte length of the given input.
772
+ /// @dev This function assumes vector's structure is valid so it must be
773
+ /// validated using e.g. `BTCUtils.validateVin` function before it
774
+ /// is passed here.
775
+ function parseTxInputAt(
776
+ bytes memory inputVector,
777
+ uint256 inputStartingIndex
778
+ )
779
+ internal
780
+ pure
781
+ returns (
782
+ bytes32 inputTxHash,
783
+ uint32 inputTxIndex,
784
+ uint256 inputLength
785
+ )
786
+ {
787
+ inputTxHash = inputVector.extractInputTxIdLeAt(inputStartingIndex);
788
+
789
+ inputTxIndex = BTCUtils.reverseUint32(
790
+ uint32(inputVector.extractTxIndexLeAt(inputStartingIndex))
791
+ );
792
+
793
+ inputLength = inputVector.determineInputLengthAt(inputStartingIndex);
794
+
795
+ return (inputTxHash, inputTxIndex, inputLength);
338
796
  }
339
797
 
340
798
  // TODO It is possible a malicious wallet can sweep deposits that can not
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@keep-network/tbtc-v2",
3
- "version": "0.1.1-dev.10+main.198a7e0affad5d74fd0efcc355ff03b27afabbbe",
3
+ "version": "0.1.1-dev.11+main.af5309c1c7457e9f1989cea4dc8b3df15077028f",
4
4
  "license": "MIT",
5
5
  "files": [
6
6
  "artifacts/",
@@ -26,7 +26,7 @@
26
26
  "prepublishOnly": "./scripts/prepare-artifacts.sh --network $npm_config_network"
27
27
  },
28
28
  "dependencies": {
29
- "@keep-network/bitcoin-spv-sol": "3.1.0-solc-0.8",
29
+ "@keep-network/bitcoin-spv-sol": "3.2.0-solc-0.8",
30
30
  "@keep-network/tbtc": ">1.1.2-dev <1.1.2-pre",
31
31
  "@openzeppelin/contracts": "^4.1.0",
32
32
  "@tenderly/hardhat-tenderly": "^1.0.12",