@keep-network/tbtc-v2 0.1.1-dev.3 → 0.1.1-dev.30

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.
Files changed (42) hide show
  1. package/README.adoc +12 -0
  2. package/artifacts/TBTC.json +19 -18
  3. package/artifacts/TBTCToken.json +19 -18
  4. package/artifacts/VendingMachine.json +20 -19
  5. package/artifacts/solcInputs/087a7a27c8cd210b7c63e594263dfed9.json +197 -0
  6. package/build/contracts/GovernanceUtils.sol/GovernanceUtils.dbg.json +1 -1
  7. package/build/contracts/GovernanceUtils.sol/GovernanceUtils.json +2 -2
  8. package/build/contracts/bank/Bank.sol/Bank.dbg.json +1 -1
  9. package/build/contracts/bank/Bank.sol/Bank.json +43 -2
  10. package/build/contracts/bridge/BitcoinTx.sol/BitcoinTx.dbg.json +4 -0
  11. package/build/contracts/bridge/BitcoinTx.sol/BitcoinTx.json +96 -0
  12. package/build/contracts/bridge/Bridge.sol/Bridge.dbg.json +1 -1
  13. package/build/contracts/bridge/Bridge.sol/Bridge.json +1598 -71
  14. package/build/contracts/bridge/Bridge.sol/IRelay.dbg.json +4 -0
  15. package/build/contracts/bridge/Bridge.sol/IRelay.json +37 -0
  16. package/build/contracts/bridge/EcdsaLib.sol/EcdsaLib.dbg.json +4 -0
  17. package/build/contracts/bridge/EcdsaLib.sol/EcdsaLib.json +10 -0
  18. package/build/contracts/bridge/Frauds.sol/Frauds.dbg.json +4 -0
  19. package/build/contracts/bridge/Frauds.sol/Frauds.json +138 -0
  20. package/build/contracts/bridge/VendingMachine.sol/VendingMachine.dbg.json +1 -1
  21. package/build/contracts/bridge/VendingMachine.sol/VendingMachine.json +2 -2
  22. package/build/contracts/bridge/Wallets.sol/Wallets.dbg.json +4 -0
  23. package/build/contracts/bridge/Wallets.sol/Wallets.json +138 -0
  24. package/build/contracts/token/TBTC.sol/TBTC.dbg.json +1 -1
  25. package/build/contracts/token/TBTC.sol/TBTC.json +2 -2
  26. package/build/contracts/vault/IVault.sol/IVault.dbg.json +4 -0
  27. package/build/contracts/vault/IVault.sol/IVault.json +47 -0
  28. package/build/contracts/vault/TBTCVault.sol/TBTCVault.dbg.json +1 -1
  29. package/build/contracts/vault/TBTCVault.sol/TBTCVault.json +38 -2
  30. package/contracts/GovernanceUtils.sol +1 -1
  31. package/contracts/bank/Bank.sol +69 -18
  32. package/contracts/bridge/BitcoinTx.sol +241 -0
  33. package/contracts/bridge/Bridge.sol +1956 -104
  34. package/contracts/bridge/EcdsaLib.sol +30 -0
  35. package/contracts/bridge/Frauds.sol +531 -0
  36. package/contracts/bridge/VendingMachine.sol +1 -1
  37. package/contracts/bridge/Wallets.sol +521 -0
  38. package/contracts/token/TBTC.sol +1 -1
  39. package/contracts/vault/IVault.sol +60 -0
  40. package/contracts/vault/TBTCVault.sol +50 -8
  41. package/package.json +28 -24
  42. package/artifacts/solcInputs/cebfa5efa019cb9c8c5e23e38703b883.json +0 -113
@@ -0,0 +1,521 @@
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
+ // The maximum age of a wallet in seconds, after which the wallet
45
+ // moving funds process can be requested.
46
+ uint32 maxAge;
47
+ // 20-byte wallet public key hash being reference to the currently
48
+ // active wallet. Can be unset to the zero value under certain
49
+ // circumstances.
50
+ bytes20 activeWalletPubKeyHash;
51
+ // Maps the 20-byte wallet public key hash (computed using Bitcoin
52
+ // HASH160 over the compressed ECDSA public key) to the basic wallet
53
+ // information like state and pending redemptions value.
54
+ mapping(bytes20 => Wallet) registeredWallets;
55
+ }
56
+
57
+ /// @notice Represents wallet state:
58
+ enum WalletState {
59
+ /// @dev The wallet is unknown to the Bridge.
60
+ Unknown,
61
+ /// @dev The wallet can sweep deposits and accept redemption requests.
62
+ Live,
63
+ /// @dev The wallet was deemed unhealthy and is expected to move their
64
+ /// outstanding funds to another wallet. The wallet can still
65
+ /// fulfill their pending redemption requests although new
66
+ /// redemption requests and new deposit reveals are not accepted.
67
+ MovingFunds,
68
+ /// @dev The wallet moved or redeemed all their funds and cannot
69
+ /// perform any action.
70
+ Closed,
71
+ /// @dev The wallet committed a fraud that was reported. The wallet is
72
+ /// blocked and can not perform any actions in the Bridge.
73
+ /// Off-chain coordination with the wallet operators is needed to
74
+ /// recover funds.
75
+ Terminated
76
+ }
77
+
78
+ /// @notice Holds information about a wallet.
79
+ struct Wallet {
80
+ // Identifier of a ECDSA Wallet registered in the ECDSA Wallet Registry.
81
+ bytes32 ecdsaWalletID;
82
+ // Latest wallet's main UTXO hash computed as
83
+ // keccak256(txHash | txOutputIndex | txOutputValue). The `tx` prefix
84
+ // refers to the transaction which created that main UTXO. The `txHash`
85
+ // is `bytes32` (ordered as in Bitcoin internally), `txOutputIndex`
86
+ // an `uint32`, and `txOutputValue` an `uint64` value.
87
+ bytes32 mainUtxoHash;
88
+ // The total redeemable value of pending redemption requests targeting
89
+ // that wallet.
90
+ uint64 pendingRedemptionsValue;
91
+ // UNIX timestamp the wallet was created at.
92
+ uint32 createdAt;
93
+ // UNIX timestamp indicating the moment the wallet was requested to
94
+ // move their funds.
95
+ uint32 moveFundsRequestedAt;
96
+ // Current state of the wallet.
97
+ WalletState state;
98
+ }
99
+
100
+ event WalletCreationPeriodUpdated(uint32 newCreationPeriod);
101
+
102
+ event WalletBtcBalanceRangeUpdated(
103
+ uint64 newMinBtcBalance,
104
+ uint64 newMaxBtcBalance
105
+ );
106
+
107
+ event WalletMaxAgeUpdated(uint32 newMaxAge);
108
+
109
+ event NewWalletRequested();
110
+
111
+ event NewWalletRegistered(
112
+ bytes32 indexed ecdsaWalletID,
113
+ bytes20 indexed walletPubKeyHash
114
+ );
115
+
116
+ event WalletMovingFunds(
117
+ bytes32 indexed ecdsaWalletID,
118
+ bytes20 indexed walletPubKeyHash
119
+ );
120
+
121
+ event WalletClosed(
122
+ bytes32 indexed ecdsaWalletID,
123
+ bytes20 indexed walletPubKeyHash
124
+ );
125
+
126
+ event WalletTerminated(
127
+ bytes32 indexed ecdsaWalletID,
128
+ bytes20 indexed walletPubKeyHash
129
+ );
130
+
131
+ /// @notice Initializes state invariants.
132
+ /// @param registry ECDSA Wallet Registry reference
133
+ /// @dev Requirements:
134
+ /// - ECDSA Wallet Registry address must not be initialized
135
+ function init(Data storage self, address registry) external {
136
+ require(
137
+ registry != address(0),
138
+ "ECDSA Wallet Registry address cannot be zero"
139
+ );
140
+ require(
141
+ address(self.registry) == address(0),
142
+ "ECDSA Wallet Registry address already set"
143
+ );
144
+
145
+ self.registry = EcdsaWalletRegistry(registry);
146
+ }
147
+
148
+ /// @notice Sets the wallet creation period.
149
+ /// @param creationPeriod New value of the wallet creation period
150
+ function setCreationPeriod(Data storage self, uint32 creationPeriod)
151
+ external
152
+ {
153
+ self.creationPeriod = creationPeriod;
154
+
155
+ emit WalletCreationPeriodUpdated(creationPeriod);
156
+ }
157
+
158
+ /// @notice Sets the minimum and maximum BTC balance parameters.
159
+ /// @param minBtcBalance New value of the minimum BTC balance
160
+ /// @param maxBtcBalance New value of the maximum BTC balance
161
+ /// @dev Requirements:
162
+ /// - Minimum BTC balance must be greater than zero
163
+ /// - Maximum BTC balance must be greater than minimum BTC balance
164
+ function setBtcBalanceRange(
165
+ Data storage self,
166
+ uint64 minBtcBalance,
167
+ uint64 maxBtcBalance
168
+ ) external {
169
+ require(minBtcBalance > 0, "Minimum must be greater than zero");
170
+ require(
171
+ maxBtcBalance > minBtcBalance,
172
+ "Maximum must be greater than the minimum"
173
+ );
174
+
175
+ self.minBtcBalance = minBtcBalance;
176
+ self.maxBtcBalance = maxBtcBalance;
177
+
178
+ emit WalletBtcBalanceRangeUpdated(minBtcBalance, maxBtcBalance);
179
+ }
180
+
181
+ /// @notice Sets the wallet maximum age.
182
+ /// @param maxAge New value of the wallet maximum age
183
+ function setMaxAge(Data storage self, uint32 maxAge) external {
184
+ self.maxAge = maxAge;
185
+
186
+ emit WalletMaxAgeUpdated(maxAge);
187
+ }
188
+
189
+ /// @notice Requests creation of a new wallet. This function just
190
+ /// forms a request and the creation process is performed
191
+ /// asynchronously. Outcome of that process should be delivered
192
+ /// using `registerNewWallet` function.
193
+ /// @param activeWalletMainUtxo Data of the active wallet's main UTXO, as
194
+ /// currently known on the Ethereum chain.
195
+ /// @dev Requirements:
196
+ /// - `activeWalletMainUtxo` components must point to the recent main
197
+ /// UTXO of the given active wallet, as currently known on the
198
+ /// Ethereum chain. If there is no active wallet at the moment, or
199
+ /// the active wallet has no main UTXO, this parameter can be
200
+ /// empty as it is ignored.
201
+ /// - Wallet creation must not be in progress
202
+ /// - If the active wallet is set, one of the following
203
+ /// conditions must be true:
204
+ /// - The active wallet BTC balance is above the minimum threshold
205
+ /// and the active wallet is old enough, i.e. the creation period
206
+ /// was elapsed since its creation time
207
+ /// - The active wallet BTC balance is above the maximum threshold
208
+ function requestNewWallet(
209
+ Data storage self,
210
+ BitcoinTx.UTXO calldata activeWalletMainUtxo
211
+ ) external {
212
+ require(
213
+ self.registry.getWalletCreationState() == EcdsaDkg.State.IDLE,
214
+ "Wallet creation already in progress"
215
+ );
216
+
217
+ bytes20 activeWalletPubKeyHash = self.activeWalletPubKeyHash;
218
+
219
+ // If the active wallet is set, fetch this wallet's details from
220
+ // storage to perform conditions check. The `registerNewWallet`
221
+ // function guarantees an active wallet is always one of the
222
+ // registered ones.
223
+ if (activeWalletPubKeyHash != bytes20(0)) {
224
+ uint64 activeWalletBtcBalance = getWalletBtcBalance(
225
+ self,
226
+ activeWalletPubKeyHash,
227
+ activeWalletMainUtxo
228
+ );
229
+ uint32 activeWalletCreatedAt = self
230
+ .registeredWallets[activeWalletPubKeyHash]
231
+ .createdAt;
232
+ /* solhint-disable-next-line not-rely-on-time */
233
+ bool activeWalletOldEnough = block.timestamp >=
234
+ activeWalletCreatedAt + self.creationPeriod;
235
+
236
+ require(
237
+ (activeWalletOldEnough &&
238
+ activeWalletBtcBalance >= self.minBtcBalance) ||
239
+ activeWalletBtcBalance >= self.maxBtcBalance,
240
+ "Wallet creation conditions are not met"
241
+ );
242
+ }
243
+
244
+ emit NewWalletRequested();
245
+
246
+ self.registry.requestNewWallet();
247
+ }
248
+
249
+ /// @notice Gets BTC balance for given the wallet.
250
+ /// @param walletPubKeyHash 20-byte public key hash of the wallet
251
+ /// @param walletMainUtxo Data of the wallet's main UTXO, as currently
252
+ /// known on the Ethereum chain.
253
+ /// @return walletBtcBalance Current BTC balance for the given wallet.
254
+ /// @dev Requirements:
255
+ /// - `walletMainUtxo` components must point to the recent main UTXO
256
+ /// of the given wallet, as currently known on the Ethereum chain.
257
+ /// If the wallet has no main UTXO, this parameter can be empty as it
258
+ /// is ignored.
259
+ function getWalletBtcBalance(
260
+ Data storage self,
261
+ bytes20 walletPubKeyHash,
262
+ BitcoinTx.UTXO calldata walletMainUtxo
263
+ ) internal view returns (uint64 walletBtcBalance) {
264
+ bytes32 walletMainUtxoHash = self
265
+ .registeredWallets[walletPubKeyHash]
266
+ .mainUtxoHash;
267
+
268
+ // If the wallet has a main UTXO hash set, cross-check it with the
269
+ // provided plain-text parameter and get the transaction output value
270
+ // as BTC balance. Otherwise, the BTC balance is just zero.
271
+ if (walletMainUtxoHash != bytes32(0)) {
272
+ require(
273
+ keccak256(
274
+ abi.encodePacked(
275
+ walletMainUtxo.txHash,
276
+ walletMainUtxo.txOutputIndex,
277
+ walletMainUtxo.txOutputValue
278
+ )
279
+ ) == walletMainUtxoHash,
280
+ "Invalid wallet main UTXO data"
281
+ );
282
+
283
+ walletBtcBalance = walletMainUtxo.txOutputValue;
284
+ }
285
+
286
+ return walletBtcBalance;
287
+ }
288
+
289
+ /// @notice Registers a new wallet. This function should be called
290
+ /// after the wallet creation process initiated using
291
+ /// `requestNewWallet` completes and brings the outcomes.
292
+ /// @param ecdsaWalletID Wallet's unique identifier.
293
+ /// @param publicKeyX Wallet's public key's X coordinate.
294
+ /// @param publicKeyY Wallet's public key's Y coordinate.
295
+ /// @dev Requirements:
296
+ /// - The only caller authorized to call this function is `registry`
297
+ /// - Given wallet data must not belong to an already registered wallet
298
+ function registerNewWallet(
299
+ Data storage self,
300
+ bytes32 ecdsaWalletID,
301
+ bytes32 publicKeyX,
302
+ bytes32 publicKeyY
303
+ ) external {
304
+ require(
305
+ msg.sender == address(self.registry),
306
+ "Caller is not the ECDSA Wallet Registry"
307
+ );
308
+
309
+ // Compress wallet's public key and calculate Bitcoin's hash160 of it.
310
+ bytes20 walletPubKeyHash = bytes20(
311
+ EcdsaLib.compressPublicKey(publicKeyX, publicKeyY).hash160()
312
+ );
313
+
314
+ Wallet storage wallet = self.registeredWallets[walletPubKeyHash];
315
+ require(
316
+ wallet.state == WalletState.Unknown,
317
+ "ECDSA wallet has been already registered"
318
+ );
319
+ wallet.ecdsaWalletID = ecdsaWalletID;
320
+ wallet.state = WalletState.Live;
321
+ /* solhint-disable-next-line not-rely-on-time */
322
+ wallet.createdAt = uint32(block.timestamp);
323
+
324
+ // Set the freshly created wallet as the new active wallet.
325
+ self.activeWalletPubKeyHash = walletPubKeyHash;
326
+
327
+ emit NewWalletRegistered(ecdsaWalletID, walletPubKeyHash);
328
+ }
329
+
330
+ /// @notice Handles a notification about a wallet heartbeat failure and
331
+ /// triggers the wallet moving funds process.
332
+ /// @param publicKeyX Wallet's public key's X coordinate.
333
+ /// @param publicKeyY Wallet's public key's Y coordinate.
334
+ /// @dev Requirements:
335
+ /// - The only caller authorized to call this function is `registry`
336
+ /// - Wallet must be in Live state
337
+ function notifyWalletHeartbeatFailed(
338
+ Data storage self,
339
+ bytes32 publicKeyX,
340
+ bytes32 publicKeyY
341
+ ) external {
342
+ require(
343
+ msg.sender == address(self.registry),
344
+ "Caller is not the ECDSA Wallet Registry"
345
+ );
346
+
347
+ // Compress wallet's public key and calculate Bitcoin's hash160 of it.
348
+ bytes20 walletPubKeyHash = bytes20(
349
+ EcdsaLib.compressPublicKey(publicKeyX, publicKeyY).hash160()
350
+ );
351
+
352
+ require(
353
+ self.registeredWallets[walletPubKeyHash].state == WalletState.Live,
354
+ "ECDSA wallet must be in Live state"
355
+ );
356
+
357
+ moveFunds(self, walletPubKeyHash);
358
+ }
359
+
360
+ /// @notice Handles a notification about a wallet redemption timeout
361
+ /// and requests slashing of the wallet operators. Triggers the
362
+ /// wallet moving funds process only if the wallet is still in the
363
+ /// Live state. That means multiple action timeouts can be reported
364
+ /// for the same wallet but only the first report requests the
365
+ /// wallet to move their funds.
366
+ /// @param walletPubKeyHash 20-byte public key hash of the wallet
367
+ /// @dev Requirements:
368
+ /// - The wallet must be in the `Live` or `MovingFunds` state
369
+ function notifyRedemptionTimedOut(
370
+ Data storage self,
371
+ bytes20 walletPubKeyHash
372
+ ) external {
373
+ WalletState walletState = self
374
+ .registeredWallets[walletPubKeyHash]
375
+ .state;
376
+
377
+ require(
378
+ walletState == WalletState.Live ||
379
+ walletState == WalletState.MovingFunds,
380
+ "ECDSA wallet must be in Live or MovingFunds state"
381
+ );
382
+
383
+ if (walletState == WalletState.Live) {
384
+ moveFunds(self, walletPubKeyHash);
385
+ }
386
+
387
+ // TODO: Perform slashing of wallet operators and transfer some of the
388
+ // slashed tokens to the caller of this function.
389
+ }
390
+
391
+ /// @notice Notifies that the wallet is either old enough or has too few
392
+ /// satoshis left and qualifies to be closed.
393
+ /// @param walletPubKeyHash 20-byte public key hash of the wallet
394
+ /// @param walletMainUtxo Data of the wallet's main UTXO, as currently
395
+ /// known on the Ethereum chain.
396
+ /// @dev Requirements:
397
+ /// - Wallet must not be set as the current active wallet
398
+ /// - Wallet must exceed the wallet maximum age OR the wallet BTC
399
+ /// balance must be lesser than the minimum threshold. If the latter
400
+ /// case is true, the `walletMainUtxo` components must point to the
401
+ /// recent main UTXO of the given wallet, as currently known on the
402
+ /// Ethereum chain. If the wallet has no main UTXO, this parameter
403
+ /// can be empty as it is ignored since the wallet balance is
404
+ /// assumed to be zero.
405
+ /// - Wallet must be in Live state
406
+ function notifyCloseableWallet(
407
+ Data storage self,
408
+ bytes20 walletPubKeyHash,
409
+ BitcoinTx.UTXO calldata walletMainUtxo
410
+ ) external {
411
+ require(
412
+ self.activeWalletPubKeyHash != walletPubKeyHash,
413
+ "Active wallet cannot be considered closeable"
414
+ );
415
+
416
+ Wallet storage wallet = self.registeredWallets[walletPubKeyHash];
417
+ require(
418
+ wallet.state == WalletState.Live,
419
+ "ECDSA wallet must be in Live state"
420
+ );
421
+
422
+ /* solhint-disable-next-line not-rely-on-time */
423
+ bool walletOldEnough = block.timestamp >=
424
+ wallet.createdAt + self.maxAge;
425
+
426
+ require(
427
+ walletOldEnough ||
428
+ getWalletBtcBalance(self, walletPubKeyHash, walletMainUtxo) <
429
+ self.minBtcBalance,
430
+ "Wallet needs to be old enough or have too few satoshis"
431
+ );
432
+
433
+ moveFunds(self, walletPubKeyHash);
434
+ }
435
+
436
+ /// @notice Requests a wallet to move their funds. If the wallet balance
437
+ /// is zero, the wallet is closed immediately and the ECDSA
438
+ /// registry is notified about this fact. If the move funds
439
+ /// request refers to the current active wallet, such a wallet
440
+ /// is no longer considered active and the active wallet slot
441
+ /// is unset allowing to trigger a new wallet creation immediately.
442
+ /// @param walletPubKeyHash 20-byte public key hash of the wallet
443
+ /// @dev Requirements:
444
+ /// - The caller must make sure that the wallet is in the Live state
445
+ function moveFunds(Data storage self, bytes20 walletPubKeyHash) internal {
446
+ Wallet storage wallet = self.registeredWallets[walletPubKeyHash];
447
+
448
+ if (wallet.mainUtxoHash == bytes32(0)) {
449
+ // If the wallet has no main UTXO, that means its BTC balance
450
+ // 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);
456
+ } else {
457
+ // Otherwise, initialize the moving funds process.
458
+ wallet.state = WalletState.MovingFunds;
459
+ /* solhint-disable-next-line not-rely-on-time */
460
+ wallet.moveFundsRequestedAt = uint32(block.timestamp);
461
+
462
+ emit WalletMovingFunds(wallet.ecdsaWalletID, walletPubKeyHash);
463
+ }
464
+
465
+ if (self.activeWalletPubKeyHash == walletPubKeyHash) {
466
+ // If the move funds request refers to the current active wallet,
467
+ // unset the active wallet and make the wallet creation process
468
+ // possible in order to get a new healthy active wallet.
469
+ delete self.activeWalletPubKeyHash;
470
+ }
471
+ }
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.
476
+
477
+ /// @notice Reports about a fraud committed by the given wallet. This
478
+ /// function performs slashing and wallet termination in reaction
479
+ /// to a proven fraud and it should only be called when the fraud
480
+ /// was confirmed.
481
+ /// @param walletPubKeyHash 20-byte public key hash of the wallet
482
+ /// @dev Requirements:
483
+ /// - Wallet must be in Live or MovingFunds state
484
+ function notifyFraud(Data storage self, bytes20 walletPubKeyHash) external {
485
+ // TODO: Perform slashing of wallet operators and add unit tests for that.
486
+
487
+ terminateWallet(self, walletPubKeyHash);
488
+ }
489
+
490
+ /// @notice Terminates the given wallet and notifies the ECDSA registry
491
+ /// about this fact. If the wallet termination refers to the current
492
+ /// active wallet, such a wallet is no longer considered active and
493
+ /// the active wallet slot is unset allowing to trigger a new wallet
494
+ /// creation immediately.
495
+ /// @param walletPubKeyHash 20-byte public key hash of the wallet
496
+ /// @dev Requirements:
497
+ /// - Wallet must be in Live or MovingFunds state
498
+ function terminateWallet(Data storage self, bytes20 walletPubKeyHash)
499
+ internal
500
+ {
501
+ 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
+
508
+ wallet.state = WalletState.Terminated;
509
+
510
+ emit WalletTerminated(wallet.ecdsaWalletID, walletPubKeyHash);
511
+
512
+ if (self.activeWalletPubKeyHash == walletPubKeyHash) {
513
+ // If termination refers to the current active wallet,
514
+ // unset the active wallet and make the wallet creation process
515
+ // possible in order to get a new healthy active wallet.
516
+ delete self.activeWalletPubKeyHash;
517
+ }
518
+
519
+ self.registry.closeWallet(wallet.ecdsaWalletID);
520
+ }
521
+ }
@@ -1,6 +1,6 @@
1
1
  // SPDX-License-Identifier: MIT
2
2
 
3
- pragma solidity 0.8.4;
3
+ pragma solidity ^0.8.9;
4
4
 
5
5
  import "@thesis/solidity-contracts/contracts/token/ERC20WithPermit.sol";
6
6
  import "@thesis/solidity-contracts/contracts/token/MisfundRecovery.sol";
@@ -0,0 +1,60 @@
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
+ /// @title Bank Vault interface
19
+ /// @notice `IVault` is an interface for a smart contract consuming Bank
20
+ /// balances of other contracts or externally owned accounts (EOA).
21
+ interface IVault {
22
+ /// @notice Called by the Bank in `approveBalanceAndCall` function after
23
+ /// the balance `owner` approved `amount` of their balance in the
24
+ /// Bank for the vault. This way, the depositor can approve balance
25
+ /// and call the vault to use the approved balance in a single
26
+ /// transaction.
27
+ /// @param owner Address of the Bank balance owner who approved their
28
+ /// balance to be used by the vault
29
+ /// @param amount The amount of the Bank balance approved by the owner
30
+ /// to be used by the vault
31
+ // @dev The implementation must ensure this function can only be called
32
+ /// by the Bank. The Bank does _not_ guarantee that the `amount`
33
+ /// approved by the `owner` currently exists on their balance. That is,
34
+ /// the `owner` could approve more balance than they currently have.
35
+ /// This works the same as `Bank.approve` function. The vault must
36
+ /// ensure the actual balance is checked before performing any action
37
+ /// based on it.
38
+ function receiveBalanceApproval(address owner, uint256 amount) external;
39
+
40
+ /// @notice Called by the Bank in `increaseBalanceAndCall` function after
41
+ /// increasing the balance in the Bank for the vault. It happens in
42
+ /// the same transaction in which deposits were swept by the Bridge.
43
+ /// This allows the depositor to route their deposit revealed to the
44
+ /// Bridge to the particular smart contract (vault) in the same
45
+ /// transaction in which the deposit is revealed. This way, the
46
+ /// depositor does not have to execute additional transaction after
47
+ /// the deposit gets swept by the Bridge to approve and transfer
48
+ /// their balance to the vault.
49
+ /// @param depositors Addresses of depositors whose deposits have been swept
50
+ /// @param depositedAmounts Amounts deposited by individual depositors and
51
+ /// swept
52
+ /// @dev The implementation must ensure this function can only be called
53
+ /// by the Bank. The Bank guarantees that the vault's balance was
54
+ /// increased by the sum of all deposited amounts before this function
55
+ /// is called, in the same transaction.
56
+ function receiveBalanceIncrease(
57
+ address[] calldata depositors,
58
+ uint256[] calldata depositedAmounts
59
+ ) external;
60
+ }
@@ -13,8 +13,9 @@
13
13
  // ▐████▌ ▐████▌
14
14
  // ▐████▌ ▐████▌
15
15
 
16
- pragma solidity 0.8.4;
16
+ pragma solidity ^0.8.9;
17
17
 
18
+ import "./IVault.sol";
18
19
  import "../bank/Bank.sol";
19
20
  import "../token/TBTC.sol";
20
21
 
@@ -26,7 +27,7 @@ import "../token/TBTC.sol";
26
27
  /// Bank.
27
28
  /// @dev TBTC Vault is the owner of TBTC token contract and is the only contract
28
29
  /// minting the token.
29
- contract TBTCVault {
30
+ contract TBTCVault is IVault {
30
31
  Bank public bank;
31
32
  TBTC public tbtcToken;
32
33
 
@@ -34,6 +35,11 @@ contract TBTCVault {
34
35
 
35
36
  event Redeemed(address indexed from, uint256 amount);
36
37
 
38
+ modifier onlyBank() {
39
+ require(msg.sender == address(bank), "Caller is not the Bank");
40
+ _;
41
+ }
42
+
37
43
  constructor(Bank _bank, TBTC _tbtcToken) {
38
44
  require(
39
45
  address(_bank) != address(0),
@@ -55,7 +61,47 @@ contract TBTCVault {
55
61
  /// for at least `amount`.
56
62
  /// @param amount Amount of TBTC to mint
57
63
  function mint(uint256 amount) external {
58
- _mint(msg.sender, amount);
64
+ address minter = msg.sender;
65
+ require(
66
+ bank.balanceOf(minter) >= amount,
67
+ "Amount exceeds balance in the bank"
68
+ );
69
+ _mint(minter, amount);
70
+ bank.transferBalanceFrom(minter, address(this), amount);
71
+ }
72
+
73
+ /// @notice Transfers the given `amount` of the Bank balance from the caller
74
+ /// to TBTC Vault and mints `amount` of TBTC to the caller.
75
+ /// @dev Can only be called by the Bank via `approveBalanceAndCall`.
76
+ /// @param owner The owner who approved their Bank balance
77
+ /// @param amount Amount of TBTC to mint
78
+ function receiveBalanceApproval(address owner, uint256 amount)
79
+ external
80
+ override
81
+ onlyBank
82
+ {
83
+ require(
84
+ bank.balanceOf(owner) >= amount,
85
+ "Amount exceeds balance in the bank"
86
+ );
87
+ _mint(owner, amount);
88
+ bank.transferBalanceFrom(owner, address(this), amount);
89
+ }
90
+
91
+ /// @notice Mints the same amount of TBTC as the deposited amount for each
92
+ /// depositor in the array. Can only be called by the Bank after the
93
+ /// Bridge swept deposits and Bank increased balance for the
94
+ /// vault.
95
+ /// @dev Fails if `depositors` array is empty. Expects the length of
96
+ /// `depositors` and `depositedAmounts` is the same.
97
+ function receiveBalanceIncrease(
98
+ address[] calldata depositors,
99
+ uint256[] calldata depositedAmounts
100
+ ) external override onlyBank {
101
+ require(depositors.length != 0, "No depositors specified");
102
+ for (uint256 i = 0; i < depositors.length; i++) {
103
+ _mint(depositors[i], depositedAmounts[i]);
104
+ }
59
105
  }
60
106
 
61
107
  /// @notice Burns `amount` of TBTC from the caller's account and transfers
@@ -86,13 +132,9 @@ contract TBTCVault {
86
132
  _redeem(from, amount);
87
133
  }
88
134
 
135
+ // slither-disable-next-line calls-loop
89
136
  function _mint(address minter, uint256 amount) internal {
90
- require(
91
- bank.balanceOf(minter) >= amount,
92
- "Amount exceeds balance in the bank"
93
- );
94
137
  emit Minted(minter, amount);
95
- bank.transferBalanceFrom(minter, address(this), amount);
96
138
  tbtcToken.mint(minter, amount);
97
139
  }
98
140