@keep-network/tbtc-v2 0.1.1-dev.22 → 0.1.1-dev.25
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.
- package/README.adoc +12 -0
- package/artifacts/TBTC.json +11 -10
- package/artifacts/TBTCToken.json +11 -10
- package/artifacts/VendingMachine.json +12 -11
- package/artifacts/solcInputs/7823c0ea8f2f46d2393b3380efff7ecb.json +191 -0
- package/build/contracts/GovernanceUtils.sol/GovernanceUtils.dbg.json +1 -1
- package/build/contracts/bank/Bank.sol/Bank.dbg.json +1 -1
- package/build/contracts/bridge/BitcoinTx.sol/BitcoinTx.dbg.json +1 -1
- package/build/contracts/bridge/Bridge.sol/Bridge.dbg.json +1 -1
- package/build/contracts/bridge/Bridge.sol/Bridge.json +440 -27
- package/build/contracts/bridge/Bridge.sol/IRelay.dbg.json +1 -1
- package/build/contracts/bridge/EcdsaLib.sol/EcdsaLib.dbg.json +4 -0
- package/build/contracts/bridge/EcdsaLib.sol/EcdsaLib.json +10 -0
- package/build/contracts/bridge/VendingMachine.sol/VendingMachine.dbg.json +1 -1
- package/build/contracts/bridge/Wallets.sol/Wallets.dbg.json +4 -0
- package/build/contracts/bridge/Wallets.sol/Wallets.json +138 -0
- package/build/contracts/token/TBTC.sol/TBTC.dbg.json +1 -1
- package/build/contracts/vault/IVault.sol/IVault.dbg.json +1 -1
- package/build/contracts/vault/TBTCVault.sol/TBTCVault.dbg.json +1 -1
- package/contracts/bridge/Bridge.sol +236 -158
- package/contracts/bridge/EcdsaLib.sol +30 -0
- package/contracts/bridge/Wallets.sol +520 -0
- package/package.json +20 -17
- package/artifacts/solcInputs/e2b5f983e9c69369a4f41eb2f0688e3c.json +0 -140
|
@@ -0,0 +1,520 @@
|
|
|
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
|
+
/// - Wallet must be in 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.
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/// @notice Notifies that the wallet is either old enough or has too few
|
|
391
|
+
/// satoshis left and qualifies to be closed.
|
|
392
|
+
/// @param walletPubKeyHash 20-byte public key hash of the wallet
|
|
393
|
+
/// @param walletMainUtxo Data of the wallet's main UTXO, as currently
|
|
394
|
+
/// known on the Ethereum chain.
|
|
395
|
+
/// @dev Requirements:
|
|
396
|
+
/// - Wallet must not be set as the current active wallet
|
|
397
|
+
/// - Wallet must exceed the wallet maximum age OR the wallet BTC
|
|
398
|
+
/// balance must be lesser than the minimum threshold. If the latter
|
|
399
|
+
/// case is true, the `walletMainUtxo` components must point to the
|
|
400
|
+
/// recent main UTXO of the given wallet, as currently known on the
|
|
401
|
+
/// Ethereum chain. If the wallet has no main UTXO, this parameter
|
|
402
|
+
/// can be empty as it is ignored since the wallet balance is
|
|
403
|
+
/// assumed to be zero.
|
|
404
|
+
/// - Wallet must be in Live state
|
|
405
|
+
function notifyCloseableWallet(
|
|
406
|
+
Data storage self,
|
|
407
|
+
bytes20 walletPubKeyHash,
|
|
408
|
+
BitcoinTx.UTXO calldata walletMainUtxo
|
|
409
|
+
) external {
|
|
410
|
+
require(
|
|
411
|
+
self.activeWalletPubKeyHash != walletPubKeyHash,
|
|
412
|
+
"Active wallet cannot be considered closeable"
|
|
413
|
+
);
|
|
414
|
+
|
|
415
|
+
Wallet storage wallet = self.registeredWallets[walletPubKeyHash];
|
|
416
|
+
require(
|
|
417
|
+
wallet.state == WalletState.Live,
|
|
418
|
+
"ECDSA wallet must be in Live state"
|
|
419
|
+
);
|
|
420
|
+
|
|
421
|
+
/* solhint-disable-next-line not-rely-on-time */
|
|
422
|
+
bool walletOldEnough = block.timestamp >=
|
|
423
|
+
wallet.createdAt + self.maxAge;
|
|
424
|
+
|
|
425
|
+
require(
|
|
426
|
+
walletOldEnough ||
|
|
427
|
+
getWalletBtcBalance(self, walletPubKeyHash, walletMainUtxo) <
|
|
428
|
+
self.minBtcBalance,
|
|
429
|
+
"Wallet needs to be old enough or have too few satoshis"
|
|
430
|
+
);
|
|
431
|
+
|
|
432
|
+
moveFunds(self, walletPubKeyHash);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/// @notice Requests a wallet to move their funds. If the wallet balance
|
|
436
|
+
/// is zero, the wallet is closed immediately and the ECDSA
|
|
437
|
+
/// registry is notified about this fact. If the move funds
|
|
438
|
+
/// request refers to the current active wallet, such a wallet
|
|
439
|
+
/// is no longer considered active and the active wallet slot
|
|
440
|
+
/// is unset allowing to trigger a new wallet creation immediately.
|
|
441
|
+
/// @param walletPubKeyHash 20-byte public key hash of the wallet
|
|
442
|
+
/// @dev Requirements:
|
|
443
|
+
/// - The caller must make sure that the wallet is in the Live state
|
|
444
|
+
function moveFunds(Data storage self, bytes20 walletPubKeyHash) internal {
|
|
445
|
+
Wallet storage wallet = self.registeredWallets[walletPubKeyHash];
|
|
446
|
+
|
|
447
|
+
if (wallet.mainUtxoHash == bytes32(0)) {
|
|
448
|
+
// If the wallet has no main UTXO, that means its BTC balance
|
|
449
|
+
// is zero and it should be closed immediately.
|
|
450
|
+
wallet.state = WalletState.Closed;
|
|
451
|
+
|
|
452
|
+
emit WalletClosed(wallet.ecdsaWalletID, walletPubKeyHash);
|
|
453
|
+
|
|
454
|
+
self.registry.closeWallet(wallet.ecdsaWalletID);
|
|
455
|
+
} else {
|
|
456
|
+
// Otherwise, initialize the moving funds process.
|
|
457
|
+
wallet.state = WalletState.MovingFunds;
|
|
458
|
+
/* solhint-disable-next-line not-rely-on-time */
|
|
459
|
+
wallet.moveFundsRequestedAt = uint32(block.timestamp);
|
|
460
|
+
|
|
461
|
+
emit WalletMovingFunds(wallet.ecdsaWalletID, walletPubKeyHash);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
if (self.activeWalletPubKeyHash == walletPubKeyHash) {
|
|
465
|
+
// If the move funds request refers to the current active wallet,
|
|
466
|
+
// unset the active wallet and make the wallet creation process
|
|
467
|
+
// possible in order to get a new healthy active wallet.
|
|
468
|
+
delete self.activeWalletPubKeyHash;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// TODO: Implement functions that will be called upon moving funds process
|
|
473
|
+
// end. Remember the moving funds process ends up with a successful
|
|
474
|
+
// proof or a timeout.
|
|
475
|
+
|
|
476
|
+
/// @notice Reports about a fraud committed by the given wallet. This
|
|
477
|
+
/// function performs slashing and wallet termination in reaction
|
|
478
|
+
/// to a proven fraud and it should only be called when the fraud
|
|
479
|
+
/// was confirmed.
|
|
480
|
+
/// @param walletPubKeyHash 20-byte public key hash of the wallet
|
|
481
|
+
/// @dev Requirements:
|
|
482
|
+
/// - Wallet must be in Live or MovingFunds state
|
|
483
|
+
function notifyFraud(Data storage self, bytes20 walletPubKeyHash) external {
|
|
484
|
+
// TODO: Perform slashing of wallet operators and add unit tests for that.
|
|
485
|
+
|
|
486
|
+
terminateWallet(self, walletPubKeyHash);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/// @notice Terminates the given wallet and notifies the ECDSA registry
|
|
490
|
+
/// about this fact. If the wallet termination refers to the current
|
|
491
|
+
/// active wallet, such a wallet is no longer considered active and
|
|
492
|
+
/// the active wallet slot is unset allowing to trigger a new wallet
|
|
493
|
+
/// creation immediately.
|
|
494
|
+
/// @param walletPubKeyHash 20-byte public key hash of the wallet
|
|
495
|
+
/// @dev Requirements:
|
|
496
|
+
/// - Wallet must be in Live or MovingFunds state
|
|
497
|
+
function terminateWallet(Data storage self, bytes20 walletPubKeyHash)
|
|
498
|
+
internal
|
|
499
|
+
{
|
|
500
|
+
Wallet storage wallet = self.registeredWallets[walletPubKeyHash];
|
|
501
|
+
require(
|
|
502
|
+
wallet.state == WalletState.Live ||
|
|
503
|
+
wallet.state == WalletState.MovingFunds,
|
|
504
|
+
"ECDSA wallet must be in Live or MovingFunds state"
|
|
505
|
+
);
|
|
506
|
+
|
|
507
|
+
wallet.state = WalletState.Terminated;
|
|
508
|
+
|
|
509
|
+
emit WalletTerminated(wallet.ecdsaWalletID, walletPubKeyHash);
|
|
510
|
+
|
|
511
|
+
if (self.activeWalletPubKeyHash == walletPubKeyHash) {
|
|
512
|
+
// If termination refers to the current active wallet,
|
|
513
|
+
// unset the active wallet and make the wallet creation process
|
|
514
|
+
// possible in order to get a new healthy active wallet.
|
|
515
|
+
delete self.activeWalletPubKeyHash;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
self.registry.closeWallet(walletPubKeyHash);
|
|
519
|
+
}
|
|
520
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@keep-network/tbtc-v2",
|
|
3
|
-
"version": "0.1.1-dev.
|
|
3
|
+
"version": "0.1.1-dev.25+main.a623e0eefa3dbc09c7ace06db035b970d49c1861",
|
|
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
|
-
"@
|
|
37
|
-
"@keep-network/hardhat-
|
|
38
|
-
"@
|
|
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.
|
|
42
|
+
"@nomiclabs/hardhat-waffle": "^2.0.2",
|
|
41
43
|
"@thesis-co/eslint-config": "github:thesis/eslint-config",
|
|
42
|
-
"@typechain/ethers-v5": "^
|
|
43
|
-
"@typechain/hardhat": "^
|
|
44
|
-
"@types/chai": "^4.
|
|
45
|
-
"@types/mocha": "^9.
|
|
46
|
-
"@types/node": "^
|
|
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.
|
|
51
|
-
"hardhat": "^2.
|
|
52
|
+
"ethers": "^5.5.3",
|
|
53
|
+
"hardhat": "^2.8.3",
|
|
52
54
|
"hardhat-contract-sizer": "^2.5.0",
|
|
53
|
-
"hardhat-
|
|
54
|
-
"hardhat-
|
|
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.
|
|
61
|
-
"typechain": "^
|
|
62
|
-
"typescript": "^4.4
|
|
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"
|