@keep-network/tbtc-v2 0.1.1-dev.9 → 0.1.1-dev.90
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/Bank.json +817 -0
- package/artifacts/Bridge.json +2666 -0
- package/artifacts/Deposit.json +117 -0
- package/artifacts/DepositSweep.json +77 -0
- package/artifacts/EcdsaDkgValidator.json +532 -0
- package/artifacts/EcdsaInactivity.json +156 -0
- package/artifacts/EcdsaSortitionPool.json +1004 -0
- package/artifacts/Fraud.json +164 -0
- package/artifacts/KeepRegistry.json +99 -0
- package/artifacts/KeepStake.json +286 -0
- package/artifacts/KeepToken.json +711 -0
- package/artifacts/KeepTokenStaking.json +483 -0
- package/artifacts/MovingFunds.json +249 -0
- package/artifacts/NuCypherStakingEscrow.json +256 -0
- package/artifacts/NuCypherToken.json +711 -0
- package/artifacts/RandomBeaconStub.json +141 -0
- package/artifacts/Redemption.json +174 -0
- package/artifacts/ReimbursementPool.json +509 -0
- package/artifacts/Relay.json +123 -0
- package/artifacts/T.json +1148 -0
- package/artifacts/TBTC.json +27 -26
- package/artifacts/TBTCToken.json +27 -26
- package/artifacts/TBTCVault.json +691 -0
- package/artifacts/TokenStaking.json +2288 -0
- package/artifacts/TokenholderGovernor.json +1795 -0
- package/artifacts/TokenholderTimelock.json +1058 -0
- package/artifacts/VendingMachine.json +30 -29
- package/artifacts/VendingMachineKeep.json +400 -0
- package/artifacts/VendingMachineNuCypher.json +400 -0
- package/artifacts/WalletRegistry.json +1843 -0
- package/artifacts/WalletRegistryGovernance.json +2754 -0
- package/artifacts/Wallets.json +186 -0
- package/artifacts/solcInputs/6e28b66ed60b6dd7b2b25b3a1c4fe89b.json +314 -0
- package/build/contracts/GovernanceUtils.sol/GovernanceUtils.dbg.json +1 -1
- package/build/contracts/GovernanceUtils.sol/GovernanceUtils.json +2 -2
- package/build/contracts/bank/Bank.sol/Bank.dbg.json +1 -1
- package/build/contracts/bank/Bank.sol/Bank.json +27 -4
- package/build/contracts/bank/IReceiveBalanceApproval.sol/IReceiveBalanceApproval.dbg.json +4 -0
- package/build/contracts/bank/IReceiveBalanceApproval.sol/IReceiveBalanceApproval.json +34 -0
- package/build/contracts/bridge/BitcoinTx.sol/BitcoinTx.dbg.json +1 -1
- package/build/contracts/bridge/BitcoinTx.sol/BitcoinTx.json +2 -2
- package/build/contracts/bridge/Bridge.sol/Bridge.dbg.json +1 -1
- package/build/contracts/bridge/Bridge.sol/Bridge.json +2547 -196
- package/build/contracts/bridge/BridgeState.sol/BridgeState.dbg.json +4 -0
- package/build/contracts/bridge/BridgeState.sol/BridgeState.json +226 -0
- package/build/contracts/bridge/Deposit.sol/Deposit.dbg.json +4 -0
- package/build/contracts/bridge/Deposit.sol/Deposit.json +72 -0
- package/build/contracts/bridge/DepositSweep.sol/DepositSweep.dbg.json +4 -0
- package/build/contracts/bridge/DepositSweep.sol/DepositSweep.json +30 -0
- 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/Fraud.sol/Fraud.dbg.json +4 -0
- package/build/contracts/bridge/Fraud.sol/Fraud.json +86 -0
- package/build/contracts/bridge/Heartbeat.sol/Heartbeat.dbg.json +4 -0
- package/build/contracts/bridge/Heartbeat.sol/Heartbeat.json +10 -0
- package/build/contracts/bridge/IRelay.sol/IRelay.dbg.json +4 -0
- package/build/contracts/bridge/IRelay.sol/IRelay.json +37 -0
- package/build/contracts/bridge/MovingFunds.sol/MovingFunds.dbg.json +4 -0
- package/build/contracts/bridge/MovingFunds.sol/MovingFunds.json +138 -0
- package/build/contracts/bridge/Redemption.sol/OutboundTx.dbg.json +4 -0
- package/build/contracts/bridge/Redemption.sol/OutboundTx.json +10 -0
- package/build/contracts/bridge/Redemption.sol/Redemption.dbg.json +4 -0
- package/build/contracts/bridge/Redemption.sol/Redemption.json +92 -0
- package/build/contracts/bridge/VendingMachine.sol/VendingMachine.dbg.json +1 -1
- package/build/contracts/bridge/VendingMachine.sol/VendingMachine.json +2 -2
- package/build/contracts/bridge/Wallets.sol/Wallets.dbg.json +4 -0
- package/build/contracts/bridge/Wallets.sol/Wallets.json +112 -0
- package/build/contracts/token/TBTC.sol/TBTC.dbg.json +1 -1
- package/build/contracts/token/TBTC.sol/TBTC.json +2 -2
- package/build/contracts/vault/DonationVault.sol/DonationVault.dbg.json +4 -0
- package/build/contracts/vault/DonationVault.sol/DonationVault.json +108 -0
- package/build/contracts/vault/IVault.sol/IVault.dbg.json +1 -1
- package/build/contracts/vault/IVault.sol/IVault.json +24 -1
- package/build/contracts/vault/TBTCVault.sol/TBTCVault.dbg.json +1 -1
- package/build/contracts/vault/TBTCVault.sol/TBTCVault.json +295 -9
- package/contracts/GovernanceUtils.sol +4 -4
- package/contracts/bank/Bank.sol +119 -57
- package/contracts/bank/IReceiveBalanceApproval.sol +45 -0
- package/contracts/bridge/BitcoinTx.sol +232 -10
- package/contracts/bridge/Bridge.sol +1692 -244
- package/contracts/bridge/BridgeState.sol +739 -0
- package/contracts/bridge/Deposit.sol +269 -0
- package/contracts/bridge/DepositSweep.sol +571 -0
- package/contracts/bridge/EcdsaLib.sol +45 -0
- package/contracts/bridge/Fraud.sol +604 -0
- package/contracts/bridge/Heartbeat.sol +112 -0
- package/contracts/bridge/IRelay.sol +28 -0
- package/contracts/bridge/MovingFunds.sol +1089 -0
- package/contracts/bridge/Redemption.sol +1021 -0
- package/contracts/bridge/VendingMachine.sol +1 -1
- package/contracts/bridge/Wallets.sol +553 -0
- package/contracts/hardhat-dependency-compiler/.hardhat-dependency-compiler +1 -0
- package/contracts/hardhat-dependency-compiler/@keep-network/ecdsa/contracts/WalletRegistry.sol +3 -0
- package/contracts/hardhat-dependency-compiler/@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol +3 -0
- package/contracts/hardhat-dependency-compiler/@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol +3 -0
- package/contracts/token/TBTC.sol +1 -1
- package/contracts/vault/DonationVault.sol +125 -0
- package/contracts/vault/IVault.sol +19 -13
- package/contracts/vault/TBTCVault.sol +200 -23
- package/deploy/00_resolve_relay.ts +28 -0
- package/deploy/{03_transfer_roles.ts → 03_transfer_vending_machine_roles.ts} +1 -1
- package/deploy/04_deploy_bank.ts +27 -0
- package/deploy/05_deploy_bridge.ts +80 -0
- package/deploy/06_deploy_tbtc_vault.ts +30 -0
- package/deploy/07_bank_update_bridge.ts +19 -0
- package/deploy/08_transfer_bank_ownership.ts +15 -0
- package/deploy/09_transfer_tbtc_vault_ownership.ts +15 -0
- package/deploy/10_transfer_bridge_governance.ts +20 -0
- package/deploy/11_initialize_wallet_owner.ts +18 -0
- package/deploy/11_transfer_proxy_admin_ownership.ts +30 -0
- package/deploy/12_deploy_proxy_admin_with_deputy.ts +33 -0
- package/export.json +16150 -443
- package/package.json +32 -25
- package/artifacts/solcInputs/58d5b3ee7688835879381470de985d6b.json +0 -128
|
@@ -0,0 +1,604 @@
|
|
|
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 {BytesLib} from "@keep-network/bitcoin-spv-sol/contracts/BytesLib.sol";
|
|
19
|
+
import {BTCUtils} from "@keep-network/bitcoin-spv-sol/contracts/BTCUtils.sol";
|
|
20
|
+
import {CheckBitcoinSigs} from "@keep-network/bitcoin-spv-sol/contracts/CheckBitcoinSigs.sol";
|
|
21
|
+
|
|
22
|
+
import "./BitcoinTx.sol";
|
|
23
|
+
import "./EcdsaLib.sol";
|
|
24
|
+
import "./BridgeState.sol";
|
|
25
|
+
import "./Heartbeat.sol";
|
|
26
|
+
import "./MovingFunds.sol";
|
|
27
|
+
import "./Wallets.sol";
|
|
28
|
+
|
|
29
|
+
/// @title Bridge fraud
|
|
30
|
+
/// @notice The library handles the logic for challenging Bridge wallets that
|
|
31
|
+
/// committed fraud.
|
|
32
|
+
/// @dev Anyone can submit a fraud challenge indicating that a UTXO being under
|
|
33
|
+
/// the wallet control was unlocked by the wallet but was not used
|
|
34
|
+
/// according to the protocol rules. That means the wallet signed
|
|
35
|
+
/// a transaction input pointing to that UTXO and there is a unique
|
|
36
|
+
/// sighash and signature pair associated with that input.
|
|
37
|
+
///
|
|
38
|
+
/// In order to defeat the challenge, the same wallet public key and
|
|
39
|
+
/// signature must be provided as were used to calculate the sighash during
|
|
40
|
+
/// the challenge. The wallet provides the preimage which produces sighash
|
|
41
|
+
/// used to generate the ECDSA signature that is the subject of the fraud
|
|
42
|
+
/// claim.
|
|
43
|
+
///
|
|
44
|
+
/// The fraud challenge defeat attempt will succeed if the inputs in the
|
|
45
|
+
/// preimage are considered honestly spent by the wallet. Therefore the
|
|
46
|
+
/// transaction spending the UTXO must be proven in the Bridge before
|
|
47
|
+
/// a challenge defeat is called.
|
|
48
|
+
///
|
|
49
|
+
/// Another option is when a malicious wallet member used a signed heartbeat
|
|
50
|
+
/// message periodically produced by the wallet off-chain to challenge the
|
|
51
|
+
/// wallet for a fraud. Anyone from the wallet can defeat the challenge by
|
|
52
|
+
/// proving the sighash and signature were produced for a heartbeat message
|
|
53
|
+
/// following a strict format.
|
|
54
|
+
library Fraud {
|
|
55
|
+
using Wallets for BridgeState.Storage;
|
|
56
|
+
|
|
57
|
+
using BytesLib for bytes;
|
|
58
|
+
using BTCUtils for bytes;
|
|
59
|
+
using BTCUtils for uint32;
|
|
60
|
+
using EcdsaLib for bytes;
|
|
61
|
+
|
|
62
|
+
struct FraudChallenge {
|
|
63
|
+
// The address of the party challenging the wallet.
|
|
64
|
+
address challenger;
|
|
65
|
+
// The amount of ETH the challenger deposited.
|
|
66
|
+
uint256 depositAmount;
|
|
67
|
+
// The timestamp the challenge was submitted at.
|
|
68
|
+
uint32 reportedAt;
|
|
69
|
+
// The flag indicating whether the challenge has been resolved.
|
|
70
|
+
bool resolved;
|
|
71
|
+
// This struct doesn't contain `__gap` property as the structure is stored
|
|
72
|
+
// in a mapping, mappings store values in different slots and they are
|
|
73
|
+
// not contiguous with other values.
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
event FraudChallengeSubmitted(
|
|
77
|
+
bytes20 indexed walletPubKeyHash,
|
|
78
|
+
bytes32 sighash,
|
|
79
|
+
uint8 v,
|
|
80
|
+
bytes32 r,
|
|
81
|
+
bytes32 s
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
event FraudChallengeDefeated(
|
|
85
|
+
bytes20 indexed walletPubKeyHash,
|
|
86
|
+
bytes32 sighash
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
event FraudChallengeDefeatTimedOut(
|
|
90
|
+
bytes20 indexed walletPubKeyHash,
|
|
91
|
+
// Sighash calculated as a Bitcoin's hash256 (double sha2) of:
|
|
92
|
+
// - a preimage of a transaction spending UTXO according to the protocol
|
|
93
|
+
// rules OR
|
|
94
|
+
// - a valid heartbeat message produced by the wallet off-chain.
|
|
95
|
+
bytes32 sighash
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
/// @notice Submits a fraud challenge indicating that a UTXO being under
|
|
99
|
+
/// wallet control was unlocked by the wallet but was not used
|
|
100
|
+
/// according to the protocol rules. That means the wallet signed
|
|
101
|
+
/// a transaction input pointing to that UTXO and there is a unique
|
|
102
|
+
/// sighash and signature pair associated with that input. This
|
|
103
|
+
/// function uses those parameters to create a fraud accusation that
|
|
104
|
+
/// proves a given transaction input unlocking the given UTXO was
|
|
105
|
+
/// actually signed by the wallet. This function cannot determine
|
|
106
|
+
/// whether the transaction was actually broadcast and the input was
|
|
107
|
+
/// consumed in a fraudulent way so it just opens a challenge period
|
|
108
|
+
/// during which the wallet can defeat the challenge by submitting
|
|
109
|
+
/// proof of a transaction that consumes the given input according
|
|
110
|
+
/// to protocol rules. To prevent spurious allegations, the caller
|
|
111
|
+
/// must deposit ETH that is returned back upon justified fraud
|
|
112
|
+
/// challenge or confiscated otherwise.
|
|
113
|
+
/// @param walletPublicKey The public key of the wallet in the uncompressed
|
|
114
|
+
/// and unprefixed format (64 bytes).
|
|
115
|
+
/// @param preimageSha256 The hash that was generated by applying SHA-256
|
|
116
|
+
/// one time over the preimage used during input signing. The preimage
|
|
117
|
+
/// is a serialized subset of the transaction and its structure
|
|
118
|
+
/// depends on the transaction input (see BIP-143 for reference).
|
|
119
|
+
/// Notice that applying SHA-256 over the `preimageSha256` results
|
|
120
|
+
/// in `sighash`. The path from `preimage` to `sighash` looks like
|
|
121
|
+
/// this:
|
|
122
|
+
/// preimage -> (SHA-256) -> preimageSha256 -> (SHA-256) -> sighash.
|
|
123
|
+
/// @param signature Bitcoin signature in the R/S/V format
|
|
124
|
+
/// @dev Requirements:
|
|
125
|
+
/// - Wallet behind `walletPublicKey` must be in Live or MovingFunds
|
|
126
|
+
/// or Closing state,
|
|
127
|
+
/// - The challenger must send appropriate amount of ETH used as
|
|
128
|
+
/// fraud challenge deposit,
|
|
129
|
+
/// - The signature (represented by r, s and v) must be generated by
|
|
130
|
+
/// the wallet behind `walletPubKey` during signing of `sighash`
|
|
131
|
+
/// which was calculated from `preimageSha256`,
|
|
132
|
+
/// - Wallet can be challenged for the given signature only once.
|
|
133
|
+
function submitFraudChallenge(
|
|
134
|
+
BridgeState.Storage storage self,
|
|
135
|
+
bytes calldata walletPublicKey,
|
|
136
|
+
bytes memory preimageSha256,
|
|
137
|
+
BitcoinTx.RSVSignature calldata signature
|
|
138
|
+
) external {
|
|
139
|
+
require(
|
|
140
|
+
msg.value >= self.fraudChallengeDepositAmount,
|
|
141
|
+
"The amount of ETH deposited is too low"
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
// To prevent ECDSA signature forgery `sighash` must be calculated
|
|
145
|
+
// inside the function and not passed as a function parameter.
|
|
146
|
+
// Signature forgery could result in a wrongful fraud accusation
|
|
147
|
+
// against a wallet.
|
|
148
|
+
bytes32 sighash = sha256(preimageSha256);
|
|
149
|
+
|
|
150
|
+
require(
|
|
151
|
+
CheckBitcoinSigs.checkSig(
|
|
152
|
+
walletPublicKey,
|
|
153
|
+
sighash,
|
|
154
|
+
signature.v,
|
|
155
|
+
signature.r,
|
|
156
|
+
signature.s
|
|
157
|
+
),
|
|
158
|
+
"Signature verification failure"
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
bytes memory compressedWalletPublicKey = EcdsaLib.compressPublicKey(
|
|
162
|
+
walletPublicKey.slice32(0),
|
|
163
|
+
walletPublicKey.slice32(32)
|
|
164
|
+
);
|
|
165
|
+
bytes20 walletPubKeyHash = compressedWalletPublicKey.hash160View();
|
|
166
|
+
|
|
167
|
+
Wallets.Wallet storage wallet = self.registeredWallets[
|
|
168
|
+
walletPubKeyHash
|
|
169
|
+
];
|
|
170
|
+
|
|
171
|
+
require(
|
|
172
|
+
wallet.state == Wallets.WalletState.Live ||
|
|
173
|
+
wallet.state == Wallets.WalletState.MovingFunds ||
|
|
174
|
+
wallet.state == Wallets.WalletState.Closing,
|
|
175
|
+
"Wallet must be in Live or MovingFunds or Closing state"
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
uint256 challengeKey = uint256(
|
|
179
|
+
keccak256(abi.encodePacked(walletPublicKey, sighash))
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
FraudChallenge storage challenge = self.fraudChallenges[challengeKey];
|
|
183
|
+
require(challenge.reportedAt == 0, "Fraud challenge already exists");
|
|
184
|
+
|
|
185
|
+
challenge.challenger = msg.sender;
|
|
186
|
+
challenge.depositAmount = msg.value;
|
|
187
|
+
/* solhint-disable-next-line not-rely-on-time */
|
|
188
|
+
challenge.reportedAt = uint32(block.timestamp);
|
|
189
|
+
challenge.resolved = false;
|
|
190
|
+
// slither-disable-next-line reentrancy-events
|
|
191
|
+
emit FraudChallengeSubmitted(
|
|
192
|
+
walletPubKeyHash,
|
|
193
|
+
sighash,
|
|
194
|
+
signature.v,
|
|
195
|
+
signature.r,
|
|
196
|
+
signature.s
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/// @notice Allows to defeat a pending fraud challenge against a wallet if
|
|
201
|
+
/// the transaction that spends the UTXO follows the protocol rules.
|
|
202
|
+
/// In order to defeat the challenge the same `walletPublicKey` and
|
|
203
|
+
/// signature (represented by `r`, `s` and `v`) must be provided as
|
|
204
|
+
/// were used to calculate the sighash during input signing.
|
|
205
|
+
/// The fraud challenge defeat attempt will only succeed if the
|
|
206
|
+
/// inputs in the preimage are considered honestly spent by the
|
|
207
|
+
/// wallet. Therefore the transaction spending the UTXO must be
|
|
208
|
+
/// proven in the Bridge before a challenge defeat is called.
|
|
209
|
+
/// If successfully defeated, the fraud challenge is marked as
|
|
210
|
+
/// resolved and the amount of ether deposited by the challenger is
|
|
211
|
+
/// sent to the treasury.
|
|
212
|
+
/// @param walletPublicKey The public key of the wallet in the uncompressed
|
|
213
|
+
/// and unprefixed format (64 bytes).
|
|
214
|
+
/// @param preimage The preimage which produces sighash used to generate the
|
|
215
|
+
/// ECDSA signature that is the subject of the fraud claim. It is a
|
|
216
|
+
/// serialized subset of the transaction. The exact subset used as
|
|
217
|
+
/// the preimage depends on the transaction input the signature is
|
|
218
|
+
/// produced for. See BIP-143 for reference.
|
|
219
|
+
/// @param witness Flag indicating whether the preimage was produced for a
|
|
220
|
+
/// witness input. True for witness, false for non-witness input.
|
|
221
|
+
/// @dev Requirements:
|
|
222
|
+
/// - `walletPublicKey` and `sighash` calculated as `hash256(preimage)`
|
|
223
|
+
/// must identify an open fraud challenge,
|
|
224
|
+
/// - the preimage must be a valid preimage of a transaction generated
|
|
225
|
+
/// according to the protocol rules and already proved in the Bridge,
|
|
226
|
+
/// - before a defeat attempt is made the transaction that spends the
|
|
227
|
+
/// given UTXO must be proven in the Bridge.
|
|
228
|
+
function defeatFraudChallenge(
|
|
229
|
+
BridgeState.Storage storage self,
|
|
230
|
+
bytes calldata walletPublicKey,
|
|
231
|
+
bytes calldata preimage,
|
|
232
|
+
bool witness
|
|
233
|
+
) external {
|
|
234
|
+
bytes32 sighash = preimage.hash256();
|
|
235
|
+
|
|
236
|
+
uint256 challengeKey = uint256(
|
|
237
|
+
keccak256(abi.encodePacked(walletPublicKey, sighash))
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
FraudChallenge storage challenge = self.fraudChallenges[challengeKey];
|
|
241
|
+
|
|
242
|
+
require(challenge.reportedAt > 0, "Fraud challenge does not exist");
|
|
243
|
+
require(
|
|
244
|
+
!challenge.resolved,
|
|
245
|
+
"Fraud challenge has already been resolved"
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
// Ensure SIGHASH_ALL type was used during signing, which is represented
|
|
249
|
+
// by type value `1`.
|
|
250
|
+
require(extractSighashType(preimage) == 1, "Wrong sighash type");
|
|
251
|
+
|
|
252
|
+
uint256 utxoKey = witness
|
|
253
|
+
? extractUtxoKeyFromWitnessPreimage(preimage)
|
|
254
|
+
: extractUtxoKeyFromNonWitnessPreimage(preimage);
|
|
255
|
+
|
|
256
|
+
// Check that the UTXO key identifies a correctly spent UTXO.
|
|
257
|
+
require(
|
|
258
|
+
self.deposits[utxoKey].sweptAt > 0 ||
|
|
259
|
+
self.spentMainUTXOs[utxoKey] ||
|
|
260
|
+
self.movedFundsSweepRequests[utxoKey].state ==
|
|
261
|
+
MovingFunds.MovedFundsSweepRequestState.Processed,
|
|
262
|
+
"Spent UTXO not found among correctly spent UTXOs"
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
resolveFraudChallenge(self, walletPublicKey, challenge, sighash);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/// @notice Allows to defeat a pending fraud challenge against a wallet by
|
|
269
|
+
/// proving the sighash and signature were produced for an off-chain
|
|
270
|
+
/// wallet heartbeat message following a strict format.
|
|
271
|
+
/// In order to defeat the challenge the same `walletPublicKey` and
|
|
272
|
+
/// signature (represented by `r`, `s` and `v`) must be provided as
|
|
273
|
+
/// were used to calculate the sighash during heartbeat message
|
|
274
|
+
/// signing. The fraud challenge defeat attempt will only succeed if
|
|
275
|
+
/// the signed message follows a strict format required for
|
|
276
|
+
/// heartbeat messages. If successfully defeated, the fraud
|
|
277
|
+
/// challenge is marked as resolved and the amount of ether
|
|
278
|
+
/// deposited by the challenger is sent to the treasury.
|
|
279
|
+
/// @param walletPublicKey The public key of the wallet in the uncompressed
|
|
280
|
+
/// and unprefixed format (64 bytes),
|
|
281
|
+
/// @param heartbeatMessage Off-chain heartbeat message meeting the heartbeat
|
|
282
|
+
/// message format requirements which produces sighash used to
|
|
283
|
+
/// generate the ECDSA signature that is the subject of the fraud
|
|
284
|
+
/// claim.
|
|
285
|
+
/// @dev Requirements:
|
|
286
|
+
/// - `walletPublicKey` and `sighash` calculated as
|
|
287
|
+
/// `hash256(heartbeatMessage)` must identify an open fraud challenge,
|
|
288
|
+
/// - `heartbeatMessage` must follow a strict format of heartbeat
|
|
289
|
+
/// messages.
|
|
290
|
+
function defeatFraudChallengeWithHeartbeat(
|
|
291
|
+
BridgeState.Storage storage self,
|
|
292
|
+
bytes calldata walletPublicKey,
|
|
293
|
+
bytes calldata heartbeatMessage
|
|
294
|
+
) external {
|
|
295
|
+
bytes32 sighash = heartbeatMessage.hash256();
|
|
296
|
+
|
|
297
|
+
uint256 challengeKey = uint256(
|
|
298
|
+
keccak256(abi.encodePacked(walletPublicKey, sighash))
|
|
299
|
+
);
|
|
300
|
+
|
|
301
|
+
FraudChallenge storage challenge = self.fraudChallenges[challengeKey];
|
|
302
|
+
|
|
303
|
+
require(challenge.reportedAt > 0, "Fraud challenge does not exist");
|
|
304
|
+
require(
|
|
305
|
+
!challenge.resolved,
|
|
306
|
+
"Fraud challenge has already been resolved"
|
|
307
|
+
);
|
|
308
|
+
|
|
309
|
+
require(
|
|
310
|
+
Heartbeat.isValidHeartbeatMessage(heartbeatMessage),
|
|
311
|
+
"Not a valid heartbeat message"
|
|
312
|
+
);
|
|
313
|
+
|
|
314
|
+
resolveFraudChallenge(self, walletPublicKey, challenge, sighash);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/// @notice Called only for successfully defeated fraud challenges.
|
|
318
|
+
/// The fraud challenge is marked as resolved and the amount of
|
|
319
|
+
/// ether deposited by the challenger is sent to the treasury.
|
|
320
|
+
/// @dev Requirements:
|
|
321
|
+
/// - Must be called only for successfully defeated fraud challenges.
|
|
322
|
+
function resolveFraudChallenge(
|
|
323
|
+
BridgeState.Storage storage self,
|
|
324
|
+
bytes calldata walletPublicKey,
|
|
325
|
+
FraudChallenge storage challenge,
|
|
326
|
+
bytes32 sighash
|
|
327
|
+
) internal {
|
|
328
|
+
// Mark the challenge as resolved as it was successfully defeated
|
|
329
|
+
challenge.resolved = true;
|
|
330
|
+
|
|
331
|
+
// Send the ether deposited by the challenger to the treasury
|
|
332
|
+
/* solhint-disable avoid-low-level-calls */
|
|
333
|
+
// slither-disable-next-line low-level-calls,unchecked-lowlevel,arbitrary-send
|
|
334
|
+
self.treasury.call{gas: 100000, value: challenge.depositAmount}("");
|
|
335
|
+
/* solhint-enable avoid-low-level-calls */
|
|
336
|
+
|
|
337
|
+
bytes memory compressedWalletPublicKey = EcdsaLib.compressPublicKey(
|
|
338
|
+
walletPublicKey.slice32(0),
|
|
339
|
+
walletPublicKey.slice32(32)
|
|
340
|
+
);
|
|
341
|
+
bytes20 walletPubKeyHash = compressedWalletPublicKey.hash160View();
|
|
342
|
+
|
|
343
|
+
// slither-disable-next-line reentrancy-events
|
|
344
|
+
emit FraudChallengeDefeated(walletPubKeyHash, sighash);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/// @notice Notifies about defeat timeout for the given fraud challenge.
|
|
348
|
+
/// Can be called only if there was a fraud challenge identified by
|
|
349
|
+
/// the provided `walletPublicKey` and `sighash` and it was not
|
|
350
|
+
/// defeated on time. The amount of time that needs to pass after
|
|
351
|
+
/// a fraud challenge is reported is indicated by the
|
|
352
|
+
/// `challengeDefeatTimeout`. After a successful fraud challenge
|
|
353
|
+
/// defeat timeout notification the fraud challenge is marked as
|
|
354
|
+
/// resolved, the stake of each operator is slashed, the ether
|
|
355
|
+
/// deposited is returned to the challenger and the challenger is
|
|
356
|
+
/// rewarded.
|
|
357
|
+
/// @param walletPublicKey The public key of the wallet in the uncompressed
|
|
358
|
+
/// and unprefixed format (64 bytes).
|
|
359
|
+
/// @param walletMembersIDs Identifiers of the wallet signing group members.
|
|
360
|
+
/// @param preimageSha256 The hash that was generated by applying SHA-256
|
|
361
|
+
/// one time over the preimage used during input signing. The preimage
|
|
362
|
+
/// is a serialized subset of the transaction and its structure
|
|
363
|
+
/// depends on the transaction input (see BIP-143 for reference).
|
|
364
|
+
/// Notice that applying SHA-256 over the `preimageSha256` results
|
|
365
|
+
/// in `sighash`. The path from `preimage` to `sighash` looks like
|
|
366
|
+
/// this:
|
|
367
|
+
/// preimage -> (SHA-256) -> preimageSha256 -> (SHA-256) -> sighash.
|
|
368
|
+
/// @dev Requirements:
|
|
369
|
+
/// - The wallet must be in the Live or MovingFunds or Closing or
|
|
370
|
+
/// Terminated state,
|
|
371
|
+
/// - The `walletPublicKey` and `sighash` calculated from
|
|
372
|
+
/// `preimageSha256` must identify an open fraud challenge,
|
|
373
|
+
/// - The expression `keccak256(abi.encode(walletMembersIDs))` must
|
|
374
|
+
/// be exactly the same as the hash stored under `membersIdsHash`
|
|
375
|
+
/// for the given `walletID`. Those IDs are not directly stored
|
|
376
|
+
/// in the contract for gas efficiency purposes but they can be
|
|
377
|
+
/// read from appropriate `DkgResultSubmitted` and `DkgResultApproved`
|
|
378
|
+
/// events of the `WalletRegistry` contract,
|
|
379
|
+
/// - The amount of time indicated by `challengeDefeatTimeout` must pass
|
|
380
|
+
/// after the challenge was reported.
|
|
381
|
+
function notifyFraudChallengeDefeatTimeout(
|
|
382
|
+
BridgeState.Storage storage self,
|
|
383
|
+
bytes calldata walletPublicKey,
|
|
384
|
+
uint32[] calldata walletMembersIDs,
|
|
385
|
+
bytes memory preimageSha256
|
|
386
|
+
) external {
|
|
387
|
+
bytes32 sighash = sha256(preimageSha256);
|
|
388
|
+
|
|
389
|
+
uint256 challengeKey = uint256(
|
|
390
|
+
keccak256(abi.encodePacked(walletPublicKey, sighash))
|
|
391
|
+
);
|
|
392
|
+
|
|
393
|
+
FraudChallenge storage challenge = self.fraudChallenges[challengeKey];
|
|
394
|
+
|
|
395
|
+
require(challenge.reportedAt > 0, "Fraud challenge does not exist");
|
|
396
|
+
|
|
397
|
+
require(
|
|
398
|
+
!challenge.resolved,
|
|
399
|
+
"Fraud challenge has already been resolved"
|
|
400
|
+
);
|
|
401
|
+
|
|
402
|
+
require(
|
|
403
|
+
/* solhint-disable-next-line not-rely-on-time */
|
|
404
|
+
block.timestamp >=
|
|
405
|
+
challenge.reportedAt + self.fraudChallengeDefeatTimeout,
|
|
406
|
+
"Fraud challenge defeat period did not time out yet"
|
|
407
|
+
);
|
|
408
|
+
|
|
409
|
+
challenge.resolved = true;
|
|
410
|
+
// Return the ether deposited by the challenger
|
|
411
|
+
/* solhint-disable avoid-low-level-calls */
|
|
412
|
+
// slither-disable-next-line low-level-calls,unchecked-lowlevel
|
|
413
|
+
challenge.challenger.call{gas: 100000, value: challenge.depositAmount}(
|
|
414
|
+
""
|
|
415
|
+
);
|
|
416
|
+
/* solhint-enable avoid-low-level-calls */
|
|
417
|
+
|
|
418
|
+
bytes memory compressedWalletPublicKey = EcdsaLib.compressPublicKey(
|
|
419
|
+
walletPublicKey.slice32(0),
|
|
420
|
+
walletPublicKey.slice32(32)
|
|
421
|
+
);
|
|
422
|
+
bytes20 walletPubKeyHash = compressedWalletPublicKey.hash160View();
|
|
423
|
+
|
|
424
|
+
Wallets.Wallet storage wallet = self.registeredWallets[
|
|
425
|
+
walletPubKeyHash
|
|
426
|
+
];
|
|
427
|
+
|
|
428
|
+
Wallets.WalletState walletState = wallet.state;
|
|
429
|
+
|
|
430
|
+
if (
|
|
431
|
+
walletState == Wallets.WalletState.Live ||
|
|
432
|
+
walletState == Wallets.WalletState.MovingFunds ||
|
|
433
|
+
walletState == Wallets.WalletState.Closing
|
|
434
|
+
) {
|
|
435
|
+
self.terminateWallet(walletPubKeyHash);
|
|
436
|
+
|
|
437
|
+
self.ecdsaWalletRegistry.seize(
|
|
438
|
+
self.fraudSlashingAmount,
|
|
439
|
+
self.fraudNotifierRewardMultiplier,
|
|
440
|
+
challenge.challenger,
|
|
441
|
+
wallet.ecdsaWalletID,
|
|
442
|
+
walletMembersIDs
|
|
443
|
+
);
|
|
444
|
+
} else if (walletState == Wallets.WalletState.Terminated) {
|
|
445
|
+
// This is a special case when the wallet was already terminated
|
|
446
|
+
// due to a previous deliberate protocol violation. In that
|
|
447
|
+
// case, this function should be still callable for other fraud
|
|
448
|
+
// challenges timeouts in order to let the challenger unlock its
|
|
449
|
+
// ETH deposit back. However, the wallet termination logic is
|
|
450
|
+
// not called and the challenger is not rewarded.
|
|
451
|
+
} else {
|
|
452
|
+
revert(
|
|
453
|
+
"Wallet must be in Live or MovingFunds or Closing or Terminated state"
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// slither-disable-next-line reentrancy-events
|
|
458
|
+
emit FraudChallengeDefeatTimedOut(walletPubKeyHash, sighash);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/// @notice Extracts the UTXO keys from the given preimage used during
|
|
462
|
+
/// signing of a witness input.
|
|
463
|
+
/// @param preimage The preimage which produces sighash used to generate the
|
|
464
|
+
/// ECDSA signature that is the subject of the fraud claim. It is a
|
|
465
|
+
/// serialized subset of the transaction. The exact subset used as
|
|
466
|
+
/// the preimage depends on the transaction input the signature is
|
|
467
|
+
/// produced for. See BIP-143 for reference
|
|
468
|
+
/// @return utxoKey UTXO key that identifies spent input.
|
|
469
|
+
function extractUtxoKeyFromWitnessPreimage(bytes calldata preimage)
|
|
470
|
+
internal
|
|
471
|
+
pure
|
|
472
|
+
returns (uint256 utxoKey)
|
|
473
|
+
{
|
|
474
|
+
// The expected structure of the preimage created during signing of a
|
|
475
|
+
// witness input:
|
|
476
|
+
// - transaction version (4 bytes)
|
|
477
|
+
// - hash of previous outpoints of all inputs (32 bytes)
|
|
478
|
+
// - hash of sequences of all inputs (32 bytes)
|
|
479
|
+
// - outpoint (hash + index) of the input being signed (36 bytes)
|
|
480
|
+
// - the unlocking script of the input (variable length)
|
|
481
|
+
// - value of the outpoint (8 bytes)
|
|
482
|
+
// - sequence of the input being signed (4 bytes)
|
|
483
|
+
// - hash of all outputs (32 bytes)
|
|
484
|
+
// - transaction locktime (4 bytes)
|
|
485
|
+
// - sighash type (4 bytes)
|
|
486
|
+
|
|
487
|
+
// See Bitcoin's BIP-143 for reference:
|
|
488
|
+
// https://github.com/bitcoin/bips/blob/master/bip-0143.mediawiki.
|
|
489
|
+
|
|
490
|
+
// The outpoint (hash and index) is located at the constant offset of
|
|
491
|
+
// 68 (4 + 32 + 32).
|
|
492
|
+
bytes32 outpointTxHash = preimage.extractInputTxIdLeAt(68);
|
|
493
|
+
uint32 outpointIndex = BTCUtils.reverseUint32(
|
|
494
|
+
uint32(preimage.extractTxIndexLeAt(68))
|
|
495
|
+
);
|
|
496
|
+
|
|
497
|
+
return
|
|
498
|
+
uint256(keccak256(abi.encodePacked(outpointTxHash, outpointIndex)));
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/// @notice Extracts the UTXO key from the given preimage used during
|
|
502
|
+
/// signing of a non-witness input.
|
|
503
|
+
/// @param preimage The preimage which produces sighash used to generate the
|
|
504
|
+
/// ECDSA signature that is the subject of the fraud claim. It is a
|
|
505
|
+
/// serialized subset of the transaction. The exact subset used as
|
|
506
|
+
/// the preimage depends on the transaction input the signature is
|
|
507
|
+
/// produced for. See BIP-143 for reference.
|
|
508
|
+
/// @return utxoKey UTXO key that identifies spent input.
|
|
509
|
+
function extractUtxoKeyFromNonWitnessPreimage(bytes calldata preimage)
|
|
510
|
+
internal
|
|
511
|
+
pure
|
|
512
|
+
returns (uint256 utxoKey)
|
|
513
|
+
{
|
|
514
|
+
// The expected structure of the preimage created during signing of a
|
|
515
|
+
// non-witness input:
|
|
516
|
+
// - transaction version (4 bytes)
|
|
517
|
+
// - number of inputs written as compactSize uint (1 byte, 3 bytes,
|
|
518
|
+
// 5 bytes or 9 bytes)
|
|
519
|
+
// - for each input
|
|
520
|
+
// - outpoint (hash and index) (36 bytes)
|
|
521
|
+
// - unlocking script for the input being signed (variable length)
|
|
522
|
+
// or `00` for all other inputs (1 byte)
|
|
523
|
+
// - input sequence (4 bytes)
|
|
524
|
+
// - number of outputs written as compactSize uint (1 byte, 3 bytes,
|
|
525
|
+
// 5 bytes or 9 bytes)
|
|
526
|
+
// - outputs (variable length)
|
|
527
|
+
// - transaction locktime (4 bytes)
|
|
528
|
+
// - sighash type (4 bytes)
|
|
529
|
+
|
|
530
|
+
// See example for reference:
|
|
531
|
+
// https://en.bitcoin.it/wiki/OP_CHECKSIG#Code_samples_and_raw_dumps.
|
|
532
|
+
|
|
533
|
+
// The input data begins at the constant offset of 4 (the first 4 bytes
|
|
534
|
+
// are for the transaction version).
|
|
535
|
+
(uint256 inputsCompactSizeUintLength, uint256 inputsCount) = preimage
|
|
536
|
+
.parseVarIntAt(4);
|
|
537
|
+
|
|
538
|
+
// To determine the first input starting index, we must jump 4 bytes
|
|
539
|
+
// over the transaction version length and the compactSize uint which
|
|
540
|
+
// prepends the input vector. One byte must be added because
|
|
541
|
+
// `BtcUtils.parseVarInt` does not include compactSize uint tag in the
|
|
542
|
+
// returned length.
|
|
543
|
+
//
|
|
544
|
+
// For >= 0 && <= 252, `BTCUtils.determineVarIntDataLengthAt`
|
|
545
|
+
// returns `0`, so we jump over one byte of compactSize uint.
|
|
546
|
+
//
|
|
547
|
+
// For >= 253 && <= 0xffff there is `0xfd` tag,
|
|
548
|
+
// `BTCUtils.determineVarIntDataLengthAt` returns `2` (no
|
|
549
|
+
// tag byte included) so we need to jump over 1+2 bytes of
|
|
550
|
+
// compactSize uint.
|
|
551
|
+
//
|
|
552
|
+
// Please refer `BTCUtils` library and compactSize uint
|
|
553
|
+
// docs in `BitcoinTx` library for more details.
|
|
554
|
+
uint256 inputStartingIndex = 4 + 1 + inputsCompactSizeUintLength;
|
|
555
|
+
|
|
556
|
+
for (uint256 i = 0; i < inputsCount; i++) {
|
|
557
|
+
uint256 inputLength = preimage.determineInputLengthAt(
|
|
558
|
+
inputStartingIndex
|
|
559
|
+
);
|
|
560
|
+
|
|
561
|
+
(, uint256 scriptSigLength) = preimage.extractScriptSigLenAt(
|
|
562
|
+
inputStartingIndex
|
|
563
|
+
);
|
|
564
|
+
|
|
565
|
+
if (scriptSigLength > 0) {
|
|
566
|
+
// The input this preimage was generated for was found.
|
|
567
|
+
// All the other inputs in the preimage are marked with a null
|
|
568
|
+
// scriptSig ("00") which has length of 1.
|
|
569
|
+
bytes32 outpointTxHash = preimage.extractInputTxIdLeAt(
|
|
570
|
+
inputStartingIndex
|
|
571
|
+
);
|
|
572
|
+
uint32 outpointIndex = BTCUtils.reverseUint32(
|
|
573
|
+
uint32(preimage.extractTxIndexLeAt(inputStartingIndex))
|
|
574
|
+
);
|
|
575
|
+
|
|
576
|
+
utxoKey = uint256(
|
|
577
|
+
keccak256(abi.encodePacked(outpointTxHash, outpointIndex))
|
|
578
|
+
);
|
|
579
|
+
|
|
580
|
+
break;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
inputStartingIndex += inputLength;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
return utxoKey;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
/// @notice Extracts the sighash type from the given preimage.
|
|
590
|
+
/// @param preimage Serialized subset of the transaction. See BIP-143 for
|
|
591
|
+
/// reference.
|
|
592
|
+
/// @dev Sighash type is stored as the last 4 bytes in the preimage (little
|
|
593
|
+
/// endian).
|
|
594
|
+
/// @return sighashType Sighash type as a 32-bit integer.
|
|
595
|
+
function extractSighashType(bytes calldata preimage)
|
|
596
|
+
internal
|
|
597
|
+
pure
|
|
598
|
+
returns (uint32 sighashType)
|
|
599
|
+
{
|
|
600
|
+
bytes4 sighashTypeBytes = preimage.slice4(preimage.length - 4);
|
|
601
|
+
uint32 sighashTypeLE = uint32(sighashTypeBytes);
|
|
602
|
+
return sighashTypeLE.reverseUint32();
|
|
603
|
+
}
|
|
604
|
+
}
|