@keep-network/tbtc-v2 0.1.1-dev.20 → 0.1.1-dev.23

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.
@@ -1,4 +1,4 @@
1
1
  {
2
2
  "_format": "hh-sol-dbg-1",
3
- "buildInfo": "../../../build-info/c6d4a581e072c3a6ede3e105233cc390.json"
3
+ "buildInfo": "../../../build-info/7c0ac1415929deeaa3e0f2f0bf2db7d9.json"
4
4
  }
@@ -1,4 +1,4 @@
1
1
  {
2
2
  "_format": "hh-sol-dbg-1",
3
- "buildInfo": "../../../build-info/c6d4a581e072c3a6ede3e105233cc390.json"
3
+ "buildInfo": "../../../build-info/7c0ac1415929deeaa3e0f2f0bf2db7d9.json"
4
4
  }
@@ -1,4 +1,4 @@
1
1
  {
2
2
  "_format": "hh-sol-dbg-1",
3
- "buildInfo": "../../../build-info/c6d4a581e072c3a6ede3e105233cc390.json"
3
+ "buildInfo": "../../../build-info/7c0ac1415929deeaa3e0f2f0bf2db7d9.json"
4
4
  }
@@ -1,4 +1,4 @@
1
1
  {
2
2
  "_format": "hh-sol-dbg-1",
3
- "buildInfo": "../../../build-info/c6d4a581e072c3a6ede3e105233cc390.json"
3
+ "buildInfo": "../../../build-info/7c0ac1415929deeaa3e0f2f0bf2db7d9.json"
4
4
  }
@@ -15,6 +15,9 @@
15
15
 
16
16
  pragma solidity ^0.8.9;
17
17
 
18
+ import {BTCUtils} from "@keep-network/bitcoin-spv-sol/contracts/BTCUtils.sol";
19
+ import {ValidateSPV} from "@keep-network/bitcoin-spv-sol/contracts/ValidateSPV.sol";
20
+
18
21
  /// @title Bitcoin transaction
19
22
  /// @notice Allows to reference Bitcoin raw transaction in Solidity.
20
23
  /// @dev See https://developer.bitcoin.org/reference/transactions.html#raw-transaction-format
@@ -69,8 +72,12 @@ pragma solidity ^0.8.9;
69
72
  /// (*) compactSize uint is often references as VarInt)
70
73
  ///
71
74
  library BitcoinTx {
72
- /// @notice Represents Bitcoin transaction data for funding BTC deposit
73
- /// P2(W)SH transaction.
75
+ using BTCUtils for bytes;
76
+ using BTCUtils for uint256;
77
+ using ValidateSPV for bytes;
78
+ using ValidateSPV for bytes32;
79
+
80
+ /// @notice Represents Bitcoin transaction data.
74
81
  struct Info {
75
82
  /// @notice Bitcoin transaction version
76
83
  /// @dev `version` from raw Bitcon transaction data.
@@ -113,6 +120,17 @@ library BitcoinTx {
113
120
  bytes bitcoinHeaders;
114
121
  }
115
122
 
123
+ /// @notice Determines the difficulty context for a Bitcoin SPV proof.
124
+ struct ProofDifficulty {
125
+ /// @notice Difficulty of the current epoch.
126
+ uint256 currentEpochDifficulty;
127
+ /// @notice Difficulty of the previous epoch.
128
+ uint256 previousEpochDifficulty;
129
+ /// @notice The number of confirmations on the Bitcoin chain required
130
+ /// to successfully evaluate an SPV proof.
131
+ uint256 difficultyFactor;
132
+ }
133
+
116
134
  /// @notice Represents info about an unspent transaction output.
117
135
  struct UTXO {
118
136
  /// @notice Hash of the transaction the output belongs to.
@@ -123,4 +141,91 @@ library BitcoinTx {
123
141
  /// @notice Value of the transaction output.
124
142
  uint64 txOutputValue;
125
143
  }
144
+
145
+ /// @notice Validates the SPV proof of the Bitcoin transaction.
146
+ /// Reverts in case the validation or proof verification fail.
147
+ /// @param txInfo Bitcoin transaction data
148
+ /// @param proof Bitcoin proof data
149
+ /// @param proofDifficulty Bitcoin proof difficulty context.
150
+ /// @return txHash Proven 32-byte transaction hash.
151
+ function validateProof(
152
+ Info calldata txInfo,
153
+ Proof calldata proof,
154
+ ProofDifficulty calldata proofDifficulty
155
+ ) external view returns (bytes32 txHash) {
156
+ require(
157
+ txInfo.inputVector.validateVin(),
158
+ "Invalid input vector provided"
159
+ );
160
+ require(
161
+ txInfo.outputVector.validateVout(),
162
+ "Invalid output vector provided"
163
+ );
164
+
165
+ txHash = abi
166
+ .encodePacked(
167
+ txInfo.version,
168
+ txInfo.inputVector,
169
+ txInfo.outputVector,
170
+ txInfo.locktime
171
+ )
172
+ .hash256View();
173
+
174
+ require(
175
+ txHash.prove(
176
+ proof.bitcoinHeaders.extractMerkleRootLE(),
177
+ proof.merkleProof,
178
+ proof.txIndexInBlock
179
+ ),
180
+ "Tx merkle proof is not valid for provided header and tx hash"
181
+ );
182
+
183
+ evaluateProofDifficulty(proof.bitcoinHeaders, proofDifficulty);
184
+
185
+ return txHash;
186
+ }
187
+
188
+ /// @notice Evaluates the given Bitcoin proof difficulty against the actual
189
+ /// Bitcoin chain difficulty provided by the relay oracle.
190
+ /// Reverts in case the evaluation fails.
191
+ /// @param bitcoinHeaders Bitcoin headers chain being part of the SPV
192
+ /// proof. Used to extract the observed proof difficulty
193
+ /// @param proofDifficulty Bitcoin proof difficulty context.
194
+ function evaluateProofDifficulty(
195
+ bytes memory bitcoinHeaders,
196
+ ProofDifficulty calldata proofDifficulty
197
+ ) internal view {
198
+ uint256 requestedDiff = 0;
199
+ uint256 firstHeaderDiff = bitcoinHeaders
200
+ .extractTarget()
201
+ .calculateDifficulty();
202
+
203
+ if (firstHeaderDiff == proofDifficulty.currentEpochDifficulty) {
204
+ requestedDiff = proofDifficulty.currentEpochDifficulty;
205
+ } else if (firstHeaderDiff == proofDifficulty.previousEpochDifficulty) {
206
+ requestedDiff = proofDifficulty.previousEpochDifficulty;
207
+ } else {
208
+ revert("Not at current or previous difficulty");
209
+ }
210
+
211
+ uint256 observedDiff = bitcoinHeaders.validateHeaderChain();
212
+
213
+ require(
214
+ observedDiff != ValidateSPV.getErrBadLength(),
215
+ "Invalid length of the headers chain"
216
+ );
217
+ require(
218
+ observedDiff != ValidateSPV.getErrInvalidChain(),
219
+ "Invalid headers chain"
220
+ );
221
+ require(
222
+ observedDiff != ValidateSPV.getErrLowWork(),
223
+ "Insufficient work in a header"
224
+ );
225
+
226
+ require(
227
+ observedDiff >= requestedDiff * proofDifficulty.difficultyFactor,
228
+ "Insufficient accumulated difficulty in header chain"
229
+ );
230
+ }
126
231
  }
@@ -19,7 +19,6 @@ 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 {ValidateSPV} from "@keep-network/bitcoin-spv-sol/contracts/ValidateSPV.sol";
23
22
 
24
23
  import "../bank/Bank.sol";
25
24
  import "./BitcoinTx.sol";
@@ -59,8 +58,6 @@ contract Bridge is Ownable {
59
58
  using BTCUtils for bytes;
60
59
  using BTCUtils for uint256;
61
60
  using BytesLib for bytes;
62
- using ValidateSPV for bytes;
63
- using ValidateSPV for bytes32;
64
61
 
65
62
  /// @notice Represents data which must be revealed by the depositor during
66
63
  /// deposit reveal.
@@ -215,13 +212,21 @@ contract Bridge is Ownable {
215
212
  /// Treasury takes part in the operators rewarding process.
216
213
  address public immutable treasury;
217
214
 
215
+ /// TODO: Make it governable.
216
+ /// @notice The minimal amount that can be requested for deposit.
217
+ /// Value of this parameter must take into account the value of
218
+ /// `depositTreasuryFeeDivisor` and `depositTxMaxFee`
219
+ /// parameters in order to make requests that can incur the
220
+ /// treasury and transaction fee and still satisfy the depositor.
221
+ uint64 public depositDustThreshold;
222
+
218
223
  /// TODO: Make it governable.
219
224
  /// @notice Divisor used to compute the treasury fee taken from each
220
225
  /// deposit and transferred to the treasury upon sweep proof
221
226
  /// submission. That fee is computed as follows:
222
227
  /// `treasuryFee = depositedAmount / depositTreasuryFeeDivisor`
223
228
  /// For example, if the treasury fee needs to be 2% of each deposit,
224
- /// the `redemptionTreasuryFeeDivisor` should be set to `50`
229
+ /// the `depositTreasuryFeeDivisor` should be set to `50`
225
230
  /// because `1/50 = 0.02 = 2%`.
226
231
  uint64 public depositTreasuryFeeDivisor;
227
232
 
@@ -384,6 +389,7 @@ contract Bridge is Ownable {
384
389
  txProofDifficultyFactor = _txProofDifficultyFactor;
385
390
 
386
391
  // TODO: Revisit initial values.
392
+ depositDustThreshold = 1000000; // 1000000 satoshi = 0.01 BTC
387
393
  depositTxMaxFee = 1000; // 1000 satoshi
388
394
  depositTreasuryFeeDivisor = 2000; // 1/2000 == 5bps == 0.05% == 0.0005
389
395
  redemptionDustThreshold = 1000000; // 1000000 satoshi = 0.01 BTC
@@ -413,6 +419,22 @@ contract Bridge is Ownable {
413
419
  emit VaultStatusUpdated(vault, isTrusted);
414
420
  }
415
421
 
422
+ /// @notice Determines the current Bitcoin SPV proof difficulty context.
423
+ /// @return proofDifficulty Bitcoin proof difficulty context.
424
+ function proofDifficultyContext()
425
+ internal
426
+ view
427
+ returns (BitcoinTx.ProofDifficulty memory proofDifficulty)
428
+ {
429
+ proofDifficulty.currentEpochDifficulty = relay
430
+ .getCurrentEpochDifficulty();
431
+ proofDifficulty.previousEpochDifficulty = relay
432
+ .getPrevEpochDifficulty();
433
+ proofDifficulty.difficultyFactor = txProofDifficultyFactor;
434
+
435
+ return proofDifficulty;
436
+ }
437
+
416
438
  /// @notice Used by the depositor to reveal information about their P2(W)SH
417
439
  /// Bitcoin deposit to the Bridge on Ethereum chain. The off-chain
418
440
  /// wallet listens for revealed deposit events and may decide to
@@ -536,7 +558,10 @@ contract Bridge is Ownable {
536
558
 
537
559
  uint64 fundingOutputAmount = fundingOutput.extractValue();
538
560
 
539
- // TODO: Check the amount against the dust threshold.
561
+ require(
562
+ fundingOutputAmount >= depositDustThreshold,
563
+ "Deposit amount too small"
564
+ );
540
565
 
541
566
  deposit.amount = fundingOutputAmount;
542
567
  deposit.depositor = reveal.depositor;
@@ -608,7 +633,11 @@ contract Bridge is Ownable {
608
633
  // can assume the transaction happened on Bitcoin chain and has
609
634
  // a sufficient number of confirmations as determined by
610
635
  // `txProofDifficultyFactor` constant.
611
- bytes32 sweepTxHash = validateBitcoinTxProof(sweepTx, sweepProof);
636
+ bytes32 sweepTxHash = BitcoinTx.validateProof(
637
+ sweepTx,
638
+ sweepProof,
639
+ proofDifficultyContext()
640
+ );
612
641
 
613
642
  // Process sweep transaction output and extract its target wallet
614
643
  // public key hash and value.
@@ -653,20 +682,34 @@ contract Bridge is Ownable {
653
682
  // all deposits.
654
683
  uint256 totalTreasuryFee = 0;
655
684
 
656
- // Calculate the transaction fee per deposit by dividing the total
657
- // transaction fee by deposits count. The total fee is just the
658
- // difference between inputs amounts sum and the output amount.
659
- // TODO: Deal with precision loss by having the last depositor pay
660
- // the higher transaction fee than others if there is a change,
661
- // just like it has been proposed in discussion:
662
- // https://github.com/keep-network/tbtc-v2/pull/128#discussion_r800555359.
663
- uint256 txFee = (inputsInfo.inputsTotalValue - sweepTxOutputValue) /
664
- inputsInfo.depositedAmounts.length;
685
+ // Determine the transaction fee that should be incurred by each deposit
686
+ // and the indivisible remainder that should be additionally incurred
687
+ // by the last deposit.
688
+ (
689
+ uint256 depositTxFee,
690
+ uint256 depositTxFeeRemainder
691
+ ) = sweepTxFeeDistribution(
692
+ inputsInfo.inputsTotalValue,
693
+ sweepTxOutputValue,
694
+ inputsInfo.depositedAmounts.length
695
+ );
665
696
 
666
- require(txFee <= depositTxMaxFee, "Transaction fee is too high");
697
+ // Make sure the highest value of the deposit transaction fee does not
698
+ // exceed the maximum value limited by the governable parameter.
699
+ require(
700
+ depositTxFee + depositTxFeeRemainder <= depositTxMaxFee,
701
+ "Transaction fee is too high"
702
+ );
667
703
 
668
704
  // Reduce each deposit amount by treasury fee and transaction fee.
669
705
  for (uint256 i = 0; i < inputsInfo.depositedAmounts.length; i++) {
706
+ // The last deposit should incur the deposit transaction fee
707
+ // remainder.
708
+ uint256 depositTxFeeIncurred = i ==
709
+ inputsInfo.depositedAmounts.length - 1
710
+ ? depositTxFee + depositTxFeeRemainder
711
+ : depositTxFee;
712
+
670
713
  // There is no need to check whether
671
714
  // `inputsInfo.depositedAmounts[i] - inputsInfo.treasuryFees[i] - txFee > 0`
672
715
  // since the `depositDustThreshold` should force that condition
@@ -674,7 +717,7 @@ contract Bridge is Ownable {
674
717
  inputsInfo.depositedAmounts[i] =
675
718
  inputsInfo.depositedAmounts[i] -
676
719
  inputsInfo.treasuryFees[i] -
677
- txFee;
720
+ depositTxFeeIncurred;
678
721
  totalTreasuryFee += inputsInfo.treasuryFees[i];
679
722
  }
680
723
 
@@ -698,103 +741,6 @@ contract Bridge is Ownable {
698
741
  // TODO: Handle deposits having `vault` set.
699
742
  }
700
743
 
701
- /// @notice Validates the SPV proof of the Bitcoin transaction.
702
- /// Reverts in case the validation or proof verification fail.
703
- /// @param txInfo Bitcoin transaction data
704
- /// @param proof Bitcoin proof data
705
- /// @return txHash Proven 32-byte transaction hash.
706
- function validateBitcoinTxProof(
707
- BitcoinTx.Info calldata txInfo,
708
- BitcoinTx.Proof calldata proof
709
- ) internal view returns (bytes32 txHash) {
710
- require(
711
- txInfo.inputVector.validateVin(),
712
- "Invalid input vector provided"
713
- );
714
- require(
715
- txInfo.outputVector.validateVout(),
716
- "Invalid output vector provided"
717
- );
718
-
719
- txHash = abi
720
- .encodePacked(
721
- txInfo.version,
722
- txInfo.inputVector,
723
- txInfo.outputVector,
724
- txInfo.locktime
725
- )
726
- .hash256View();
727
-
728
- checkProofFromTxHash(txHash, proof);
729
-
730
- return txHash;
731
- }
732
-
733
- /// @notice Checks the given Bitcoin transaction hash against the SPV proof.
734
- /// Reverts in case the check fails.
735
- /// @param txHash 32-byte hash of the checked Bitcoin transaction
736
- /// @param proof Bitcoin proof data
737
- function checkProofFromTxHash(
738
- bytes32 txHash,
739
- BitcoinTx.Proof calldata proof
740
- ) internal view {
741
- require(
742
- txHash.prove(
743
- proof.bitcoinHeaders.extractMerkleRootLE(),
744
- proof.merkleProof,
745
- proof.txIndexInBlock
746
- ),
747
- "Tx merkle proof is not valid for provided header and tx hash"
748
- );
749
-
750
- evaluateProofDifficulty(proof.bitcoinHeaders);
751
- }
752
-
753
- /// @notice Evaluates the given Bitcoin proof difficulty against the actual
754
- /// Bitcoin chain difficulty provided by the relay oracle.
755
- /// Reverts in case the evaluation fails.
756
- /// @param bitcoinHeaders Bitcoin headers chain being part of the SPV
757
- /// proof. Used to extract the observed proof difficulty
758
- function evaluateProofDifficulty(bytes memory bitcoinHeaders)
759
- internal
760
- view
761
- {
762
- uint256 requestedDiff = 0;
763
- uint256 currentDiff = relay.getCurrentEpochDifficulty();
764
- uint256 previousDiff = relay.getPrevEpochDifficulty();
765
- uint256 firstHeaderDiff = bitcoinHeaders
766
- .extractTarget()
767
- .calculateDifficulty();
768
-
769
- if (firstHeaderDiff == currentDiff) {
770
- requestedDiff = currentDiff;
771
- } else if (firstHeaderDiff == previousDiff) {
772
- requestedDiff = previousDiff;
773
- } else {
774
- revert("Not at current or previous difficulty");
775
- }
776
-
777
- uint256 observedDiff = bitcoinHeaders.validateHeaderChain();
778
-
779
- require(
780
- observedDiff != ValidateSPV.getErrBadLength(),
781
- "Invalid length of the headers chain"
782
- );
783
- require(
784
- observedDiff != ValidateSPV.getErrInvalidChain(),
785
- "Invalid headers chain"
786
- );
787
- require(
788
- observedDiff != ValidateSPV.getErrLowWork(),
789
- "Insufficient work in a header"
790
- );
791
-
792
- require(
793
- observedDiff >= requestedDiff * txProofDifficultyFactor,
794
- "Insufficient accumulated difficulty in header chain"
795
- );
796
- }
797
-
798
744
  /// @notice Processes the Bitcoin sweep transaction output vector by
799
745
  /// extracting the single output and using it to gain additional
800
746
  /// information required for further processing (e.g. value and
@@ -1012,6 +958,40 @@ contract Bridge is Ownable {
1012
958
  return (outpointTxHash, outpointIndex, inputLength);
1013
959
  }
1014
960
 
961
+ /// @notice Determines the distribution of the sweep transaction fee
962
+ /// over swept deposits.
963
+ /// @param sweepTxInputsTotalValue Total value of all sweep transaction inputs.
964
+ /// @param sweepTxOutputValue Value of the sweep transaction output.
965
+ /// @param depositsCount Count of the deposits swept by the sweep transaction.
966
+ /// @return depositTxFee Transaction fee per deposit determined by evenly
967
+ /// spreading the divisible part of the sweep transaction fee
968
+ /// over all deposits.
969
+ /// @return depositTxFeeRemainder The indivisible part of the sweep
970
+ /// transaction fee than cannot be distributed over all deposits.
971
+ /// @dev It is up to the caller to decide how the remainder should be
972
+ /// counted in. This function only computes its value.
973
+ function sweepTxFeeDistribution(
974
+ uint256 sweepTxInputsTotalValue,
975
+ uint256 sweepTxOutputValue,
976
+ uint256 depositsCount
977
+ )
978
+ internal
979
+ pure
980
+ returns (uint256 depositTxFee, uint256 depositTxFeeRemainder)
981
+ {
982
+ // The sweep transaction fee is just the difference between inputs
983
+ // amounts sum and the output amount.
984
+ uint256 sweepTxFee = sweepTxInputsTotalValue - sweepTxOutputValue;
985
+ // Compute the indivisible remainder that remains after dividing the
986
+ // sweep transaction fee over all deposits evenly.
987
+ depositTxFeeRemainder = sweepTxFee % depositsCount;
988
+ // Compute the transaction fee per deposit by dividing the sweep
989
+ // transaction fee (reduced by the remainder) by the number of deposits.
990
+ depositTxFee = (sweepTxFee - depositTxFeeRemainder) / depositsCount;
991
+
992
+ return (depositTxFee, depositTxFeeRemainder);
993
+ }
994
+
1015
995
  /// @notice Requests redemption of the given amount from the specified
1016
996
  /// wallet to the redeemer Bitcoin output script.
1017
997
  /// @param walletPubKeyHash The 20-byte wallet public key hash (computed
@@ -1223,9 +1203,10 @@ contract Bridge is Ownable {
1223
1203
  // can assume the transaction happened on Bitcoin chain and has
1224
1204
  // a sufficient number of confirmations as determined by
1225
1205
  // `txProofDifficultyFactor` constant.
1226
- bytes32 redemptionTxHash = validateBitcoinTxProof(
1206
+ bytes32 redemptionTxHash = BitcoinTx.validateProof(
1227
1207
  redemptionTx,
1228
- redemptionProof
1208
+ redemptionProof,
1209
+ proofDifficultyContext()
1229
1210
  );
1230
1211
 
1231
1212
  // Perform validation of the redemption transaction input. Specifically,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@keep-network/tbtc-v2",
3
- "version": "0.1.1-dev.20+main.be110a9d184eaf03e1013c9ef6ea4ad257d6b3d1",
3
+ "version": "0.1.1-dev.23+main.853bc8ff0601c82bb02cfaa0c904ba98d7d92fe5",
4
4
  "license": "MIT",
5
5
  "files": [
6
6
  "artifacts/",
@@ -49,6 +49,7 @@
49
49
  "ethereum-waffle": "^3.4.0",
50
50
  "ethers": "^5.4.7",
51
51
  "hardhat": "^2.6.4",
52
+ "hardhat-contract-sizer": "^2.5.0",
52
53
  "hardhat-deploy": "^0.8.11",
53
54
  "hardhat-gas-reporter": "^1.0.4",
54
55
  "prettier": "^2.5.1",