@keep-network/tbtc-v2 0.1.1-dev.31 → 0.1.1-dev.32

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.
@@ -58,6 +58,11 @@ interface IRelay {
58
58
  /// wallet informs the Bridge about the sweep increasing appropriate
59
59
  /// balances in the Bank.
60
60
  /// @dev Bridge is an upgradeable component of the Bank.
61
+ ///
62
+ /// TODO: All wallets-related operations that are currently done directly
63
+ /// by the Bridge can be probably delegated to the Wallets library.
64
+ /// Examples of such operations are main UTXO or pending redemptions
65
+ /// value updates.
61
66
  contract Bridge is Ownable, EcdsaWalletOwner {
62
67
  using BTCUtils for bytes;
63
68
  using BTCUtils for uint256;
@@ -211,6 +216,7 @@ contract Bridge is Ownable, EcdsaWalletOwner {
211
216
  /// each swept deposit being part of the given sweep
212
217
  /// transaction. If the maximum BTC transaction fee is exceeded,
213
218
  /// such transaction is considered a fraud.
219
+ /// @dev This is a per-deposit input max fee for the sweep transaction.
214
220
  uint64 public depositTxMaxFee;
215
221
 
216
222
  /// TODO: Make it governable.
@@ -236,6 +242,7 @@ contract Bridge is Ownable, EcdsaWalletOwner {
236
242
  /// each redemption request being part of the given redemption
237
243
  /// transaction. If the maximum BTC transaction fee is exceeded, such
238
244
  /// transaction is considered a fraud.
245
+ /// @dev This is a per-redemption output max fee for the redemption transaction.
239
246
  uint64 public redemptionTxMaxFee;
240
247
 
241
248
  /// TODO: Make it governable.
@@ -246,6 +253,15 @@ contract Bridge is Ownable, EcdsaWalletOwner {
246
253
  /// to the redeemer in full amount.
247
254
  uint256 public redemptionTimeout;
248
255
 
256
+ /// TODO: Make it governable.
257
+ /// @notice Maximum amount of the total BTC transaction fee that is
258
+ /// acceptable in a single moving funds transaction.
259
+ /// @dev This is a TOTAL max fee for the moving funds transaction. Note that
260
+ /// `depositTxMaxFee` is per single deposit and `redemptionTxMaxFee`
261
+ /// if per single redemption. `movingFundsTxMaxTotalFee` is a total fee
262
+ /// for the entire transaction.
263
+ uint64 public movingFundsTxMaxTotalFee;
264
+
249
265
  /// @notice Indicates if the vault with the given address is trusted or not.
250
266
  /// Depositors can route their revealed deposits only to trusted
251
267
  /// vaults and have trusted vaults notified about new deposits as
@@ -263,8 +279,6 @@ contract Bridge is Ownable, EcdsaWalletOwner {
263
279
  /// validating them before attempting to execute a sweep.
264
280
  mapping(uint256 => DepositRequest) public deposits;
265
281
 
266
- //TODO: Remember to update this map when implementing transferring funds
267
- // between wallets (insert the main UTXO that was used as the input).
268
282
  /// @notice Collection of main UTXOs that are honestly spent indexed by
269
283
  /// keccak256(fundingTxHash | fundingOutputIndex). The fundingTxHash
270
284
  /// is bytes32 (ordered as in Bitcoin internally) and
@@ -406,6 +420,11 @@ contract Bridge is Ownable, EcdsaWalletOwner {
406
420
  bytes32 sighash
407
421
  );
408
422
 
423
+ event MovingFundsCompleted(
424
+ bytes20 walletPubKeyHash,
425
+ bytes32 movingFundsTxHash
426
+ );
427
+
409
428
  constructor(
410
429
  address _bank,
411
430
  address _relay,
@@ -426,12 +445,15 @@ contract Bridge is Ownable, EcdsaWalletOwner {
426
445
 
427
446
  // TODO: Revisit initial values.
428
447
  depositDustThreshold = 1000000; // 1000000 satoshi = 0.01 BTC
429
- depositTxMaxFee = 1000; // 1000 satoshi
448
+ depositTxMaxFee = 10000; // 10000 satoshi
430
449
  depositTreasuryFeeDivisor = 2000; // 1/2000 == 5bps == 0.05% == 0.0005
431
450
  redemptionDustThreshold = 1000000; // 1000000 satoshi = 0.01 BTC
432
451
  redemptionTreasuryFeeDivisor = 2000; // 1/2000 == 5bps == 0.05% == 0.0005
433
- redemptionTxMaxFee = 1000; // 1000 satoshi
452
+ redemptionTxMaxFee = 10000; // 10000 satoshi
434
453
  redemptionTimeout = 172800; // 48 hours
454
+ movingFundsTxMaxTotalFee = 10000; // 10000 satoshi
455
+
456
+ // TODO: Revisit initial values.
435
457
  frauds.setSlashingAmount(10000 * 1e18); // 10000 T
436
458
  frauds.setNotifierRewardMultiplier(100); // 100%
437
459
  frauds.setChallengeDefeatTimeout(7 days);
@@ -1595,9 +1617,9 @@ contract Bridge is Ownable, EcdsaWalletOwner {
1595
1617
  proofDifficultyContext()
1596
1618
  );
1597
1619
 
1598
- // Perform validation of the redemption transaction input. Specifically,
1599
- // check if it refers to the expected wallet's main UTXO.
1600
- validateRedemptionTxInput(
1620
+ // Process the redemption transaction input. Specifically, check if it
1621
+ // refers to the expected wallet's main UTXO.
1622
+ processWalletOutboundTxInput(
1601
1623
  redemptionTx.inputVector,
1602
1624
  mainUtxo,
1603
1625
  walletPubKeyHash
@@ -1646,10 +1668,14 @@ contract Bridge is Ownable, EcdsaWalletOwner {
1646
1668
  bank.transferBalance(treasury, outputsInfo.totalTreasuryFee);
1647
1669
  }
1648
1670
 
1649
- /// @notice Validates whether the redemption Bitcoin transaction input
1650
- /// vector contains a single input referring to the wallet's main
1651
- /// UTXO. Reverts in case the validation fails.
1652
- /// @param redemptionTxInputVector Bitcoin redemption transaction input
1671
+ /// @notice Checks whether an outbound Bitcoin transaction performed from
1672
+ /// the given wallet has an input vector that contains a single
1673
+ /// input referring to the wallet's main UTXO. Marks that main UTXO
1674
+ /// as correctly spent if the validation succeeds. Reverts otherwise.
1675
+ /// There are two outbound transactions from a wallet possible: a
1676
+ /// redemption transaction or a moving funds to another wallet
1677
+ /// transaction.
1678
+ /// @param walletOutboundTxInputVector Bitcoin outbound transaction's input
1653
1679
  /// vector. This function assumes vector's structure is valid so it
1654
1680
  /// must be validated using e.g. `BTCUtils.validateVin` function
1655
1681
  /// before it is passed here
@@ -1657,9 +1683,9 @@ contract Bridge is Ownable, EcdsaWalletOwner {
1657
1683
  /// the Ethereum chain.
1658
1684
  /// @param walletPubKeyHash 20-byte public key hash (computed using Bitcoin
1659
1685
  // HASH160 over the compressed ECDSA public key) of the wallet which
1660
- /// performed the redemption transaction.
1661
- function validateRedemptionTxInput(
1662
- bytes memory redemptionTxInputVector,
1686
+ /// performed the outbound transaction.
1687
+ function processWalletOutboundTxInput(
1688
+ bytes memory walletOutboundTxInputVector,
1663
1689
  BitcoinTx.UTXO calldata mainUtxo,
1664
1690
  bytes20 walletPubKeyHash
1665
1691
  ) internal {
@@ -1682,16 +1708,16 @@ contract Bridge is Ownable, EcdsaWalletOwner {
1682
1708
  "Invalid main UTXO data"
1683
1709
  );
1684
1710
 
1685
- // Assert that the single redemption transaction input actually
1711
+ // Assert that the single outbound transaction input actually
1686
1712
  // refers to the wallet's main UTXO.
1687
1713
  (
1688
- bytes32 redemptionTxOutpointTxHash,
1689
- uint32 redemptionTxOutpointIndex
1690
- ) = processRedemptionTxInput(redemptionTxInputVector);
1714
+ bytes32 outpointTxHash,
1715
+ uint32 outpointIndex
1716
+ ) = parseWalletOutboundTxInput(walletOutboundTxInputVector);
1691
1717
  require(
1692
- mainUtxo.txHash == redemptionTxOutpointTxHash &&
1693
- mainUtxo.txOutputIndex == redemptionTxOutpointIndex,
1694
- "Redemption transaction input must point to the wallet's main UTXO"
1718
+ mainUtxo.txHash == outpointTxHash &&
1719
+ mainUtxo.txOutputIndex == outpointIndex,
1720
+ "Outbound transaction input must point to the wallet's main UTXO"
1695
1721
  );
1696
1722
 
1697
1723
  // Main UTXO used as an input, mark it as spent.
@@ -1704,10 +1730,13 @@ contract Bridge is Ownable, EcdsaWalletOwner {
1704
1730
  ] = true;
1705
1731
  }
1706
1732
 
1707
- /// @notice Processes the Bitcoin redemption transaction input vector. It
1708
- /// extracts the single input then the transaction hash and output
1709
- /// index from its outpoint.
1710
- /// @param redemptionTxInputVector Bitcoin redemption transaction input
1733
+ /// @notice Parses the input vector of an outbound Bitcoin transaction
1734
+ /// performed from the given wallet. It extracts the single input
1735
+ /// then the transaction hash and output index from its outpoint.
1736
+ /// There are two outbound transactions from a wallet possible: a
1737
+ /// redemption transaction or a moving funds to another wallet
1738
+ /// transaction.
1739
+ /// @param walletOutboundTxInputVector Bitcoin outbound transaction input
1711
1740
  /// vector. This function assumes vector's structure is valid so it
1712
1741
  /// must be validated using e.g. `BTCUtils.validateVin` function
1713
1742
  /// before it is passed here
@@ -1715,12 +1744,10 @@ contract Bridge is Ownable, EcdsaWalletOwner {
1715
1744
  /// pointed in the input's outpoint.
1716
1745
  /// @return outpointIndex 4-byte index of the Bitcoin transaction output
1717
1746
  /// which is pointed in the input's outpoint.
1718
- function processRedemptionTxInput(bytes memory redemptionTxInputVector)
1719
- internal
1720
- pure
1721
- returns (bytes32 outpointTxHash, uint32 outpointIndex)
1722
- {
1723
- // To determine the total number of redemption transaction inputs,
1747
+ function parseWalletOutboundTxInput(
1748
+ bytes memory walletOutboundTxInputVector
1749
+ ) internal pure returns (bytes32 outpointTxHash, uint32 outpointIndex) {
1750
+ // To determine the total number of Bitcoin transaction inputs,
1724
1751
  // we need to parse the compactSize uint (VarInt) the input vector is
1725
1752
  // prepended by. That compactSize uint encodes the number of vector
1726
1753
  // elements using the format presented in:
@@ -1728,13 +1755,13 @@ contract Bridge is Ownable, EcdsaWalletOwner {
1728
1755
  // We don't need asserting the compactSize uint is parseable since it
1729
1756
  // was already checked during `validateVin` validation.
1730
1757
  // See `BitcoinTx.inputVector` docs for more details.
1731
- (, uint256 inputsCount) = redemptionTxInputVector.parseVarInt();
1758
+ (, uint256 inputsCount) = walletOutboundTxInputVector.parseVarInt();
1732
1759
  require(
1733
1760
  inputsCount == 1,
1734
- "Redemption transaction must have a single input"
1761
+ "Outbound transaction must have a single input"
1735
1762
  );
1736
1763
 
1737
- bytes memory input = redemptionTxInputVector.extractInputAtIndex(0);
1764
+ bytes memory input = walletOutboundTxInputVector.extractInputAtIndex(0);
1738
1765
 
1739
1766
  outpointTxHash = input.extractInputTxIdLE();
1740
1767
 
@@ -1797,11 +1824,26 @@ contract Bridge is Ownable, EcdsaWalletOwner {
1797
1824
  // scripts that can be used to lock the change. This is done upfront to
1798
1825
  // save on gas. Both scripts have a strict format defined by Bitcoin.
1799
1826
  //
1800
- // The P2PKH script has format <0x1976a914> <20-byte PKH> <0x88ac>.
1827
+ // The P2PKH script has the byte format: <0x1976a914> <20-byte PKH> <0x88ac>.
1828
+ // According to https://en.bitcoin.it/wiki/Script#Opcodes this translates to:
1829
+ // - 0x19: Byte length of the entire script
1830
+ // - 0x76: OP_DUP
1831
+ // - 0xa9: OP_HASH160
1832
+ // - 0x14: Byte length of the public key hash
1833
+ // - 0x88: OP_EQUALVERIFY
1834
+ // - 0xac: OP_CHECKSIG
1835
+ // which matches the P2PKH structure as per:
1836
+ // https://en.bitcoin.it/wiki/Transaction#Pay-to-PubkeyHash
1801
1837
  bytes32 walletP2PKHScriptKeccak = keccak256(
1802
1838
  abi.encodePacked(hex"1976a914", walletPubKeyHash, hex"88ac")
1803
1839
  );
1804
- // The P2WPKH script has format <0x160014> <20-byte PKH>.
1840
+ // The P2WPKH script has the byte format: <0x160014> <20-byte PKH>.
1841
+ // According to https://en.bitcoin.it/wiki/Script#Opcodes this translates to:
1842
+ // - 0x16: Byte length of the entire script
1843
+ // - 0x00: OP_0
1844
+ // - 0x14: Byte length of the public key hash
1845
+ // which matches the P2WPKH structure as per:
1846
+ // https://github.com/bitcoin/bips/blob/master/bip-0141.mediawiki#P2WPKH
1805
1847
  bytes32 walletP2WPKHScriptKeccak = keccak256(
1806
1848
  abi.encodePacked(hex"160014", walletPubKeyHash)
1807
1849
  );
@@ -1827,7 +1869,7 @@ contract Bridge is Ownable, EcdsaWalletOwner {
1827
1869
  // Extract the value from given output.
1828
1870
  uint64 outputValue = output.extractValue();
1829
1871
  // The output consists of an 8-byte value and a variable length
1830
- // script. To extract that script we slice the output staring from
1872
+ // script. To extract that script we slice the output starting from
1831
1873
  // 9th byte until the end.
1832
1874
  bytes memory outputScript = output.slice(8, output.length - 8);
1833
1875
 
@@ -2006,4 +2048,244 @@ contract Bridge is Ownable, EcdsaWalletOwner {
2006
2048
  // Return the requested amount of tokens to the redeemer
2007
2049
  bank.transferBalance(request.redeemer, request.requestedAmount);
2008
2050
  }
2051
+
2052
+ /// @notice Used by the wallet to prove the BTC moving funds transaction
2053
+ /// and to make the necessary state changes. Moving funds is only
2054
+ /// accepted if it satisfies SPV proof.
2055
+ ///
2056
+ /// The function validates the moving funds transaction structure
2057
+ /// by checking if it actually spends the main UTXO of the declared
2058
+ /// wallet and locks the value on the pre-committed target wallets
2059
+ /// using a reasonable transaction fee. If all preconditions are
2060
+ /// met, this functions closes the source wallet.
2061
+ ///
2062
+ /// It is possible to prove the given moving funds transaction only
2063
+ /// one time.
2064
+ /// @param movingFundsTx Bitcoin moving funds transaction data
2065
+ /// @param movingFundsProof Bitcoin moving funds proof data
2066
+ /// @param mainUtxo Data of the wallet's main UTXO, as currently known on
2067
+ /// the Ethereum chain
2068
+ /// @param walletPubKeyHash 20-byte public key hash (computed using Bitcoin
2069
+ /// HASH160 over the compressed ECDSA public key) of the wallet
2070
+ /// which performed the moving funds transaction
2071
+ /// @dev Requirements:
2072
+ /// - `movingFundsTx` components must match the expected structure. See
2073
+ /// `BitcoinTx.Info` docs for reference. Their values must exactly
2074
+ /// correspond to appropriate Bitcoin transaction fields to produce
2075
+ /// a provable transaction hash.
2076
+ /// - The `movingFundsTx` should represent a Bitcoin transaction with
2077
+ /// exactly 1 input that refers to the wallet's main UTXO. That
2078
+ /// transaction should have 1..n outputs corresponding to the
2079
+ /// pre-committed target wallets. Outputs must be ordered in the
2080
+ /// same way as their corresponding target wallets are ordered
2081
+ /// within the target wallets commitment.
2082
+ /// - `movingFundsProof` components must match the expected structure.
2083
+ /// See `BitcoinTx.Proof` docs for reference. The `bitcoinHeaders`
2084
+ /// field must contain a valid number of block headers, not less
2085
+ /// than the `txProofDifficultyFactor` contract constant.
2086
+ /// - `mainUtxo` components must point to the recent main UTXO
2087
+ /// of the given wallet, as currently known on the Ethereum chain.
2088
+ /// Additionally, the recent main UTXO on Ethereum must be set.
2089
+ /// - `walletPubKeyHash` must be connected with the main UTXO used
2090
+ /// as transaction single input.
2091
+ /// - The wallet that `walletPubKeyHash` points to must be in the
2092
+ /// MovingFunds state.
2093
+ /// - The target wallets commitment must be submitted by the wallet
2094
+ /// that `walletPubKeyHash` points to.
2095
+ /// - The total Bitcoin transaction fee must be lesser or equal
2096
+ /// to `movingFundsTxMaxTotalFee` governable parameter.
2097
+ function submitMovingFundsProof(
2098
+ BitcoinTx.Info calldata movingFundsTx,
2099
+ BitcoinTx.Proof calldata movingFundsProof,
2100
+ BitcoinTx.UTXO calldata mainUtxo,
2101
+ bytes20 walletPubKeyHash
2102
+ ) external {
2103
+ // The actual transaction proof is performed here. After that point, we
2104
+ // can assume the transaction happened on Bitcoin chain and has
2105
+ // a sufficient number of confirmations as determined by
2106
+ // `txProofDifficultyFactor` constant.
2107
+ bytes32 movingFundsTxHash = BitcoinTx.validateProof(
2108
+ movingFundsTx,
2109
+ movingFundsProof,
2110
+ proofDifficultyContext()
2111
+ );
2112
+
2113
+ // Process the moving funds transaction input. Specifically, check if
2114
+ // it refers to the expected wallet's main UTXO.
2115
+ processWalletOutboundTxInput(
2116
+ movingFundsTx.inputVector,
2117
+ mainUtxo,
2118
+ walletPubKeyHash
2119
+ );
2120
+
2121
+ (
2122
+ bytes32 targetWalletsHash,
2123
+ uint256 outputsTotalValue
2124
+ ) = processMovingFundsTxOutputs(movingFundsTx.outputVector);
2125
+
2126
+ require(
2127
+ mainUtxo.txOutputValue - outputsTotalValue <=
2128
+ movingFundsTxMaxTotalFee,
2129
+ "Transaction fee is too high"
2130
+ );
2131
+
2132
+ wallets.notifyFundsMoved(walletPubKeyHash, targetWalletsHash);
2133
+
2134
+ emit MovingFundsCompleted(walletPubKeyHash, movingFundsTxHash);
2135
+ }
2136
+
2137
+ /// @notice Processes the moving funds Bitcoin transaction output vector
2138
+ /// and extracts information required for further processing.
2139
+ /// @param movingFundsTxOutputVector Bitcoin moving funds transaction output
2140
+ /// vector. This function assumes vector's structure is valid so it
2141
+ /// must be validated using e.g. `BTCUtils.validateVout` function
2142
+ /// before it is passed here
2143
+ /// @return targetWalletsHash keccak256 hash over the list of actual
2144
+ /// target wallets used in the transaction.
2145
+ /// @return outputsTotalValue Sum of all outputs values.
2146
+ /// @dev Requirements:
2147
+ /// - The `movingFundsTxOutputVector` must be parseable, i.e. must
2148
+ /// be validated by the caller as stated in their parameter doc.
2149
+ /// - Each output must refer to a 20-byte public key hash.
2150
+ /// - The total outputs value must be evenly divided over all outputs.
2151
+ function processMovingFundsTxOutputs(bytes memory movingFundsTxOutputVector)
2152
+ internal
2153
+ view
2154
+ returns (bytes32 targetWalletsHash, uint256 outputsTotalValue)
2155
+ {
2156
+ // Determining the total number of Bitcoin transaction outputs in
2157
+ // the same way as for number of inputs. See `BitcoinTx.outputVector`
2158
+ // docs for more details.
2159
+ (
2160
+ uint256 outputsCompactSizeUintLength,
2161
+ uint256 outputsCount
2162
+ ) = movingFundsTxOutputVector.parseVarInt();
2163
+
2164
+ // To determine the first output starting index, we must jump over
2165
+ // the compactSize uint which prepends the output vector. One byte
2166
+ // must be added because `BtcUtils.parseVarInt` does not include
2167
+ // compactSize uint tag in the returned length.
2168
+ //
2169
+ // For >= 0 && <= 252, `BTCUtils.determineVarIntDataLengthAt`
2170
+ // returns `0`, so we jump over one byte of compactSize uint.
2171
+ //
2172
+ // For >= 253 && <= 0xffff there is `0xfd` tag,
2173
+ // `BTCUtils.determineVarIntDataLengthAt` returns `2` (no
2174
+ // tag byte included) so we need to jump over 1+2 bytes of
2175
+ // compactSize uint.
2176
+ //
2177
+ // Please refer `BTCUtils` library and compactSize uint
2178
+ // docs in `BitcoinTx` library for more details.
2179
+ uint256 outputStartingIndex = 1 + outputsCompactSizeUintLength;
2180
+
2181
+ bytes20[] memory targetWallets = new bytes20[](outputsCount);
2182
+ uint64[] memory outputsValues = new uint64[](outputsCount);
2183
+
2184
+ // Outputs processing loop.
2185
+ for (uint256 i = 0; i < outputsCount; i++) {
2186
+ uint256 outputLength = movingFundsTxOutputVector
2187
+ .determineOutputLengthAt(outputStartingIndex);
2188
+
2189
+ bytes memory output = movingFundsTxOutputVector.slice(
2190
+ outputStartingIndex,
2191
+ outputLength
2192
+ );
2193
+
2194
+ // Extract the output script payload.
2195
+ bytes memory targetWalletPubKeyHashBytes = output.extractHash();
2196
+ // Output script payload must refer to a known wallet public key
2197
+ // hash which is always 20-byte.
2198
+ require(
2199
+ targetWalletPubKeyHashBytes.length == 20,
2200
+ "Target wallet public key hash must have 20 bytes"
2201
+ );
2202
+
2203
+ bytes20 targetWalletPubKeyHash = targetWalletPubKeyHashBytes
2204
+ .slice20(0);
2205
+
2206
+ // The next step is making sure that the 20-byte public key hash
2207
+ // is actually used in the right context of a P2PKH or P2WPKH
2208
+ // output. To do so, we must extract the full script from the output
2209
+ // and compare with the expected P2PKH and P2WPKH scripts
2210
+ // referring to that 20-byte public key hash. The output consists
2211
+ // of an 8-byte value and a variable length script. To extract the
2212
+ // script we slice the output starting from 9th byte until the end.
2213
+ bytes32 outputScriptKeccak = keccak256(
2214
+ output.slice(8, output.length - 8)
2215
+ );
2216
+ // Build the expected P2PKH script which has the following byte
2217
+ // format: <0x1976a914> <20-byte PKH> <0x88ac>. According to
2218
+ // https://en.bitcoin.it/wiki/Script#Opcodes this translates to:
2219
+ // - 0x19: Byte length of the entire script
2220
+ // - 0x76: OP_DUP
2221
+ // - 0xa9: OP_HASH160
2222
+ // - 0x14: Byte length of the public key hash
2223
+ // - 0x88: OP_EQUALVERIFY
2224
+ // - 0xac: OP_CHECKSIG
2225
+ // which matches the P2PKH structure as per:
2226
+ // https://en.bitcoin.it/wiki/Transaction#Pay-to-PubkeyHash
2227
+ bytes32 targetWalletP2PKHScriptKeccak = keccak256(
2228
+ abi.encodePacked(
2229
+ hex"1976a914",
2230
+ targetWalletPubKeyHash,
2231
+ hex"88ac"
2232
+ )
2233
+ );
2234
+ // Build the expected P2WPKH script which has the following format:
2235
+ // <0x160014> <20-byte PKH>. According to
2236
+ // https://en.bitcoin.it/wiki/Script#Opcodes this translates to:
2237
+ // - 0x16: Byte length of the entire script
2238
+ // - 0x00: OP_0
2239
+ // - 0x14: Byte length of the public key hash
2240
+ // which matches the P2WPKH structure as per:
2241
+ // https://github.com/bitcoin/bips/blob/master/bip-0141.mediawiki#P2WPKH
2242
+ bytes32 targetWalletP2WPKHScriptKeccak = keccak256(
2243
+ abi.encodePacked(hex"160014", targetWalletPubKeyHash)
2244
+ );
2245
+ // Make sure the actual output script matches either the P2PKH
2246
+ // or P2WPKH format.
2247
+ require(
2248
+ outputScriptKeccak == targetWalletP2PKHScriptKeccak ||
2249
+ outputScriptKeccak == targetWalletP2WPKHScriptKeccak,
2250
+ "Output must be P2PKH or P2WPKH"
2251
+ );
2252
+
2253
+ // Add the wallet public key hash to the list that will be used
2254
+ // to build the result list hash. There is no need to check if
2255
+ // given output is a change here because the actual target wallet
2256
+ // list must be exactly the same as the pre-committed target wallet
2257
+ // list which is guaranteed to be valid.
2258
+ targetWallets[i] = targetWalletPubKeyHash;
2259
+
2260
+ // Extract the value from given output.
2261
+ outputsValues[i] = output.extractValue();
2262
+ outputsTotalValue += outputsValues[i];
2263
+
2264
+ // Make the `outputStartingIndex` pointing to the next output by
2265
+ // increasing it by current output's length.
2266
+ outputStartingIndex += outputLength;
2267
+ }
2268
+
2269
+ // Compute the indivisible remainder that remains after dividing the
2270
+ // outputs total value over all outputs evenly.
2271
+ uint256 outputsTotalValueRemainder = outputsTotalValue % outputsCount;
2272
+ // Compute the minimum allowed output value by dividing the outputs
2273
+ // total value (reduced by the remainder) by the number of outputs.
2274
+ uint256 minOutputValue = (outputsTotalValue -
2275
+ outputsTotalValueRemainder) / outputsCount;
2276
+ // Maximum possible value is the minimum value with the remainder included.
2277
+ uint256 maxOutputValue = minOutputValue + outputsTotalValueRemainder;
2278
+
2279
+ for (uint256 i = 0; i < outputsCount; i++) {
2280
+ require(
2281
+ minOutputValue <= outputsValues[i] &&
2282
+ outputsValues[i] <= maxOutputValue,
2283
+ "Transaction amount is not distributed evenly"
2284
+ );
2285
+ }
2286
+
2287
+ targetWalletsHash = keccak256(abi.encodePacked(targetWallets));
2288
+
2289
+ return (targetWalletsHash, outputsTotalValue);
2290
+ }
2009
2291
  }
@@ -92,9 +92,13 @@ library Wallets {
92
92
  uint32 createdAt;
93
93
  // UNIX timestamp indicating the moment the wallet was requested to
94
94
  // move their funds.
95
- uint32 moveFundsRequestedAt;
95
+ uint32 movingFundsRequestedAt;
96
96
  // Current state of the wallet.
97
97
  WalletState state;
98
+ // Moving funds target wallet commitment submitted by the wallet. It
99
+ // is built by applying the keccak256 hash over the list of 20-byte
100
+ // public key hashes of the target wallets.
101
+ bytes32 movingFundsTargetWalletsCommitmentHash;
98
102
  }
99
103
 
100
104
  event WalletCreationPeriodUpdated(uint32 newCreationPeriod);
@@ -308,7 +312,7 @@ library Wallets {
308
312
 
309
313
  // Compress wallet's public key and calculate Bitcoin's hash160 of it.
310
314
  bytes20 walletPubKeyHash = bytes20(
311
- EcdsaLib.compressPublicKey(publicKeyX, publicKeyY).hash160()
315
+ EcdsaLib.compressPublicKey(publicKeyX, publicKeyY).hash160View()
312
316
  );
313
317
 
314
318
  Wallet storage wallet = self.registeredWallets[walletPubKeyHash];
@@ -346,7 +350,7 @@ library Wallets {
346
350
 
347
351
  // Compress wallet's public key and calculate Bitcoin's hash160 of it.
348
352
  bytes20 walletPubKeyHash = bytes20(
349
- EcdsaLib.compressPublicKey(publicKeyX, publicKeyY).hash160()
353
+ EcdsaLib.compressPublicKey(publicKeyX, publicKeyY).hash160View()
350
354
  );
351
355
 
352
356
  require(
@@ -448,16 +452,12 @@ library Wallets {
448
452
  if (wallet.mainUtxoHash == bytes32(0)) {
449
453
  // If the wallet has no main UTXO, that means its BTC balance
450
454
  // is zero and it should be closed immediately.
451
- wallet.state = WalletState.Closed;
452
-
453
- emit WalletClosed(wallet.ecdsaWalletID, walletPubKeyHash);
454
-
455
- self.registry.closeWallet(wallet.ecdsaWalletID);
455
+ closeWallet(self, walletPubKeyHash);
456
456
  } else {
457
457
  // Otherwise, initialize the moving funds process.
458
458
  wallet.state = WalletState.MovingFunds;
459
459
  /* solhint-disable-next-line not-rely-on-time */
460
- wallet.moveFundsRequestedAt = uint32(block.timestamp);
460
+ wallet.movingFundsRequestedAt = uint32(block.timestamp);
461
461
 
462
462
  emit WalletMovingFunds(wallet.ecdsaWalletID, walletPubKeyHash);
463
463
  }
@@ -470,9 +470,21 @@ library Wallets {
470
470
  }
471
471
  }
472
472
 
473
- // TODO: Implement functions that will be called upon moving funds process
474
- // end. Remember the moving funds process ends up with a successful
475
- // proof or a timeout.
473
+ /// @notice Closes the given wallet and notifies the ECDSA registry
474
+ /// about this fact.
475
+ /// @param walletPubKeyHash 20-byte public key hash of the wallet
476
+ /// @dev Requirements:
477
+ /// - The caller must make sure that the wallet is in the
478
+ /// Live or MovingFunds state.
479
+ function closeWallet(Data storage self, bytes20 walletPubKeyHash) internal {
480
+ Wallet storage wallet = self.registeredWallets[walletPubKeyHash];
481
+
482
+ wallet.state = WalletState.Closed;
483
+
484
+ emit WalletClosed(wallet.ecdsaWalletID, walletPubKeyHash);
485
+
486
+ self.registry.closeWallet(wallet.ecdsaWalletID);
487
+ }
476
488
 
477
489
  /// @notice Reports about a fraud committed by the given wallet. This
478
490
  /// function performs slashing and wallet termination in reaction
@@ -482,9 +494,19 @@ library Wallets {
482
494
  /// @dev Requirements:
483
495
  /// - Wallet must be in Live or MovingFunds state
484
496
  function notifyFraud(Data storage self, bytes20 walletPubKeyHash) external {
485
- // TODO: Perform slashing of wallet operators and add unit tests for that.
497
+ WalletState walletState = self
498
+ .registeredWallets[walletPubKeyHash]
499
+ .state;
500
+
501
+ require(
502
+ walletState == WalletState.Live ||
503
+ walletState == WalletState.MovingFunds,
504
+ "ECDSA wallet must be in Live or MovingFunds state"
505
+ );
486
506
 
487
507
  terminateWallet(self, walletPubKeyHash);
508
+
509
+ // TODO: Perform slashing of wallet operators and add unit tests for that.
488
510
  }
489
511
 
490
512
  /// @notice Terminates the given wallet and notifies the ECDSA registry
@@ -494,16 +516,12 @@ library Wallets {
494
516
  /// creation immediately.
495
517
  /// @param walletPubKeyHash 20-byte public key hash of the wallet
496
518
  /// @dev Requirements:
497
- /// - Wallet must be in Live or MovingFunds state
519
+ /// - The caller must make sure that the wallet is in the
520
+ /// Live or MovingFunds state.
498
521
  function terminateWallet(Data storage self, bytes20 walletPubKeyHash)
499
522
  internal
500
523
  {
501
524
  Wallet storage wallet = self.registeredWallets[walletPubKeyHash];
502
- require(
503
- wallet.state == WalletState.Live ||
504
- wallet.state == WalletState.MovingFunds,
505
- "ECDSA wallet must be in Live or MovingFunds state"
506
- );
507
525
 
508
526
  wallet.state = WalletState.Terminated;
509
527
 
@@ -518,4 +536,56 @@ library Wallets {
518
536
 
519
537
  self.registry.closeWallet(wallet.ecdsaWalletID);
520
538
  }
539
+
540
+ /// @notice Notifies that the wallet completed the moving funds process
541
+ /// successfully. Checks if the funds were moved to the expected
542
+ /// target wallets. Closes the source wallet if everything went
543
+ /// good and reverts otherwise.
544
+ /// @param walletPubKeyHash 20-byte public key hash of the wallet
545
+ /// @param targetWalletsHash 32-byte keccak256 hash over the list of
546
+ /// 20-byte public key hashes of the target wallets actually used
547
+ /// within the moving funds transactions.
548
+ /// @dev Requirements:
549
+ /// - The caller must make sure the moving funds transaction actually
550
+ /// happened on Bitcoin chain and fits the protocol requirements.
551
+ /// - The source wallet must be in the MovingFunds state
552
+ /// - The target wallets commitment must be submitted by the source
553
+ /// wallet.
554
+ /// - The actual target wallets used in the moving funds transaction
555
+ /// must be exactly the same as the target wallets commitment.
556
+ function notifyFundsMoved(
557
+ Data storage self,
558
+ bytes20 walletPubKeyHash,
559
+ bytes32 targetWalletsHash
560
+ ) external {
561
+ Wallet storage wallet = self.registeredWallets[walletPubKeyHash];
562
+ // Check that the wallet is in the MovingFunds state but don't check
563
+ // if the moving funds timeout is exceeded. That should give a
564
+ // possibility to move funds in case when timeout was hit but was
565
+ // not reported yet.
566
+ require(
567
+ wallet.state == WalletState.MovingFunds,
568
+ "ECDSA wallet must be in MovingFunds state"
569
+ );
570
+
571
+ bytes32 targetWalletsCommitmentHash = wallet
572
+ .movingFundsTargetWalletsCommitmentHash;
573
+
574
+ require(
575
+ targetWalletsCommitmentHash != bytes32(0),
576
+ "Target wallets commitment not submitted yet"
577
+ );
578
+
579
+ // Make sure that the target wallets where funds were moved to are
580
+ // exactly the same as the ones the source wallet committed to.
581
+ require(
582
+ targetWalletsCommitmentHash == targetWalletsHash,
583
+ "Target wallets don't correspond to the commitment"
584
+ );
585
+
586
+ // If funds were moved, the wallet has no longer a main UTXO.
587
+ delete wallet.mainUtxoHash;
588
+
589
+ closeWallet(self, walletPubKeyHash);
590
+ }
521
591
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@keep-network/tbtc-v2",
3
- "version": "0.1.1-dev.31+main.749b9391b64c3a3fcc1eb2a1412a5f5620189e34",
3
+ "version": "0.1.1-dev.32+main.87f7a68d24fa6c31f061522279c60e7fd0860c48",
4
4
  "license": "MIT",
5
5
  "files": [
6
6
  "artifacts/",