@keep-network/tbtc-v2 0.1.0 → 0.1.1-dev

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (296) hide show
  1. package/README.adoc +12 -0
  2. package/artifacts/.chainId +1 -1
  3. package/artifacts/Bank.json +807 -0
  4. package/artifacts/Bridge.json +2300 -0
  5. package/artifacts/Deposit.json +117 -0
  6. package/artifacts/DepositSweep.json +77 -0
  7. package/artifacts/EcdsaDkgValidator.json +532 -0
  8. package/artifacts/EcdsaInactivity.json +156 -0
  9. package/artifacts/EcdsaSortitionPool.json +1004 -0
  10. package/artifacts/Fraud.json +164 -0
  11. package/artifacts/KeepRegistry.json +99 -0
  12. package/artifacts/KeepStake.json +286 -0
  13. package/artifacts/KeepToken.json +711 -0
  14. package/artifacts/KeepTokenStaking.json +483 -0
  15. package/artifacts/MovingFunds.json +249 -0
  16. package/artifacts/NuCypherStakingEscrow.json +256 -0
  17. package/artifacts/NuCypherToken.json +711 -0
  18. package/artifacts/RandomBeaconStub.json +141 -0
  19. package/artifacts/Redemption.json +174 -0
  20. package/artifacts/ReimbursementPool.json +509 -0
  21. package/artifacts/Relay.json +123 -0
  22. package/artifacts/T.json +1148 -0
  23. package/artifacts/TBTC.json +36 -35
  24. package/artifacts/TBTCToken.json +738 -0
  25. package/artifacts/TBTCVault.json +691 -0
  26. package/artifacts/TokenStaking.json +2288 -0
  27. package/artifacts/TokenholderGovernor.json +1795 -0
  28. package/artifacts/TokenholderTimelock.json +1058 -0
  29. package/artifacts/VendingMachine.json +34 -33
  30. package/artifacts/VendingMachineKeep.json +400 -0
  31. package/artifacts/VendingMachineNuCypher.json +400 -0
  32. package/artifacts/WalletRegistry.json +1843 -0
  33. package/artifacts/WalletRegistryGovernance.json +2754 -0
  34. package/artifacts/Wallets.json +186 -0
  35. package/artifacts/solcInputs/5e62cff1ead0900b07facca4b559e818.json +314 -0
  36. package/build/contracts/GovernanceUtils.sol/GovernanceUtils.dbg.json +1 -1
  37. package/build/contracts/GovernanceUtils.sol/GovernanceUtils.json +2 -2
  38. package/build/contracts/bank/Bank.sol/Bank.dbg.json +4 -0
  39. package/build/contracts/bank/Bank.sol/Bank.json +542 -0
  40. package/build/contracts/bank/IReceiveBalanceApproval.sol/IReceiveBalanceApproval.dbg.json +4 -0
  41. package/build/contracts/bank/IReceiveBalanceApproval.sol/IReceiveBalanceApproval.json +34 -0
  42. package/build/contracts/bridge/BitcoinTx.sol/BitcoinTx.dbg.json +4 -0
  43. package/build/contracts/bridge/BitcoinTx.sol/BitcoinTx.json +10 -0
  44. package/build/contracts/bridge/Bridge.sol/Bridge.dbg.json +4 -0
  45. package/build/contracts/bridge/Bridge.sol/Bridge.json +2686 -0
  46. package/build/contracts/bridge/BridgeState.sol/BridgeState.dbg.json +4 -0
  47. package/build/contracts/bridge/BridgeState.sol/BridgeState.json +226 -0
  48. package/build/contracts/bridge/Deposit.sol/Deposit.dbg.json +4 -0
  49. package/build/contracts/bridge/Deposit.sol/Deposit.json +72 -0
  50. package/build/contracts/bridge/DepositSweep.sol/DepositSweep.dbg.json +4 -0
  51. package/build/contracts/bridge/DepositSweep.sol/DepositSweep.json +30 -0
  52. package/build/contracts/bridge/EcdsaLib.sol/EcdsaLib.dbg.json +4 -0
  53. package/build/contracts/bridge/EcdsaLib.sol/EcdsaLib.json +10 -0
  54. package/build/contracts/bridge/Fraud.sol/Fraud.dbg.json +4 -0
  55. package/build/contracts/bridge/Fraud.sol/Fraud.json +86 -0
  56. package/build/contracts/bridge/Heartbeat.sol/Heartbeat.dbg.json +4 -0
  57. package/build/contracts/bridge/Heartbeat.sol/Heartbeat.json +10 -0
  58. package/build/contracts/bridge/IRelay.sol/IRelay.dbg.json +4 -0
  59. package/build/contracts/bridge/IRelay.sol/IRelay.json +37 -0
  60. package/build/contracts/bridge/MovingFunds.sol/MovingFunds.dbg.json +4 -0
  61. package/build/contracts/bridge/MovingFunds.sol/MovingFunds.json +138 -0
  62. package/build/contracts/bridge/Redemption.sol/OutboundTx.dbg.json +4 -0
  63. package/build/contracts/bridge/Redemption.sol/OutboundTx.json +10 -0
  64. package/build/contracts/bridge/Redemption.sol/Redemption.dbg.json +4 -0
  65. package/build/contracts/bridge/Redemption.sol/Redemption.json +92 -0
  66. package/build/contracts/bridge/VendingMachine.sol/VendingMachine.dbg.json +1 -1
  67. package/build/contracts/bridge/VendingMachine.sol/VendingMachine.json +2 -2
  68. package/build/contracts/bridge/Wallets.sol/Wallets.dbg.json +4 -0
  69. package/build/contracts/bridge/Wallets.sol/Wallets.json +112 -0
  70. package/build/contracts/token/TBTC.sol/TBTC.dbg.json +1 -1
  71. package/build/contracts/token/TBTC.sol/TBTC.json +4 -4
  72. package/build/contracts/vault/DonationVault.sol/DonationVault.dbg.json +4 -0
  73. package/build/contracts/vault/DonationVault.sol/DonationVault.json +108 -0
  74. package/build/contracts/vault/IVault.sol/IVault.dbg.json +4 -0
  75. package/build/contracts/vault/IVault.sol/IVault.json +52 -0
  76. package/build/contracts/vault/TBTCVault.sol/TBTCVault.dbg.json +4 -0
  77. package/build/contracts/vault/TBTCVault.sol/TBTCVault.json +449 -0
  78. package/contracts/GovernanceUtils.sol +4 -4
  79. package/contracts/bank/Bank.sol +436 -0
  80. package/contracts/bank/IReceiveBalanceApproval.sol +45 -0
  81. package/contracts/bridge/BitcoinTx.sol +326 -0
  82. package/contracts/bridge/Bridge.sol +1793 -0
  83. package/contracts/bridge/BridgeState.sol +739 -0
  84. package/contracts/bridge/Deposit.sol +269 -0
  85. package/contracts/bridge/DepositSweep.sol +574 -0
  86. package/contracts/bridge/EcdsaLib.sol +45 -0
  87. package/contracts/bridge/Fraud.sol +579 -0
  88. package/contracts/bridge/Heartbeat.sol +112 -0
  89. package/contracts/bridge/IRelay.sol +28 -0
  90. package/contracts/bridge/MovingFunds.sol +1077 -0
  91. package/contracts/bridge/Redemption.sol +1020 -0
  92. package/contracts/bridge/VendingMachine.sol +2 -2
  93. package/contracts/bridge/Wallets.sol +719 -0
  94. package/contracts/hardhat-dependency-compiler/.hardhat-dependency-compiler +1 -0
  95. package/contracts/hardhat-dependency-compiler/@keep-network/ecdsa/contracts/WalletRegistry.sol +3 -0
  96. package/contracts/hardhat-dependency-compiler/@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol +3 -0
  97. package/contracts/hardhat-dependency-compiler/@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol +3 -0
  98. package/contracts/token/TBTC.sol +1 -1
  99. package/contracts/vault/DonationVault.sol +125 -0
  100. package/contracts/vault/IVault.sol +44 -0
  101. package/contracts/vault/TBTCVault.sol +305 -0
  102. package/deploy/00_resolve_relay.ts +28 -0
  103. package/deploy/00_resolve_tbtc_v1_token.ts +1 -1
  104. package/deploy/01_deploy_tbtc_v2_token.ts +8 -1
  105. package/deploy/02_deploy_vending_machine.ts +7 -0
  106. package/deploy/{03_transfer_roles.ts → 03_transfer_vending_machine_roles.ts} +1 -1
  107. package/deploy/04_deploy_bank.ts +27 -0
  108. package/deploy/05_deploy_bridge.ts +80 -0
  109. package/deploy/06_deploy_tbtc_vault.ts +30 -0
  110. package/deploy/07_bank_update_bridge.ts +19 -0
  111. package/deploy/08_transfer_bank_ownership.ts +15 -0
  112. package/deploy/09_transfer_tbtc_vault_ownership.ts +15 -0
  113. package/deploy/10_transfer_bridge_governance.ts +20 -0
  114. package/deploy/11_initialize_wallet_owner.ts +18 -0
  115. package/deploy/11_transfer_proxy_admin_ownership.ts +30 -0
  116. package/deploy/12_deploy_proxy_admin_with_deputy.ts +33 -0
  117. package/export/deploy/00_resolve_relay.js +24 -0
  118. package/export/deploy/00_resolve_tbtc_v1_token.js +24 -0
  119. package/export/deploy/01_deploy_tbtc_v2_token.js +19 -0
  120. package/export/deploy/02_deploy_vending_machine.js +25 -0
  121. package/export/deploy/03_transfer_vending_machine_roles.js +19 -0
  122. package/export/deploy/04_deploy_bank.js +21 -0
  123. package/export/deploy/05_deploy_bridge.js +69 -0
  124. package/export/deploy/06_deploy_tbtc_vault.js +24 -0
  125. package/export/deploy/07_bank_update_bridge.js +13 -0
  126. package/export/deploy/08_transfer_bank_ownership.js +11 -0
  127. package/export/deploy/09_transfer_tbtc_vault_ownership.js +11 -0
  128. package/export/deploy/10_transfer_bridge_governance.js +11 -0
  129. package/export/deploy/11_initialize_wallet_owner.js +14 -0
  130. package/export/deploy/11_transfer_proxy_admin_ownership.js +23 -0
  131. package/export/deploy/12_deploy_proxy_admin_with_deputy.js +22 -0
  132. package/export/hardhat.config.js +169 -0
  133. package/export/test/bank/Bank.test.js +1012 -0
  134. package/export/test/bridge/Bridge.Deployment.test.js +76 -0
  135. package/export/test/bridge/Bridge.Deposit.test.js +1834 -0
  136. package/export/test/bridge/Bridge.Frauds.test.js +1349 -0
  137. package/export/test/bridge/Bridge.MovingFunds.test.js +2437 -0
  138. package/export/test/bridge/Bridge.Parameters.test.js +400 -0
  139. package/export/test/bridge/Bridge.Redemption.test.js +2523 -0
  140. package/export/test/bridge/Bridge.Vaults.test.js +74 -0
  141. package/export/test/bridge/Bridge.Wallets.test.js +1017 -0
  142. package/export/test/bridge/EcdsaLib.test.js +46 -0
  143. package/export/test/bridge/Heartbeat.test.js +77 -0
  144. package/export/test/bridge/VendingMachine.Upgrade.test.js +160 -0
  145. package/export/test/bridge/VendingMachine.test.js +762 -0
  146. package/export/test/data/deposit-sweep.js +655 -0
  147. package/export/test/data/ecdsa.js +18 -0
  148. package/export/test/data/fraud.js +158 -0
  149. package/export/test/data/moving-funds.js +815 -0
  150. package/export/test/data/redemption.js +1011 -0
  151. package/export/test/fixtures/bridge.js +54 -0
  152. package/export/test/fixtures/index.js +57 -0
  153. package/export/test/helpers/contract-test-helpers.js +18 -0
  154. package/export/test/integration/Slashing.test.js +279 -0
  155. package/export/test/integration/WalleCreation.test.js +66 -0
  156. package/export/test/integration/utils/ecdsa-wallet-registry.js +137 -0
  157. package/export/test/integration/utils/fixture.js +77 -0
  158. package/export/test/integration/utils/gas.js +36 -0
  159. package/export/test/integration/utils/random-beacon.js +26 -0
  160. package/export/test/integration/utils/staking.js +19 -0
  161. package/export/test/vault/DonationVault.test.js +202 -0
  162. package/export/test/vault/TBTCVault.Redemption.test.js +357 -0
  163. package/export/test/vault/TBTCVault.test.js +768 -0
  164. package/export/typechain/BTCUtils.js +2 -0
  165. package/export/typechain/Bank.js +2 -0
  166. package/export/typechain/BankStub.js +2 -0
  167. package/export/typechain/Bridge.js +2 -0
  168. package/export/typechain/BridgeState.js +2 -0
  169. package/export/typechain/BridgeStub.js +2 -0
  170. package/export/typechain/Deposit.js +2 -0
  171. package/export/typechain/DepositSweep.js +2 -0
  172. package/export/typechain/DonationVault.js +2 -0
  173. package/export/typechain/ERC165.js +2 -0
  174. package/export/typechain/ERC1967Proxy.js +2 -0
  175. package/export/typechain/ERC1967Upgrade.js +2 -0
  176. package/export/typechain/ERC20WithPermit.js +2 -0
  177. package/export/typechain/ERC721.js +2 -0
  178. package/export/typechain/EcdsaAuthorization.js +2 -0
  179. package/export/typechain/EcdsaDkg.js +2 -0
  180. package/export/typechain/EcdsaDkgValidator.js +2 -0
  181. package/export/typechain/EcdsaInactivity.js +2 -0
  182. package/export/typechain/Fraud.js +2 -0
  183. package/export/typechain/Governable.js +2 -0
  184. package/export/typechain/HeartbeatStub.js +2 -0
  185. package/export/typechain/IApplication.js +2 -0
  186. package/export/typechain/IApproveAndCall.js +2 -0
  187. package/export/typechain/IBeacon.js +2 -0
  188. package/export/typechain/IERC165.js +2 -0
  189. package/export/typechain/IERC1822Proxiable.js +2 -0
  190. package/export/typechain/IERC20.js +2 -0
  191. package/export/typechain/IERC20Metadata.js +2 -0
  192. package/export/typechain/IERC20WithPermit.js +2 -0
  193. package/export/typechain/IERC721.js +2 -0
  194. package/export/typechain/IERC721Metadata.js +2 -0
  195. package/export/typechain/IERC721Receiver.js +2 -0
  196. package/export/typechain/IRandomBeacon.js +2 -0
  197. package/export/typechain/IRandomBeaconConsumer.js +2 -0
  198. package/export/typechain/IReceiveApproval.js +2 -0
  199. package/export/typechain/IReceiveBalanceApproval.js +2 -0
  200. package/export/typechain/IRelay.js +2 -0
  201. package/export/typechain/IStaking.js +2 -0
  202. package/export/typechain/IVault.js +2 -0
  203. package/export/typechain/IWalletOwner.js +2 -0
  204. package/export/typechain/IWalletRegistry.js +2 -0
  205. package/export/typechain/Initializable.js +2 -0
  206. package/export/typechain/MisfundRecovery.js +2 -0
  207. package/export/typechain/MovingFunds.js +2 -0
  208. package/export/typechain/Ownable.js +2 -0
  209. package/export/typechain/Proxy.js +2 -0
  210. package/export/typechain/ProxyAdmin.js +2 -0
  211. package/export/typechain/ReceiveApprovalStub.js +2 -0
  212. package/export/typechain/Redemption.js +2 -0
  213. package/export/typechain/Reimbursable.js +2 -0
  214. package/export/typechain/ReimbursementPool.js +2 -0
  215. package/export/typechain/Rewards.js +2 -0
  216. package/export/typechain/SortitionPool.js +2 -0
  217. package/export/typechain/SortitionTree.js +2 -0
  218. package/export/typechain/TBTC.js +2 -0
  219. package/export/typechain/TBTCVault.js +2 -0
  220. package/export/typechain/TestERC20.js +2 -0
  221. package/export/typechain/TestERC721.js +2 -0
  222. package/export/typechain/TestEcdsaLib.js +2 -0
  223. package/export/typechain/TestRelay.js +2 -0
  224. package/export/typechain/TransparentUpgradeableProxy.js +2 -0
  225. package/export/typechain/VendingMachine.js +2 -0
  226. package/export/typechain/WalletRegistry.js +2 -0
  227. package/export/typechain/Wallets.js +2 -0
  228. package/export/typechain/common.js +2 -0
  229. package/export/typechain/factories/BTCUtils__factory.js +94 -0
  230. package/export/typechain/factories/BankStub__factory.js +586 -0
  231. package/export/typechain/factories/Bank__factory.js +573 -0
  232. package/export/typechain/factories/BridgeState__factory.js +257 -0
  233. package/export/typechain/factories/BridgeStub__factory.js +2912 -0
  234. package/export/typechain/factories/Bridge__factory.js +2526 -0
  235. package/export/typechain/factories/DepositSweep__factory.js +61 -0
  236. package/export/typechain/factories/Deposit__factory.js +103 -0
  237. package/export/typechain/factories/DonationVault__factory.js +139 -0
  238. package/export/typechain/factories/ERC165__factory.js +38 -0
  239. package/export/typechain/factories/ERC1967Proxy__factory.js +111 -0
  240. package/export/typechain/factories/ERC1967Upgrade__factory.js +64 -0
  241. package/export/typechain/factories/ERC20WithPermit__factory.js +524 -0
  242. package/export/typechain/factories/ERC721__factory.js +388 -0
  243. package/export/typechain/factories/EcdsaAuthorization__factory.js +211 -0
  244. package/export/typechain/factories/EcdsaDkgValidator__factory.js +441 -0
  245. package/export/typechain/factories/EcdsaDkg__factory.js +192 -0
  246. package/export/typechain/factories/EcdsaInactivity__factory.js +134 -0
  247. package/export/typechain/factories/Fraud__factory.js +117 -0
  248. package/export/typechain/factories/Governable__factory.js +64 -0
  249. package/export/typechain/factories/HeartbeatStub__factory.js +61 -0
  250. package/export/typechain/factories/IApplication__factory.js +152 -0
  251. package/export/typechain/factories/IApproveAndCall__factory.js +48 -0
  252. package/export/typechain/factories/IBeacon__factory.js +32 -0
  253. package/export/typechain/factories/IERC165__factory.js +38 -0
  254. package/export/typechain/factories/IERC1822Proxiable__factory.js +32 -0
  255. package/export/typechain/factories/IERC20Metadata__factory.js +241 -0
  256. package/export/typechain/factories/IERC20WithPermit__factory.js +389 -0
  257. package/export/typechain/factories/IERC20__factory.js +202 -0
  258. package/export/typechain/factories/IERC721Metadata__factory.js +349 -0
  259. package/export/typechain/factories/IERC721Receiver__factory.js +53 -0
  260. package/export/typechain/factories/IERC721__factory.js +304 -0
  261. package/export/typechain/factories/IRandomBeaconConsumer__factory.js +37 -0
  262. package/export/typechain/factories/IRandomBeacon__factory.js +32 -0
  263. package/export/typechain/factories/IReceiveApproval__factory.js +47 -0
  264. package/export/typechain/factories/IReceiveBalanceApproval__factory.js +42 -0
  265. package/export/typechain/factories/IRelay__factory.js +45 -0
  266. package/export/typechain/factories/IStaking__factory.js +722 -0
  267. package/export/typechain/factories/IVault__factory.js +60 -0
  268. package/export/typechain/factories/IWalletOwner__factory.js +65 -0
  269. package/export/typechain/factories/IWalletRegistry__factory.js +138 -0
  270. package/export/typechain/factories/Initializable__factory.js +32 -0
  271. package/export/typechain/factories/MisfundRecovery__factory.js +145 -0
  272. package/export/typechain/factories/MovingFunds__factory.js +169 -0
  273. package/export/typechain/factories/Ownable__factory.js +71 -0
  274. package/export/typechain/factories/ProxyAdmin__factory.js +191 -0
  275. package/export/typechain/factories/Proxy__factory.js +27 -0
  276. package/export/typechain/factories/ReceiveApprovalStub__factory.js +127 -0
  277. package/export/typechain/factories/Redemption__factory.js +123 -0
  278. package/export/typechain/factories/Reimbursable__factory.js +58 -0
  279. package/export/typechain/factories/ReimbursementPool__factory.js +350 -0
  280. package/export/typechain/factories/Rewards__factory.js +117 -0
  281. package/export/typechain/factories/SortitionPool__factory.js +610 -0
  282. package/export/typechain/factories/SortitionTree__factory.js +149 -0
  283. package/export/typechain/factories/TBTCVault__factory.js +480 -0
  284. package/export/typechain/factories/TBTC__factory.js +564 -0
  285. package/export/typechain/factories/TestERC20__factory.js +539 -0
  286. package/export/typechain/factories/TestERC721__factory.js +421 -0
  287. package/export/typechain/factories/TestEcdsaLib__factory.js +66 -0
  288. package/export/typechain/factories/TestRelay__factory.js +94 -0
  289. package/export/typechain/factories/TransparentUpgradeableProxy__factory.js +186 -0
  290. package/export/typechain/factories/VendingMachine__factory.js +549 -0
  291. package/export/typechain/factories/WalletRegistry__factory.js +1919 -0
  292. package/export/typechain/factories/Wallets__factory.js +143 -0
  293. package/export/typechain/index.js +132 -0
  294. package/export.json +15932 -503
  295. package/package.json +47 -26
  296. package/artifacts/solcInputs/7cc3eda3cb3ff2522d18b5e7b31ea228.json +0 -104
@@ -0,0 +1,2437 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || function (mod) {
19
+ if (mod && mod.__esModule) return mod;
20
+ var result = {};
21
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
+ __setModuleDefault(result, mod);
23
+ return result;
24
+ };
25
+ var __importDefault = (this && this.__importDefault) || function (mod) {
26
+ return (mod && mod.__esModule) ? mod : { "default": mod };
27
+ };
28
+ Object.defineProperty(exports, "__esModule", { value: true });
29
+ /* eslint-disable no-underscore-dangle */
30
+ const hardhat_1 = require("hardhat");
31
+ const chai_1 = __importStar(require("chai"));
32
+ const smock_1 = require("@defi-wonderland/smock");
33
+ const bridge_1 = __importDefault(require("../fixtures/bridge"));
34
+ const fixtures_1 = require("../fixtures");
35
+ const moving_funds_1 = require("../data/moving-funds");
36
+ const ecdsa_1 = require("../data/ecdsa");
37
+ const deposit_sweep_1 = require("../data/deposit-sweep");
38
+ const contract_test_helpers_1 = require("../helpers/contract-test-helpers");
39
+ chai_1.default.use(smock_1.smock.matchers);
40
+ const { createSnapshot, restoreSnapshot } = hardhat_1.helpers.snapshot;
41
+ const { lastBlockTime, increaseTime } = hardhat_1.helpers.time;
42
+ describe("Bridge - Moving funds", () => {
43
+ let thirdParty;
44
+ let treasury;
45
+ let bank;
46
+ let relay;
47
+ let walletRegistry;
48
+ let bridge;
49
+ let BridgeFactory;
50
+ let movingFundsTimeoutResetDelay;
51
+ let movingFundsTimeout;
52
+ let movingFundsTimeoutSlashingAmount;
53
+ let movingFundsTimeoutNotifierRewardMultiplier;
54
+ let movedFundsSweepTimeout;
55
+ let movedFundsSweepTimeoutSlashingAmount;
56
+ let movedFundsSweepTimeoutNotifierRewardMultiplier;
57
+ before(async () => {
58
+ // eslint-disable-next-line @typescript-eslint/no-extra-semi
59
+ ;
60
+ ({
61
+ thirdParty,
62
+ treasury,
63
+ bank,
64
+ relay,
65
+ walletRegistry,
66
+ bridge,
67
+ BridgeFactory,
68
+ } = await hardhat_1.waffle.loadFixture(bridge_1.default));
69
+ ({
70
+ movingFundsTimeoutResetDelay,
71
+ movingFundsTimeout,
72
+ movingFundsTimeoutSlashingAmount,
73
+ movingFundsTimeoutNotifierRewardMultiplier,
74
+ movedFundsSweepTimeout,
75
+ movedFundsSweepTimeoutSlashingAmount,
76
+ movedFundsSweepTimeoutNotifierRewardMultiplier,
77
+ } = await bridge.movingFundsParameters());
78
+ });
79
+ describe("submitMovingFundsCommitment", () => {
80
+ const walletDraft = {
81
+ ecdsaWalletID: ecdsa_1.ecdsaWalletTestData.walletID,
82
+ mainUtxoHash: hardhat_1.ethers.constants.HashZero,
83
+ pendingRedemptionsValue: 0,
84
+ createdAt: 0,
85
+ movingFundsRequestedAt: 0,
86
+ closingStartedAt: 0,
87
+ pendingMovedFundsSweepRequestsCount: 0,
88
+ state: fixtures_1.walletState.Unknown,
89
+ movingFundsTargetWalletsCommitmentHash: hardhat_1.ethers.constants.HashZero,
90
+ };
91
+ context("when source wallet is in the MovingFunds state", () => {
92
+ before(async () => {
93
+ await createSnapshot();
94
+ await bridge.setWallet(ecdsa_1.ecdsaWalletTestData.pubKeyHash160, {
95
+ ...walletDraft,
96
+ state: fixtures_1.walletState.MovingFunds,
97
+ });
98
+ });
99
+ after(async () => {
100
+ await restoreSnapshot();
101
+ });
102
+ context("when source wallet has no pending redemptions", () => {
103
+ // The wallet created using the `walletDraft` has no pending redemptions
104
+ // by default. No need to do anything here.
105
+ context("when source wallet has no pending moved funds sweep requests", () => {
106
+ // The wallet created using the `walletDraft` has no pending moved
107
+ // funds sweep requests by default. No need to do anything here.
108
+ context("when the commitment was not submitted yet", () => {
109
+ // The wallet created using the `walletDraft` has no commitment
110
+ // submitted by default. No need to do anything here.
111
+ context("when the caller is a member of the source wallet", () => {
112
+ const walletMembersIDs = [1, 2, 3, 4, 5];
113
+ const walletMemberIndex = 2;
114
+ let caller;
115
+ before(async () => {
116
+ await createSnapshot();
117
+ caller = thirdParty;
118
+ walletRegistry.isWalletMember
119
+ .whenCalledWith(ecdsa_1.ecdsaWalletTestData.walletID, walletMembersIDs, caller.address, walletMemberIndex)
120
+ .returns(true);
121
+ });
122
+ after(async () => {
123
+ walletRegistry.isWalletMember.reset();
124
+ await restoreSnapshot();
125
+ });
126
+ context("when passed wallet main UTXO is valid", () => {
127
+ context("when wallet balance is greater than zero", () => {
128
+ // Just an arbitrary main UTXO with value of 26 BTC.
129
+ const mainUtxo = {
130
+ txHash: "0xc9e58780c6c289c25ae1fe293f85a4db4d0af4f305172f2a1868ddd917458bdf",
131
+ txOutputIndex: 0,
132
+ txOutputValue: (0, contract_test_helpers_1.to1ePrecision)(26, 8),
133
+ };
134
+ before(async () => {
135
+ await createSnapshot();
136
+ // Set up a main UTXO for the source wallet.
137
+ await bridge.setWalletMainUtxo(ecdsa_1.ecdsaWalletTestData.pubKeyHash160, mainUtxo);
138
+ });
139
+ after(async () => {
140
+ await restoreSnapshot();
141
+ });
142
+ context("when the expected target wallets count is greater than zero", () => {
143
+ // Just some arbitrary 20-byte hashes to simulate live
144
+ // wallets PKHs. They are ordered in the expected way, i.e.
145
+ // the hashes represented as numbers form a strictly
146
+ // increasing sequence.
147
+ const liveWallets = [
148
+ "0x4b440cb29c80c3f256212d8fdd4f2125366f3c91",
149
+ "0x888f01315e0268bfa05d5e522f8d63f6824d9a96",
150
+ "0xb2a89e53a4227dbe530a52a1c419040735fa636c",
151
+ "0xbf198e8fff0f90af01024153701da99b9bc08dc5",
152
+ "0xffb9e05013f5cd126915bc03d340cc5c1be81862",
153
+ ];
154
+ before(async () => {
155
+ await createSnapshot();
156
+ for (let i = 0; i < liveWallets.length; i++) {
157
+ // eslint-disable-next-line no-await-in-loop
158
+ await bridge.setWallet(liveWallets[i], {
159
+ ...walletDraft,
160
+ state: fixtures_1.walletState.Live,
161
+ });
162
+ }
163
+ });
164
+ after(async () => {
165
+ await restoreSnapshot();
166
+ });
167
+ context("when the submitted target wallets count is same as the expected", () => {
168
+ // The source wallet has a main UTXO with value of 26 BTC,
169
+ // the max BTC transfer is 10 BTC by default (see
170
+ // `constants.walletMaxBtcTransfer`) and the count of
171
+ // live wallets is `5`. We compute the expected target
172
+ // wallets count as:
173
+ // `N = min(liveWalletsCount, ceil(walletBtcBalance / walletMaxBtcTransfer))`
174
+ // so we have `N = min(5, 26 / 10) = min(5, 3) = 3`
175
+ const expectedTargetWalletsCount = 3;
176
+ context("when all target wallets are different than the source wallet", () => {
177
+ context("when all target wallets follow the expected order", () => {
178
+ context("when all target wallets are in the Live state", () => {
179
+ let tx;
180
+ const targetWallets = liveWallets.slice(0, expectedTargetWalletsCount);
181
+ before(async () => {
182
+ await createSnapshot();
183
+ tx = await bridge
184
+ .connect(caller)
185
+ .submitMovingFundsCommitment(ecdsa_1.ecdsaWalletTestData.pubKeyHash160, mainUtxo, walletMembersIDs, walletMemberIndex, targetWallets);
186
+ });
187
+ after(async () => {
188
+ await restoreSnapshot();
189
+ });
190
+ it("should store the target wallets commitment for the given wallet", async () => {
191
+ (0, chai_1.expect)((await bridge.wallets(ecdsa_1.ecdsaWalletTestData.pubKeyHash160))
192
+ .movingFundsTargetWalletsCommitmentHash).to.be.equal(hardhat_1.ethers.utils.solidityKeccak256(["bytes20[]"], [targetWallets]));
193
+ });
194
+ it("should emit the MovingFundsCommitmentSubmitted event", async () => {
195
+ await (0, chai_1.expect)(tx)
196
+ .to.emit(bridge, "MovingFundsCommitmentSubmitted")
197
+ .withArgs(ecdsa_1.ecdsaWalletTestData.pubKeyHash160, targetWallets, caller.address);
198
+ });
199
+ });
200
+ context("when one of the target wallets is not in the Live state", () => {
201
+ it("should revert", async () => {
202
+ // Put an Unknown wallet into the mix.
203
+ const targetWallets = [
204
+ "0x2313e29d08e6b5e0d3cda040ed7f664ce9c840c4",
205
+ liveWallets[0],
206
+ liveWallets[1],
207
+ ];
208
+ await (0, chai_1.expect)(bridge
209
+ .connect(caller)
210
+ .submitMovingFundsCommitment(ecdsa_1.ecdsaWalletTestData.pubKeyHash160, mainUtxo, walletMembersIDs, walletMemberIndex, targetWallets)).to.be.revertedWith("Submitted target wallet must be in Live state");
211
+ });
212
+ });
213
+ });
214
+ context("when one of the target wallets break the expected order", () => {
215
+ it("should revert", async () => {
216
+ const targetWallets = [
217
+ liveWallets[0],
218
+ liveWallets[1],
219
+ liveWallets[1],
220
+ ];
221
+ await (0, chai_1.expect)(bridge
222
+ .connect(caller)
223
+ .submitMovingFundsCommitment(ecdsa_1.ecdsaWalletTestData.pubKeyHash160, mainUtxo, walletMembersIDs, walletMemberIndex, targetWallets)).to.be.revertedWith("Submitted target wallet breaks the expected order");
224
+ });
225
+ });
226
+ });
227
+ context("when one of the target wallets is same as the source wallet", () => {
228
+ it("should revert", async () => {
229
+ const targetWallets = [
230
+ liveWallets[0],
231
+ ecdsa_1.ecdsaWalletTestData.pubKeyHash160,
232
+ liveWallets[1],
233
+ ];
234
+ await (0, chai_1.expect)(bridge
235
+ .connect(caller)
236
+ .submitMovingFundsCommitment(ecdsa_1.ecdsaWalletTestData.pubKeyHash160, mainUtxo, walletMembersIDs, walletMemberIndex, targetWallets)).to.be.revertedWith("Submitted target wallet cannot be equal to the source wallet");
237
+ });
238
+ });
239
+ });
240
+ context("when the submitted target wallets count is other than the expected", () => {
241
+ it("should revert", async () => {
242
+ await (0, chai_1.expect)(bridge
243
+ .connect(caller)
244
+ .submitMovingFundsCommitment(ecdsa_1.ecdsaWalletTestData.pubKeyHash160, mainUtxo, walletMembersIDs, walletMemberIndex, [liveWallets[0], liveWallets[1]])).to.be.revertedWith("Submitted target wallets count is other than expected");
245
+ });
246
+ });
247
+ });
248
+ context("when the expected target wallets count is zero", () => {
249
+ it("should revert", async () => {
250
+ await (0, chai_1.expect)(
251
+ // The last parameter doesn't matter in this scenario.
252
+ bridge
253
+ .connect(caller)
254
+ .submitMovingFundsCommitment(ecdsa_1.ecdsaWalletTestData.pubKeyHash160, mainUtxo, walletMembersIDs, walletMemberIndex, [])).to.be.revertedWith("No target wallets available");
255
+ });
256
+ });
257
+ });
258
+ context("when wallet balance is zero", () => {
259
+ it("should revert", async () => {
260
+ await (0, chai_1.expect)(
261
+ // The last parameter doesn't matter in this scenario.
262
+ bridge.connect(caller).submitMovingFundsCommitment(ecdsa_1.ecdsaWalletTestData.pubKeyHash160, deposit_sweep_1.NO_MAIN_UTXO, // That makes the balance to be 0 BTC.
263
+ walletMembersIDs, walletMemberIndex, [])).to.be.revertedWith("Wallet BTC balance is zero");
264
+ });
265
+ });
266
+ });
267
+ context("when passed wallet main UTXO is invalid", () => {
268
+ const mainUtxo = {
269
+ txHash: "0xc9e58780c6c289c25ae1fe293f85a4db4d0af4f305172f2a1868ddd917458bdf",
270
+ txOutputIndex: 0,
271
+ txOutputValue: (0, contract_test_helpers_1.to1ePrecision)(26, 8), // 26 BTC
272
+ };
273
+ before(async () => {
274
+ await createSnapshot();
275
+ await bridge.setWalletMainUtxo(ecdsa_1.ecdsaWalletTestData.pubKeyHash160, mainUtxo);
276
+ });
277
+ after(async () => {
278
+ await restoreSnapshot();
279
+ });
280
+ it("should revert", async () => {
281
+ // Changing the `txOutputValue` to a value other than `0` will
282
+ // make that scenario happen.
283
+ const corruptedMainUtxo = {
284
+ ...mainUtxo,
285
+ txOutputValue: 1,
286
+ };
287
+ await (0, chai_1.expect)(
288
+ // The last parameter doesn't matter in this scenario.
289
+ bridge
290
+ .connect(caller)
291
+ .submitMovingFundsCommitment(ecdsa_1.ecdsaWalletTestData.pubKeyHash160, corruptedMainUtxo, walletMembersIDs, walletMemberIndex, [])).to.be.revertedWith("Invalid wallet main UTXO data");
292
+ });
293
+ });
294
+ });
295
+ context("when the caller is not a member of the source wallet", () => {
296
+ const walletMembersIDs = [1, 2, 3, 4, 5];
297
+ const walletMemberIndex = 2;
298
+ before(async () => {
299
+ await createSnapshot();
300
+ // That's the default behavior, but we just make it explicit.
301
+ walletRegistry.isWalletMember.returns(false);
302
+ });
303
+ after(async () => {
304
+ walletRegistry.isWalletMember.reset();
305
+ await restoreSnapshot();
306
+ });
307
+ it("should revert", async () => {
308
+ await (0, chai_1.expect)(
309
+ // The last parameter doesn't matter in this scenario.
310
+ bridge
311
+ .connect(thirdParty)
312
+ .submitMovingFundsCommitment(ecdsa_1.ecdsaWalletTestData.pubKeyHash160, deposit_sweep_1.NO_MAIN_UTXO, walletMembersIDs, walletMemberIndex, [])).to.be.revertedWith("Caller is not a member of the source wallet");
313
+ });
314
+ });
315
+ });
316
+ context("when the commitment was already submitted", () => {
317
+ before(async () => {
318
+ await createSnapshot();
319
+ await bridge.setWallet(ecdsa_1.ecdsaWalletTestData.pubKeyHash160, {
320
+ ...walletDraft,
321
+ state: fixtures_1.walletState.MovingFunds,
322
+ // Set a non-zero commitment to make this scenario work.
323
+ movingFundsTargetWalletsCommitmentHash: "0x959e95e0bd83d34878f77ead61cb4e955bf5e3bdc9e16cdfbd51c4c20ab7e6b4",
324
+ });
325
+ });
326
+ after(async () => {
327
+ await restoreSnapshot();
328
+ });
329
+ it("should revert", async () => {
330
+ await (0, chai_1.expect)(
331
+ // Parameters others than the first doesn't matter in this scenario.
332
+ bridge.submitMovingFundsCommitment(ecdsa_1.ecdsaWalletTestData.pubKeyHash160, deposit_sweep_1.NO_MAIN_UTXO, [], 0, [])).to.be.revertedWith("Target wallets commitment already submitted");
333
+ });
334
+ });
335
+ });
336
+ context("when source wallet has pending moved funds sweep requests", () => {
337
+ before(async () => {
338
+ await createSnapshot();
339
+ await bridge.setWallet(ecdsa_1.ecdsaWalletTestData.pubKeyHash160, {
340
+ ...walletDraft,
341
+ state: fixtures_1.walletState.MovingFunds,
342
+ // Set non-zero pending requests count to make this scenario work.
343
+ pendingMovedFundsSweepRequestsCount: 1,
344
+ });
345
+ });
346
+ after(async () => {
347
+ await restoreSnapshot();
348
+ });
349
+ it("should revert", async () => {
350
+ await (0, chai_1.expect)(
351
+ // Parameters others than the first doesn't matter in this scenario.
352
+ bridge.submitMovingFundsCommitment(ecdsa_1.ecdsaWalletTestData.pubKeyHash160, deposit_sweep_1.NO_MAIN_UTXO, [], 0, [])).to.be.revertedWith("Source wallet must handle all pending moved funds sweep requests first");
353
+ });
354
+ });
355
+ });
356
+ context("when source wallet has pending redemptions", () => {
357
+ before(async () => {
358
+ await createSnapshot();
359
+ await bridge.setWallet(ecdsa_1.ecdsaWalletTestData.pubKeyHash160, {
360
+ ...walletDraft,
361
+ state: fixtures_1.walletState.MovingFunds,
362
+ // Set non-zero pending redemptions value to make this scenario work.
363
+ pendingRedemptionsValue: 10000,
364
+ });
365
+ });
366
+ after(async () => {
367
+ await restoreSnapshot();
368
+ });
369
+ it("should revert", async () => {
370
+ await (0, chai_1.expect)(
371
+ // Parameters others than the first doesn't matter in this scenario.
372
+ bridge.submitMovingFundsCommitment(ecdsa_1.ecdsaWalletTestData.pubKeyHash160, deposit_sweep_1.NO_MAIN_UTXO, [], 0, [])).to.be.revertedWith("Source wallet must handle all pending redemptions first");
373
+ });
374
+ });
375
+ });
376
+ context("when source wallet is not in the MovingFunds state", () => {
377
+ const testData = [
378
+ {
379
+ testName: "when the source wallet is in the Unknown state",
380
+ state: fixtures_1.walletState.Unknown,
381
+ },
382
+ {
383
+ testName: "when the source wallet is in the Live state",
384
+ state: fixtures_1.walletState.Live,
385
+ },
386
+ {
387
+ testName: "when the source wallet is in the Closing state",
388
+ state: fixtures_1.walletState.Closing,
389
+ },
390
+ {
391
+ testName: "when the source wallet is in the Closed state",
392
+ state: fixtures_1.walletState.Closed,
393
+ },
394
+ {
395
+ testName: "when the source wallet is in the Terminated state",
396
+ state: fixtures_1.walletState.Terminated,
397
+ },
398
+ ];
399
+ testData.forEach((test) => {
400
+ context(test.testName, () => {
401
+ before(async () => {
402
+ await createSnapshot();
403
+ await bridge.setWallet(ecdsa_1.ecdsaWalletTestData.pubKeyHash160, {
404
+ ...walletDraft,
405
+ state: test.state,
406
+ });
407
+ });
408
+ after(async () => {
409
+ await restoreSnapshot();
410
+ });
411
+ it("should revert", async () => {
412
+ await (0, chai_1.expect)(
413
+ // Parameters other than the first doesn't matter in this scenario.
414
+ bridge.submitMovingFundsCommitment(ecdsa_1.ecdsaWalletTestData.pubKeyHash160, deposit_sweep_1.NO_MAIN_UTXO, [], 0, [])).to.be.revertedWith("Source wallet must be in MovingFunds state");
415
+ });
416
+ });
417
+ });
418
+ });
419
+ });
420
+ describe("resetMovingFundsTimeout", () => {
421
+ const walletDraft = {
422
+ ecdsaWalletID: ecdsa_1.ecdsaWalletTestData.walletID,
423
+ mainUtxoHash: hardhat_1.ethers.constants.HashZero,
424
+ pendingRedemptionsValue: 0,
425
+ createdAt: 0,
426
+ movingFundsRequestedAt: 0,
427
+ closingStartedAt: 0,
428
+ pendingMovedFundsSweepRequestsCount: 0,
429
+ state: fixtures_1.walletState.Unknown,
430
+ movingFundsTargetWalletsCommitmentHash: hardhat_1.ethers.constants.HashZero,
431
+ };
432
+ context("when the wallet is in the MovingFunds state", () => {
433
+ before(async () => {
434
+ await createSnapshot();
435
+ await bridge.setWallet(ecdsa_1.ecdsaWalletTestData.pubKeyHash160, {
436
+ ...walletDraft,
437
+ state: fixtures_1.walletState.MovingFunds,
438
+ });
439
+ });
440
+ after(async () => {
441
+ await restoreSnapshot();
442
+ });
443
+ context("when the wallet's commitment is not submitted yet", () => {
444
+ context("when Live wallets count is zero", () => {
445
+ // No need to do any specific setup. There is only one MovingFunds
446
+ // wallet in the system and its commitment is not yet submitted.
447
+ // Those preconditions are met by default.
448
+ context("when reset delay has elapsed", () => {
449
+ let tx;
450
+ before(async () => {
451
+ await createSnapshot();
452
+ // Set the timestamp of the block that contains the `setWallet` tx.
453
+ await bridge.setWallet(ecdsa_1.ecdsaWalletTestData.pubKeyHash160, {
454
+ ...(await bridge.wallets(ecdsa_1.ecdsaWalletTestData.pubKeyHash160)),
455
+ movingFundsRequestedAt: (await lastBlockTime()) + 1,
456
+ });
457
+ await increaseTime(movingFundsTimeoutResetDelay);
458
+ tx = await bridge.resetMovingFundsTimeout(ecdsa_1.ecdsaWalletTestData.pubKeyHash160);
459
+ });
460
+ after(async () => {
461
+ await restoreSnapshot();
462
+ });
463
+ it("should reset the moving funds timeout", async () => {
464
+ (0, chai_1.expect)((await bridge.wallets(ecdsa_1.ecdsaWalletTestData.pubKeyHash160))
465
+ .movingFundsRequestedAt).to.be.equal(await lastBlockTime());
466
+ });
467
+ it("should emit MovingFundsTimeoutReset event", async () => {
468
+ await (0, chai_1.expect)(tx)
469
+ .to.emit(bridge, "MovingFundsTimeoutReset")
470
+ .withArgs(ecdsa_1.ecdsaWalletTestData.pubKeyHash160);
471
+ });
472
+ });
473
+ context("when reset delay has not elapsed yet", () => {
474
+ before(async () => {
475
+ await createSnapshot();
476
+ // Set the timestamp of the block that contains the `setWallet` tx.
477
+ await bridge.setWallet(ecdsa_1.ecdsaWalletTestData.pubKeyHash160, {
478
+ ...(await bridge.wallets(ecdsa_1.ecdsaWalletTestData.pubKeyHash160)),
479
+ movingFundsRequestedAt: (await lastBlockTime()) + 1,
480
+ });
481
+ await increaseTime(movingFundsTimeoutResetDelay - 1);
482
+ });
483
+ after(async () => {
484
+ await restoreSnapshot();
485
+ });
486
+ it("should revert", async () => {
487
+ await (0, chai_1.expect)(bridge.resetMovingFundsTimeout(ecdsa_1.ecdsaWalletTestData.pubKeyHash160)).to.be.revertedWith("Moving funds timeout cannot be reset yet");
488
+ });
489
+ });
490
+ context("when one reset occurred and the reset delay has elapsed again", () => {
491
+ let tx;
492
+ before(async () => {
493
+ await createSnapshot();
494
+ // Set the timestamp of the block that contains the `setWallet` tx.
495
+ await bridge.setWallet(ecdsa_1.ecdsaWalletTestData.pubKeyHash160, {
496
+ ...(await bridge.wallets(ecdsa_1.ecdsaWalletTestData.pubKeyHash160)),
497
+ movingFundsRequestedAt: (await lastBlockTime()) + 1,
498
+ });
499
+ await increaseTime(movingFundsTimeoutResetDelay);
500
+ // Reset for the first time.
501
+ await bridge.resetMovingFundsTimeout(ecdsa_1.ecdsaWalletTestData.pubKeyHash160);
502
+ // The reset delay elapses again.
503
+ await increaseTime(movingFundsTimeoutResetDelay);
504
+ // The next reset.
505
+ tx = await bridge.resetMovingFundsTimeout(ecdsa_1.ecdsaWalletTestData.pubKeyHash160);
506
+ });
507
+ after(async () => {
508
+ await restoreSnapshot();
509
+ });
510
+ it("should reset the moving funds timeout", async () => {
511
+ (0, chai_1.expect)((await bridge.wallets(ecdsa_1.ecdsaWalletTestData.pubKeyHash160))
512
+ .movingFundsRequestedAt).to.be.equal(await lastBlockTime());
513
+ });
514
+ it("should emit MovingFundsTimeoutReset event", async () => {
515
+ await (0, chai_1.expect)(tx)
516
+ .to.emit(bridge, "MovingFundsTimeoutReset")
517
+ .withArgs(ecdsa_1.ecdsaWalletTestData.pubKeyHash160);
518
+ });
519
+ });
520
+ context("when one reset occurred and the reset delay has not elapsed yet", () => {
521
+ before(async () => {
522
+ await createSnapshot();
523
+ // Set the timestamp of the block that contains the `setWallet` tx.
524
+ await bridge.setWallet(ecdsa_1.ecdsaWalletTestData.pubKeyHash160, {
525
+ ...(await bridge.wallets(ecdsa_1.ecdsaWalletTestData.pubKeyHash160)),
526
+ movingFundsRequestedAt: (await lastBlockTime()) + 1,
527
+ });
528
+ await increaseTime(movingFundsTimeoutResetDelay);
529
+ // Reset for the first time.
530
+ await bridge.resetMovingFundsTimeout(ecdsa_1.ecdsaWalletTestData.pubKeyHash160);
531
+ // The reset delay has not elapsed again yet.
532
+ await increaseTime(movingFundsTimeoutResetDelay - 1);
533
+ });
534
+ after(async () => {
535
+ await restoreSnapshot();
536
+ });
537
+ it("should revert", async () => {
538
+ await (0, chai_1.expect)(bridge.resetMovingFundsTimeout(ecdsa_1.ecdsaWalletTestData.pubKeyHash160)).to.be.revertedWith("Moving funds timeout cannot be reset yet");
539
+ });
540
+ });
541
+ });
542
+ context("when Live wallets count is not zero", () => {
543
+ before(async () => {
544
+ await createSnapshot();
545
+ // This call will add one Live wallet and increase the Live wallet
546
+ // counter accordingly. Note that the wallet's public key hash
547
+ // must be different from the PKH of the tested wallet.
548
+ await bridge.setWallet("0x7ac2d9378a1c47e589dfb8095ca95ed2140d2726", {
549
+ ...walletDraft,
550
+ state: fixtures_1.walletState.Live,
551
+ });
552
+ });
553
+ after(async () => {
554
+ await restoreSnapshot();
555
+ });
556
+ it("should revert", async () => {
557
+ await (0, chai_1.expect)(bridge.resetMovingFundsTimeout(ecdsa_1.ecdsaWalletTestData.pubKeyHash160)).to.be.revertedWith("Live wallets count must be zero");
558
+ });
559
+ });
560
+ });
561
+ context("when the wallet's commitment is already submitted", () => {
562
+ before(async () => {
563
+ await createSnapshot();
564
+ // Set an arbitrary non-zero commitment.
565
+ await bridge.setWallet(ecdsa_1.ecdsaWalletTestData.pubKeyHash160, {
566
+ ...(await bridge.wallets(ecdsa_1.ecdsaWalletTestData.pubKeyHash160)),
567
+ movingFundsTargetWalletsCommitmentHash: hardhat_1.ethers.utils.solidityKeccak256(["bytes20"], ["0xc214a5e9ec1b7792af9894e8f9ff0dd9bf427d79"]),
568
+ });
569
+ });
570
+ after(async () => {
571
+ await restoreSnapshot();
572
+ });
573
+ it("should revert", async () => {
574
+ await (0, chai_1.expect)(bridge.resetMovingFundsTimeout(ecdsa_1.ecdsaWalletTestData.pubKeyHash160)).to.be.revertedWith("Target wallets commitment already submitted");
575
+ });
576
+ });
577
+ });
578
+ context("when the wallet is not in the MovingFunds state", () => {
579
+ const testData = [
580
+ {
581
+ testName: "when the wallet is in the Unknown state",
582
+ walletState: fixtures_1.walletState.Unknown,
583
+ },
584
+ {
585
+ testName: "when the wallet is in the Live state",
586
+ walletState: fixtures_1.walletState.Live,
587
+ },
588
+ {
589
+ testName: "when the wallet is in the Closing state",
590
+ walletState: fixtures_1.walletState.Closing,
591
+ },
592
+ {
593
+ testName: "when the wallet is in the Closed state",
594
+ walletState: fixtures_1.walletState.Closed,
595
+ },
596
+ {
597
+ testName: "when the wallet is in the Terminated state",
598
+ walletState: fixtures_1.walletState.Terminated,
599
+ },
600
+ ];
601
+ testData.forEach((test) => {
602
+ context(test.testName, () => {
603
+ before(async () => {
604
+ await createSnapshot();
605
+ await bridge.setWallet(ecdsa_1.ecdsaWalletTestData.pubKeyHash160, {
606
+ ...walletDraft,
607
+ state: test.walletState,
608
+ });
609
+ });
610
+ after(async () => {
611
+ await restoreSnapshot();
612
+ });
613
+ it("should revert", async () => {
614
+ await (0, chai_1.expect)(bridge.resetMovingFundsTimeout(ecdsa_1.ecdsaWalletTestData.pubKeyHash160)).to.be.revertedWith("Wallet must be in MovingFunds state");
615
+ });
616
+ });
617
+ });
618
+ });
619
+ });
620
+ describe("submitMovingFundsProof", () => {
621
+ context("when transaction proof is valid", () => {
622
+ context("when there is a main UTXO for the given wallet", () => {
623
+ context("when main UTXO data are valid", () => {
624
+ context("when there is only one input", () => {
625
+ context("when the single input points to the wallet's main UTXO", () => {
626
+ context("when the output vector references only 20-byte hashes", () => {
627
+ context("when the output vector has only P2PKH and P2WPKH outputs", () => {
628
+ context("when transaction amount is distributed evenly", () => {
629
+ context("when transaction fee is not too high", () => {
630
+ context("when source wallet is in the MovingFunds state", () => {
631
+ context("when target wallets commitment is submitted", () => {
632
+ context("when actual target wallets correspond to the commitment", () => {
633
+ const testData = [
634
+ {
635
+ testName: "when there is a single target wallet",
636
+ data: moving_funds_1.SingleTargetWallet,
637
+ },
638
+ {
639
+ testName: "when there are multiple target wallets and the amount is indivisible",
640
+ data: moving_funds_1.MultipleTargetWalletsAndIndivisibleAmount,
641
+ },
642
+ {
643
+ testName: "when there are multiple target wallets and the amount is divisible",
644
+ data: moving_funds_1.MultipleTargetWalletsAndDivisibleAmount,
645
+ },
646
+ ];
647
+ testData.forEach((test) => {
648
+ context(test.testName, () => {
649
+ let tx;
650
+ before(async () => {
651
+ await createSnapshot();
652
+ tx =
653
+ await runMovingFundsScenario(test.data);
654
+ });
655
+ after(async () => {
656
+ await restoreSnapshot();
657
+ });
658
+ it("should mark the main UTXO as correctly spent", async () => {
659
+ const key = hardhat_1.ethers.utils.solidityKeccak256(["bytes32", "uint32"], [
660
+ test.data.mainUtxo
661
+ .txHash,
662
+ test.data.mainUtxo
663
+ .txOutputIndex,
664
+ ]);
665
+ // eslint-disable-next-line @typescript-eslint/no-unused-expressions
666
+ (0, chai_1.expect)(await bridge.spentMainUTXOs(key)).to.be.true;
667
+ });
668
+ it("should unset the main UTXO for the source wallet", async () => {
669
+ (0, chai_1.expect)((await bridge.wallets(test.data.wallet
670
+ .pubKeyHash)).mainUtxoHash).to.be.equal(hardhat_1.ethers.constants.HashZero);
671
+ });
672
+ it("should put the source wallet in the Closing state", async () => {
673
+ (0, chai_1.expect)((await bridge.wallets(test.data.wallet
674
+ .pubKeyHash)).state).to.be.equal(fixtures_1.walletState.Closing);
675
+ });
676
+ it("should set the closing started timestamp", async () => {
677
+ (0, chai_1.expect)((await bridge.wallets(test.data.wallet
678
+ .pubKeyHash)).closingStartedAt).to.be.equal(await lastBlockTime());
679
+ });
680
+ it("should emit the WalletClosing event", async () => {
681
+ await (0, chai_1.expect)(tx)
682
+ .to.emit(bridge, "WalletClosing")
683
+ .withArgs(test.data.wallet
684
+ .ecdsaWalletID, test.data.wallet
685
+ .pubKeyHash);
686
+ });
687
+ it("should emit the MovingFundsCompleted event", async () => {
688
+ await (0, chai_1.expect)(tx)
689
+ .to.emit(bridge, "MovingFundsCompleted")
690
+ .withArgs(test.data.wallet
691
+ .pubKeyHash, test.data.movingFundsTx
692
+ .hash);
693
+ });
694
+ it("should create appropriate moved funds sweep requests", async () => {
695
+ for (let i = 0; i <
696
+ test.data
697
+ .expectedMovedFundsSweepRequests
698
+ .length; i++) {
699
+ const expectedMovedFundsSweepRequest = test.data
700
+ .expectedMovedFundsSweepRequests[i];
701
+ const requestKey = hardhat_1.ethers.utils.solidityKeccak256(["bytes32", "uint32"], [
702
+ expectedMovedFundsSweepRequest.txHash,
703
+ expectedMovedFundsSweepRequest.txOutputIndex,
704
+ ]);
705
+ const actualMovedFundsSweepRequest =
706
+ // eslint-disable-next-line no-await-in-loop
707
+ await bridge.movedFundsSweepRequests(requestKey);
708
+ (0, chai_1.expect)(actualMovedFundsSweepRequest.walletPubKeyHash).to.be.equal(expectedMovedFundsSweepRequest.walletPubKeyHash, `Unexpected wallet for sweep request ${i}`);
709
+ (0, chai_1.expect)(actualMovedFundsSweepRequest.value).to.be.equal(expectedMovedFundsSweepRequest.txOutputValue, `Unexpected value for sweep request ${i}`);
710
+ (0, chai_1.expect)(actualMovedFundsSweepRequest.createdAt).to.be.equal(
711
+ // eslint-disable-next-line no-await-in-loop
712
+ await lastBlockTime(), `Unexpected created timestamp for sweep request ${i}`);
713
+ (0, chai_1.expect)(actualMovedFundsSweepRequest.state).to.be.equal(fixtures_1.movedFundsSweepRequestState.Pending, `Unexpected state for sweep request ${i}`);
714
+ /* eslint-disable no-await-in-loop */
715
+ (0, chai_1.expect)((await bridge.wallets(expectedMovedFundsSweepRequest.walletPubKeyHash))
716
+ .pendingMovedFundsSweepRequestsCount).to.be.equal(1);
717
+ /* eslint-enable no-await-in-loop */
718
+ }
719
+ });
720
+ });
721
+ });
722
+ });
723
+ context("when actual target wallets does not correspond to the commitment", () => {
724
+ // The below test data send funds to
725
+ // three wallets with the following
726
+ // 20-byte PKHs (in this order):
727
+ // - 0x2cd680318747b720d67bf4246eb7403b476adb34
728
+ // - 0x8900de8fc6e4cd1db4c7ab0759d28503b4cb0ab1
729
+ // - 0xaf7a841e055fc19bf31acf4cbed5ef548a2cc453
730
+ // If the commitment is not exactly
731
+ // this list, the moving funds proof
732
+ // will revert.
733
+ const data = moving_funds_1.MultipleTargetWalletsAndIndivisibleAmount;
734
+ const testData = [
735
+ {
736
+ testName: "when funds were sent to more wallets than submitted in the commitment",
737
+ modifyData: (dataCopy) =>
738
+ // Simulate that the commitment
739
+ // contains only the two first
740
+ // wallets used in the transaction.
741
+ ({
742
+ ...dataCopy,
743
+ targetWalletsCommitment: [
744
+ dataCopy
745
+ .targetWalletsCommitment[0],
746
+ dataCopy
747
+ .targetWalletsCommitment[1],
748
+ ],
749
+ }),
750
+ },
751
+ {
752
+ testName: "when funds were sent to less wallets than submitted in the commitment",
753
+ modifyData: (dataCopy) =>
754
+ // Simulate that the commitment
755
+ // contains an additional wallet apart
756
+ // from all wallets used in the transaction.
757
+ ({
758
+ ...dataCopy,
759
+ targetWalletsCommitment: [
760
+ dataCopy
761
+ .targetWalletsCommitment[0],
762
+ dataCopy
763
+ .targetWalletsCommitment[1],
764
+ dataCopy
765
+ .targetWalletsCommitment[2],
766
+ "0xe04f5dbeafea147699fce4e0e12027aa0bc12e78",
767
+ ],
768
+ }),
769
+ },
770
+ {
771
+ testName: "when funds were sent to completely different wallets than submitted in the commitment",
772
+ modifyData: (dataCopy) =>
773
+ // Simulate that the commitment
774
+ // contains three different wallets
775
+ // than the wallets used in the
776
+ // transaction.
777
+ ({
778
+ ...dataCopy,
779
+ targetWalletsCommitment: [
780
+ "0x1e445df2d9136831193d90c5e2c6b7ea8a0882fb",
781
+ "0x9d134f065a6566bcc2f99fffe21856e129638526",
782
+ "0x4f524b05f817bd8f3be613ff5bc5ef87f0b68b46",
783
+ ],
784
+ }),
785
+ },
786
+ {
787
+ testName: "when funds were sent to the wallets submitted in the commitment but with a wrong order",
788
+ modifyData: (dataCopy) =>
789
+ // Simulate that the commitment
790
+ // contains three different wallets
791
+ // than the wallets used in the
792
+ // transaction.
793
+ ({
794
+ ...dataCopy,
795
+ targetWalletsCommitment: [
796
+ dataCopy
797
+ .targetWalletsCommitment[2],
798
+ dataCopy
799
+ .targetWalletsCommitment[1],
800
+ dataCopy
801
+ .targetWalletsCommitment[0],
802
+ ],
803
+ }),
804
+ },
805
+ ];
806
+ testData.forEach((test) => {
807
+ context(test.testName, () => {
808
+ let tx;
809
+ before(async () => {
810
+ await createSnapshot();
811
+ // Pass a copy of the original data.
812
+ const modifiedData = test.modifyData(JSON.parse(JSON.stringify(data)));
813
+ tx =
814
+ runMovingFundsScenario(modifiedData);
815
+ });
816
+ after(async () => {
817
+ await restoreSnapshot();
818
+ });
819
+ it("should revert", async () => {
820
+ await (0, chai_1.expect)(tx).to.be.revertedWith("Target wallets don't correspond to the commitment");
821
+ });
822
+ });
823
+ });
824
+ });
825
+ });
826
+ context("when target wallets commitment is not submitted", () => {
827
+ const data = JSON.parse(JSON.stringify(moving_funds_1.SingleTargetWallet));
828
+ let tx;
829
+ before(async () => {
830
+ await createSnapshot();
831
+ tx = runMovingFundsScenario({
832
+ ...data,
833
+ targetWalletsCommitment: [],
834
+ });
835
+ });
836
+ after(async () => {
837
+ await restoreSnapshot();
838
+ });
839
+ it("should revert", async () => {
840
+ await (0, chai_1.expect)(tx).to.be.revertedWith("Target wallets commitment not submitted yet");
841
+ });
842
+ });
843
+ });
844
+ context("when source wallet is not in the MovingFunds state", () => {
845
+ const testData = [
846
+ {
847
+ testName: "when wallet state is Unknown",
848
+ state: fixtures_1.walletState.Unknown,
849
+ },
850
+ {
851
+ testName: "when wallet state is Live",
852
+ state: fixtures_1.walletState.Live,
853
+ },
854
+ {
855
+ testName: "when wallet state is Closing",
856
+ state: fixtures_1.walletState.Closing,
857
+ },
858
+ {
859
+ testName: "when wallet state is Closed",
860
+ state: fixtures_1.walletState.Closed,
861
+ },
862
+ {
863
+ testName: "when wallet state is Terminated",
864
+ state: fixtures_1.walletState.Terminated,
865
+ },
866
+ ];
867
+ testData.forEach((test) => {
868
+ context(test.testName, () => {
869
+ const data = JSON.parse(JSON.stringify(moving_funds_1.SingleTargetWallet));
870
+ let tx;
871
+ before(async () => {
872
+ await createSnapshot();
873
+ data.wallet.state = test.state;
874
+ tx = runMovingFundsScenario(data);
875
+ });
876
+ after(async () => {
877
+ await restoreSnapshot();
878
+ });
879
+ it("should revert", async () => {
880
+ await (0, chai_1.expect)(tx).to.be.revertedWith("Wallet must be in MovingFunds state");
881
+ });
882
+ });
883
+ });
884
+ });
885
+ });
886
+ context("when transaction fee is too high", () => {
887
+ const data = moving_funds_1.SingleTargetWallet;
888
+ let tx;
889
+ before(async () => {
890
+ await createSnapshot();
891
+ const beforeProofActions = async () => {
892
+ // The transaction used by this scenario's
893
+ // test data has a fee of 9000 satoshis. Lowering
894
+ // the max fee in the Bridge by one should
895
+ // cause the expected failure.
896
+ await bridge.setMovingFundsTxMaxTotalFee(8999);
897
+ };
898
+ tx = runMovingFundsScenario(data, beforeProofActions);
899
+ });
900
+ after(async () => {
901
+ await restoreSnapshot();
902
+ });
903
+ it("should revert", async () => {
904
+ await (0, chai_1.expect)(tx).to.be.revertedWith("Transaction fee is too high");
905
+ });
906
+ });
907
+ });
908
+ context("when transaction amount is not distributed evenly", () => {
909
+ const data = moving_funds_1.MultipleTargetWalletsButAmountDistributedUnevenly;
910
+ let tx;
911
+ before(async () => {
912
+ await createSnapshot();
913
+ tx = runMovingFundsScenario(data);
914
+ });
915
+ after(async () => {
916
+ await restoreSnapshot();
917
+ });
918
+ it("should revert", async () => {
919
+ await (0, chai_1.expect)(tx).to.be.revertedWith("Transaction amount is not distributed evenly");
920
+ });
921
+ });
922
+ });
923
+ context("when the output vector contains P2SH output", () => {
924
+ // The only possible case is P2SH which contains the
925
+ // 20-byte payload, just like P2PKH and P2WPKH.
926
+ // No need to check P2WSH as it uses a 32-byte payload
927
+ // so it would fail earlier, at the payload length
928
+ // assertion.
929
+ const data = moving_funds_1.SingleTargetWalletButP2SH;
930
+ let tx;
931
+ before(async () => {
932
+ await createSnapshot();
933
+ tx = runMovingFundsScenario(data);
934
+ });
935
+ after(async () => {
936
+ await restoreSnapshot();
937
+ });
938
+ it("should revert", async () => {
939
+ await (0, chai_1.expect)(tx).to.be.revertedWith("Output must be P2PKH or P2WPKH");
940
+ });
941
+ });
942
+ });
943
+ context("when the output vector does not only reference 20-byte hashes", () => {
944
+ // Use a provably unspendable output whose payload length
945
+ // is zero so it should cause a failure upon the assertion
946
+ // that makes sure the output payload is 20-byte.
947
+ const data = moving_funds_1.SingleProvablyUnspendable;
948
+ let tx;
949
+ before(async () => {
950
+ await createSnapshot();
951
+ tx = runMovingFundsScenario(data);
952
+ });
953
+ after(async () => {
954
+ await restoreSnapshot();
955
+ });
956
+ it("should revert", async () => {
957
+ await (0, chai_1.expect)(tx).to.be.revertedWith("Output's public key hash must have 20 bytes");
958
+ });
959
+ });
960
+ });
961
+ context("when the single input doesn't point to the wallet's main UTXO", () => {
962
+ const data = JSON.parse(JSON.stringify(moving_funds_1.SingleTargetWallet));
963
+ let tx;
964
+ before(async () => {
965
+ await createSnapshot();
966
+ // Corrupt the wallet's main UTXO that is injected to
967
+ // the Bridge state by the test runner in order to make it
968
+ // different than the input used by the actual Bitcoin
969
+ // transaction thus make the tested scenario happen. The
970
+ // proper value of `txOutputIndex` is `1` so any other value
971
+ // will do the trick.
972
+ data.mainUtxo.txOutputIndex = 0;
973
+ tx = runMovingFundsScenario(data);
974
+ });
975
+ after(async () => {
976
+ await restoreSnapshot();
977
+ });
978
+ it("should revert", async () => {
979
+ await (0, chai_1.expect)(tx).to.be.revertedWith("Outbound transaction input must point to the wallet's main UTXO");
980
+ });
981
+ });
982
+ });
983
+ context("when input count is other than one", () => {
984
+ const data = moving_funds_1.MultipleInputs;
985
+ let tx;
986
+ before(async () => {
987
+ await createSnapshot();
988
+ tx = runMovingFundsScenario(data);
989
+ });
990
+ after(async () => {
991
+ await restoreSnapshot();
992
+ });
993
+ it("should revert", async () => {
994
+ await (0, chai_1.expect)(tx).to.be.revertedWith("Outbound transaction must have a single input");
995
+ });
996
+ });
997
+ });
998
+ context("when main UTXO data are invalid", () => {
999
+ const data = moving_funds_1.SingleTargetWallet;
1000
+ before(async () => {
1001
+ await createSnapshot();
1002
+ // Required for a successful SPV proof.
1003
+ relay.getPrevEpochDifficulty.returns(data.chainDifficulty);
1004
+ relay.getCurrentEpochDifficulty.returns(data.chainDifficulty);
1005
+ // Wallet main UTXO must be set on the Bridge side to make
1006
+ // that scenario happen.
1007
+ await bridge.setWalletMainUtxo(data.wallet.pubKeyHash, data.mainUtxo);
1008
+ });
1009
+ after(async () => {
1010
+ relay.getPrevEpochDifficulty.reset();
1011
+ relay.getCurrentEpochDifficulty.reset();
1012
+ await restoreSnapshot();
1013
+ });
1014
+ it("should revert", async () => {
1015
+ // Corrupt the main UTXO parameter passed during
1016
+ // `submitMovingFundsProof` call. The proper value of
1017
+ // `txOutputIndex` for this test data set is `1` so any other
1018
+ // value will make this test scenario happen.
1019
+ const corruptedMainUtxo = {
1020
+ ...data.mainUtxo,
1021
+ txOutputIndex: 0,
1022
+ };
1023
+ await (0, chai_1.expect)(bridge.submitMovingFundsProof(data.movingFundsTx, data.movingFundsProof, corruptedMainUtxo, data.wallet.pubKeyHash)).to.be.revertedWith("Invalid main UTXO data");
1024
+ });
1025
+ });
1026
+ });
1027
+ context("when there is no main UTXO for the given wallet", () => {
1028
+ const data = moving_funds_1.SingleTargetWallet;
1029
+ before(async () => {
1030
+ await createSnapshot();
1031
+ // Required for a successful SPV proof.
1032
+ relay.getPrevEpochDifficulty.returns(data.chainDifficulty);
1033
+ relay.getCurrentEpochDifficulty.returns(data.chainDifficulty);
1034
+ });
1035
+ after(async () => {
1036
+ relay.getPrevEpochDifficulty.reset();
1037
+ relay.getCurrentEpochDifficulty.reset();
1038
+ await restoreSnapshot();
1039
+ });
1040
+ it("should revert", async () => {
1041
+ // There was no preparations before `submitMovingFundsProof` call
1042
+ // so no main UTXO is set for the given wallet.
1043
+ await (0, chai_1.expect)(bridge.submitMovingFundsProof(data.movingFundsTx, data.movingFundsProof, data.mainUtxo, data.wallet.pubKeyHash)).to.be.revertedWith("No main UTXO for given wallet");
1044
+ });
1045
+ });
1046
+ });
1047
+ context("when transaction proof is not valid", () => {
1048
+ context("when input vector is not valid", () => {
1049
+ const data = JSON.parse(JSON.stringify(moving_funds_1.SingleTargetWallet));
1050
+ before(async () => {
1051
+ await createSnapshot();
1052
+ });
1053
+ after(async () => {
1054
+ await restoreSnapshot();
1055
+ });
1056
+ it("should revert", async () => {
1057
+ // Corrupt the input vector by setting a compactSize uint claiming
1058
+ // there is no inputs at all.
1059
+ data.movingFundsTx.inputVector =
1060
+ "0x00b69a2869840aa6fdfd143136ff4514ca46ea2d876855040892ad74ab" +
1061
+ "8c5274220100000000ffffffff";
1062
+ await (0, chai_1.expect)(runMovingFundsScenario(data)).to.be.revertedWith("Invalid input vector provided");
1063
+ });
1064
+ });
1065
+ context("when output vector is not valid", () => {
1066
+ const data = JSON.parse(JSON.stringify(moving_funds_1.SingleTargetWallet));
1067
+ before(async () => {
1068
+ await createSnapshot();
1069
+ });
1070
+ after(async () => {
1071
+ await restoreSnapshot();
1072
+ });
1073
+ it("should revert", async () => {
1074
+ // Corrupt the output vector by setting a compactSize uint claiming
1075
+ // there is no outputs at all.
1076
+ data.movingFundsTx.outputVector =
1077
+ "0x005cf511000000000017a91486884e6be1525dab5ae0b451bd2c72cee6" +
1078
+ "7dcf4187";
1079
+ await (0, chai_1.expect)(runMovingFundsScenario(data)).to.be.revertedWith("Invalid output vector provided");
1080
+ });
1081
+ });
1082
+ context("when merkle proof is not valid", () => {
1083
+ const data = JSON.parse(JSON.stringify(moving_funds_1.SingleTargetWallet));
1084
+ before(async () => {
1085
+ await createSnapshot();
1086
+ });
1087
+ after(async () => {
1088
+ await restoreSnapshot();
1089
+ });
1090
+ it("should revert", async () => {
1091
+ // Corrupt the merkle proof by changing tx index in block to an
1092
+ // invalid one. The proper one is 1 so any other will do the trick.
1093
+ data.movingFundsProof.txIndexInBlock = 30;
1094
+ await (0, chai_1.expect)(runMovingFundsScenario(data)).to.be.revertedWith("Tx merkle proof is not valid for provided header and tx hash");
1095
+ });
1096
+ });
1097
+ context("when proof difficulty is not current nor previous", () => {
1098
+ const data = JSON.parse(JSON.stringify(moving_funds_1.SingleTargetWallet));
1099
+ before(async () => {
1100
+ await createSnapshot();
1101
+ });
1102
+ after(async () => {
1103
+ await restoreSnapshot();
1104
+ });
1105
+ it("should revert", async () => {
1106
+ // To pass the proof validation, the difficulty returned by the relay
1107
+ // must be 21461933 for test data used in this scenario. Setting
1108
+ // a different value will cause difficulty comparison failure.
1109
+ data.chainDifficulty = 2;
1110
+ await (0, chai_1.expect)(runMovingFundsScenario(data)).to.be.revertedWith("Not at current or previous difficulty");
1111
+ });
1112
+ });
1113
+ context("when headers chain length is not valid", () => {
1114
+ const data = JSON.parse(JSON.stringify(moving_funds_1.SingleTargetWallet));
1115
+ before(async () => {
1116
+ await createSnapshot();
1117
+ });
1118
+ after(async () => {
1119
+ await restoreSnapshot();
1120
+ });
1121
+ it("should revert", async () => {
1122
+ // Corrupt the bitcoin headers length in the moving funds proof. The
1123
+ // proper value is length divisible by 80 so any length violating
1124
+ // this rule will cause failure. In this case, we just remove the
1125
+ // last byte from proper headers chain.
1126
+ const properHeaders = data.movingFundsProof.bitcoinHeaders.toString();
1127
+ data.movingFundsProof.bitcoinHeaders = properHeaders.substring(0, properHeaders.length - 2);
1128
+ await (0, chai_1.expect)(runMovingFundsScenario(data)).to.be.revertedWith("Invalid length of the headers chain");
1129
+ });
1130
+ });
1131
+ context("when headers chain is not valid", () => {
1132
+ const data = JSON.parse(JSON.stringify(moving_funds_1.SingleTargetWallet));
1133
+ before(async () => {
1134
+ await createSnapshot();
1135
+ });
1136
+ after(async () => {
1137
+ await restoreSnapshot();
1138
+ });
1139
+ it("should revert", async () => {
1140
+ // Bitcoin headers must form a chain to pass the proof validation.
1141
+ // That means the `previous block hash` encoded in the given block
1142
+ // header must match the actual previous header's hash. To test
1143
+ // that scenario, we corrupt the `previous block hash` of the
1144
+ // second header. Each header is 80 bytes length. First 4 bytes
1145
+ // of each header is `version` and 32 subsequent bytes is
1146
+ // `previous block hash`. Changing byte 85 of the whole chain will
1147
+ // do the work.
1148
+ const properHeaders = data.movingFundsProof.bitcoinHeaders.toString();
1149
+ data.movingFundsProof.bitcoinHeaders = `${properHeaders.substring(0, 170)}ff${properHeaders.substring(172)}`;
1150
+ await (0, chai_1.expect)(runMovingFundsScenario(data)).to.be.revertedWith("Invalid headers chain");
1151
+ });
1152
+ });
1153
+ context("when the work in the header is insufficient", () => {
1154
+ const data = JSON.parse(JSON.stringify(moving_funds_1.SingleTargetWallet));
1155
+ before(async () => {
1156
+ await createSnapshot();
1157
+ });
1158
+ after(async () => {
1159
+ await restoreSnapshot();
1160
+ });
1161
+ it("should revert", async () => {
1162
+ // Each header encodes a `difficulty target` field in bytes 72-76.
1163
+ // The given header's hash (interpreted as uint) must be bigger than
1164
+ // the `difficulty target`. To test this scenario, we change the
1165
+ // last byte of the last header in such a way their hash becomes
1166
+ // lower than their `difficulty target`.
1167
+ const properHeaders = data.movingFundsProof.bitcoinHeaders.toString();
1168
+ data.movingFundsProof.bitcoinHeaders = `${properHeaders.substring(0, properHeaders.length - 2)}ff`;
1169
+ await (0, chai_1.expect)(runMovingFundsScenario(data)).to.be.revertedWith("Insufficient work in a header");
1170
+ });
1171
+ });
1172
+ context("when accumulated difficulty in headers chain is insufficient", () => {
1173
+ let otherBridge;
1174
+ const data = JSON.parse(JSON.stringify(moving_funds_1.SingleTargetWallet));
1175
+ before(async () => {
1176
+ await createSnapshot();
1177
+ // Necessary to pass the first part of proof validation.
1178
+ relay.getCurrentEpochDifficulty.returns(data.chainDifficulty);
1179
+ relay.getPrevEpochDifficulty.returns(data.chainDifficulty);
1180
+ // Deploy another bridge which has higher `txProofDifficultyFactor`
1181
+ // than the original bridge. That means it will need 12 confirmations
1182
+ // to deem transaction proof validity. This scenario uses test
1183
+ // data which has only 6 confirmations. That should force the
1184
+ // failure we expect within this scenario.
1185
+ otherBridge = await BridgeFactory.deploy();
1186
+ await otherBridge.initialize(bank.address, relay.address, treasury.address, walletRegistry.address, 12);
1187
+ });
1188
+ after(async () => {
1189
+ relay.getCurrentEpochDifficulty.reset();
1190
+ relay.getPrevEpochDifficulty.reset();
1191
+ await restoreSnapshot();
1192
+ });
1193
+ it("should revert", async () => {
1194
+ await (0, chai_1.expect)(otherBridge.submitMovingFundsProof(data.movingFundsTx, data.movingFundsProof, data.mainUtxo, data.wallet.pubKeyHash)).to.be.revertedWith("Insufficient accumulated difficulty in header chain");
1195
+ });
1196
+ });
1197
+ });
1198
+ });
1199
+ describe("notifyMovingFundsTimeout", () => {
1200
+ const walletDraft = {
1201
+ ecdsaWalletID: ecdsa_1.ecdsaWalletTestData.walletID,
1202
+ mainUtxoHash: hardhat_1.ethers.constants.HashZero,
1203
+ pendingRedemptionsValue: 0,
1204
+ createdAt: 0,
1205
+ movingFundsRequestedAt: 0,
1206
+ closingStartedAt: 0,
1207
+ pendingMovedFundsSweepRequestsCount: 0,
1208
+ state: fixtures_1.walletState.Unknown,
1209
+ movingFundsTargetWalletsCommitmentHash: hardhat_1.ethers.constants.HashZero,
1210
+ };
1211
+ context("when source wallet is in the MovingFunds state", () => {
1212
+ before(async () => {
1213
+ await createSnapshot();
1214
+ await bridge.setWallet(ecdsa_1.ecdsaWalletTestData.pubKeyHash160, {
1215
+ ...walletDraft,
1216
+ state: fixtures_1.walletState.Live,
1217
+ });
1218
+ // Wallet must have funds to be not closed immediately by
1219
+ // the following `__ecdsaWalletHeartbeatFailedCallback` call.
1220
+ await bridge.setWalletMainUtxo(ecdsa_1.ecdsaWalletTestData.pubKeyHash160, {
1221
+ txHash: hardhat_1.ethers.constants.HashZero,
1222
+ txOutputIndex: 0,
1223
+ txOutputValue: (0, contract_test_helpers_1.to1ePrecision)(10, 8),
1224
+ });
1225
+ // Switches the wallet to moving funds.
1226
+ await bridge
1227
+ .connect(walletRegistry.wallet)
1228
+ .__ecdsaWalletHeartbeatFailedCallback(ecdsa_1.ecdsaWalletTestData.walletID, ecdsa_1.ecdsaWalletTestData.publicKeyX, ecdsa_1.ecdsaWalletTestData.publicKeyY);
1229
+ });
1230
+ after(async () => {
1231
+ await restoreSnapshot();
1232
+ });
1233
+ context("when the moving funds process has timed out", () => {
1234
+ let tx;
1235
+ const walletMembersIDs = [1, 2, 3, 4, 5];
1236
+ before(async () => {
1237
+ await createSnapshot();
1238
+ walletRegistry.closeWallet.reset();
1239
+ walletRegistry.seize.reset();
1240
+ await increaseTime(movingFundsTimeout);
1241
+ tx = await bridge
1242
+ .connect(thirdParty)
1243
+ .notifyMovingFundsTimeout(ecdsa_1.ecdsaWalletTestData.pubKeyHash160, walletMembersIDs);
1244
+ });
1245
+ after(async () => {
1246
+ walletRegistry.closeWallet.reset();
1247
+ walletRegistry.seize.reset();
1248
+ await restoreSnapshot();
1249
+ });
1250
+ it("should switch the wallet to Terminated state", async () => {
1251
+ (0, chai_1.expect)((await bridge.wallets(ecdsa_1.ecdsaWalletTestData.pubKeyHash160)).state).to.be.equal(fixtures_1.walletState.Terminated);
1252
+ });
1253
+ it("should emit WalletTerminated event", async () => {
1254
+ await (0, chai_1.expect)(tx)
1255
+ .to.emit(bridge, "WalletTerminated")
1256
+ .withArgs(ecdsa_1.ecdsaWalletTestData.walletID, ecdsa_1.ecdsaWalletTestData.pubKeyHash160);
1257
+ });
1258
+ it("should call ECDSA Wallet Registry's closeWallet function", async () => {
1259
+ // eslint-disable-next-line @typescript-eslint/no-unused-expressions
1260
+ (0, chai_1.expect)(walletRegistry.closeWallet).to.have.been.calledOnceWith(ecdsa_1.ecdsaWalletTestData.walletID);
1261
+ });
1262
+ it("should call the ECDSA wallet registry's seize function", async () => {
1263
+ (0, chai_1.expect)(walletRegistry.seize).to.have.been.calledOnceWith(movingFundsTimeoutSlashingAmount, movingFundsTimeoutNotifierRewardMultiplier, await thirdParty.getAddress(), ecdsa_1.ecdsaWalletTestData.walletID, walletMembersIDs);
1264
+ });
1265
+ it("should emit MovingFundsTimedOut event", async () => {
1266
+ await (0, chai_1.expect)(tx)
1267
+ .to.emit(bridge, "MovingFundsTimedOut")
1268
+ .withArgs(ecdsa_1.ecdsaWalletTestData.pubKeyHash160);
1269
+ });
1270
+ });
1271
+ context("when the moving funds process has not timed out", () => {
1272
+ before(async () => {
1273
+ await createSnapshot();
1274
+ await increaseTime(movingFundsTimeout - 1);
1275
+ });
1276
+ after(async () => {
1277
+ await restoreSnapshot();
1278
+ });
1279
+ it("should revert", async () => {
1280
+ await (0, chai_1.expect)(bridge.notifyMovingFundsTimeout(ecdsa_1.ecdsaWalletTestData.pubKeyHash160, [])).to.be.revertedWith("Moving funds has not timed out yet");
1281
+ });
1282
+ });
1283
+ });
1284
+ context("when source wallet is not in the MovingFunds state", () => {
1285
+ const testData = [
1286
+ {
1287
+ testName: "when the source wallet is in the Unknown state",
1288
+ state: fixtures_1.walletState.Unknown,
1289
+ },
1290
+ {
1291
+ testName: "when the source wallet is in the Live state",
1292
+ state: fixtures_1.walletState.Live,
1293
+ },
1294
+ {
1295
+ testName: "when the source wallet is in the Closing state",
1296
+ state: fixtures_1.walletState.Closing,
1297
+ },
1298
+ {
1299
+ testName: "when the source wallet is in the Closed state",
1300
+ state: fixtures_1.walletState.Closed,
1301
+ },
1302
+ {
1303
+ testName: "when the source wallet is in the Terminated state",
1304
+ state: fixtures_1.walletState.Terminated,
1305
+ },
1306
+ ];
1307
+ testData.forEach((test) => {
1308
+ context(test.testName, () => {
1309
+ before(async () => {
1310
+ await createSnapshot();
1311
+ await bridge.setWallet(ecdsa_1.ecdsaWalletTestData.pubKeyHash160, {
1312
+ ...walletDraft,
1313
+ state: test.state,
1314
+ });
1315
+ });
1316
+ after(async () => {
1317
+ await restoreSnapshot();
1318
+ });
1319
+ it("should revert", async () => {
1320
+ await (0, chai_1.expect)(bridge.notifyMovingFundsTimeout(ecdsa_1.ecdsaWalletTestData.pubKeyHash160, [])).to.be.revertedWith("Wallet must be in MovingFunds state");
1321
+ });
1322
+ });
1323
+ });
1324
+ });
1325
+ });
1326
+ describe("notifyMovingFundsBelowDust", () => {
1327
+ const walletDraft = {
1328
+ ecdsaWalletID: ecdsa_1.ecdsaWalletTestData.walletID,
1329
+ mainUtxoHash: hardhat_1.ethers.constants.HashZero,
1330
+ pendingRedemptionsValue: 0,
1331
+ createdAt: 0,
1332
+ movingFundsRequestedAt: 0,
1333
+ closingStartedAt: 0,
1334
+ pendingMovedFundsSweepRequestsCount: 0,
1335
+ state: fixtures_1.walletState.Unknown,
1336
+ movingFundsTargetWalletsCommitmentHash: hardhat_1.ethers.constants.HashZero,
1337
+ };
1338
+ context("when the wallet is in the MovingFunds state", () => {
1339
+ before(async () => {
1340
+ await createSnapshot();
1341
+ await bridge.setWallet(ecdsa_1.ecdsaWalletTestData.pubKeyHash160, {
1342
+ ...walletDraft,
1343
+ state: fixtures_1.walletState.MovingFunds,
1344
+ });
1345
+ });
1346
+ after(async () => {
1347
+ await restoreSnapshot();
1348
+ });
1349
+ context("when the main UTXO parameter is valid", () => {
1350
+ context("when the balance is below the dust threshold", () => {
1351
+ const mainUtxo = {
1352
+ txHash: hardhat_1.ethers.constants.HashZero,
1353
+ txOutputIndex: 0,
1354
+ txOutputValue: fixtures_1.constants.movingFundsDustThreshold - 1,
1355
+ };
1356
+ let tx;
1357
+ before(async () => {
1358
+ await createSnapshot();
1359
+ await bridge.setWalletMainUtxo(ecdsa_1.ecdsaWalletTestData.pubKeyHash160, mainUtxo);
1360
+ tx = await bridge.notifyMovingFundsBelowDust(ecdsa_1.ecdsaWalletTestData.pubKeyHash160, mainUtxo);
1361
+ });
1362
+ after(async () => {
1363
+ await restoreSnapshot();
1364
+ });
1365
+ it("should change wallet's state to Closing", async () => {
1366
+ const { state } = await bridge.wallets(ecdsa_1.ecdsaWalletTestData.pubKeyHash160);
1367
+ (0, chai_1.expect)(state).to.be.equal(fixtures_1.walletState.Closing);
1368
+ });
1369
+ it("should set the wallet's closing started timestamp", async () => {
1370
+ const wallet = await bridge.wallets(ecdsa_1.ecdsaWalletTestData.pubKeyHash160);
1371
+ (0, chai_1.expect)(wallet.closingStartedAt).to.be.equal(await lastBlockTime());
1372
+ });
1373
+ it("should emit WalletClosing event", async () => {
1374
+ await (0, chai_1.expect)(tx)
1375
+ .to.emit(bridge, "WalletClosing")
1376
+ .withArgs(walletDraft.ecdsaWalletID, ecdsa_1.ecdsaWalletTestData.pubKeyHash160);
1377
+ });
1378
+ it("should emit MovingFundsBelowDustReported event", async () => {
1379
+ await (0, chai_1.expect)(tx)
1380
+ .to.emit(bridge, "MovingFundsBelowDustReported")
1381
+ .withArgs(ecdsa_1.ecdsaWalletTestData.pubKeyHash160);
1382
+ });
1383
+ });
1384
+ context("when the balance is not below the dust threshold", () => {
1385
+ const mainUtxo = {
1386
+ txHash: hardhat_1.ethers.constants.HashZero,
1387
+ txOutputIndex: 0,
1388
+ txOutputValue: fixtures_1.constants.movingFundsDustThreshold,
1389
+ };
1390
+ before(async () => {
1391
+ await createSnapshot();
1392
+ await bridge.setWalletMainUtxo(ecdsa_1.ecdsaWalletTestData.pubKeyHash160, mainUtxo);
1393
+ });
1394
+ after(async () => {
1395
+ await restoreSnapshot();
1396
+ });
1397
+ it("should revert", async () => {
1398
+ await (0, chai_1.expect)(bridge.notifyMovingFundsBelowDust(ecdsa_1.ecdsaWalletTestData.pubKeyHash160, mainUtxo)).to.be.revertedWith("Wallet BTC balance must be below the moving funds dust threshold");
1399
+ });
1400
+ });
1401
+ });
1402
+ context("when the main UTXO parameter is invalid", () => {
1403
+ const mainUtxo = {
1404
+ txHash: hardhat_1.ethers.constants.HashZero,
1405
+ txOutputIndex: 0,
1406
+ txOutputValue: (0, contract_test_helpers_1.to1ePrecision)(1, 8),
1407
+ };
1408
+ before(async () => {
1409
+ await createSnapshot();
1410
+ await bridge.setWallet(ecdsa_1.ecdsaWalletTestData.pubKeyHash160, {
1411
+ ...walletDraft,
1412
+ state: fixtures_1.walletState.MovingFunds,
1413
+ });
1414
+ await bridge.setWalletMainUtxo(ecdsa_1.ecdsaWalletTestData.pubKeyHash160, mainUtxo);
1415
+ });
1416
+ after(async () => {
1417
+ await restoreSnapshot();
1418
+ });
1419
+ it("should revert", async () => {
1420
+ const corruptedMainUtxo = {
1421
+ ...mainUtxo,
1422
+ txOutputIndex: 1,
1423
+ };
1424
+ await (0, chai_1.expect)(bridge.notifyMovingFundsBelowDust(ecdsa_1.ecdsaWalletTestData.pubKeyHash160, corruptedMainUtxo)).to.be.revertedWith("Invalid wallet main UTXO data");
1425
+ });
1426
+ });
1427
+ });
1428
+ context("when the wallet is not in the MovingFunds state", () => {
1429
+ const testData = [
1430
+ {
1431
+ testName: "when wallet state is Unknown",
1432
+ walletState: fixtures_1.walletState.Unknown,
1433
+ },
1434
+ {
1435
+ testName: "when wallet state is Live",
1436
+ walletState: fixtures_1.walletState.Live,
1437
+ },
1438
+ {
1439
+ testName: "when wallet state is Closing",
1440
+ walletState: fixtures_1.walletState.Closing,
1441
+ },
1442
+ {
1443
+ testName: "when wallet state is Closed",
1444
+ walletState: fixtures_1.walletState.Closed,
1445
+ },
1446
+ {
1447
+ testName: "when wallet state is Terminated",
1448
+ walletState: fixtures_1.walletState.Terminated,
1449
+ },
1450
+ ];
1451
+ testData.forEach((test) => {
1452
+ context(test.testName, () => {
1453
+ before(async () => {
1454
+ await createSnapshot();
1455
+ await bridge.setWallet(ecdsa_1.ecdsaWalletTestData.pubKeyHash160, {
1456
+ ...walletDraft,
1457
+ state: test.walletState,
1458
+ });
1459
+ });
1460
+ after(async () => {
1461
+ await restoreSnapshot();
1462
+ });
1463
+ it("should revert", async () => {
1464
+ await (0, chai_1.expect)(bridge.notifyMovingFundsBelowDust(ecdsa_1.ecdsaWalletTestData.pubKeyHash160, deposit_sweep_1.NO_MAIN_UTXO)).to.be.revertedWith("Wallet must be in MovingFunds state");
1465
+ });
1466
+ });
1467
+ });
1468
+ });
1469
+ });
1470
+ describe("submitMovedFundsSweepProof", () => {
1471
+ context("when transaction proof is valid", () => {
1472
+ context("when there is only one output", () => {
1473
+ context("when the single output is 20-byte", () => {
1474
+ context("when single output is either P2PKH or P2WPKH", () => {
1475
+ context("when sweeping wallet is either in the Live or MovingFunds state", () => {
1476
+ context("when sweeping wallet is in the Live state", () => {
1477
+ context("when main UTXO data are valid", () => {
1478
+ context("when transaction fee does not exceed the sweep transaction maximum fee", () => {
1479
+ context("when the sweeping wallet has no main UTXO set", () => {
1480
+ context("when there is a single input referring to a Pending sweep request", () => {
1481
+ const data = moving_funds_1.MovedFundsSweepWithoutMainUtxo;
1482
+ let tx;
1483
+ before(async () => {
1484
+ await createSnapshot();
1485
+ tx = await runMovedFundsSweepScenario(data);
1486
+ });
1487
+ after(async () => {
1488
+ await restoreSnapshot();
1489
+ });
1490
+ it("should mark the sweep request as processed", async () => {
1491
+ const key = hardhat_1.ethers.utils.solidityKeccak256(["bytes32", "uint32"], [
1492
+ data.movedFundsSweepRequest.txHash,
1493
+ data.movedFundsSweepRequest.txOutputIndex,
1494
+ ]);
1495
+ // eslint-disable-next-line @typescript-eslint/no-unused-expressions
1496
+ (0, chai_1.expect)((await bridge.movedFundsSweepRequests(key))
1497
+ .state).to.be.equal(fixtures_1.movedFundsSweepRequestState.Processed);
1498
+ });
1499
+ it("should decrease the sweeping wallet's pending requests count", async () => {
1500
+ // The `setPendingMovedFundsSweepRequest` call
1501
+ // made as part of `runMovedFundsSweepScenario`
1502
+ // set this counter to 1. Eventually, it
1503
+ // should be decreased back to 0.
1504
+ (0, chai_1.expect)((await bridge.wallets(data.wallet.pubKeyHash)).pendingMovedFundsSweepRequestsCount).to.be.equal(0);
1505
+ });
1506
+ it("should set the transaction output as new sweeping wallet main UTXO", async () => {
1507
+ // Amount can be checked by opening the sweep tx
1508
+ // in a Bitcoin testnet explorer. In this case,
1509
+ // the output value is 16500.
1510
+ const expectedMainUtxoHash = hardhat_1.ethers.utils.solidityKeccak256(["bytes32", "uint32", "uint64"], [data.sweepTx.hash, 0, 16500]);
1511
+ (0, chai_1.expect)((await bridge.wallets(data.wallet.pubKeyHash)).mainUtxoHash).to.be.equal(expectedMainUtxoHash);
1512
+ });
1513
+ it("should emit the MovedFundsSwept event", async () => {
1514
+ await (0, chai_1.expect)(tx)
1515
+ .to.emit(bridge, "MovedFundsSwept")
1516
+ .withArgs(data.wallet.pubKeyHash, data.sweepTx.hash);
1517
+ });
1518
+ });
1519
+ context("when the single input does not refer to a Pending sweep request", () => {
1520
+ context("when the single input refers to an Unknown sweep request", () => {
1521
+ const data = moving_funds_1.MovedFundsSweepWithoutMainUtxo;
1522
+ before(async () => {
1523
+ await createSnapshot();
1524
+ });
1525
+ after(async () => {
1526
+ await restoreSnapshot();
1527
+ });
1528
+ it("should revert", async () => {
1529
+ // Getting rid of the `movedFundsSweepRequest`
1530
+ // allows running that scenario because
1531
+ // the sweep request will not exist in the system.
1532
+ await (0, chai_1.expect)(runMovedFundsSweepScenario({
1533
+ ...data,
1534
+ movedFundsSweepRequest: null,
1535
+ })).to.be.revertedWith("Sweep request must be in Pending state");
1536
+ });
1537
+ });
1538
+ context("when the single input refers to a Processed sweep request", () => {
1539
+ const data = moving_funds_1.MovedFundsSweepWithoutMainUtxo;
1540
+ let tx;
1541
+ before(async () => {
1542
+ await createSnapshot();
1543
+ const beforeProofActions = async () => {
1544
+ await bridge.processPendingMovedFundsSweepRequest(data.movedFundsSweepRequest
1545
+ .walletPubKeyHash, data.movedFundsSweepRequest);
1546
+ };
1547
+ tx = runMovedFundsSweepScenario(data, beforeProofActions);
1548
+ });
1549
+ after(async () => {
1550
+ await restoreSnapshot();
1551
+ });
1552
+ it("should revert", async () => {
1553
+ await (0, chai_1.expect)(tx).to.be.revertedWith("Sweep request must be in Pending state");
1554
+ });
1555
+ });
1556
+ context("when the single input refers to a TimedOut sweep request", () => {
1557
+ const data = moving_funds_1.MovedFundsSweepWithoutMainUtxo;
1558
+ let tx;
1559
+ before(async () => {
1560
+ await createSnapshot();
1561
+ const beforeProofActions = async () => {
1562
+ await bridge.timeoutPendingMovedFundsSweepRequest(data.movedFundsSweepRequest
1563
+ .walletPubKeyHash, data.movedFundsSweepRequest);
1564
+ };
1565
+ tx = runMovedFundsSweepScenario(data, beforeProofActions);
1566
+ });
1567
+ after(async () => {
1568
+ await restoreSnapshot();
1569
+ });
1570
+ it("should revert", async () => {
1571
+ await (0, chai_1.expect)(tx).to.be.revertedWith("Sweep request must be in Pending state");
1572
+ });
1573
+ });
1574
+ });
1575
+ context("when the single input does refer to a Pending sweep request that belongs to another wallet", () => {
1576
+ const data = moving_funds_1.MovedFundsSweepWithoutMainUtxo;
1577
+ before(async () => {
1578
+ await createSnapshot();
1579
+ });
1580
+ after(async () => {
1581
+ await restoreSnapshot();
1582
+ });
1583
+ it("should revert", async () => {
1584
+ // To make this scenario happen, we just
1585
+ // change the wallet in the test data' sweep
1586
+ // request.
1587
+ await (0, chai_1.expect)(runMovedFundsSweepScenario({
1588
+ ...data,
1589
+ movedFundsSweepRequest: {
1590
+ ...data.movedFundsSweepRequest,
1591
+ walletPubKeyHash: "0x7ac2d9378a1c47e589dfb8095ca95ed2140d2726",
1592
+ },
1593
+ })).to.be.revertedWith("Sweep request belongs to another wallet");
1594
+ });
1595
+ });
1596
+ context("when the number of inputs is other than one", () => {
1597
+ // Use a test data that contains a two-input
1598
+ // transaction.
1599
+ const data = moving_funds_1.MovedFundsSweepWithMainUtxo;
1600
+ before(async () => {
1601
+ await createSnapshot();
1602
+ });
1603
+ after(async () => {
1604
+ await restoreSnapshot();
1605
+ });
1606
+ it("should revert", async () => {
1607
+ // However, do not set wallet main UTXO. In
1608
+ // that case, the system will expect a
1609
+ // sweep transaction with a single input.
1610
+ await (0, chai_1.expect)(runMovedFundsSweepScenario({
1611
+ ...data,
1612
+ mainUtxo: deposit_sweep_1.NO_MAIN_UTXO,
1613
+ })).to.be.revertedWith("Moved funds sweep transaction must have a proper inputs count");
1614
+ });
1615
+ });
1616
+ });
1617
+ context("when the sweeping wallet has a main UTXO set", () => {
1618
+ context("when the first input refers to a Pending sweep request and the second input refers to the sweeping wallet main UTXO", () => {
1619
+ const data = moving_funds_1.MovedFundsSweepWithMainUtxo;
1620
+ let tx;
1621
+ before(async () => {
1622
+ await createSnapshot();
1623
+ tx = await runMovedFundsSweepScenario(data);
1624
+ });
1625
+ after(async () => {
1626
+ await restoreSnapshot();
1627
+ });
1628
+ it("should mark the sweep request as processed", async () => {
1629
+ const key = hardhat_1.ethers.utils.solidityKeccak256(["bytes32", "uint32"], [
1630
+ data.movedFundsSweepRequest.txHash,
1631
+ data.movedFundsSweepRequest.txOutputIndex,
1632
+ ]);
1633
+ // eslint-disable-next-line @typescript-eslint/no-unused-expressions
1634
+ (0, chai_1.expect)((await bridge.movedFundsSweepRequests(key))
1635
+ .state).to.be.equal(fixtures_1.movedFundsSweepRequestState.Processed);
1636
+ });
1637
+ it("should decrease the sweeping wallet's pending requests count", async () => {
1638
+ // The `setPendingMovedFundsSweepRequest` call
1639
+ // made as part of `runMovedFundsSweepScenario`
1640
+ // set this counter to 1. Eventually, it
1641
+ // should be decreased back to 0.
1642
+ (0, chai_1.expect)((await bridge.wallets(data.wallet.pubKeyHash)).pendingMovedFundsSweepRequestsCount).to.be.equal(0);
1643
+ });
1644
+ it("should set the transaction output as new sweeping wallet main UTXO", async () => {
1645
+ // Amount can be checked by opening the sweep tx
1646
+ // in a Bitcoin testnet explorer. In this case,
1647
+ // the output value is 2612530.
1648
+ const expectedMainUtxoHash = hardhat_1.ethers.utils.solidityKeccak256(["bytes32", "uint32", "uint64"], [data.sweepTx.hash, 0, 2612530]);
1649
+ (0, chai_1.expect)((await bridge.wallets(data.wallet.pubKeyHash)).mainUtxoHash).to.be.equal(expectedMainUtxoHash);
1650
+ });
1651
+ it("should emit the MovedFundsSwept event", async () => {
1652
+ await (0, chai_1.expect)(tx)
1653
+ .to.emit(bridge, "MovedFundsSwept")
1654
+ .withArgs(data.wallet.pubKeyHash, data.sweepTx.hash);
1655
+ });
1656
+ it("should mark the current sweeping wallet main UTXO as correctly spent", async () => {
1657
+ const key = hardhat_1.ethers.utils.solidityKeccak256(["bytes32", "uint32"], [
1658
+ data.mainUtxo.txHash,
1659
+ data.mainUtxo.txOutputIndex,
1660
+ ]);
1661
+ // eslint-disable-next-line @typescript-eslint/no-unused-expressions
1662
+ (0, chai_1.expect)(await bridge.spentMainUTXOs(key)).to.be
1663
+ .true;
1664
+ });
1665
+ });
1666
+ context("when the first input refers to the sweeping wallet main UTXO and the second input refers to a Pending sweep request", () => {
1667
+ // The sweep transaction used by this test data
1668
+ // has two inputs. The first input is registered
1669
+ // as a sweep request (i.e. it is referred by
1670
+ // `movedFundsSweepRequest`) and the second one
1671
+ // is meant to be the main UTXO (i.e. it is
1672
+ // referred by `mainUtxo`).
1673
+ const data = moving_funds_1.MovedFundsSweepWithMainUtxo;
1674
+ before(async () => {
1675
+ await createSnapshot();
1676
+ });
1677
+ after(async () => {
1678
+ await restoreSnapshot();
1679
+ });
1680
+ it("should revert", async () => {
1681
+ // To make that scenario happen, we just
1682
+ // let the test runner to register the first
1683
+ // input as the main UTXO and the second
1684
+ // one as the sweep request.
1685
+ const movedFundsSweepRequest = {
1686
+ ...data.mainUtxo,
1687
+ walletPubKeyHash: data.movedFundsSweepRequest
1688
+ .walletPubKeyHash,
1689
+ };
1690
+ const mainUtxo = {
1691
+ ...data.movedFundsSweepRequest,
1692
+ };
1693
+ await (0, chai_1.expect)(runMovedFundsSweepScenario({
1694
+ ...data,
1695
+ movedFundsSweepRequest,
1696
+ mainUtxo,
1697
+ })).to.be.revertedWith("Sweep request must be in Pending state");
1698
+ });
1699
+ });
1700
+ context("when the first input does not refer to a Pending sweep request and the second input refers to the sweeping wallet main UTXO", () => {
1701
+ context("when the first input refers to an Unknown sweep request", () => {
1702
+ const data = moving_funds_1.MovedFundsSweepWithMainUtxo;
1703
+ before(async () => {
1704
+ await createSnapshot();
1705
+ });
1706
+ after(async () => {
1707
+ await restoreSnapshot();
1708
+ });
1709
+ it("should revert", async () => {
1710
+ // Getting rid of the `movedFundsSweepRequest`
1711
+ // allows running that scenario because
1712
+ // the sweep request will not exist in the system.
1713
+ await (0, chai_1.expect)(runMovedFundsSweepScenario({
1714
+ ...data,
1715
+ movedFundsSweepRequest: null,
1716
+ })).to.be.revertedWith("Sweep request must be in Pending state");
1717
+ });
1718
+ });
1719
+ context("when the first input refers to a Processed sweep request", () => {
1720
+ const data = moving_funds_1.MovedFundsSweepWithMainUtxo;
1721
+ let tx;
1722
+ before(async () => {
1723
+ await createSnapshot();
1724
+ const beforeProofActions = async () => {
1725
+ await bridge.processPendingMovedFundsSweepRequest(data.movedFundsSweepRequest
1726
+ .walletPubKeyHash, data.movedFundsSweepRequest);
1727
+ };
1728
+ tx = runMovedFundsSweepScenario(data, beforeProofActions);
1729
+ });
1730
+ after(async () => {
1731
+ await restoreSnapshot();
1732
+ });
1733
+ it("should revert", async () => {
1734
+ await (0, chai_1.expect)(tx).to.be.revertedWith("Sweep request must be in Pending state");
1735
+ });
1736
+ });
1737
+ context("when the first input refers to a TimedOut sweep request", () => {
1738
+ const data = moving_funds_1.MovedFundsSweepWithMainUtxo;
1739
+ let tx;
1740
+ before(async () => {
1741
+ await createSnapshot();
1742
+ const beforeProofActions = async () => {
1743
+ await bridge.timeoutPendingMovedFundsSweepRequest(data.movedFundsSweepRequest
1744
+ .walletPubKeyHash, data.movedFundsSweepRequest);
1745
+ };
1746
+ tx = runMovedFundsSweepScenario(data, beforeProofActions);
1747
+ });
1748
+ after(async () => {
1749
+ await restoreSnapshot();
1750
+ });
1751
+ it("should revert", async () => {
1752
+ await (0, chai_1.expect)(tx).to.be.revertedWith("Sweep request must be in Pending state");
1753
+ });
1754
+ });
1755
+ });
1756
+ context("when the first input refers to a Pending sweep request that belongs to another wallet and the second input refers to the sweeping wallet main UTXO", () => {
1757
+ const data = moving_funds_1.MovedFundsSweepWithMainUtxo;
1758
+ before(async () => {
1759
+ await createSnapshot();
1760
+ });
1761
+ after(async () => {
1762
+ await restoreSnapshot();
1763
+ });
1764
+ it("should revert", async () => {
1765
+ // To make this scenario happen, we just
1766
+ // change the wallet in the test data' sweep
1767
+ // request.
1768
+ await (0, chai_1.expect)(runMovedFundsSweepScenario({
1769
+ ...data,
1770
+ movedFundsSweepRequest: {
1771
+ ...data.movedFundsSweepRequest,
1772
+ walletPubKeyHash: "0x8db50eb52063ea9d98b3eac91489a90f738986f6",
1773
+ },
1774
+ })).to.be.revertedWith("Sweep request belongs to another wallet");
1775
+ });
1776
+ });
1777
+ context("when the first input refers to a Pending sweep request and the second input does not refer to the sweeping wallet main UTXO", () => {
1778
+ const data = moving_funds_1.MovedFundsSweepWithMainUtxo;
1779
+ before(async () => {
1780
+ await createSnapshot();
1781
+ });
1782
+ after(async () => {
1783
+ await restoreSnapshot();
1784
+ });
1785
+ it("should revert", async () => {
1786
+ // To make this scenario happen, we just need
1787
+ // to simulate that the sweeping wallet has
1788
+ // a different main UTXO than the one used
1789
+ // by the second transaction input.
1790
+ await (0, chai_1.expect)(runMovedFundsSweepScenario({
1791
+ ...data,
1792
+ mainUtxo: {
1793
+ ...data.mainUtxo,
1794
+ txOutputIndex: 2,
1795
+ },
1796
+ })).to.be.revertedWith("Second input must point to the wallet's main UTXO");
1797
+ });
1798
+ });
1799
+ context("when the number of inputs is other than two", () => {
1800
+ // Use a test data with a one-input transaction.
1801
+ const data = moving_funds_1.MovedFundsSweepWithoutMainUtxo;
1802
+ before(async () => {
1803
+ await createSnapshot();
1804
+ });
1805
+ after(async () => {
1806
+ await restoreSnapshot();
1807
+ });
1808
+ it("should revert", async () => {
1809
+ // However, register a main UTXO for the
1810
+ // sweeping wallet in order to force the
1811
+ // system to expect a two-input transaction
1812
+ // for that sweeping wallet.
1813
+ await (0, chai_1.expect)(runMovedFundsSweepScenario({
1814
+ ...data,
1815
+ // Just an arbitrary main UTXO
1816
+ mainUtxo: {
1817
+ txHash: "0x7d5f7d4ae705d6adb8a402e5cd7f25f839a3f3ed243a8961c8ac5887d5aaf528",
1818
+ txOutputIndex: 0,
1819
+ txOutputValue: 873510,
1820
+ },
1821
+ })).to.be.revertedWith("Moved funds sweep transaction must have a proper inputs count");
1822
+ });
1823
+ });
1824
+ });
1825
+ });
1826
+ context("when transaction fee exceeds the sweep transaction maximum fee", () => {
1827
+ // Use a test data where the sweep transaction has
1828
+ // a fee of 2000 satoshi.
1829
+ const data = moving_funds_1.MovedFundsSweepWithoutMainUtxo;
1830
+ before(async () => {
1831
+ await createSnapshot();
1832
+ // Set the max fee to one satoshi less than the fee
1833
+ // used by the transaction.
1834
+ await bridge.setMovedFundsSweepTxMaxTotalFee(1999);
1835
+ });
1836
+ after(async () => {
1837
+ await restoreSnapshot();
1838
+ });
1839
+ it("should revert", async () => {
1840
+ await (0, chai_1.expect)(runMovedFundsSweepScenario(data)).to.be.revertedWith("Transaction fee is too high");
1841
+ });
1842
+ });
1843
+ });
1844
+ context("when main UTXO data are invalid", () => {
1845
+ const data = moving_funds_1.MovedFundsSweepWithMainUtxo;
1846
+ let tx;
1847
+ before(async () => {
1848
+ await createSnapshot();
1849
+ const beforeProofAction = async () => {
1850
+ // Swap the main UTXO just before the proof to make
1851
+ // this scenario happen.
1852
+ await bridge.setWalletMainUtxo(data.wallet.pubKeyHash, {
1853
+ ...data.mainUtxo,
1854
+ txOutputIndex: 2,
1855
+ });
1856
+ };
1857
+ tx = runMovedFundsSweepScenario(data, beforeProofAction);
1858
+ });
1859
+ after(async () => {
1860
+ await restoreSnapshot();
1861
+ });
1862
+ it("should revert", async () => {
1863
+ await (0, chai_1.expect)(tx).to.be.revertedWith("Invalid main UTXO data");
1864
+ });
1865
+ });
1866
+ });
1867
+ context("when sweeping wallet is in the MovingFunds state", () => {
1868
+ const data = moving_funds_1.MovedFundsSweepWithoutMainUtxo;
1869
+ let tx;
1870
+ before(async () => {
1871
+ await createSnapshot();
1872
+ tx = runMovedFundsSweepScenario({
1873
+ ...data,
1874
+ wallet: {
1875
+ ...data.wallet,
1876
+ state: fixtures_1.walletState.MovingFunds,
1877
+ },
1878
+ });
1879
+ });
1880
+ after(async () => {
1881
+ await restoreSnapshot();
1882
+ });
1883
+ it("should succeed", async () => {
1884
+ // The assertions were already performed for Live wallet
1885
+ // scenarios. Here we just make sure the transaction
1886
+ // succeeds for a MovingFunds wallet.
1887
+ await (0, chai_1.expect)(tx).to.not.be.reverted;
1888
+ });
1889
+ });
1890
+ });
1891
+ context("when sweeping wallet is neither in the Live nor MovingFunds state", () => {
1892
+ const testData = [
1893
+ {
1894
+ testName: "when sweeping wallet is in the Unknown state",
1895
+ walletState: fixtures_1.walletState.Unknown,
1896
+ },
1897
+ {
1898
+ testName: "when sweeping wallet is in the Closing state",
1899
+ walletState: fixtures_1.walletState.Closing,
1900
+ },
1901
+ {
1902
+ testName: "when sweeping wallet is in the Closed state",
1903
+ walletState: fixtures_1.walletState.Closed,
1904
+ },
1905
+ {
1906
+ testName: "when sweeping wallet is in the Terminated state",
1907
+ walletState: fixtures_1.walletState.Terminated,
1908
+ },
1909
+ ];
1910
+ testData.forEach((test) => {
1911
+ context(test.testName, () => {
1912
+ const data = moving_funds_1.MovedFundsSweepWithoutMainUtxo;
1913
+ let tx;
1914
+ before(async () => {
1915
+ await createSnapshot();
1916
+ tx = runMovedFundsSweepScenario({
1917
+ ...data,
1918
+ wallet: {
1919
+ ...data.wallet,
1920
+ state: test.walletState,
1921
+ },
1922
+ });
1923
+ });
1924
+ after(async () => {
1925
+ await restoreSnapshot();
1926
+ });
1927
+ it("should revert", async () => {
1928
+ await (0, chai_1.expect)(tx).to.be.revertedWith("Wallet must be in Live or MovingFunds state");
1929
+ });
1930
+ });
1931
+ });
1932
+ });
1933
+ });
1934
+ context("when single output is neither P2PKH nor P2WPKH", () => {
1935
+ const data = moving_funds_1.MovedFundsSweepP2SHOutput;
1936
+ before(async () => {
1937
+ await createSnapshot();
1938
+ });
1939
+ after(async () => {
1940
+ await restoreSnapshot();
1941
+ });
1942
+ it("should revert", async () => {
1943
+ await (0, chai_1.expect)(runMovedFundsSweepScenario(data)).to.be.revertedWith("Output must be P2PKH or P2WPKH");
1944
+ });
1945
+ });
1946
+ });
1947
+ context("when the single output is not 20-byte", () => {
1948
+ const data = moving_funds_1.MovedFundsSweepProvablyUnspendableOutput;
1949
+ before(async () => {
1950
+ await createSnapshot();
1951
+ });
1952
+ after(async () => {
1953
+ await restoreSnapshot();
1954
+ });
1955
+ it("should revert", async () => {
1956
+ await (0, chai_1.expect)(runMovedFundsSweepScenario(data)).to.be.revertedWith("Output's public key hash must have 20 bytes");
1957
+ });
1958
+ });
1959
+ });
1960
+ context("when output count is other than one", () => {
1961
+ const data = moving_funds_1.MovedFundsSweepMultipleOutputs;
1962
+ before(async () => {
1963
+ await createSnapshot();
1964
+ });
1965
+ after(async () => {
1966
+ await restoreSnapshot();
1967
+ });
1968
+ it("should revert", async () => {
1969
+ await (0, chai_1.expect)(runMovedFundsSweepScenario(data)).to.be.revertedWith("Moved funds sweep transaction must have a single output");
1970
+ });
1971
+ });
1972
+ });
1973
+ context("when transaction proof is not valid", () => {
1974
+ context("when input vector is not valid", () => {
1975
+ const data = JSON.parse(JSON.stringify(moving_funds_1.MovedFundsSweepWithoutMainUtxo));
1976
+ before(async () => {
1977
+ await createSnapshot();
1978
+ });
1979
+ after(async () => {
1980
+ await restoreSnapshot();
1981
+ });
1982
+ it("should revert", async () => {
1983
+ // Corrupt the input vector by setting a compactSize uint claiming
1984
+ // there are no inputs at all.
1985
+ data.sweepTx.inputVector =
1986
+ "0x00b69a2869840aa6fdfd143136ff4514ca46ea2d876855040892ad74ab" +
1987
+ "8c5274220100000000ffffffff";
1988
+ await (0, chai_1.expect)(runMovedFundsSweepScenario(data)).to.be.revertedWith("Invalid input vector provided");
1989
+ });
1990
+ });
1991
+ context("when output vector is not valid", () => {
1992
+ const data = JSON.parse(JSON.stringify(moving_funds_1.MovedFundsSweepWithoutMainUtxo));
1993
+ before(async () => {
1994
+ await createSnapshot();
1995
+ });
1996
+ after(async () => {
1997
+ await restoreSnapshot();
1998
+ });
1999
+ it("should revert", async () => {
2000
+ // Corrupt the output vector by setting a compactSize uint claiming
2001
+ // there is no outputs at all.
2002
+ data.sweepTx.outputVector =
2003
+ "0x005cf511000000000017a91486884e6be1525dab5ae0b451bd2c72cee6" +
2004
+ "7dcf4187";
2005
+ await (0, chai_1.expect)(runMovedFundsSweepScenario(data)).to.be.revertedWith("Invalid output vector provided");
2006
+ });
2007
+ });
2008
+ context("when merkle proof is not valid", () => {
2009
+ const data = JSON.parse(JSON.stringify(moving_funds_1.MovedFundsSweepWithoutMainUtxo));
2010
+ before(async () => {
2011
+ await createSnapshot();
2012
+ });
2013
+ after(async () => {
2014
+ await restoreSnapshot();
2015
+ });
2016
+ it("should revert", async () => {
2017
+ // Corrupt the merkle proof by changing tx index in block to an
2018
+ // invalid one. The proper one is 12 so any other will do the trick.
2019
+ data.sweepProof.txIndexInBlock = 30;
2020
+ await (0, chai_1.expect)(runMovedFundsSweepScenario(data)).to.be.revertedWith("Tx merkle proof is not valid for provided header and tx hash");
2021
+ });
2022
+ });
2023
+ context("when proof difficulty is not current nor previous", () => {
2024
+ const data = JSON.parse(JSON.stringify(moving_funds_1.MovedFundsSweepWithoutMainUtxo));
2025
+ before(async () => {
2026
+ await createSnapshot();
2027
+ });
2028
+ after(async () => {
2029
+ await restoreSnapshot();
2030
+ });
2031
+ it("should revert", async () => {
2032
+ // To pass the proof validation, the difficulty returned by the relay
2033
+ // must be 1 for test data used in this scenario. Setting
2034
+ // a different value will cause difficulty comparison failure.
2035
+ data.chainDifficulty = 2;
2036
+ await (0, chai_1.expect)(runMovedFundsSweepScenario(data)).to.be.revertedWith("Not at current or previous difficulty");
2037
+ });
2038
+ });
2039
+ context("when headers chain length is not valid", () => {
2040
+ const data = JSON.parse(JSON.stringify(moving_funds_1.MovedFundsSweepWithoutMainUtxo));
2041
+ before(async () => {
2042
+ await createSnapshot();
2043
+ });
2044
+ after(async () => {
2045
+ await restoreSnapshot();
2046
+ });
2047
+ it("should revert", async () => {
2048
+ // Corrupt the bitcoin headers length in the moving funds proof. The
2049
+ // proper value is length divisible by 80 so any length violating
2050
+ // this rule will cause failure. In this case, we just remove the
2051
+ // last byte from proper headers chain.
2052
+ const properHeaders = data.sweepProof.bitcoinHeaders.toString();
2053
+ data.sweepProof.bitcoinHeaders = properHeaders.substring(0, properHeaders.length - 2);
2054
+ await (0, chai_1.expect)(runMovedFundsSweepScenario(data)).to.be.revertedWith("Invalid length of the headers chain");
2055
+ });
2056
+ });
2057
+ context("when headers chain is not valid", () => {
2058
+ const data = JSON.parse(JSON.stringify(moving_funds_1.MovedFundsSweepWithoutMainUtxo));
2059
+ before(async () => {
2060
+ await createSnapshot();
2061
+ });
2062
+ after(async () => {
2063
+ await restoreSnapshot();
2064
+ });
2065
+ it("should revert", async () => {
2066
+ // Bitcoin headers must form a chain to pass the proof validation.
2067
+ // That means the `previous block hash` encoded in the given block
2068
+ // header must match the actual previous header's hash. To test
2069
+ // that scenario, we corrupt the `previous block hash` of the
2070
+ // second header. Each header is 80 bytes length. First 4 bytes
2071
+ // of each header is `version` and 32 subsequent bytes is
2072
+ // `previous block hash`. Changing byte 85 of the whole chain will
2073
+ // do the work.
2074
+ const properHeaders = data.sweepProof.bitcoinHeaders.toString();
2075
+ data.sweepProof.bitcoinHeaders = `${properHeaders.substring(0, 170)}ff${properHeaders.substring(172)}`;
2076
+ await (0, chai_1.expect)(runMovedFundsSweepScenario(data)).to.be.revertedWith("Invalid headers chain");
2077
+ });
2078
+ });
2079
+ context("when the work in the header is insufficient", () => {
2080
+ const data = JSON.parse(JSON.stringify(moving_funds_1.MovedFundsSweepWithoutMainUtxo));
2081
+ before(async () => {
2082
+ await createSnapshot();
2083
+ });
2084
+ after(async () => {
2085
+ await restoreSnapshot();
2086
+ });
2087
+ it("should revert", async () => {
2088
+ // Each header encodes a `difficulty target` field in bytes 72-76.
2089
+ // The given header's hash (interpreted as uint) must be bigger than
2090
+ // the `difficulty target`. To test this scenario, we change the
2091
+ // last byte of the last header in such a way their hash becomes
2092
+ // lower than their `difficulty target`.
2093
+ const properHeaders = data.sweepProof.bitcoinHeaders.toString();
2094
+ data.sweepProof.bitcoinHeaders = `${properHeaders.substring(0, properHeaders.length - 2)}ff`;
2095
+ await (0, chai_1.expect)(runMovedFundsSweepScenario(data)).to.be.revertedWith("Insufficient work in a header");
2096
+ });
2097
+ });
2098
+ context("when accumulated difficulty in headers chain is insufficient", () => {
2099
+ let otherBridge;
2100
+ const data = JSON.parse(JSON.stringify(moving_funds_1.MovedFundsSweepWithMainUtxo));
2101
+ before(async () => {
2102
+ await createSnapshot();
2103
+ // Necessary to pass the first part of proof validation.
2104
+ relay.getCurrentEpochDifficulty.returns(data.chainDifficulty);
2105
+ relay.getPrevEpochDifficulty.returns(data.chainDifficulty);
2106
+ // Deploy another bridge which has higher `txProofDifficultyFactor`
2107
+ // than the original bridge. That means it will need 12 confirmations
2108
+ // to deem transaction proof validity. This scenario uses test
2109
+ // data which has only 6 confirmations. That should force the
2110
+ // failure we expect within this scenario.
2111
+ otherBridge = await BridgeFactory.deploy();
2112
+ await otherBridge.initialize(bank.address, relay.address, treasury.address, walletRegistry.address, 12);
2113
+ await otherBridge.deployed();
2114
+ });
2115
+ after(async () => {
2116
+ relay.getCurrentEpochDifficulty.reset();
2117
+ relay.getPrevEpochDifficulty.reset();
2118
+ await restoreSnapshot();
2119
+ });
2120
+ it("should revert", async () => {
2121
+ await (0, chai_1.expect)(otherBridge.submitMovedFundsSweepProof(data.sweepTx, data.sweepProof, data.mainUtxo)).to.be.revertedWith("Insufficient accumulated difficulty in header chain");
2122
+ });
2123
+ });
2124
+ });
2125
+ });
2126
+ describe("notifyMovedFundsSweepTimeout", () => {
2127
+ const walletDraft = {
2128
+ ecdsaWalletID: ecdsa_1.ecdsaWalletTestData.walletID,
2129
+ mainUtxoHash: hardhat_1.ethers.constants.HashZero,
2130
+ pendingRedemptionsValue: 0,
2131
+ createdAt: 0,
2132
+ movingFundsRequestedAt: 0,
2133
+ closingStartedAt: 0,
2134
+ pendingMovedFundsSweepRequestsCount: 0,
2135
+ state: fixtures_1.walletState.Unknown,
2136
+ movingFundsTargetWalletsCommitmentHash: hardhat_1.ethers.constants.HashZero,
2137
+ };
2138
+ const walletMembersIDs = [1, 2, 3, 4, 5];
2139
+ // Just an arbitrary request for test purposes.
2140
+ const movedFundsSweepRequest = {
2141
+ walletPubKeyHash: ecdsa_1.ecdsaWalletTestData.pubKeyHash160,
2142
+ txHash: "0x7d5f7d4ae705d6adb8a402e5cd7f25f839a3f3ed243a8961c8ac5887d5aaf528",
2143
+ txOutputIndex: 1,
2144
+ txOutputValue: 1747020,
2145
+ };
2146
+ context("when moved funds sweep request is in the Pending state", () => {
2147
+ before(async () => {
2148
+ await createSnapshot();
2149
+ await bridge.setWallet(ecdsa_1.ecdsaWalletTestData.pubKeyHash160, walletDraft);
2150
+ await bridge.setPendingMovedFundsSweepRequest(movedFundsSweepRequest.walletPubKeyHash, movedFundsSweepRequest);
2151
+ });
2152
+ after(async () => {
2153
+ await restoreSnapshot();
2154
+ });
2155
+ context("when moved funds sweep request has timed out", () => {
2156
+ before(async () => {
2157
+ await createSnapshot();
2158
+ await increaseTime(movedFundsSweepTimeout);
2159
+ });
2160
+ after(async () => {
2161
+ await restoreSnapshot();
2162
+ });
2163
+ context("when the wallet is either in the Live or MovingFunds state", () => {
2164
+ const testData = [
2165
+ {
2166
+ testName: "when the wallet is in the Live state but the wallet is not the active one",
2167
+ walletState: fixtures_1.walletState.Live,
2168
+ additionalSetup: async () => {
2169
+ // The active wallet is a different wallet than the tested one
2170
+ await bridge.setActiveWallet("0x0b9f85c224b0e018a5865392927b3f9e16cf5e79");
2171
+ },
2172
+ additionalAssertions: async () => {
2173
+ it("should decrease the live wallets count", async () => {
2174
+ (0, chai_1.expect)(await bridge.liveWalletsCount()).to.be.equal(0);
2175
+ });
2176
+ it("should not unset the active wallet", async () => {
2177
+ (0, chai_1.expect)(await bridge.activeWalletPubKeyHash()).to.be.not.equal("0x0000000000000000000000000000000000000000");
2178
+ });
2179
+ },
2180
+ },
2181
+ {
2182
+ testName: "when the wallet is in the Live state and the wallet is the active one",
2183
+ walletState: fixtures_1.walletState.Live,
2184
+ additionalSetup: async () => {
2185
+ await bridge.setActiveWallet(movedFundsSweepRequest.walletPubKeyHash);
2186
+ },
2187
+ additionalAssertions: async () => {
2188
+ it("should decrease the live wallets count", async () => {
2189
+ (0, chai_1.expect)(await bridge.liveWalletsCount()).to.be.equal(0);
2190
+ });
2191
+ it("should unset the active wallet", async () => {
2192
+ (0, chai_1.expect)(await bridge.activeWalletPubKeyHash()).to.be.equal("0x0000000000000000000000000000000000000000");
2193
+ });
2194
+ },
2195
+ },
2196
+ {
2197
+ testName: "when the wallet is in the MovingFunds state",
2198
+ walletState: fixtures_1.walletState.MovingFunds,
2199
+ additionalSetup: async () => { },
2200
+ additionalAssertions: async () => { },
2201
+ },
2202
+ ];
2203
+ testData.forEach((test) => {
2204
+ context(test.testName, async () => {
2205
+ let tx;
2206
+ before(async () => {
2207
+ await createSnapshot();
2208
+ // We change the wallet state while preserving other fields.
2209
+ // We don't have an update function in the stub so we just use
2210
+ // the getter to get wallet fields and set them through the
2211
+ // setter with state field overwritten.
2212
+ await bridge.setWallet(movedFundsSweepRequest.walletPubKeyHash, {
2213
+ ...(await bridge.wallets(movedFundsSweepRequest.walletPubKeyHash)),
2214
+ state: test.walletState,
2215
+ });
2216
+ await test.additionalSetup();
2217
+ tx = await bridge
2218
+ .connect(thirdParty)
2219
+ .notifyMovedFundsSweepTimeout(movedFundsSweepRequest.txHash, movedFundsSweepRequest.txOutputIndex, walletMembersIDs);
2220
+ });
2221
+ after(async () => {
2222
+ walletRegistry.closeWallet.reset();
2223
+ walletRegistry.seize.reset();
2224
+ await restoreSnapshot();
2225
+ });
2226
+ it("should switch the moved funds sweep request to the TimedOut state", async () => {
2227
+ const requestKey = hardhat_1.ethers.utils.solidityKeccak256(["bytes32", "uint32"], [
2228
+ movedFundsSweepRequest.txHash,
2229
+ movedFundsSweepRequest.txOutputIndex,
2230
+ ]);
2231
+ (0, chai_1.expect)((await bridge.movedFundsSweepRequests(requestKey)).state).to.be.equal(fixtures_1.movedFundsSweepRequestState.TimedOut);
2232
+ });
2233
+ it("should decrease the number of pending moved funds sweep requests for the given wallet", async () => (0, chai_1.expect)((await bridge.wallets(movedFundsSweepRequest.walletPubKeyHash)).pendingMovedFundsSweepRequestsCount).to.be.equal(0));
2234
+ it("should switch the wallet to Terminated state", async () => {
2235
+ (0, chai_1.expect)((await bridge.wallets(movedFundsSweepRequest.walletPubKeyHash)).state).to.be.equal(fixtures_1.walletState.Terminated);
2236
+ });
2237
+ it("should emit WalletTerminated event", async () => {
2238
+ await (0, chai_1.expect)(tx)
2239
+ .to.emit(bridge, "WalletTerminated")
2240
+ .withArgs(walletDraft.ecdsaWalletID, movedFundsSweepRequest.walletPubKeyHash);
2241
+ });
2242
+ it("should call ECDSA Wallet Registry's closeWallet function", async () => {
2243
+ // eslint-disable-next-line @typescript-eslint/no-unused-expressions
2244
+ (0, chai_1.expect)(walletRegistry.closeWallet).to.have.been.calledOnceWith(walletDraft.ecdsaWalletID);
2245
+ });
2246
+ it("should call the ECDSA wallet registry's seize function", async () => {
2247
+ (0, chai_1.expect)(walletRegistry.seize).to.have.been.calledOnceWith(movedFundsSweepTimeoutSlashingAmount, movedFundsSweepTimeoutNotifierRewardMultiplier, await thirdParty.getAddress(), walletDraft.ecdsaWalletID, walletMembersIDs);
2248
+ });
2249
+ it("should emit MovedFundsSweepTimedOut event", async () => {
2250
+ await (0, chai_1.expect)(tx)
2251
+ .to.emit(bridge, "MovedFundsSweepTimedOut")
2252
+ .withArgs(movedFundsSweepRequest.walletPubKeyHash, movedFundsSweepRequest.txHash, movedFundsSweepRequest.txOutputIndex);
2253
+ });
2254
+ await test.additionalAssertions();
2255
+ });
2256
+ });
2257
+ });
2258
+ context("when the wallet is in the Terminated state", () => {
2259
+ let tx;
2260
+ before(async () => {
2261
+ await createSnapshot();
2262
+ await bridge.setWallet(movedFundsSweepRequest.walletPubKeyHash, {
2263
+ ...(await bridge.wallets(movedFundsSweepRequest.walletPubKeyHash)),
2264
+ state: fixtures_1.walletState.Terminated,
2265
+ });
2266
+ tx = await bridge
2267
+ .connect(thirdParty)
2268
+ .notifyMovedFundsSweepTimeout(movedFundsSweepRequest.txHash, movedFundsSweepRequest.txOutputIndex, walletMembersIDs);
2269
+ });
2270
+ after(async () => {
2271
+ await restoreSnapshot();
2272
+ });
2273
+ it("should switch the moved funds sweep request to the TimedOut state", async () => {
2274
+ const requestKey = hardhat_1.ethers.utils.solidityKeccak256(["bytes32", "uint32"], [
2275
+ movedFundsSweepRequest.txHash,
2276
+ movedFundsSweepRequest.txOutputIndex,
2277
+ ]);
2278
+ (0, chai_1.expect)((await bridge.movedFundsSweepRequests(requestKey)).state).to.be.equal(fixtures_1.movedFundsSweepRequestState.TimedOut);
2279
+ });
2280
+ it("should decrease the number of pending moved funds sweep requests for the given wallet", async () => (0, chai_1.expect)((await bridge.wallets(movedFundsSweepRequest.walletPubKeyHash))
2281
+ .pendingMovedFundsSweepRequestsCount).to.be.equal(0));
2282
+ it("should not change the wallet state", async () => {
2283
+ (0, chai_1.expect)((await bridge.wallets(movedFundsSweepRequest.walletPubKeyHash))
2284
+ .state).to.be.equal(fixtures_1.walletState.Terminated);
2285
+ });
2286
+ });
2287
+ context("when the wallet is neither in the Live nor MovingFunds nor Terminated state", () => {
2288
+ const testData = [
2289
+ {
2290
+ testName: "when the wallet is in the Unknown state",
2291
+ walletState: fixtures_1.walletState.Unknown,
2292
+ },
2293
+ {
2294
+ testName: "when the wallet is in the Closing state",
2295
+ walletState: fixtures_1.walletState.Closing,
2296
+ },
2297
+ {
2298
+ testName: "when the wallet is in the Closed state",
2299
+ walletState: fixtures_1.walletState.Closed,
2300
+ },
2301
+ ];
2302
+ testData.forEach((test) => {
2303
+ context(test.testName, async () => {
2304
+ before(async () => {
2305
+ await createSnapshot();
2306
+ await bridge.setWallet(movedFundsSweepRequest.walletPubKeyHash, {
2307
+ ...(await bridge.wallets(movedFundsSweepRequest.walletPubKeyHash)),
2308
+ state: test.walletState,
2309
+ });
2310
+ });
2311
+ after(async () => {
2312
+ await restoreSnapshot();
2313
+ });
2314
+ it("should revert", async () => {
2315
+ await (0, chai_1.expect)(bridge.notifyMovedFundsSweepTimeout(movedFundsSweepRequest.txHash, movedFundsSweepRequest.txOutputIndex, walletMembersIDs)).to.be.revertedWith("Wallet must be in Live or MovingFunds or Terminated state");
2316
+ });
2317
+ });
2318
+ });
2319
+ });
2320
+ });
2321
+ context("when moved funds sweep request has not timed out yet", () => {
2322
+ before(async () => {
2323
+ await createSnapshot();
2324
+ await increaseTime(movedFundsSweepTimeout - 1);
2325
+ });
2326
+ after(async () => {
2327
+ await restoreSnapshot();
2328
+ });
2329
+ it("should revert", async () => {
2330
+ await (0, chai_1.expect)(bridge.notifyMovedFundsSweepTimeout(movedFundsSweepRequest.txHash, movedFundsSweepRequest.txOutputIndex, walletMembersIDs)).to.be.revertedWith("Sweep request has not timed out yet");
2331
+ });
2332
+ });
2333
+ });
2334
+ context("when moved funds sweep request is not in the Pending state", () => {
2335
+ context("when moved funds sweep request is in the Unknown state", () => {
2336
+ before(async () => {
2337
+ await createSnapshot();
2338
+ });
2339
+ after(async () => {
2340
+ await restoreSnapshot();
2341
+ });
2342
+ it("should revert", async () => {
2343
+ await (0, chai_1.expect)(bridge.notifyMovedFundsSweepTimeout(movedFundsSweepRequest.txHash, movedFundsSweepRequest.txOutputIndex, walletMembersIDs)).to.be.revertedWith("Sweep request must be in Pending state");
2344
+ });
2345
+ });
2346
+ context("when moved funds sweep request is in the Processed state", () => {
2347
+ before(async () => {
2348
+ await createSnapshot();
2349
+ await bridge.setWallet(ecdsa_1.ecdsaWalletTestData.pubKeyHash160, walletDraft);
2350
+ await bridge.setPendingMovedFundsSweepRequest(movedFundsSweepRequest.walletPubKeyHash, movedFundsSweepRequest);
2351
+ await bridge.processPendingMovedFundsSweepRequest(movedFundsSweepRequest.walletPubKeyHash, movedFundsSweepRequest);
2352
+ });
2353
+ after(async () => {
2354
+ await restoreSnapshot();
2355
+ });
2356
+ it("should revert", async () => {
2357
+ await (0, chai_1.expect)(bridge.notifyMovedFundsSweepTimeout(movedFundsSweepRequest.txHash, movedFundsSweepRequest.txOutputIndex, walletMembersIDs)).to.be.revertedWith("Sweep request must be in Pending state");
2358
+ });
2359
+ });
2360
+ context("when moved funds sweep request is in the TimedOut state", () => {
2361
+ before(async () => {
2362
+ await createSnapshot();
2363
+ await bridge.setWallet(ecdsa_1.ecdsaWalletTestData.pubKeyHash160, walletDraft);
2364
+ await bridge.setPendingMovedFundsSweepRequest(movedFundsSweepRequest.walletPubKeyHash, movedFundsSweepRequest);
2365
+ await bridge.timeoutPendingMovedFundsSweepRequest(movedFundsSweepRequest.walletPubKeyHash, movedFundsSweepRequest);
2366
+ });
2367
+ after(async () => {
2368
+ await restoreSnapshot();
2369
+ });
2370
+ it("should revert", async () => {
2371
+ await (0, chai_1.expect)(bridge.notifyMovedFundsSweepTimeout(movedFundsSweepRequest.txHash, movedFundsSweepRequest.txOutputIndex, walletMembersIDs)).to.be.revertedWith("Sweep request must be in Pending state");
2372
+ });
2373
+ });
2374
+ });
2375
+ });
2376
+ async function runMovingFundsScenario(data, beforeProofActions) {
2377
+ relay.getCurrentEpochDifficulty.returns(data.chainDifficulty);
2378
+ relay.getPrevEpochDifficulty.returns(data.chainDifficulty);
2379
+ // Simulate the wallet is a registered one.
2380
+ await bridge.setWallet(data.wallet.pubKeyHash, {
2381
+ ecdsaWalletID: data.wallet.ecdsaWalletID,
2382
+ mainUtxoHash: hardhat_1.ethers.constants.HashZero,
2383
+ pendingRedemptionsValue: 0,
2384
+ createdAt: await lastBlockTime(),
2385
+ movingFundsRequestedAt: await lastBlockTime(),
2386
+ closingStartedAt: 0,
2387
+ pendingMovedFundsSweepRequestsCount: 0,
2388
+ state: data.wallet.state,
2389
+ movingFundsTargetWalletsCommitmentHash: data.targetWalletsCommitment.length > 0
2390
+ ? hardhat_1.ethers.utils.solidityKeccak256(["bytes20[]"], [data.targetWalletsCommitment])
2391
+ : hardhat_1.ethers.constants.HashZero,
2392
+ });
2393
+ // Simulate the prepared main UTXO belongs to the wallet.
2394
+ await bridge.setWalletMainUtxo(data.wallet.pubKeyHash, data.mainUtxo);
2395
+ if (beforeProofActions) {
2396
+ await beforeProofActions();
2397
+ }
2398
+ const tx = await bridge.submitMovingFundsProof(data.movingFundsTx, data.movingFundsProof, data.mainUtxo, data.wallet.pubKeyHash);
2399
+ relay.getCurrentEpochDifficulty.reset();
2400
+ relay.getPrevEpochDifficulty.reset();
2401
+ return tx;
2402
+ }
2403
+ async function runMovedFundsSweepScenario(data, beforeProofActions) {
2404
+ relay.getCurrentEpochDifficulty.returns(data.chainDifficulty);
2405
+ relay.getPrevEpochDifficulty.returns(data.chainDifficulty);
2406
+ // Simulate the wallet is a registered one.
2407
+ await bridge.setWallet(data.wallet.pubKeyHash, {
2408
+ ecdsaWalletID: data.wallet.ecdsaWalletID,
2409
+ mainUtxoHash: hardhat_1.ethers.constants.HashZero,
2410
+ pendingRedemptionsValue: 0,
2411
+ createdAt: await lastBlockTime(),
2412
+ movingFundsRequestedAt: 0,
2413
+ closingStartedAt: 0,
2414
+ pendingMovedFundsSweepRequestsCount: 0,
2415
+ state: data.wallet.state,
2416
+ movingFundsTargetWalletsCommitmentHash: hardhat_1.ethers.constants.HashZero,
2417
+ });
2418
+ if (data.mainUtxo.txHash !== hardhat_1.ethers.constants.HashZero) {
2419
+ // Simulate the prepared main UTXO belongs to the wallet.
2420
+ await bridge.setWalletMainUtxo(data.wallet.pubKeyHash, data.mainUtxo);
2421
+ }
2422
+ if (data.movedFundsSweepRequest) {
2423
+ await bridge.setPendingMovedFundsSweepRequest(data.movedFundsSweepRequest.walletPubKeyHash, data.movedFundsSweepRequest);
2424
+ // Just make sure the stub function `setPendingMovedFundsSweepRequest`
2425
+ // initialized the counter properly.
2426
+ (0, chai_1.assert)((await bridge.wallets(data.movedFundsSweepRequest.walletPubKeyHash))
2427
+ .pendingMovedFundsSweepRequestsCount === 1, "Pending moved funds request counter for the sweeping wallet should be set up to 1");
2428
+ }
2429
+ if (beforeProofActions) {
2430
+ await beforeProofActions();
2431
+ }
2432
+ const tx = await bridge.submitMovedFundsSweepProof(data.sweepTx, data.sweepProof, data.mainUtxo);
2433
+ relay.getCurrentEpochDifficulty.reset();
2434
+ relay.getPrevEpochDifficulty.reset();
2435
+ return tx;
2436
+ }
2437
+ });