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

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.
@@ -0,0 +1,352 @@
1
+ // SPDX-License-Identifier: MIT
2
+
3
+ // ██████████████ ▐████▌ ██████████████
4
+ // ██████████████ ▐████▌ ██████████████
5
+ // ▐████▌ ▐████▌
6
+ // ▐████▌ ▐████▌
7
+ // ██████████████ ▐████▌ ██████████████
8
+ // ██████████████ ▐████▌ ██████████████
9
+ // ▐████▌ ▐████▌
10
+ // ▐████▌ ▐████▌
11
+ // ▐████▌ ▐████▌
12
+ // ▐████▌ ▐████▌
13
+ // ▐████▌ ▐████▌
14
+ // ▐████▌ ▐████▌
15
+
16
+ pragma solidity ^0.8.9;
17
+
18
+ import {BTCUtils} from "@keep-network/bitcoin-spv-sol/contracts/BTCUtils.sol";
19
+ import {IWalletRegistry as EcdsaWalletRegistry} from "@keep-network/ecdsa/contracts/api/IWalletRegistry.sol";
20
+ import {EcdsaDkg} from "@keep-network/ecdsa/contracts/libraries/EcdsaDkg.sol";
21
+
22
+ import "./BitcoinTx.sol";
23
+ import "./EcdsaLib.sol";
24
+
25
+ /// @title Wallet library
26
+ /// @notice Library responsible for handling integration between Bridge
27
+ /// contract and ECDSA wallets.
28
+ library Wallets {
29
+ using BTCUtils for bytes;
30
+
31
+ /// @notice Struct that groups the state managed by the library.
32
+ struct Data {
33
+ // ECDSA Wallet Registry contract handle.
34
+ EcdsaWalletRegistry registry;
35
+ // Determines how frequently a new wallet creation can be requested.
36
+ // Value in seconds.
37
+ uint32 creationPeriod;
38
+ // The minimum BTC threshold in satoshi that is used to decide about
39
+ // wallet creation or closing.
40
+ uint64 minBtcBalance;
41
+ // The maximum BTC threshold in satoshi that is used to decide about
42
+ // wallet creation.
43
+ uint64 maxBtcBalance;
44
+ // TODO: Make sure the `activeWalletPubKeyHash` is zeroed in case
45
+ // the active wallet becomes non-Live. This will be implemented
46
+ // soon, along with the code for closing wallets.
47
+ //
48
+ // 20-byte wallet public key hash being reference to the currently
49
+ // active wallet. Can be unset to the zero value under certain
50
+ // circumstances.
51
+ bytes20 activeWalletPubKeyHash;
52
+ // Maps the 20-byte wallet public key hash (computed using Bitcoin
53
+ // HASH160 over the compressed ECDSA public key) to the basic wallet
54
+ // information like state and pending redemptions value.
55
+ mapping(bytes20 => Wallet) registeredWallets;
56
+ }
57
+
58
+ /// @notice Represents wallet state:
59
+ enum WalletState {
60
+ /// @dev The wallet is unknown to the Bridge.
61
+ Unknown,
62
+ /// @dev The wallet can sweep deposits and accept redemption requests.
63
+ Live,
64
+ /// @dev The wallet was deemed unhealthy and is expected to move their
65
+ /// outstanding funds to another wallet. The wallet can still
66
+ /// fulfill their pending redemption requests although new
67
+ /// redemption requests and new deposit reveals are not accepted.
68
+ MovingFunds,
69
+ /// @dev The wallet moved or redeemed all their funds and cannot
70
+ /// perform any action.
71
+ Closed,
72
+ /// @dev The wallet committed a fraud that was reported. The wallet is
73
+ /// blocked and can not perform any actions in the Bridge.
74
+ /// Off-chain coordination with the wallet operators is needed to
75
+ /// recover funds.
76
+ Terminated
77
+ }
78
+
79
+ /// @notice Holds information about a wallet.
80
+ struct Wallet {
81
+ // Identifier of a ECDSA Wallet registered in the ECDSA Wallet Registry.
82
+ bytes32 ecdsaWalletID;
83
+ // Latest wallet's main UTXO hash computed as
84
+ // keccak256(txHash | txOutputIndex | txOutputValue). The `tx` prefix
85
+ // refers to the transaction which created that main UTXO. The `txHash`
86
+ // is `bytes32` (ordered as in Bitcoin internally), `txOutputIndex`
87
+ // an `uint32`, and `txOutputValue` an `uint64` value.
88
+ bytes32 mainUtxoHash;
89
+ // The total redeemable value of pending redemption requests targeting
90
+ // that wallet.
91
+ uint64 pendingRedemptionsValue;
92
+ // UNIX timestamp the wallet was created at.
93
+ uint32 createdAt;
94
+ // Current state of the wallet.
95
+ WalletState state;
96
+ }
97
+
98
+ event WalletCreationPeriodUpdated(uint32 newCreationPeriod);
99
+
100
+ event WalletBtcBalanceRangeUpdated(
101
+ uint64 newMinBtcBalance,
102
+ uint64 newMaxBtcBalance
103
+ );
104
+
105
+ event NewWalletRequested();
106
+
107
+ event NewWalletRegistered(
108
+ bytes32 indexed ecdsaWalletID,
109
+ bytes20 indexed walletPubKeyHash
110
+ );
111
+
112
+ event WalletTerminated(
113
+ bytes32 indexed ecdsaWalletID,
114
+ bytes20 indexed walletPubKeyHash
115
+ );
116
+
117
+ /// @notice Initializes state invariants.
118
+ /// @param registry ECDSA Wallet Registry reference
119
+ /// @dev Requirements:
120
+ /// - ECDSA Wallet Registry address must not be initialized
121
+ function init(Data storage self, address registry) external {
122
+ require(
123
+ registry != address(0),
124
+ "ECDSA Wallet Registry address cannot be zero"
125
+ );
126
+ require(
127
+ address(self.registry) == address(0),
128
+ "ECDSA Wallet Registry address already set"
129
+ );
130
+
131
+ self.registry = EcdsaWalletRegistry(registry);
132
+ }
133
+
134
+ /// @notice Sets the wallet creation period.
135
+ /// @param creationPeriod New value of the wallet creation period
136
+ function setCreationPeriod(Data storage self, uint32 creationPeriod)
137
+ external
138
+ {
139
+ self.creationPeriod = creationPeriod;
140
+
141
+ emit WalletCreationPeriodUpdated(creationPeriod);
142
+ }
143
+
144
+ /// @notice Sets the minimum and maximum BTC balance parameters.
145
+ /// @param minBtcBalance New value of the minimum BTC balance
146
+ /// @param maxBtcBalance New value of the maximum BTC balance
147
+ /// @dev Requirements:
148
+ /// - Minimum BTC balance must be greater than zero
149
+ /// - Maximum BTC balance must be greater than minimum BTC balance
150
+ function setBtcBalanceRange(
151
+ Data storage self,
152
+ uint64 minBtcBalance,
153
+ uint64 maxBtcBalance
154
+ ) external {
155
+ require(minBtcBalance > 0, "Minimum must be greater than zero");
156
+ require(
157
+ maxBtcBalance > minBtcBalance,
158
+ "Maximum must be greater than the minimum"
159
+ );
160
+
161
+ self.minBtcBalance = minBtcBalance;
162
+ self.maxBtcBalance = maxBtcBalance;
163
+
164
+ emit WalletBtcBalanceRangeUpdated(minBtcBalance, maxBtcBalance);
165
+ }
166
+
167
+ /// @notice Requests creation of a new wallet. This function just
168
+ /// forms a request and the creation process is performed
169
+ /// asynchronously. Outcome of that process should be delivered
170
+ /// using `registerNewWallet` function.
171
+ /// @param activeWalletMainUtxo Data of the active wallet's main UTXO, as
172
+ /// currently known on the Ethereum chain.
173
+ /// @dev Requirements:
174
+ /// - `activeWalletMainUtxo` components must point to the recent main
175
+ /// UTXO of the given active wallet, as currently known on the
176
+ /// Ethereum chain. If there is no active wallet at the moment, or
177
+ /// the active wallet has no main UTXO, this parameter can be
178
+ /// empty as it is ignored.
179
+ /// - Wallet creation must not be in progress
180
+ /// - If the active wallet is set, one of the following
181
+ /// conditions must be true:
182
+ /// - The active wallet BTC balance is above the minimum threshold
183
+ /// and the active wallet is old enough, i.e. the creation period
184
+ /// was elapsed since its creation time
185
+ /// - The active wallet BTC balance is above the maximum threshold
186
+ function requestNewWallet(
187
+ Data storage self,
188
+ BitcoinTx.UTXO calldata activeWalletMainUtxo
189
+ ) external {
190
+ require(
191
+ self.registry.getWalletCreationState() == EcdsaDkg.State.IDLE,
192
+ "Wallet creation already in progress"
193
+ );
194
+
195
+ bytes20 activeWalletPubKeyHash = self.activeWalletPubKeyHash;
196
+
197
+ // If the active wallet is set, fetch this wallet's details from
198
+ // storage to perform conditions check. The `registerNewWallet`
199
+ // function guarantees an active wallet is always one of the
200
+ // registered ones.
201
+ if (activeWalletPubKeyHash != bytes20(0)) {
202
+ uint64 activeWalletBtcBalance = getWalletBtcBalance(
203
+ self,
204
+ activeWalletPubKeyHash,
205
+ activeWalletMainUtxo
206
+ );
207
+ uint32 activeWalletCreatedAt = self
208
+ .registeredWallets[activeWalletPubKeyHash]
209
+ .createdAt;
210
+ /* solhint-disable-next-line not-rely-on-time */
211
+ bool activeWalletOldEnough = block.timestamp >=
212
+ activeWalletCreatedAt + self.creationPeriod;
213
+
214
+ require(
215
+ (activeWalletOldEnough &&
216
+ activeWalletBtcBalance >= self.minBtcBalance) ||
217
+ activeWalletBtcBalance >= self.maxBtcBalance,
218
+ "Wallet creation conditions are not met"
219
+ );
220
+ }
221
+
222
+ emit NewWalletRequested();
223
+
224
+ self.registry.requestNewWallet();
225
+ }
226
+
227
+ /// @notice Gets BTC balance for given the wallet.
228
+ /// @param walletPubKeyHash 20-byte public key hash of the wallet
229
+ /// @param walletMainUtxo Data of the wallet's main UTXO, as currently
230
+ /// known on the Ethereum chain.
231
+ /// @return walletBtcBalance Current BTC balance for the given wallet.
232
+ /// @dev Requirements:
233
+ /// - `walletMainUtxo` components must point to the recent main UTXO
234
+ /// of the given wallet, as currently known on the Ethereum chain.
235
+ /// If the wallet has no main UTXO, this parameter can be empty as it
236
+ /// is ignored.
237
+ function getWalletBtcBalance(
238
+ Data storage self,
239
+ bytes20 walletPubKeyHash,
240
+ BitcoinTx.UTXO calldata walletMainUtxo
241
+ ) internal view returns (uint64 walletBtcBalance) {
242
+ bytes32 walletMainUtxoHash = self
243
+ .registeredWallets[walletPubKeyHash]
244
+ .mainUtxoHash;
245
+
246
+ // If the wallet has a main UTXO hash set, cross-check it with the
247
+ // provided plain-text parameter and get the transaction output value
248
+ // as BTC balance. Otherwise, the BTC balance is just zero.
249
+ if (walletMainUtxoHash != bytes32(0)) {
250
+ require(
251
+ keccak256(
252
+ abi.encodePacked(
253
+ walletMainUtxo.txHash,
254
+ walletMainUtxo.txOutputIndex,
255
+ walletMainUtxo.txOutputValue
256
+ )
257
+ ) == walletMainUtxoHash,
258
+ "Invalid wallet main UTXO data"
259
+ );
260
+
261
+ walletBtcBalance = walletMainUtxo.txOutputValue;
262
+ }
263
+
264
+ return walletBtcBalance;
265
+ }
266
+
267
+ /// @notice Registers a new wallet. This function should be called
268
+ /// after the wallet creation process initiated using
269
+ /// `requestNewWallet` completes and brings the outcomes.
270
+ /// @param ecdsaWalletID Wallet's unique identifier.
271
+ /// @param publicKeyX Wallet's public key's X coordinate.
272
+ /// @param publicKeyY Wallet's public key's Y coordinate.
273
+ /// @dev Requirements:
274
+ /// - The only caller authorized to call this function is `registry`
275
+ /// - Given wallet data must not belong to an already registered wallet
276
+ function registerNewWallet(
277
+ Data storage self,
278
+ bytes32 ecdsaWalletID,
279
+ bytes32 publicKeyX,
280
+ bytes32 publicKeyY
281
+ ) external {
282
+ require(
283
+ msg.sender == address(self.registry),
284
+ "Caller is not the ECDSA Wallet Registry"
285
+ );
286
+
287
+ // Compress wallet's public key and calculate Bitcoin's hash160 of it.
288
+ bytes20 walletPubKeyHash = bytes20(
289
+ EcdsaLib.compressPublicKey(publicKeyX, publicKeyY).hash160()
290
+ );
291
+
292
+ Wallet storage wallet = self.registeredWallets[walletPubKeyHash];
293
+ require(
294
+ wallet.state == WalletState.Unknown,
295
+ "ECDSA wallet has been already registered"
296
+ );
297
+ wallet.ecdsaWalletID = ecdsaWalletID;
298
+ wallet.state = WalletState.Live;
299
+ /* solhint-disable-next-line not-rely-on-time */
300
+ wallet.createdAt = uint32(block.timestamp);
301
+
302
+ // Set the freshly created wallet as the new active wallet.
303
+ self.activeWalletPubKeyHash = walletPubKeyHash;
304
+
305
+ emit NewWalletRegistered(ecdsaWalletID, walletPubKeyHash);
306
+ }
307
+
308
+ /// @notice Reports about a fraud committed by the given wallet. This
309
+ /// function performs slashing and wallet termination in reaction
310
+ /// to a proven fraud and it should only be called when the fraud
311
+ /// was confirmed.
312
+ /// @param walletPubKeyHash 20-byte public key hash of the wallet
313
+ /// @dev Requirements:
314
+ /// - Wallet must be in Live or MovingFunds state
315
+ function notifyFraud(Data storage self, bytes20 walletPubKeyHash) external {
316
+ // TODO: Perform slashing of wallet operators and add unit tests for that.
317
+
318
+ terminateWallet(self, walletPubKeyHash);
319
+ }
320
+
321
+ /// @notice Terminates the given wallet and notifies the ECDSA registry
322
+ /// about this fact. If the wallet termination refers to the current
323
+ /// active wallet, such a wallet is no longer considered active and
324
+ /// the active wallet slot is unset allowing to trigger a new wallet
325
+ /// creation immediately.
326
+ /// @param walletPubKeyHash 20-byte public key hash of the wallet
327
+ /// @dev Requirements:
328
+ /// - Wallet must be in Live or MovingFunds state
329
+ function terminateWallet(Data storage self, bytes20 walletPubKeyHash)
330
+ internal
331
+ {
332
+ Wallet storage wallet = self.registeredWallets[walletPubKeyHash];
333
+ require(
334
+ wallet.state == WalletState.Live ||
335
+ wallet.state == WalletState.MovingFunds,
336
+ "ECDSA wallet must be in Live or MovingFunds state"
337
+ );
338
+
339
+ wallet.state = WalletState.Terminated;
340
+
341
+ emit WalletTerminated(wallet.ecdsaWalletID, walletPubKeyHash);
342
+
343
+ if (self.activeWalletPubKeyHash == walletPubKeyHash) {
344
+ // If termination refers to the current active wallet,
345
+ // unset the active wallet and make the wallet creation process
346
+ // possible in order to get a new healthy active wallet.
347
+ delete self.activeWalletPubKeyHash;
348
+ }
349
+
350
+ self.registry.closeWallet(walletPubKeyHash);
351
+ }
352
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@keep-network/tbtc-v2",
3
- "version": "0.1.1-dev.23+main.853bc8ff0601c82bb02cfaa0c904ba98d7d92fe5",
3
+ "version": "0.1.1-dev.24+main.50617ecfbbb2c15796c627b2acc6daabc61da166",
4
4
  "license": "MIT",
5
5
  "files": [
6
6
  "artifacts/",
@@ -27,39 +27,42 @@
27
27
  },
28
28
  "dependencies": {
29
29
  "@keep-network/bitcoin-spv-sol": "3.3.0-solc-0.8",
30
+ "@keep-network/ecdsa": "2.0.0-dev.4",
30
31
  "@keep-network/tbtc": ">1.1.2-dev <1.1.2-pre",
31
32
  "@openzeppelin/contracts": "^4.1.0",
32
33
  "@tenderly/hardhat-tenderly": "^1.0.12",
33
34
  "@thesis/solidity-contracts": "github:thesis/solidity-contracts#4985bcf"
34
35
  },
35
36
  "devDependencies": {
36
- "@keep-network/hardhat-helpers": "0.4.1-pre.1",
37
- "@keep-network/hardhat-local-networks-config": "^0.1.0-pre.0",
38
- "@nomiclabs/hardhat-ethers": "^2.0.2",
37
+ "@defi-wonderland/smock": "^2.0.7",
38
+ "@keep-network/hardhat-helpers": "^0.4.1-pre.2",
39
+ "@keep-network/hardhat-local-networks-config": "^0.1.0-pre.4",
40
+ "@nomiclabs/hardhat-ethers": "npm:hardhat-deploy-ethers",
39
41
  "@nomiclabs/hardhat-etherscan": "^2.1.4",
40
- "@nomiclabs/hardhat-waffle": "^2.0.1",
42
+ "@nomiclabs/hardhat-waffle": "^2.0.2",
41
43
  "@thesis-co/eslint-config": "github:thesis/eslint-config",
42
- "@typechain/ethers-v5": "^7.2.0",
43
- "@typechain/hardhat": "^2.3.1",
44
- "@types/chai": "^4.2.22",
45
- "@types/mocha": "^9.0.0",
46
- "@types/node": "^16.10.5",
44
+ "@typechain/ethers-v5": "^8.0.5",
45
+ "@typechain/hardhat": "^4.0.0",
46
+ "@types/chai": "^4.3.0",
47
+ "@types/mocha": "^9.1.0",
48
+ "@types/node": "^17.0.10",
47
49
  "chai": "^4.3.4",
48
50
  "eslint": "^7.32.0",
49
51
  "ethereum-waffle": "^3.4.0",
50
- "ethers": "^5.4.7",
51
- "hardhat": "^2.6.4",
52
+ "ethers": "^5.5.3",
53
+ "hardhat": "^2.8.3",
52
54
  "hardhat-contract-sizer": "^2.5.0",
53
- "hardhat-deploy": "^0.8.11",
54
- "hardhat-gas-reporter": "^1.0.4",
55
+ "hardhat-dependency-compiler": "^1.1.2",
56
+ "hardhat-deploy": "^0.9.27",
57
+ "hardhat-gas-reporter": "^1.0.6",
55
58
  "prettier": "^2.5.1",
56
59
  "prettier-plugin-sh": "^0.8.1",
57
60
  "prettier-plugin-solidity": "^1.0.0-beta.19",
58
61
  "solhint": "^3.3.7",
59
62
  "solhint-config-keep": "github:keep-network/solhint-config-keep",
60
- "ts-node": "^10.2.1",
61
- "typechain": "^5.2.0",
62
- "typescript": "^4.4.3"
63
+ "ts-node": "^10.4.0",
64
+ "typechain": "^6.1.0",
65
+ "typescript": "^4.5.4"
63
66
  },
64
67
  "engines": {
65
68
  "node": ">= 14.0.0"