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