@keep-network/tbtc-v2 0.1.1-dev.5 → 0.1.1-dev.9

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.
@@ -15,106 +15,282 @@
15
15
 
16
16
  pragma solidity 0.8.4;
17
17
 
18
- /// @title BTC Bridge
19
- /// @notice Bridge manages BTC deposit and redemption and is increasing and
18
+ import "@openzeppelin/contracts/access/Ownable.sol";
19
+
20
+ import {BTCUtils} from "@keep-network/bitcoin-spv-sol/contracts/BTCUtils.sol";
21
+ import {BytesLib} from "@keep-network/bitcoin-spv-sol/contracts/BytesLib.sol";
22
+
23
+ import "./BitcoinTx.sol";
24
+
25
+ /// @title Bitcoin Bridge
26
+ /// @notice Bridge manages BTC deposit and redemption flow and is increasing and
20
27
  /// decreasing balances in the Bank as a result of BTC deposit and
21
- /// redemption operations.
28
+ /// redemption operations performed by depositors and redeemers.
22
29
  ///
23
- /// Depositors send BTC funds to the most-recently-created-wallet of the
24
- /// bridge using pay-to-script-hash (P2SH) which contains hashed
25
- /// information about the depositor’s minting Ethereum address. Then,
26
- /// the depositor reveals their desired Ethereum minting address to the
27
- /// Ethereum chain. The Bridge listens for these sorts of messages and
28
- /// when it gets one, it checks the Bitcoin network to make sure the
29
- /// funds line up. If they do, the off-chain wallet may decide to pick
30
- /// this transaction for sweeping, and when the sweep operation is
31
- /// confirmed on the Bitcoin network, the wallet informs the Bridge
32
- /// about the sweep increasing appropriate balances in the Bank.
30
+ /// Depositors send BTC funds to the most recently created off-chain
31
+ /// ECDSA wallet of the bridge using pay-to-script-hash (P2SH) or
32
+ /// pay-to-witness-script-hash (P2WSH) containing hashed information
33
+ /// about the depositor’s Ethereum address. Then, the depositor reveals
34
+ /// their Ethereum address along with their deposit blinding factor,
35
+ /// refund public key hash and refund locktime to the Bridge on Ethereum
36
+ /// chain. The off-chain ECDSA wallet listens for these sorts of
37
+ /// messages and when it gets one, it checks the Bitcoin network to make
38
+ /// sure the deposit lines up. If it does, the off-chain ECDSA wallet
39
+ /// may decide to pick the deposit transaction for sweeping, and when
40
+ /// the sweep operation is confirmed on the Bitcoin network, the ECDSA
41
+ /// wallet informs the Bridge about the sweep increasing appropriate
42
+ /// balances in the Bank.
33
43
  /// @dev Bridge is an upgradeable component of the Bank.
34
- contract Bridge {
35
- struct DepositInfo {
36
- uint64 amount;
44
+ contract Bridge is Ownable {
45
+ using BTCUtils for bytes;
46
+ using BytesLib for bytes;
47
+
48
+ /// @notice Represents data which must be revealed by the depositor during
49
+ /// deposit reveal.
50
+ struct RevealInfo {
51
+ // Index of the funding output belonging to the funding transaction.
52
+ uint8 fundingOutputIndex;
53
+ // Ethereum depositor address.
54
+ address depositor;
55
+ // The blinding factor as 8 bytes. Byte endianness doesn't matter
56
+ // as this factor is not interpreted as uint.
57
+ bytes8 blindingFactor;
58
+ // The compressed Bitcoin public key (33 bytes and 02 or 03 prefix)
59
+ // of the deposit's wallet hashed in the HASH160 Bitcoin opcode style.
60
+ bytes20 walletPubKeyHash;
61
+ // The compressed Bitcoin public key (33 bytes and 02 or 03 prefix)
62
+ // that can be used to make the deposit refund after the refund
63
+ // locktime passes. Hashed in the HASH160 Bitcoin opcode style.
64
+ bytes20 refundPubKeyHash;
65
+ // The refund locktime (4-byte LE). Interpreted according to locktime
66
+ // parsing rules described in:
67
+ // https://developer.bitcoin.org/devguide/transactions.html#locktime-and-sequence-number
68
+ // and used with OP_CHECKLOCKTIMEVERIFY opcode as described in:
69
+ // https://github.com/bitcoin/bips/blob/master/bip-0065.mediawiki
70
+ bytes4 refundLocktime;
71
+ // Address of the Bank vault to which the deposit is routed to.
72
+ // Optional, can be 0x0. The vault must be trusted by the Bridge.
37
73
  address vault;
74
+ }
75
+
76
+ /// @notice Represents tBTC deposit data.
77
+ struct DepositInfo {
78
+ // Ethereum depositor address.
79
+ address depositor;
80
+ // Deposit amount in satoshi (8-byte LE). For example:
81
+ // 0.0001 BTC = 10000 satoshi = 0x1027000000000000
82
+ bytes8 amount;
83
+ // UNIX timestamp the deposit was revealed at.
38
84
  uint32 revealedAt;
85
+ // Address of the Bank vault the deposit is routed to.
86
+ // Optional, can be 0x0.
87
+ address vault;
39
88
  }
40
89
 
90
+ /// @notice Indicates if the vault with the given address is trusted or not.
91
+ /// Depositors can route their revealed deposits only to trusted
92
+ /// vaults and have trusted vaults notified about new deposits as
93
+ /// soon as these deposits get swept. Vaults not trusted by the
94
+ /// Bridge can still be used by Bank balance owners on their own
95
+ /// responsibility - anyone can approve their Bank balance to any
96
+ /// address.
97
+ mapping(address => bool) public isVaultTrusted;
98
+
41
99
  /// @notice Collection of all unswept deposits indexed by
42
- /// keccak256(fundingTxHash | fundingOutputIndex | depositorAddress).
100
+ /// keccak256(fundingTxHash | fundingOutputIndex).
101
+ /// The fundingTxHash is LE bytes32 and fundingOutputIndex an uint8.
43
102
  /// This mapping may contain valid and invalid deposits and the
44
103
  /// wallet is responsible for validating them before attempting to
45
104
  /// execute a sweep.
105
+ ///
106
+ /// TODO: Explore the possibility of storing just a hash of DepositInfo.
46
107
  mapping(uint256 => DepositInfo) public unswept;
47
108
 
48
109
  event DepositRevealed(
49
- uint256 depositId,
50
110
  bytes32 fundingTxHash,
51
111
  uint8 fundingOutputIndex,
52
112
  address depositor,
53
- uint64 blindingFactor,
54
- bytes refundPubKey,
55
- uint64 amount,
113
+ bytes8 blindingFactor,
114
+ bytes20 walletPubKeyHash,
115
+ bytes20 refundPubKeyHash,
116
+ bytes4 refundLocktime,
56
117
  address vault
57
118
  );
58
119
 
59
- /// @notice Used by the depositor to reveal information about their P2SH
120
+ event VaultStatusUpdated(address indexed vault, bool isTrusted);
121
+
122
+ /// @notice Allows the Governance to mark the given vault address as trusted
123
+ /// or no longer trusted. Vaults are not trusted by default.
124
+ /// Trusted vault must meet the following criteria:
125
+ /// - `IVault.onBalanceIncreased` must have a known, low gas cost.
126
+ /// - `IVault.onBalanceIncreased` must never revert.
127
+ /// @dev Without restricting reveal only to trusted vaults, malicious
128
+ /// vaults not meeting the criteria would be able to nuke sweep proof
129
+ /// transactions executed by ECDSA wallet with deposits routed to
130
+ /// them.
131
+ /// @param vault The address of the vault
132
+ /// @param isTrusted flag indicating whether the vault is trusted or not
133
+ /// @dev Can only be called by the Governance.
134
+ function setVaultStatus(address vault, bool isTrusted) external onlyOwner {
135
+ isVaultTrusted[vault] = isTrusted;
136
+ emit VaultStatusUpdated(vault, isTrusted);
137
+ }
138
+
139
+ /// @notice Used by the depositor to reveal information about their P2(W)SH
60
140
  /// Bitcoin deposit to the Bridge on Ethereum chain. The off-chain
61
141
  /// wallet listens for revealed deposit events and may decide to
62
142
  /// include the revealed deposit in the next executed sweep.
63
143
  /// Information about the Bitcoin deposit can be revealed before or
64
- /// after the Bitcoin transaction with P2SH deposit is mined on the
65
- /// Bitcoin chain.
66
- /// @param fundingTxHash The BTC transaction hash containing BTC P2SH
67
- /// deposit funding transaction
68
- /// @param fundingOutputIndex The index of the transaction output in the
69
- /// funding TX with P2SH deposit, max 256
70
- /// @param blindingFactor The blinding factor used in the BTC P2SH deposit,
71
- /// max 2^64
72
- /// @param refundPubKey The refund pub key used in the BTC P2SH deposit
73
- /// @param amount The amount locked in the BTC P2SH deposit
74
- /// @param vault Bank vault to which the swept deposit should be routed
144
+ /// after the Bitcoin transaction with P2(W)SH deposit is mined on
145
+ /// the Bitcoin chain. Worth noting, the gas cost of this function
146
+ /// scales with the number of P2(W)SH transaction inputs and
147
+ /// outputs. The deposit may be routed to one of the trusted vaults.
148
+ /// When a deposit is routed to a vault, vault gets notified when
149
+ /// the deposit gets swept and it may execute the appropriate action.
150
+ /// @param fundingTx Bitcoin funding transaction data, see `BitcoinTx.Info`
151
+ /// @param reveal Deposit reveal data, see `RevealInfo struct
75
152
  /// @dev Requirements:
76
- /// - `msg.sender` must be the Ethereum address used in the P2SH BTC deposit,
77
- /// - `blindingFactor` must be the blinding factor used in the P2SH BTC deposit,
78
- /// - `refundPubKey` must be the refund pub key used in the P2SH BTC deposit,
79
- /// - `amount` must be the same as locked in the P2SH BTC deposit,
153
+ /// - `reveal.vault` must be 0x0 or point to a trusted vault
154
+ /// - `reveal.fundingOutputIndex` must point to the actual P2(W)SH
155
+ /// output of the BTC deposit transaction
156
+ /// - `reveal.depositor` must be the Ethereum address used in the
157
+ /// P2(W)SH BTC deposit transaction,
158
+ /// - `reveal.blindingFactor` must be the blinding factor used in the
159
+ /// P2(W)SH BTC deposit transaction,
160
+ /// - `reveal.walletPubKeyHash` must be the wallet pub key hash used in
161
+ /// the P2(W)SH BTC deposit transaction,
162
+ /// - `reveal.refundPubKeyHash` must be the refund pub key hash used in
163
+ /// the P2(W)SH BTC deposit transaction,
164
+ /// - `reveal.refundLocktime` must be the refund locktime used in the
165
+ /// P2(W)SH BTC deposit transaction,
80
166
  /// - BTC deposit for the given `fundingTxHash`, `fundingOutputIndex`
81
- /// can be revealed by `msg.sender` only one time.
167
+ /// can be revealed only one time.
82
168
  ///
83
169
  /// If any of these requirements is not met, the wallet _must_ refuse
84
170
  /// to sweep the deposit and the depositor has to wait until the
85
171
  /// deposit script unlocks to receive their BTC back.
86
172
  function revealDeposit(
87
- bytes32 fundingTxHash,
88
- uint8 fundingOutputIndex,
89
- uint64 blindingFactor,
90
- bytes calldata refundPubKey,
91
- uint64 amount,
92
- address vault
173
+ BitcoinTx.Info calldata fundingTx,
174
+ RevealInfo calldata reveal
93
175
  ) external {
94
- uint256 depositId =
95
- uint256(
96
- keccak256(
97
- abi.encode(fundingTxHash, fundingOutputIndex, msg.sender)
98
- )
176
+ require(
177
+ reveal.vault == address(0) || isVaultTrusted[reveal.vault],
178
+ "Vault is not trusted"
179
+ );
180
+
181
+ bytes memory expectedScript =
182
+ abi.encodePacked(
183
+ hex"14", // Byte length of depositor Ethereum address.
184
+ reveal.depositor,
185
+ hex"75", // OP_DROP
186
+ hex"08", // Byte length of blinding factor value.
187
+ reveal.blindingFactor,
188
+ hex"75", // OP_DROP
189
+ hex"76", // OP_DUP
190
+ hex"a9", // OP_HASH160
191
+ hex"14", // Byte length of a compressed Bitcoin public key hash.
192
+ reveal.walletPubKeyHash,
193
+ hex"87", // OP_EQUAL
194
+ hex"63", // OP_IF
195
+ hex"ac", // OP_CHECKSIG
196
+ hex"67", // OP_ELSE
197
+ hex"76", // OP_DUP
198
+ hex"a9", // OP_HASH160
199
+ hex"14", // Byte length of a compressed Bitcoin public key hash.
200
+ reveal.refundPubKeyHash,
201
+ hex"88", // OP_EQUALVERIFY
202
+ hex"04", // Byte length of refund locktime value.
203
+ reveal.refundLocktime,
204
+ hex"b1", // OP_CHECKLOCKTIMEVERIFY
205
+ hex"75", // OP_DROP
206
+ hex"ac", // OP_CHECKSIG
207
+ hex"68" // OP_ENDIF
208
+ );
209
+
210
+ bytes memory fundingOutput =
211
+ fundingTx.outputVector.extractOutputAtIndex(
212
+ reveal.fundingOutputIndex
213
+ );
214
+ bytes memory fundingOutputHash = fundingOutput.extractHash();
215
+
216
+ if (fundingOutputHash.length == 20) {
217
+ // A 20-byte output hash is used by P2SH. That hash is constructed
218
+ // by applying OP_HASH160 on the locking script. A 20-byte output
219
+ // hash is used as well by P2PKH and P2WPKH (OP_HASH160 on the
220
+ // public key). However, since we compare the actual output hash
221
+ // with an expected locking script hash, this check will succeed only
222
+ // for P2SH transaction type with expected script hash value. For
223
+ // P2PKH and P2WPKH, it will fail on the output hash comparison with
224
+ // the expected locking script hash.
225
+ require(
226
+ keccak256(fundingOutputHash) ==
227
+ keccak256(expectedScript.hash160()),
228
+ "Wrong 20-byte script hash"
229
+ );
230
+ } else if (fundingOutputHash.length == 32) {
231
+ // A 32-byte output hash is used by P2WSH. That hash is constructed
232
+ // by applying OP_SHA256 on the locking script.
233
+ require(
234
+ fundingOutputHash.toBytes32() == sha256(expectedScript),
235
+ "Wrong 32-byte script hash"
99
236
  );
237
+ } else {
238
+ revert("Wrong script hash length");
239
+ }
100
240
 
101
- DepositInfo storage deposit = unswept[depositId];
241
+ // Resulting TX hash is in native Bitcoin little-endian format.
242
+ bytes32 fundingTxHash =
243
+ abi
244
+ .encodePacked(
245
+ fundingTx
246
+ .version,
247
+ fundingTx
248
+ .inputVector,
249
+ fundingTx
250
+ .outputVector,
251
+ fundingTx
252
+ .locktime
253
+ )
254
+ .hash256();
255
+
256
+ DepositInfo storage deposit =
257
+ unswept[
258
+ uint256(
259
+ keccak256(
260
+ abi.encodePacked(
261
+ fundingTxHash,
262
+ reveal.fundingOutputIndex
263
+ )
264
+ )
265
+ )
266
+ ];
102
267
  require(deposit.revealedAt == 0, "Deposit already revealed");
103
268
 
104
- deposit.amount = amount;
105
- deposit.vault = vault;
269
+ bytes8 fundingOutputAmount;
270
+ /* solhint-disable-next-line no-inline-assembly */
271
+ assembly {
272
+ // First 8 bytes (little-endian) of the funding output represents
273
+ // its value. To take the value, we need to jump over the first
274
+ // word determining the array length, load the array, and trim it
275
+ // by putting it to a bytes8.
276
+ fundingOutputAmount := mload(add(fundingOutput, 32))
277
+ }
278
+
279
+ deposit.amount = fundingOutputAmount;
280
+ deposit.depositor = reveal.depositor;
106
281
  /* solhint-disable-next-line not-rely-on-time */
107
282
  deposit.revealedAt = uint32(block.timestamp);
283
+ deposit.vault = reveal.vault;
108
284
 
109
285
  emit DepositRevealed(
110
- depositId,
111
286
  fundingTxHash,
112
- fundingOutputIndex,
113
- msg.sender,
114
- blindingFactor,
115
- refundPubKey,
116
- amount,
117
- vault
287
+ reveal.fundingOutputIndex,
288
+ reveal.depositor,
289
+ reveal.blindingFactor,
290
+ reveal.walletPubKeyHash,
291
+ reveal.refundPubKeyHash,
292
+ reveal.refundLocktime,
293
+ reveal.vault
118
294
  );
119
295
  }
120
296
 
@@ -125,45 +301,37 @@ contract Bridge {
125
301
  /// The function is performing Bank balance updates by first
126
302
  /// computing the Bitcoin fee for the sweep transaction. The fee is
127
303
  /// divided evenly between all swept deposits. Each depositor
128
- /// receives a balance in the bank equal to the amount they have
129
- /// declared during the reveal transaction, minus their fee share.
304
+ /// receives a balance in the bank equal to the amount inferred
305
+ /// during the reveal transaction, minus their fee share.
130
306
  ///
131
307
  /// It is possible to prove the given sweep only one time.
132
- /// @param txVersion Transaction version number (4-byte LE)
133
- /// @param txInputVector All transaction inputs prepended by the number of
134
- /// inputs encoded as a VarInt, max 0xFC(252) inputs
135
- /// @param txOutput Single sweep transaction output
136
- /// @param txLocktime Final 4 bytes of the transaction
137
- /// @param merkleProof The merkle proof of transaction inclusion in a block
138
- /// @param txIndexInBlock Transaction index in the block (0-indexed)
308
+ /// @param sweepTx Bitcoin sweep transaction data.
309
+ /// @param merkleProof The merkle proof of transaction inclusion in a block.
310
+ /// @param txIndexInBlock Transaction index in the block (0-indexed).
139
311
  /// @param bitcoinHeaders Single bytestring of 80-byte bitcoin headers,
140
- /// lowest height first
312
+ /// lowest height first.
141
313
  function sweep(
142
- bytes4 txVersion,
143
- bytes memory txInputVector,
144
- bytes memory txOutput,
145
- bytes4 txLocktime,
314
+ BitcoinTx.Info calldata sweepTx,
146
315
  bytes memory merkleProof,
147
316
  uint256 txIndexInBlock,
148
317
  bytes memory bitcoinHeaders
149
318
  ) external {
150
- // TODO We need to read `fundingTxHash`, `fundingOutputIndex` and
151
- // P2SH script depositor address from `txInputVector`.
152
- // We then hash them to obtain deposit identifier and read
153
- // DepositInfo. From DepositInfo we know what amount was declared
154
- // by the depositor in their reveal transaction and we use that
155
- // amount to update their Bank balance, minus fee.
319
+ // TODO We need to read `fundingTxHash`, `fundingOutputIndex` from
320
+ // `sweepTx.inputVector`. We then hash them to obtain deposit
321
+ // identifier and read DepositInfo. From DepositInfo we know what
322
+ // amount was inferred during deposit reveal transaction and we
323
+ // use that amount to update their Bank balance, minus fee.
156
324
  //
157
325
  // TODO We need to validate if the sum in the output minus the
158
326
  // amount from the previous wallet balance input minus fees is
159
327
  // equal to the amount by which Bank balances were increased.
160
328
  //
161
- // TODO We need to validate txOutput to see if the balance was not
162
- // transferred away from the wallet before increasing balances in
163
- // the bank.
329
+ // TODO We need to validate `sweepTx.outputVector` to see if the balance
330
+ // was not transferred away from the wallet before increasing
331
+ // balances in the bank.
164
332
  //
165
333
  // TODO Delete deposit from unswept mapping or mark it as swept
166
- // depending on the gas costs. Alternativly, do not allow to
334
+ // depending on the gas costs. Alternatively, do not allow to
167
335
  // use the same TX input vector twice. Sweep should be provable
168
336
  // only one time.
169
337
  }
@@ -173,4 +341,5 @@ contract Bridge {
173
341
  // an incorrect amount revealed. We need to provide a function for honest
174
342
  // depositors, next to sweep, to prove their swept balances on Ethereum
175
343
  // selectively, based on deposits they have earlier received.
344
+ // (UPDATE PR #90: Is it still the case since amounts are inferred?)
176
345
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@keep-network/tbtc-v2",
3
- "version": "0.1.1-dev.5+main.3acedd01fc600bf979696aa8c68149bd44794418",
3
+ "version": "0.1.1-dev.9+main.885d0c498cdc3a7512eee6573df02feea7036688",
4
4
  "license": "MIT",
5
5
  "files": [
6
6
  "artifacts/",
@@ -26,6 +26,7 @@
26
26
  "prepublishOnly": "./scripts/prepare-artifacts.sh --network $npm_config_network"
27
27
  },
28
28
  "dependencies": {
29
+ "@keep-network/bitcoin-spv-sol": "3.1.0-solc-0.8",
29
30
  "@keep-network/tbtc": ">1.1.2-dev <1.1.2-pre",
30
31
  "@openzeppelin/contracts": "^4.1.0",
31
32
  "@tenderly/hardhat-tenderly": "^1.0.12",