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

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