@keep-network/tbtc-v2 1.6.0-dev.5 → 1.6.0-dev.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (140) hide show
  1. package/artifacts/BLS.json +1 -1
  2. package/artifacts/Bank.json +3 -3
  3. package/artifacts/BeaconAuthorization.json +1 -1
  4. package/artifacts/BeaconDkg.json +1 -1
  5. package/artifacts/BeaconDkgValidator.json +1 -1
  6. package/artifacts/BeaconInactivity.json +1 -1
  7. package/artifacts/BeaconSortitionPool.json +3 -3
  8. package/artifacts/Bridge.json +5 -5
  9. package/artifacts/BridgeGovernance.json +2 -2
  10. package/artifacts/BridgeGovernanceParameters.json +2 -2
  11. package/artifacts/Deposit.json +2 -2
  12. package/artifacts/DepositSweep.json +2 -2
  13. package/artifacts/DonationVault.json +3 -3
  14. package/artifacts/EcdsaDkgValidator.json +1 -1
  15. package/artifacts/EcdsaInactivity.json +1 -1
  16. package/artifacts/EcdsaSortitionPool.json +3 -3
  17. package/artifacts/Fraud.json +2 -2
  18. package/artifacts/KeepRegistry.json +1 -1
  19. package/artifacts/KeepStake.json +2 -2
  20. package/artifacts/KeepToken.json +2 -2
  21. package/artifacts/KeepTokenStaking.json +1 -1
  22. package/artifacts/LightRelay.json +18 -18
  23. package/artifacts/LightRelayMaintainerProxy.json +17 -17
  24. package/artifacts/MaintainerProxy.json +19 -19
  25. package/artifacts/MovingFunds.json +2 -2
  26. package/artifacts/NuCypherStakingEscrow.json +1 -1
  27. package/artifacts/NuCypherToken.json +2 -2
  28. package/artifacts/RandomBeacon.json +2 -2
  29. package/artifacts/RandomBeaconChaosnet.json +2 -2
  30. package/artifacts/RandomBeaconGovernance.json +2 -2
  31. package/artifacts/Redemption.json +2 -2
  32. package/artifacts/ReimbursementPool.json +2 -2
  33. package/artifacts/T.json +2 -2
  34. package/artifacts/TBTC.json +3 -3
  35. package/artifacts/TBTCToken.json +3 -3
  36. package/artifacts/TBTCVault.json +23 -23
  37. package/artifacts/TokenStaking.json +1 -1
  38. package/artifacts/TokenholderGovernor.json +9 -9
  39. package/artifacts/TokenholderTimelock.json +8 -8
  40. package/artifacts/VendingMachine.json +3 -3
  41. package/artifacts/VendingMachineKeep.json +1 -1
  42. package/artifacts/VendingMachineNuCypher.json +1 -1
  43. package/artifacts/VendingMachineV2.json +3 -3
  44. package/artifacts/VendingMachineV3.json +3 -3
  45. package/artifacts/WalletProposalValidator.json +389 -0
  46. package/artifacts/WalletRegistry.json +5 -5
  47. package/artifacts/WalletRegistryGovernance.json +2 -2
  48. package/artifacts/Wallets.json +2 -2
  49. package/artifacts/solcInputs/{d2d7e276da75d7184610fe11a4a103b7.json → d46fa1d8846c35adf326ab51a3910266.json} +2 -2
  50. package/build/contracts/GovernanceUtils.sol/GovernanceUtils.dbg.json +1 -1
  51. package/build/contracts/bank/Bank.sol/Bank.dbg.json +1 -1
  52. package/build/contracts/bank/IReceiveBalanceApproval.sol/IReceiveBalanceApproval.dbg.json +1 -1
  53. package/build/contracts/bridge/BitcoinTx.sol/BitcoinTx.dbg.json +1 -1
  54. package/build/contracts/bridge/Bridge.sol/Bridge.dbg.json +1 -1
  55. package/build/contracts/bridge/BridgeGovernanceParameters.sol/BridgeGovernanceParameters.dbg.json +1 -1
  56. package/build/contracts/bridge/BridgeState.sol/BridgeState.dbg.json +1 -1
  57. package/build/contracts/bridge/Deposit.sol/Deposit.dbg.json +1 -1
  58. package/build/contracts/bridge/DepositSweep.sol/DepositSweep.dbg.json +1 -1
  59. package/build/contracts/bridge/EcdsaLib.sol/EcdsaLib.dbg.json +1 -1
  60. package/build/contracts/bridge/Fraud.sol/Fraud.dbg.json +1 -1
  61. package/build/contracts/bridge/Heartbeat.sol/Heartbeat.dbg.json +1 -1
  62. package/build/contracts/bridge/IRelay.sol/IRelay.dbg.json +1 -1
  63. package/build/contracts/bridge/MovingFunds.sol/MovingFunds.dbg.json +1 -1
  64. package/build/contracts/bridge/Redemption.sol/OutboundTx.dbg.json +1 -1
  65. package/build/contracts/bridge/Redemption.sol/Redemption.dbg.json +1 -1
  66. package/build/contracts/bridge/VendingMachine.sol/VendingMachine.dbg.json +1 -1
  67. package/build/contracts/bridge/VendingMachineV2.sol/VendingMachineV2.dbg.json +1 -1
  68. package/build/contracts/bridge/VendingMachineV3.sol/VendingMachineV3.dbg.json +1 -1
  69. package/build/contracts/bridge/WalletProposalValidator.sol/WalletProposalValidator.dbg.json +4 -0
  70. package/build/contracts/bridge/WalletProposalValidator.sol/WalletProposalValidator.json +287 -0
  71. package/build/contracts/bridge/Wallets.sol/Wallets.dbg.json +1 -1
  72. package/build/contracts/l2/L2TBTC.sol/L2TBTC.dbg.json +1 -1
  73. package/build/contracts/l2/L2WormholeGateway.sol/IWormholeTokenBridge.dbg.json +1 -1
  74. package/build/contracts/l2/L2WormholeGateway.sol/L2WormholeGateway.dbg.json +1 -1
  75. package/build/contracts/maintainer/MaintainerProxy.sol/MaintainerProxy.dbg.json +1 -1
  76. package/build/contracts/relay/LightRelay.sol/ILightRelay.dbg.json +1 -1
  77. package/build/contracts/relay/LightRelay.sol/LightRelay.dbg.json +1 -1
  78. package/build/contracts/relay/LightRelay.sol/RelayUtils.dbg.json +1 -1
  79. package/build/contracts/relay/LightRelayMaintainerProxy.sol/LightRelayMaintainerProxy.dbg.json +1 -1
  80. package/build/contracts/token/TBTC.sol/TBTC.dbg.json +1 -1
  81. package/build/contracts/vault/DonationVault.sol/DonationVault.dbg.json +1 -1
  82. package/build/contracts/vault/IVault.sol/IVault.dbg.json +1 -1
  83. package/build/contracts/vault/TBTCOptimisticMinting.sol/TBTCOptimisticMinting.dbg.json +1 -1
  84. package/build/contracts/vault/TBTCVault.sol/TBTCVault.dbg.json +1 -1
  85. package/contracts/bridge/{WalletCoordinator.sol → WalletProposalValidator.sol} +55 -448
  86. package/deploy/39_deploy_wallet_proposal_validator.ts +33 -0
  87. package/export/artifacts/contracts/bridge/Bridge.sol/Bridge.json +22 -22
  88. package/export/artifacts/contracts/bridge/VendingMachine.sol/VendingMachine.json +6 -6
  89. package/export/artifacts/contracts/bridge/VendingMachineV2.sol/VendingMachineV2.json +6 -6
  90. package/export/artifacts/contracts/bridge/VendingMachineV3.sol/VendingMachineV3.json +6 -6
  91. package/export/artifacts/contracts/bridge/WalletProposalValidator.sol/WalletProposalValidator.json +22598 -0
  92. package/export/artifacts/contracts/l2/L2TBTC.sol/L2TBTC.json +40 -40
  93. package/export/artifacts/contracts/l2/L2WormholeGateway.sol/L2WormholeGateway.json +47 -47
  94. package/export/artifacts/contracts/maintainer/MaintainerProxy.sol/MaintainerProxy.json +88 -88
  95. package/export/artifacts/contracts/relay/LightRelay.sol/LightRelay.json +57 -57
  96. package/export/artifacts/contracts/relay/LightRelayMaintainerProxy.sol/LightRelayMaintainerProxy.json +31 -31
  97. package/export/artifacts/contracts/test/BankStub.sol/BankStub.json +2 -2
  98. package/export/artifacts/contracts/test/BridgeStub.sol/BridgeStub.json +58 -58
  99. package/export/artifacts/contracts/test/GoerliLightRelay.sol/GoerliLightRelay.json +59 -59
  100. package/export/artifacts/contracts/test/HeartbeatStub.sol/HeartbeatStub.json +2 -2
  101. package/export/artifacts/contracts/test/LightRelayStub.sol/LightRelayStub.json +59 -59
  102. package/export/artifacts/contracts/test/ReceiveApprovalStub.sol/ReceiveApprovalStub.json +7 -7
  103. package/export/artifacts/contracts/test/SepoliaLightRelay.sol/SepoliaLightRelay.json +59 -59
  104. package/export/artifacts/contracts/test/SystemTestRelay.sol/SystemTestRelay.json +14 -14
  105. package/export/artifacts/contracts/test/TestERC20.sol/TestERC20.json +6 -6
  106. package/export/artifacts/contracts/test/TestERC721.sol/TestERC721.json +8 -8
  107. package/export/artifacts/contracts/test/TestEcdsaLib.sol/TestEcdsaLib.json +2 -2
  108. package/export/artifacts/contracts/test/WormholeBridgeStub.sol/WormholeBridgeStub.json +37 -37
  109. package/export/artifacts/contracts/token/TBTC.sol/TBTC.json +2 -2
  110. package/export/artifacts/contracts/vault/DonationVault.sol/DonationVault.json +11 -11
  111. package/export/artifacts/contracts/vault/TBTCVault.sol/TBTCVault.json +135 -135
  112. package/export/deploy/39_deploy_wallet_proposal_validator.js +82 -0
  113. package/export/hardhat.config.js +0 -6
  114. package/export/typechain/factories/WalletProposalValidator__factory.js +366 -0
  115. package/export/typechain/index.js +3 -3
  116. package/package.json +1 -1
  117. package/artifacts/WalletCoordinator.json +0 -1107
  118. package/build/contracts/bridge/WalletCoordinator.sol/WalletCoordinator.dbg.json +0 -4
  119. package/build/contracts/bridge/WalletCoordinator.sol/WalletCoordinator.json +0 -1042
  120. package/deploy/34_deploy_wallet_coordinator.ts +0 -43
  121. package/deploy/35_add_coordinator_address.ts +0 -20
  122. package/deploy/36_transfer_wallet_coordinator_ownership.ts +0 -19
  123. package/deploy/81_upgrade_wallet_coordinator_v2.ts +0 -99
  124. package/export/artifacts/contracts/bridge/WalletCoordinator.sol/WalletCoordinator.json +0 -33310
  125. package/export/deploy/34_deploy_wallet_coordinator.js +0 -115
  126. package/export/deploy/35_add_coordinator_address.js +0 -60
  127. package/export/deploy/36_transfer_wallet_coordinator_ownership.js +0 -60
  128. package/export/deploy/81_upgrade_wallet_coordinator_v2.js +0 -140
  129. package/export/typechain/factories/WalletCoordinator__factory.js +0 -1121
  130. /package/deploy/{37_deploy_light_relay_maintainer_proxy.ts → 34_deploy_light_relay_maintainer_proxy.ts} +0 -0
  131. /package/deploy/{38_authorize_maintainer_in_light_relay_maintainer_proxy.ts → 35_authorize_maintainer_in_light_relay_maintainer_proxy.ts} +0 -0
  132. /package/deploy/{39_transfer_light_relay_maintainer_proxy_ownership.ts → 36_transfer_light_relay_maintainer_proxy_ownership.ts} +0 -0
  133. /package/deploy/{40_authorize_light_relay_maintainer_proxy_in_reimbursement_pool.ts → 37_authorize_light_relay_maintainer_proxy_in_reimbursement_pool.ts} +0 -0
  134. /package/deploy/{41_authorize_light_relay_maintainer_proxy_in_light_relay.ts → 38_authorize_light_relay_maintainer_proxy_in_light_relay.ts} +0 -0
  135. /package/export/deploy/{37_deploy_light_relay_maintainer_proxy.js → 34_deploy_light_relay_maintainer_proxy.js} +0 -0
  136. /package/export/deploy/{38_authorize_maintainer_in_light_relay_maintainer_proxy.js → 35_authorize_maintainer_in_light_relay_maintainer_proxy.js} +0 -0
  137. /package/export/deploy/{39_transfer_light_relay_maintainer_proxy_ownership.js → 36_transfer_light_relay_maintainer_proxy_ownership.js} +0 -0
  138. /package/export/deploy/{40_authorize_light_relay_maintainer_proxy_in_reimbursement_pool.js → 37_authorize_light_relay_maintainer_proxy_in_reimbursement_pool.js} +0 -0
  139. /package/export/deploy/{41_authorize_light_relay_maintainer_proxy_in_light_relay.js → 38_authorize_light_relay_maintainer_proxy_in_light_relay.js} +0 -0
  140. /package/export/typechain/{WalletCoordinator.js → WalletProposalValidator.js} +0 -0
@@ -295,8 +295,8 @@
295
295
  "contracts/bridge/VendingMachineV3.sol": {
296
296
  "content": "// SPDX-License-Identifier: GPL-3.0-only\n\npragma solidity 0.8.17;\n\nimport \"@openzeppelin/contracts/access/Ownable.sol\";\nimport \"@openzeppelin/contracts/token/ERC20/IERC20.sol\";\nimport \"@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol\";\n\nimport \"../token/TBTC.sol\";\n\n/// @title VendingMachineV3\n/// @notice VendingMachineV3 is used to exchange tBTC v1 to tBTC v2 in a 1:1\n/// ratio after the tBTC v1 bridge sunsetting is completed. Since\n/// tBTC v1 bridge is no longer working, tBTC v1 tokens can not be used\n/// to perform BTC redemptions. This contract allows tBTC v1 owners to\n/// upgrade to tBTC v2 without any deadline. This way, tBTC v1 tokens\n/// left on the market are always backed by Bitcoin. The governance will\n/// deposit tBTC v2 into the contract in the amount equal to tBTC v1\n/// supply. The governance is allowed to withdraw tBTC v2 only if tBTC\n/// v2 left in this contract is enough to cover the upgrade of all tBTC\n/// v1 left on the market. This contract is owned by the governance.\ncontract VendingMachineV3 is Ownable {\n using SafeERC20 for IERC20;\n using SafeERC20 for TBTC;\n\n IERC20 public immutable tbtcV1;\n TBTC public immutable tbtcV2;\n\n event Exchanged(address indexed to, uint256 amount);\n event Deposited(address from, uint256 amount);\n event TbtcV2Withdrawn(address to, uint256 amount);\n event FundsRecovered(address token, address to, uint256 amount);\n\n constructor(IERC20 _tbtcV1, TBTC _tbtcV2) {\n tbtcV1 = _tbtcV1;\n tbtcV2 = _tbtcV2;\n }\n\n /// @notice Exchange tBTC v1 for tBTC v2 in a 1:1 ratio.\n /// The caller needs to have at least `amount` of tBTC v1 balance\n /// approved for transfer to the `VendingMachineV3` before calling\n /// this function.\n /// @param amount The amount of tBTC v1 to exchange for tBTC v2.\n function exchange(uint256 amount) external {\n _exchange(msg.sender, amount);\n }\n\n /// @notice Exchange tBTC v1 for tBTC v2 in a 1:1 ratio.\n /// The caller needs to have at least `amount` of tBTC v1 balance\n /// approved for transfer to the `VendingMachineV3` before calling\n /// this function.\n /// @dev This function is a shortcut for `approve` + `exchange`. Only tBTC\n /// v1 caller is allowed and only tBTC v1 is allowed as a token to\n /// transfer.\n /// @param from tBTC v1 token holder exchanging tBTC v1 to tBTC v2.\n /// @param amount The amount of tBTC v1 to exchange for tBTC v2.\n /// @param token tBTC v1 token address.\n function receiveApproval(\n address from,\n uint256 amount,\n address token,\n bytes calldata\n ) external {\n require(token == address(tbtcV1), \"Token is not tBTC v1\");\n require(msg.sender == address(tbtcV1), \"Only tBTC v1 caller allowed\");\n _exchange(from, amount);\n }\n\n /// @notice Allows to deposit tBTC v2 tokens to the contract.\n /// `VendingMachineV3` can not mint tBTC v2 tokens so tBTC v2 needs\n /// to be deposited into the contract so that tBTC v1 to tBTC v2\n /// exchange can happen.\n /// The caller needs to have at least `amount` of tBTC v2 balance\n /// approved for transfer to the `VendingMachineV3` before calling\n /// this function.\n /// @dev This function is for the redeemer and tBTC v1 operators. This is\n /// NOT a function for tBTC v1 token holders.\n /// @param amount The amount of tBTC v2 to deposit into the contract.\n function depositTbtcV2(uint256 amount) external {\n emit Deposited(msg.sender, amount);\n tbtcV2.safeTransferFrom(msg.sender, address(this), amount);\n }\n\n /// @notice Allows the governance to withdraw tBTC v2 deposited into this\n /// contract. The governance is allowed to withdraw tBTC v2\n /// only if tBTC v2 left in this contract is enough to cover the\n /// upgrade of all tBTC v1 left on the market.\n /// @param recipient The address which should receive withdrawn tokens.\n /// @param amount The amount to withdraw.\n function withdrawTbtcV2(address recipient, uint256 amount)\n external\n onlyOwner\n {\n require(\n tbtcV1.totalSupply() <= tbtcV2.balanceOf(address(this)) - amount,\n \"tBTC v1 must not be left unbacked\"\n );\n\n emit TbtcV2Withdrawn(recipient, amount);\n tbtcV2.safeTransfer(recipient, amount);\n }\n\n /// @notice Allows the governance to recover ERC20 sent to this contract\n /// by mistake or tBTC v1 locked in the contract to exchange to\n /// tBTC v2. No tBTC v2 can be withdrawn using this function.\n /// @param token The address of a token to recover.\n /// @param recipient The address which should receive recovered tokens.\n /// @param amount The amount to recover.\n function recoverFunds(\n IERC20 token,\n address recipient,\n uint256 amount\n ) external onlyOwner {\n require(\n address(token) != address(tbtcV2),\n \"tBTC v2 tokens can not be recovered, use withdrawTbtcV2 instead\"\n );\n\n emit FundsRecovered(address(token), recipient, amount);\n token.safeTransfer(recipient, amount);\n }\n\n function _exchange(address tokenOwner, uint256 amount) internal {\n require(\n tbtcV2.balanceOf(address(this)) >= amount,\n \"Not enough tBTC v2 available in the Vending Machine\"\n );\n\n emit Exchanged(tokenOwner, amount);\n tbtcV1.safeTransferFrom(tokenOwner, address(this), amount);\n\n tbtcV2.safeTransfer(tokenOwner, amount);\n }\n}\n"
297
297
  },
298
- "contracts/bridge/WalletCoordinator.sol": {
299
- "content": "// SPDX-License-Identifier: GPL-3.0-only\n\n// ██████████████ ▐████▌ ██████████████\n// ██████████████ ▐████▌ ██████████████\n// ▐████▌ ▐████▌\n// ▐████▌ ▐████▌\n// ██████████████ ▐████▌ ██████████████\n// ██████████████ ▐████▌ ██████████████\n// ▐████▌ ▐████▌\n// ▐████▌ ▐████▌\n// ▐████▌ ▐████▌\n// ▐████▌ ▐████▌\n// ▐████▌ ▐████▌\n// ▐████▌ ▐████▌\n\npragma solidity 0.8.17;\n\nimport {BTCUtils} from \"@keep-network/bitcoin-spv-sol/contracts/BTCUtils.sol\";\nimport {BytesLib} from \"@keep-network/bitcoin-spv-sol/contracts/BytesLib.sol\";\nimport \"@keep-network/random-beacon/contracts/Reimbursable.sol\";\nimport \"@keep-network/random-beacon/contracts/ReimbursementPool.sol\";\n\nimport \"@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol\";\n\nimport \"./BitcoinTx.sol\";\nimport \"./Bridge.sol\";\nimport \"./Deposit.sol\";\nimport \"./Redemption.sol\";\nimport \"./Wallets.sol\";\n\n/// @title Wallet coordinator.\n/// @notice The wallet coordinator contract aims to facilitate the coordination\n/// of the off-chain wallet members during complex multi-chain wallet\n/// operations like deposit sweeping, redemptions, or moving funds.\n/// Such processes involve various moving parts and many steps that each\n/// individual wallet member must do. Given the distributed nature of\n/// the off-chain wallet software, full off-chain implementation is\n/// challenging and prone to errors, especially byzantine faults.\n/// This contract provides a single and trusted on-chain coordination\n/// point thus taking the riskiest part out of the off-chain software.\n/// The off-chain wallet members can focus on the core tasks and do not\n/// bother about electing a trusted coordinator or aligning internal\n/// states using complex consensus algorithms.\ncontract WalletCoordinator is OwnableUpgradeable, Reimbursable {\n using BTCUtils for bytes;\n using BytesLib for bytes;\n\n /// @notice Represents wallet action:\n enum WalletAction {\n /// @dev The wallet does not perform any action.\n Idle,\n /// @dev The wallet is executing heartbeat.\n Heartbeat,\n /// @dev The wallet is handling a deposit sweep action.\n DepositSweep,\n /// @dev The wallet is handling a redemption action.\n Redemption,\n /// @dev The wallet is handling a moving funds action.\n MovingFunds,\n /// @dev The wallet is handling a moved funds sweep action.\n MovedFundsSweep\n }\n\n /// @notice Holds information about a wallet time lock.\n struct WalletLock {\n /// @notice A UNIX timestamp defining the moment until which the wallet\n /// is locked and cannot receive new proposals. The value of 0\n /// means the wallet is not locked and can receive a proposal\n /// at any time.\n uint32 expiresAt;\n /// @notice The wallet action being the cause of the lock.\n WalletAction cause;\n }\n\n /// @notice Helper structure representing a deposit sweep proposal.\n struct DepositSweepProposal {\n // 20-byte public key hash of the target wallet.\n bytes20 walletPubKeyHash;\n // Deposits that should be part of the sweep.\n DepositKey[] depositsKeys;\n // Proposed BTC fee for the entire transaction.\n uint256 sweepTxFee;\n // Array containing the reveal blocks of each deposit. This information\n // strongly facilitates the off-chain processing. Using those blocks,\n // wallet operators can quickly fetch corresponding Bridge.DepositRevealed\n // events carrying deposit data necessary to perform proposal validation.\n // This field is not explicitly validated within the validateDepositSweepProposal\n // function because if something is wrong here the off-chain wallet\n // operators will fail anyway as they won't be able to gather deposit\n // data necessary to perform the on-chain validation using the\n // validateDepositSweepProposal function.\n uint256[] depositsRevealBlocks;\n }\n\n /// @notice Helper structure representing a plain-text deposit key.\n /// Each deposit can be identified by their 32-byte funding\n /// transaction hash (Bitcoin internal byte order) an the funding\n /// output index (0-based).\n /// @dev Do not confuse this structure with the deposit key used within the\n /// Bridge contract to store deposits. Here we have the plain-text\n /// components of the key while the Bridge uses a uint representation of\n /// keccak256(fundingTxHash | fundingOutputIndex) for gas efficiency.\n struct DepositKey {\n bytes32 fundingTxHash;\n uint32 fundingOutputIndex;\n }\n\n /// @notice Helper structure holding deposit extra data required during\n /// deposit sweep proposal validation. Basically, this structure\n /// is a combination of BitcoinTx.Info and relevant parts of\n /// Deposit.DepositRevealInfo.\n /// @dev These data can be pulled from respective `DepositRevealed` events\n /// emitted by the `Bridge.revealDeposit` function. The `fundingTx`\n /// field must be taken directly from the Bitcoin chain, using the\n /// `DepositRevealed.fundingTxHash` as transaction identifier.\n struct DepositExtraInfo {\n BitcoinTx.Info fundingTx;\n bytes8 blindingFactor;\n bytes20 walletPubKeyHash;\n bytes20 refundPubKeyHash;\n bytes4 refundLocktime;\n }\n\n /// @notice Helper structure representing a redemption proposal.\n struct RedemptionProposal {\n // 20-byte public key hash of the target wallet.\n bytes20 walletPubKeyHash;\n // Array of the redeemers' output scripts that should be part of\n // the redemption. Each output script MUST BE prefixed by its byte\n // length, i.e. passed in the exactly same format as during the\n // `Bridge.requestRedemption` transaction.\n bytes[] redeemersOutputScripts;\n // Proposed BTC fee for the entire transaction.\n uint256 redemptionTxFee;\n }\n\n /// @notice Mapping that holds addresses allowed to submit proposals and\n /// request heartbeats.\n mapping(address => bool) public isCoordinator;\n\n /// @notice Mapping that holds wallet time locks. The key is a 20-byte\n /// wallet public key hash.\n mapping(bytes20 => WalletLock) public walletLock;\n\n /// @notice Handle to the Bridge contract.\n Bridge public bridge;\n\n /// @notice Determines the wallet heartbeat request validity time. In other\n /// words, this is the worst-case time for a wallet heartbeat\n /// during which the wallet is busy and canot take other actions.\n /// This is also the duration of the time lock applied to the wallet\n /// once a new heartbeat request is submitted.\n ///\n /// For example, if a deposit sweep proposal was submitted at\n /// 2 pm and heartbeatRequestValidity is 1 hour, the next request or\n /// proposal (of any type) can be submitted after 3 pm.\n uint32 public heartbeatRequestValidity;\n\n /// @notice Gas that is meant to balance the heartbeat request overall cost.\n /// Can be updated by the owner based on the current conditions.\n uint32 public heartbeatRequestGasOffset;\n\n /// @notice Determines the deposit sweep proposal validity time. In other\n /// words, this is the worst-case time for a deposit sweep during\n /// which the wallet is busy and cannot take another actions. This\n /// is also the duration of the time lock applied to the wallet\n /// once a new deposit sweep proposal is submitted.\n ///\n /// For example, if a deposit sweep proposal was submitted at\n /// 2 pm and depositSweepProposalValidity is 4 hours, the next\n /// proposal (of any type) can be submitted after 6 pm.\n uint32 public depositSweepProposalValidity;\n\n /// @notice The minimum time that must elapse since the deposit reveal\n /// before a deposit becomes eligible for a deposit sweep.\n ///\n /// For example, if a deposit was revealed at 9 am and depositMinAge\n /// is 2 hours, the deposit is eligible for sweep after 11 am.\n ///\n /// @dev Forcing deposit minimum age ensures block finality for Ethereum.\n /// In the happy path case, i.e. where the deposit is revealed immediately\n /// after being broadcast on the Bitcoin network, the minimum age\n /// check also ensures block finality for Bitcoin.\n uint32 public depositMinAge;\n\n /// @notice Each deposit can be technically swept until it reaches its\n /// refund timestamp after which it can be taken back by the depositor.\n /// However, allowing the wallet to sweep deposits that are close\n /// to their refund timestamp may cause a race between the wallet\n /// and the depositor. In result, the wallet may sign an invalid\n /// sweep transaction that aims to sweep an already refunded deposit.\n /// Such tx signature may be used to create an undefeatable fraud\n /// challenge against the wallet. In order to mitigate that problem,\n /// this parameter determines a safety margin that puts the latest\n /// moment a deposit can be swept far before the point after which\n /// the deposit becomes refundable.\n ///\n /// For example, if a deposit becomes refundable after 8 pm and\n /// depositRefundSafetyMargin is 6 hours, the deposit is valid for\n /// for a sweep only before 2 pm.\n uint32 public depositRefundSafetyMargin;\n\n /// @notice The maximum count of deposits that can be swept within a\n /// single sweep.\n uint16 public depositSweepMaxSize;\n\n /// @notice Gas that is meant to balance the deposit sweep proposal\n /// submission overall cost. Can be updated by the owner based on\n /// the current conditions.\n uint32 public depositSweepProposalSubmissionGasOffset;\n\n /// @notice Determines the redemption proposal validity time. In other\n /// words, this is the worst-case time for a redemption during\n /// which the wallet is busy and cannot take another actions. This\n /// is also the duration of the time lock applied to the wallet\n /// once a new redemption proposal is submitted.\n ///\n /// For example, if a redemption proposal was submitted at\n /// 2 pm and redemptionProposalValidity is 2 hours, the next\n /// proposal (of any type) can be submitted after 4 pm.\n uint32 public redemptionProposalValidity;\n\n /// @notice The minimum time that must elapse since the redemption request\n /// creation before a request becomes eligible for a processing.\n ///\n /// For example, if a request was created at 9 am and\n /// redemptionRequestMinAge is 2 hours, the request is eligible for\n /// processing after 11 am.\n ///\n /// @dev Forcing request minimum age ensures block finality for Ethereum.\n uint32 public redemptionRequestMinAge;\n\n /// @notice Each redemption request can be technically handled until it\n /// reaches its timeout timestamp after which it can be reported\n /// as timed out. However, allowing the wallet to handle requests\n /// that are close to their timeout timestamp may cause a race\n /// between the wallet and the redeemer. In result, the wallet may\n /// redeem the requested funds even though the redeemer already\n /// received back their tBTC (locked during redemption request) upon\n /// reporting the request timeout. In effect, the redeemer may end\n /// out with both tBTC and redeemed BTC in their hands which has\n /// a negative impact on the tBTC <-> BTC peg. In order to mitigate\n /// that problem, this parameter determines a safety margin that\n /// puts the latest moment a request can be handled far before the\n /// point after which the request can be reported as timed out.\n ///\n /// For example, if a request times out after 8 pm and\n /// redemptionRequestTimeoutSafetyMargin is 2 hours, the request is\n /// valid for processing only before 6 pm.\n uint32 public redemptionRequestTimeoutSafetyMargin;\n\n /// @notice The maximum count of redemption requests that can be processed\n /// within a single redemption.\n uint16 public redemptionMaxSize;\n\n /// @notice Gas that is meant to balance the redemption proposal\n /// submission overall cost. Can be updated by the owner based on\n /// the current conditions.\n uint32 public redemptionProposalSubmissionGasOffset;\n\n event CoordinatorAdded(address indexed coordinator);\n\n event CoordinatorRemoved(address indexed coordinator);\n\n event WalletManuallyUnlocked(bytes20 indexed walletPubKeyHash);\n\n event HeartbeatRequestParametersUpdated(\n uint32 heartbeatRequestValidity,\n uint32 heartbeatRequestGasOffset\n );\n\n event HeartbeatRequestSubmitted(\n bytes20 walletPubKeyHash,\n bytes message,\n address indexed coordinator\n );\n\n event DepositSweepProposalParametersUpdated(\n uint32 depositSweepProposalValidity,\n uint32 depositMinAge,\n uint32 depositRefundSafetyMargin,\n uint16 depositSweepMaxSize,\n uint32 depositSweepProposalSubmissionGasOffset\n );\n\n event DepositSweepProposalSubmitted(\n DepositSweepProposal proposal,\n address indexed coordinator\n );\n\n event RedemptionProposalParametersUpdated(\n uint32 redemptionProposalValidity,\n uint32 redemptionRequestMinAge,\n uint32 redemptionRequestTimeoutSafetyMargin,\n uint16 redemptionMaxSize,\n uint32 redemptionProposalSubmissionGasOffset\n );\n\n event RedemptionProposalSubmitted(\n RedemptionProposal proposal,\n address indexed coordinator\n );\n\n modifier onlyCoordinator() {\n require(isCoordinator[msg.sender], \"Caller is not a coordinator\");\n _;\n }\n\n modifier onlyAfterWalletLock(bytes20 walletPubKeyHash) {\n require(\n /* solhint-disable-next-line not-rely-on-time */\n block.timestamp > walletLock[walletPubKeyHash].expiresAt,\n \"Wallet locked\"\n );\n _;\n }\n\n modifier onlyReimbursableAdmin() override {\n require(owner() == msg.sender, \"Caller is not the owner\");\n _;\n }\n\n function initialize(Bridge _bridge) external initializer {\n __Ownable_init();\n\n bridge = _bridge;\n // Pre-fetch addresses to save gas later.\n (, , , reimbursementPool) = _bridge.contractReferences();\n\n heartbeatRequestValidity = 1 hours;\n heartbeatRequestGasOffset = 10_000;\n\n depositSweepProposalValidity = 4 hours;\n depositMinAge = 2 hours;\n depositRefundSafetyMargin = 24 hours;\n depositSweepMaxSize = 5;\n depositSweepProposalSubmissionGasOffset = 20_000; // optimized for 10 inputs\n\n redemptionProposalValidity = 2 hours;\n redemptionRequestMinAge = 600; // 10 minutes or ~50 blocks.\n redemptionRequestTimeoutSafetyMargin = 2 hours;\n redemptionMaxSize = 20;\n redemptionProposalSubmissionGasOffset = 20_000;\n }\n\n /// @notice Adds the given address to the set of coordinator addresses.\n /// @param coordinator Address of the new coordinator.\n /// @dev Requirements:\n /// - The caller must be the owner,\n /// - The `coordinator` must not be an existing coordinator.\n function addCoordinator(address coordinator) external onlyOwner {\n require(\n !isCoordinator[coordinator],\n \"This address is already a coordinator\"\n );\n isCoordinator[coordinator] = true;\n emit CoordinatorAdded(coordinator);\n }\n\n /// @notice Removes the given address from the set of coordinator addresses.\n /// @param coordinator Address of the existing coordinator.\n /// @dev Requirements:\n /// - The caller must be the owner,\n /// - The `coordinator` must be an existing coordinator.\n function removeCoordinator(address coordinator) external onlyOwner {\n require(\n isCoordinator[coordinator],\n \"This address is not a coordinator\"\n );\n delete isCoordinator[coordinator];\n emit CoordinatorRemoved(coordinator);\n }\n\n /// @notice Allows to unlock the given wallet before their time lock expires.\n /// This function should be used in exceptional cases where\n /// something went wrong and there is a need to unlock the wallet\n /// without waiting.\n /// @param walletPubKeyHash 20-byte public key hash of the wallet\n /// @dev Requirements:\n /// - The caller must be the owner.\n function unlockWallet(bytes20 walletPubKeyHash) external onlyOwner {\n // Just in case, allow the owner to unlock the wallet earlier.\n walletLock[walletPubKeyHash] = WalletLock(0, WalletAction.Idle);\n emit WalletManuallyUnlocked(walletPubKeyHash);\n }\n\n /// @notice Updates parameters related to heartbeat request.\n /// @param _heartbeatRequestValidity The new value of `heartbeatRequestValidity`.\n /// @param _heartbeatRequestGasOffset The new value of `heartbeatRequestGasOffset`.\n /// @dev Requirements:\n /// - The caller must be the owner.\n function updateHeartbeatRequestParameters(\n uint32 _heartbeatRequestValidity,\n uint32 _heartbeatRequestGasOffset\n ) external onlyOwner {\n heartbeatRequestValidity = _heartbeatRequestValidity;\n heartbeatRequestGasOffset = _heartbeatRequestGasOffset;\n emit HeartbeatRequestParametersUpdated(\n _heartbeatRequestValidity,\n _heartbeatRequestGasOffset\n );\n }\n\n /// @notice Updates parameters related to deposit sweep proposal.\n /// @param _depositSweepProposalValidity The new value of `depositSweepProposalValidity`.\n /// @param _depositMinAge The new value of `depositMinAge`.\n /// @param _depositRefundSafetyMargin The new value of `depositRefundSafetyMargin`.\n /// @param _depositSweepMaxSize The new value of `depositSweepMaxSize`.\n /// @dev Requirements:\n /// - The caller must be the owner.\n function updateDepositSweepProposalParameters(\n uint32 _depositSweepProposalValidity,\n uint32 _depositMinAge,\n uint32 _depositRefundSafetyMargin,\n uint16 _depositSweepMaxSize,\n uint32 _depositSweepProposalSubmissionGasOffset\n ) external onlyOwner {\n depositSweepProposalValidity = _depositSweepProposalValidity;\n depositMinAge = _depositMinAge;\n depositRefundSafetyMargin = _depositRefundSafetyMargin;\n depositSweepMaxSize = _depositSweepMaxSize;\n depositSweepProposalSubmissionGasOffset = _depositSweepProposalSubmissionGasOffset;\n\n emit DepositSweepProposalParametersUpdated(\n _depositSweepProposalValidity,\n _depositMinAge,\n _depositRefundSafetyMargin,\n _depositSweepMaxSize,\n _depositSweepProposalSubmissionGasOffset\n );\n }\n\n /// @notice Submits a heartbeat request from the wallet. Locks the wallet\n /// for a specific time, equal to the request validity period.\n /// This function validates the proposed heartbeat messge to see\n /// if it matches the heartbeat format expected by the Bridge.\n /// @param walletPubKeyHash 20-byte public key hash of the wallet that is\n /// supposed to execute the heartbeat.\n /// @param message The proposed heartbeat message for the wallet to sign.\n /// @dev Requirements:\n /// - The caller is a coordinator,\n /// - The wallet is not time-locked,\n /// - The message to sign is a valid heartbeat message.\n function requestHeartbeat(bytes20 walletPubKeyHash, bytes calldata message)\n public\n onlyCoordinator\n onlyAfterWalletLock(walletPubKeyHash)\n {\n require(\n Heartbeat.isValidHeartbeatMessage(message),\n \"Not a valid heartbeat message\"\n );\n\n walletLock[walletPubKeyHash] = WalletLock(\n /* solhint-disable-next-line not-rely-on-time */\n uint32(block.timestamp) + heartbeatRequestValidity,\n WalletAction.Heartbeat\n );\n\n emit HeartbeatRequestSubmitted(walletPubKeyHash, message, msg.sender);\n }\n\n /// @notice Wraps `requestHeartbeat` call and reimburses the caller's\n /// transaction cost.\n /// @dev See `requestHeartbeat` function documentation.\n function requestHeartbeatWithReimbursement(\n bytes20 walletPubKeyHash,\n bytes calldata message\n ) external {\n uint256 gasStart = gasleft();\n\n requestHeartbeat(walletPubKeyHash, message);\n\n reimbursementPool.refund(\n (gasStart - gasleft()) + heartbeatRequestGasOffset,\n msg.sender\n );\n }\n\n /// @notice Submits a deposit sweep proposal. Locks the target wallet\n /// for a specific time, equal to the proposal validity period.\n /// This function does not store the proposal in the state but\n /// just emits an event that serves as a guiding light for wallet\n /// off-chain members. Wallet members are supposed to validate\n /// the proposal on their own, before taking any action.\n /// @param proposal The deposit sweep proposal\n /// @dev Requirements:\n /// - The caller is a coordinator,\n /// - The wallet is not time-locked.\n function submitDepositSweepProposal(DepositSweepProposal calldata proposal)\n public\n onlyCoordinator\n onlyAfterWalletLock(proposal.walletPubKeyHash)\n {\n walletLock[proposal.walletPubKeyHash] = WalletLock(\n /* solhint-disable-next-line not-rely-on-time */\n uint32(block.timestamp) + depositSweepProposalValidity,\n WalletAction.DepositSweep\n );\n\n emit DepositSweepProposalSubmitted(proposal, msg.sender);\n }\n\n /// @notice Wraps `submitDepositSweepProposal` call and reimburses the\n /// caller's transaction cost.\n /// @dev See `submitDepositSweepProposal` function documentation.\n function submitDepositSweepProposalWithReimbursement(\n DepositSweepProposal calldata proposal\n ) external {\n uint256 gasStart = gasleft();\n\n submitDepositSweepProposal(proposal);\n\n reimbursementPool.refund(\n (gasStart - gasleft()) + depositSweepProposalSubmissionGasOffset,\n msg.sender\n );\n }\n\n /// @notice View function encapsulating the main rules of a valid deposit\n /// sweep proposal. This function is meant to facilitate the off-chain\n /// validation of the incoming proposals. Thanks to it, most\n /// of the work can be done using a single readonly contract call.\n /// Worth noting, the validation done here is not exhaustive as some\n /// conditions may not be verifiable within the on-chain function or\n /// checking them may be easier on the off-chain side. For example,\n /// this function does not check the SPV proofs and confirmations of\n /// the deposit funding transactions as this would require an\n /// integration with the difficulty relay that greatly increases\n /// complexity. Instead of that, each off-chain wallet member is\n /// supposed to do that check on their own.\n /// @param proposal The sweeping proposal to validate.\n /// @param depositsExtraInfo Deposits extra data required to perform the validation.\n /// @return True if the proposal is valid. Reverts otherwise.\n /// @dev Requirements:\n /// - The target wallet must be in the Live state,\n /// - The number of deposits included in the sweep must be in\n /// the range [1, `depositSweepMaxSize`],\n /// - The length of `depositsExtraInfo` array must be equal to the\n /// length of `proposal.depositsKeys`, i.e. each deposit must\n /// have exactly one set of corresponding extra data,\n /// - The proposed sweep tx fee must be grater than zero,\n /// - The proposed maximum per-deposit sweep tx fee must be lesser than\n /// or equal the maximum fee allowed by the Bridge (`Bridge.depositTxMaxFee`),\n /// - Each deposit must be revealed to the Bridge,\n /// - Each deposit must be old enough, i.e. at least `depositMinAge`\n /// elapsed since their reveal time,\n /// - Each deposit must not be swept yet,\n /// - Each deposit must have valid extra data (see `validateDepositExtraInfo`),\n /// - Each deposit must have the refund safety margin preserved,\n /// - Each deposit must be controlled by the same wallet,\n /// - Each deposit must target the same vault,\n /// - Each deposit must be unique.\n ///\n /// The following off-chain validation must be performed as a bare minimum:\n /// - Inputs used for the sweep transaction have enough Bitcoin confirmations,\n /// - Deposits revealed to the Bridge have enough Ethereum confirmations.\n function validateDepositSweepProposal(\n DepositSweepProposal calldata proposal,\n DepositExtraInfo[] calldata depositsExtraInfo\n ) external view returns (bool) {\n require(\n bridge.wallets(proposal.walletPubKeyHash).state ==\n Wallets.WalletState.Live,\n \"Wallet is not in Live state\"\n );\n\n require(proposal.depositsKeys.length > 0, \"Sweep below the min size\");\n\n require(\n proposal.depositsKeys.length <= depositSweepMaxSize,\n \"Sweep exceeds the max size\"\n );\n\n require(\n proposal.depositsKeys.length == depositsExtraInfo.length,\n \"Each deposit key must have matching extra data\"\n );\n\n validateSweepTxFee(proposal.sweepTxFee, proposal.depositsKeys.length);\n\n address proposalVault = address(0);\n\n uint256[] memory processedDepositKeys = new uint256[](\n proposal.depositsKeys.length\n );\n\n for (uint256 i = 0; i < proposal.depositsKeys.length; i++) {\n DepositKey memory depositKey = proposal.depositsKeys[i];\n DepositExtraInfo memory depositExtraInfo = depositsExtraInfo[i];\n\n uint256 depositKeyUint = uint256(\n keccak256(\n abi.encodePacked(\n depositKey.fundingTxHash,\n depositKey.fundingOutputIndex\n )\n )\n );\n\n // slither-disable-next-line calls-loop\n Deposit.DepositRequest memory depositRequest = bridge.deposits(\n depositKeyUint\n );\n\n require(depositRequest.revealedAt != 0, \"Deposit not revealed\");\n\n require(\n /* solhint-disable-next-line not-rely-on-time */\n block.timestamp > depositRequest.revealedAt + depositMinAge,\n \"Deposit min age not achieved yet\"\n );\n\n require(depositRequest.sweptAt == 0, \"Deposit already swept\");\n\n validateDepositExtraInfo(\n depositKey,\n depositRequest.depositor,\n depositExtraInfo\n );\n\n uint32 depositRefundableTimestamp = BTCUtils.reverseUint32(\n uint32(depositExtraInfo.refundLocktime)\n );\n require(\n /* solhint-disable-next-line not-rely-on-time */\n block.timestamp <\n depositRefundableTimestamp - depositRefundSafetyMargin,\n \"Deposit refund safety margin is not preserved\"\n );\n\n require(\n depositExtraInfo.walletPubKeyHash == proposal.walletPubKeyHash,\n \"Deposit controlled by different wallet\"\n );\n\n // Make sure all deposits target the same vault by using the\n // vault of the first deposit as a reference.\n if (i == 0) {\n proposalVault = depositRequest.vault;\n }\n require(\n depositRequest.vault == proposalVault,\n \"Deposit targets different vault\"\n );\n\n // Make sure there are no duplicates in the deposits list.\n for (uint256 j = 0; j < i; j++) {\n require(\n processedDepositKeys[j] != depositKeyUint,\n \"Duplicated deposit\"\n );\n }\n\n processedDepositKeys[i] = depositKeyUint;\n }\n\n return true;\n }\n\n /// @notice Validates the sweep tx fee by checking if the part of the fee\n /// incurred by each deposit does not exceed the maximum value\n /// allowed by the Bridge. This function is heavily based on\n /// `DepositSweep.depositSweepTxFeeDistribution` function.\n /// @param sweepTxFee The sweep transaction fee.\n /// @param depositsCount Count of the deposits swept by the sweep transaction.\n /// @dev Requirements:\n /// - The sweep tx fee must be grater than zero,\n /// - The maximum per-deposit sweep tx fee must be lesser than or equal\n /// the maximum fee allowed by the Bridge (`Bridge.depositTxMaxFee`).\n function validateSweepTxFee(uint256 sweepTxFee, uint256 depositsCount)\n internal\n view\n {\n require(sweepTxFee > 0, \"Proposed transaction fee cannot be zero\");\n\n // Compute the indivisible remainder that remains after dividing the\n // sweep transaction fee over all deposits evenly.\n uint256 depositTxFeeRemainder = sweepTxFee % depositsCount;\n // Compute the transaction fee per deposit by dividing the sweep\n // transaction fee (reduced by the remainder) by the number of deposits.\n uint256 depositTxFee = (sweepTxFee - depositTxFeeRemainder) /\n depositsCount;\n\n (, , uint64 depositTxMaxFee, ) = bridge.depositParameters();\n\n // The transaction fee is incurred by each deposit evenly except for the last\n // deposit that has the indivisible remainder additionally incurred.\n // See `DepositSweep.submitDepositSweepProof`.\n // We must make sure the highest value of the deposit transaction fee does\n // not exceed the maximum value limited by the governable parameter.\n require(\n depositTxFee + depositTxFeeRemainder <= depositTxMaxFee,\n \"Proposed transaction fee is too high\"\n );\n }\n\n /// @notice Validates the extra data for the given deposit. This function\n /// is heavily based on `Deposit.revealDeposit` function.\n /// @param depositKey Key of the given deposit.\n /// @param depositor Depositor that revealed the deposit.\n /// @param depositExtraInfo Extra data being subject of the validation.\n /// @dev Requirements:\n /// - The transaction hash computed using `depositExtraInfo.fundingTx`\n /// must match the `depositKey.fundingTxHash`. This requirement\n /// ensures the funding transaction data provided in the extra\n /// data container actually represent the funding transaction of\n /// the given deposit.\n /// - The P2(W)SH script inferred from `depositExtraInfo` is actually\n /// used to lock funds by the `depositKey.fundingOutputIndex` output\n /// of the `depositExtraInfo.fundingTx` transaction. This requirement\n /// ensures the reveal data provided in the extra data container\n /// actually matches the given deposit.\n function validateDepositExtraInfo(\n DepositKey memory depositKey,\n address depositor,\n DepositExtraInfo memory depositExtraInfo\n ) internal view {\n bytes32 depositExtraFundingTxHash = abi\n .encodePacked(\n depositExtraInfo.fundingTx.version,\n depositExtraInfo.fundingTx.inputVector,\n depositExtraInfo.fundingTx.outputVector,\n depositExtraInfo.fundingTx.locktime\n )\n .hash256View();\n\n // Make sure the funding tx provided as part of deposit extra data\n // actually matches the deposit referred by the given deposit key.\n if (depositKey.fundingTxHash != depositExtraFundingTxHash) {\n revert(\"Extra info funding tx hash does not match\");\n }\n\n bytes memory expectedScript = abi.encodePacked(\n hex\"14\", // Byte length of depositor Ethereum address.\n depositor,\n hex\"75\", // OP_DROP\n hex\"08\", // Byte length of blinding factor value.\n depositExtraInfo.blindingFactor,\n hex\"75\", // OP_DROP\n hex\"76\", // OP_DUP\n hex\"a9\", // OP_HASH160\n hex\"14\", // Byte length of a compressed Bitcoin public key hash.\n depositExtraInfo.walletPubKeyHash,\n hex\"87\", // OP_EQUAL\n hex\"63\", // OP_IF\n hex\"ac\", // OP_CHECKSIG\n hex\"67\", // OP_ELSE\n hex\"76\", // OP_DUP\n hex\"a9\", // OP_HASH160\n hex\"14\", // Byte length of a compressed Bitcoin public key hash.\n depositExtraInfo.refundPubKeyHash,\n hex\"88\", // OP_EQUALVERIFY\n hex\"04\", // Byte length of refund locktime value.\n depositExtraInfo.refundLocktime,\n hex\"b1\", // OP_CHECKLOCKTIMEVERIFY\n hex\"75\", // OP_DROP\n hex\"ac\", // OP_CHECKSIG\n hex\"68\" // OP_ENDIF\n );\n\n bytes memory fundingOutput = depositExtraInfo\n .fundingTx\n .outputVector\n .extractOutputAtIndex(depositKey.fundingOutputIndex);\n bytes memory fundingOutputHash = fundingOutput.extractHash();\n\n // Path that checks the deposit extra data validity in case the\n // referred deposit is a P2SH.\n if (\n // slither-disable-next-line calls-loop\n fundingOutputHash.length == 20 &&\n fundingOutputHash.slice20(0) == expectedScript.hash160View()\n ) {\n return;\n }\n\n // Path that checks the deposit extra data validity in case the\n // referred deposit is a P2WSH.\n if (\n fundingOutputHash.length == 32 &&\n fundingOutputHash.toBytes32() == sha256(expectedScript)\n ) {\n return;\n }\n\n revert(\"Extra info funding output script does not match\");\n }\n\n /// @notice Updates parameters related to redemption proposal.\n /// @param _redemptionProposalValidity The new value of `redemptionProposalValidity`.\n /// @param _redemptionRequestMinAge The new value of `redemptionRequestMinAge`.\n /// @param _redemptionRequestTimeoutSafetyMargin The new value of\n /// `redemptionRequestTimeoutSafetyMargin`.\n /// @param _redemptionMaxSize The new value of `redemptionMaxSize`.\n /// @param _redemptionProposalSubmissionGasOffset The new value of\n /// `redemptionProposalSubmissionGasOffset`.\n /// @dev Requirements:\n /// - The caller must be the owner.\n function updateRedemptionProposalParameters(\n uint32 _redemptionProposalValidity,\n uint32 _redemptionRequestMinAge,\n uint32 _redemptionRequestTimeoutSafetyMargin,\n uint16 _redemptionMaxSize,\n uint32 _redemptionProposalSubmissionGasOffset\n ) external onlyOwner {\n redemptionProposalValidity = _redemptionProposalValidity;\n redemptionRequestMinAge = _redemptionRequestMinAge;\n redemptionRequestTimeoutSafetyMargin = _redemptionRequestTimeoutSafetyMargin;\n redemptionMaxSize = _redemptionMaxSize;\n redemptionProposalSubmissionGasOffset = _redemptionProposalSubmissionGasOffset;\n\n emit RedemptionProposalParametersUpdated(\n _redemptionProposalValidity,\n _redemptionRequestMinAge,\n _redemptionRequestTimeoutSafetyMargin,\n _redemptionMaxSize,\n _redemptionProposalSubmissionGasOffset\n );\n }\n\n /// @notice Submits a redemption proposal. Locks the target wallet\n /// for a specific time, equal to the proposal validity period.\n /// This function does not store the proposal in the state but\n /// just emits an event that serves as a guiding light for wallet\n /// off-chain members. Wallet members are supposed to validate\n /// the proposal on their own, before taking any action.\n /// @param proposal The redemption proposal\n /// @dev Requirements:\n /// - The caller is a coordinator,\n /// - The wallet is not time-locked.\n function submitRedemptionProposal(RedemptionProposal calldata proposal)\n public\n onlyCoordinator\n onlyAfterWalletLock(proposal.walletPubKeyHash)\n {\n walletLock[proposal.walletPubKeyHash] = WalletLock(\n /* solhint-disable-next-line not-rely-on-time */\n uint32(block.timestamp) + redemptionProposalValidity,\n WalletAction.Redemption\n );\n\n emit RedemptionProposalSubmitted(proposal, msg.sender);\n }\n\n /// @notice Wraps `submitRedemptionProposal` call and reimburses the\n /// caller's transaction cost.\n /// @dev See `submitRedemptionProposal` function documentation.\n function submitRedemptionProposalWithReimbursement(\n RedemptionProposal calldata proposal\n ) external {\n uint256 gasStart = gasleft();\n\n submitRedemptionProposal(proposal);\n\n reimbursementPool.refund(\n (gasStart - gasleft()) + redemptionProposalSubmissionGasOffset,\n msg.sender\n );\n }\n\n /// @notice View function encapsulating the main rules of a valid redemption\n /// proposal. This function is meant to facilitate the off-chain\n /// validation of the incoming proposals. Thanks to it, most\n /// of the work can be done using a single readonly contract call.\n /// @param proposal The redemption proposal to validate.\n /// @return True if the proposal is valid. Reverts otherwise.\n /// @dev Requirements:\n /// - The target wallet must be in the Live state,\n /// - The number of redemption requests included in the redemption\n /// proposal must be in the range [1, `redemptionMaxSize`],\n /// - The proposed redemption tx fee must be grater than zero,\n /// - The proposed redemption tx fee must be lesser than or equal to\n /// the maximum total fee allowed by the Bridge\n /// (`Bridge.redemptionTxMaxTotalFee`),\n /// - The proposed maximum per-request redemption tx fee share must be\n /// lesser than or equal to the maximum fee share allowed by the\n /// given request (`RedemptionRequest.txMaxFee`),\n /// - Each request must be a pending request registered in the Bridge,\n /// - Each request must be old enough, i.e. at least `redemptionRequestMinAge`\n /// elapsed since their creation time,\n /// - Each request must have the timeout safety margin preserved,\n /// - Each request must be unique.\n function validateRedemptionProposal(RedemptionProposal calldata proposal)\n external\n view\n returns (bool)\n {\n require(\n bridge.wallets(proposal.walletPubKeyHash).state ==\n Wallets.WalletState.Live,\n \"Wallet is not in Live state\"\n );\n\n uint256 requestsCount = proposal.redeemersOutputScripts.length;\n\n require(requestsCount > 0, \"Redemption below the min size\");\n\n require(\n requestsCount <= redemptionMaxSize,\n \"Redemption exceeds the max size\"\n );\n\n (\n ,\n ,\n ,\n uint64 redemptionTxMaxTotalFee,\n uint32 redemptionTimeout,\n ,\n\n ) = bridge.redemptionParameters();\n\n require(\n proposal.redemptionTxFee > 0,\n \"Proposed transaction fee cannot be zero\"\n );\n\n // Make sure the proposed fee does not exceed the total fee limit.\n require(\n proposal.redemptionTxFee <= redemptionTxMaxTotalFee,\n \"Proposed transaction fee is too high\"\n );\n\n // Compute the indivisible remainder that remains after dividing the\n // redemption transaction fee over all requests evenly.\n uint256 redemptionTxFeeRemainder = proposal.redemptionTxFee %\n requestsCount;\n // Compute the transaction fee per request by dividing the redemption\n // transaction fee (reduced by the remainder) by the number of requests.\n uint256 redemptionTxFeePerRequest = (proposal.redemptionTxFee -\n redemptionTxFeeRemainder) / requestsCount;\n\n uint256[] memory processedRedemptionKeys = new uint256[](requestsCount);\n\n for (uint256 i = 0; i < requestsCount; i++) {\n bytes memory script = proposal.redeemersOutputScripts[i];\n\n // As the wallet public key hash is part of the redemption key,\n // we have an implicit guarantee that all requests being part\n // of the proposal target the same wallet.\n uint256 redemptionKey = uint256(\n keccak256(\n abi.encodePacked(\n keccak256(script),\n proposal.walletPubKeyHash\n )\n )\n );\n\n // slither-disable-next-line calls-loop\n Redemption.RedemptionRequest memory redemptionRequest = bridge\n .pendingRedemptions(redemptionKey);\n\n require(\n redemptionRequest.requestedAt != 0,\n \"Not a pending redemption request\"\n );\n\n require(\n /* solhint-disable-next-line not-rely-on-time */\n block.timestamp >\n redemptionRequest.requestedAt + redemptionRequestMinAge,\n \"Redemption request min age not achieved yet\"\n );\n\n // Calculate the timeout the given request times out at.\n uint32 requestTimeout = redemptionRequest.requestedAt +\n redemptionTimeout;\n // Make sure we are far enough from the moment the request times out.\n require(\n /* solhint-disable-next-line not-rely-on-time */\n block.timestamp <\n requestTimeout - redemptionRequestTimeoutSafetyMargin,\n \"Redemption request timeout safety margin is not preserved\"\n );\n\n uint256 feePerRequest = redemptionTxFeePerRequest;\n // The last request incurs the fee remainder.\n if (i == requestsCount - 1) {\n feePerRequest += redemptionTxFeeRemainder;\n }\n // Make sure the redemption transaction fee share incurred by\n // the given request fits in the limit for that request.\n require(\n feePerRequest <= redemptionRequest.txMaxFee,\n \"Proposed transaction per-request fee share is too high\"\n );\n\n // Make sure there are no duplicates in the requests list.\n for (uint256 j = 0; j < i; j++) {\n require(\n processedRedemptionKeys[j] != redemptionKey,\n \"Duplicated request\"\n );\n }\n\n processedRedemptionKeys[i] = redemptionKey;\n }\n\n return true;\n }\n}\n"
298
+ "contracts/bridge/WalletProposalValidator.sol": {
299
+ "content": "// SPDX-License-Identifier: GPL-3.0-only\n\n// ██████████████ ▐████▌ ██████████████\n// ██████████████ ▐████▌ ██████████████\n// ▐████▌ ▐████▌\n// ▐████▌ ▐████▌\n// ██████████████ ▐████▌ ██████████████\n// ██████████████ ▐████▌ ██████████████\n// ▐████▌ ▐████▌\n// ▐████▌ ▐████▌\n// ▐████▌ ▐████▌\n// ▐████▌ ▐████▌\n// ▐████▌ ▐████▌\n// ▐████▌ ▐████▌\n\npragma solidity 0.8.17;\n\nimport {BTCUtils} from \"@keep-network/bitcoin-spv-sol/contracts/BTCUtils.sol\";\nimport {BytesLib} from \"@keep-network/bitcoin-spv-sol/contracts/BytesLib.sol\";\n\nimport \"./BitcoinTx.sol\";\nimport \"./Bridge.sol\";\nimport \"./Deposit.sol\";\nimport \"./Redemption.sol\";\nimport \"./Wallets.sol\";\n\n/// @title Wallet proposal validator.\n/// @notice This contract exposes several view functions allowing to validate\n/// specific wallet action proposals. This contract is non-upgradeable\n/// and does not have any write functions.\ncontract WalletProposalValidator {\n using BTCUtils for bytes;\n using BytesLib for bytes;\n\n /// @notice Helper structure representing a deposit sweep proposal.\n struct DepositSweepProposal {\n // 20-byte public key hash of the target wallet.\n bytes20 walletPubKeyHash;\n // Deposits that should be part of the sweep.\n DepositKey[] depositsKeys;\n // Proposed BTC fee for the entire transaction.\n uint256 sweepTxFee;\n // Array containing the reveal blocks of each deposit. This information\n // strongly facilitates the off-chain processing. Using those blocks,\n // wallet operators can quickly fetch corresponding Bridge.DepositRevealed\n // events carrying deposit data necessary to perform proposal validation.\n // This field is not explicitly validated within the validateDepositSweepProposal\n // function because if something is wrong here the off-chain wallet\n // operators will fail anyway as they won't be able to gather deposit\n // data necessary to perform the on-chain validation using the\n // validateDepositSweepProposal function.\n uint256[] depositsRevealBlocks;\n }\n\n /// @notice Helper structure representing a plain-text deposit key.\n /// Each deposit can be identified by their 32-byte funding\n /// transaction hash (Bitcoin internal byte order) an the funding\n /// output index (0-based).\n /// @dev Do not confuse this structure with the deposit key used within the\n /// Bridge contract to store deposits. Here we have the plain-text\n /// components of the key while the Bridge uses a uint representation of\n /// keccak256(fundingTxHash | fundingOutputIndex) for gas efficiency.\n struct DepositKey {\n bytes32 fundingTxHash;\n uint32 fundingOutputIndex;\n }\n\n /// @notice Helper structure holding deposit extra data required during\n /// deposit sweep proposal validation. Basically, this structure\n /// is a combination of BitcoinTx.Info and relevant parts of\n /// Deposit.DepositRevealInfo.\n /// @dev These data can be pulled from respective `DepositRevealed` events\n /// emitted by the `Bridge.revealDeposit` function. The `fundingTx`\n /// field must be taken directly from the Bitcoin chain, using the\n /// `DepositRevealed.fundingTxHash` as transaction identifier.\n struct DepositExtraInfo {\n BitcoinTx.Info fundingTx;\n bytes8 blindingFactor;\n bytes20 walletPubKeyHash;\n bytes20 refundPubKeyHash;\n bytes4 refundLocktime;\n }\n\n /// @notice Helper structure representing a redemption proposal.\n struct RedemptionProposal {\n // 20-byte public key hash of the target wallet.\n bytes20 walletPubKeyHash;\n // Array of the redeemers' output scripts that should be part of\n // the redemption. Each output script MUST BE prefixed by its byte\n // length, i.e. passed in the exactly same format as during the\n // `Bridge.requestRedemption` transaction.\n bytes[] redeemersOutputScripts;\n // Proposed BTC fee for the entire transaction.\n uint256 redemptionTxFee;\n }\n\n /// @notice Helper structure representing a heartbeat proposal.\n struct HeartbeatProposal {\n // 20-byte public key hash of the target wallet.\n bytes20 walletPubKeyHash;\n // Message to be signed as part of the heartbeat.\n bytes message;\n }\n\n /// @notice Handle to the Bridge contract.\n Bridge public immutable bridge;\n\n /// @notice The minimum time that must elapse since the deposit reveal\n /// before a deposit becomes eligible for a deposit sweep.\n ///\n /// For example, if a deposit was revealed at 9 am and DEPOSIT_MIN_AGE\n /// is 2 hours, the deposit is eligible for sweep after 11 am.\n ///\n /// @dev Forcing deposit minimum age ensures block finality for Ethereum.\n /// In the happy path case, i.e. where the deposit is revealed immediately\n /// after being broadcast on the Bitcoin network, the minimum age\n /// check also ensures block finality for Bitcoin.\n uint32 public constant DEPOSIT_MIN_AGE = 2 hours;\n\n /// @notice Each deposit can be technically swept until it reaches its\n /// refund timestamp after which it can be taken back by the depositor.\n /// However, allowing the wallet to sweep deposits that are close\n /// to their refund timestamp may cause a race between the wallet\n /// and the depositor. In result, the wallet may sign an invalid\n /// sweep transaction that aims to sweep an already refunded deposit.\n /// Such tx signature may be used to create an undefeatable fraud\n /// challenge against the wallet. In order to mitigate that problem,\n /// this parameter determines a safety margin that puts the latest\n /// moment a deposit can be swept far before the point after which\n /// the deposit becomes refundable.\n ///\n /// For example, if a deposit becomes refundable after 8 pm and\n /// DEPOSIT_REFUND_SAFETY_MARGIN is 6 hours, the deposit is valid\n /// for a sweep only before 2 pm.\n uint32 public constant DEPOSIT_REFUND_SAFETY_MARGIN = 24 hours;\n\n /// @notice The maximum count of deposits that can be swept within a\n /// single sweep.\n uint16 public constant DEPOSIT_SWEEP_MAX_SIZE = 20;\n\n /// @notice The minimum time that must elapse since the redemption request\n /// creation before a request becomes eligible for a processing.\n ///\n /// For example, if a request was created at 9 am and\n /// REDEMPTION_REQUEST_MIN_AGE is 2 hours, the request is\n /// eligible for processing after 11 am.\n ///\n /// @dev Forcing request minimum age ensures block finality for Ethereum.\n uint32 public constant REDEMPTION_REQUEST_MIN_AGE = 600; // 10 minutes or ~50 blocks.\n\n /// @notice Each redemption request can be technically handled until it\n /// reaches its timeout timestamp after which it can be reported\n /// as timed out. However, allowing the wallet to handle requests\n /// that are close to their timeout timestamp may cause a race\n /// between the wallet and the redeemer. In result, the wallet may\n /// redeem the requested funds even though the redeemer already\n /// received back their tBTC (locked during redemption request) upon\n /// reporting the request timeout. In effect, the redeemer may end\n /// out with both tBTC and redeemed BTC in their hands which has\n /// a negative impact on the tBTC <-> BTC peg. In order to mitigate\n /// that problem, this parameter determines a safety margin that\n /// puts the latest moment a request can be handled far before the\n /// point after which the request can be reported as timed out.\n ///\n /// For example, if a request times out after 8 pm and\n /// REDEMPTION_REQUEST_TIMEOUT_SAFETY_MARGIN is 2 hours, the\n /// request is valid for processing only before 6 pm.\n uint32 public constant REDEMPTION_REQUEST_TIMEOUT_SAFETY_MARGIN = 2 hours;\n\n /// @notice The maximum count of redemption requests that can be processed\n /// within a single redemption.\n uint16 public constant REDEMPTION_MAX_SIZE = 20;\n\n constructor(Bridge _bridge) {\n bridge = _bridge;\n }\n\n /// @notice View function encapsulating the main rules of a valid deposit\n /// sweep proposal. This function is meant to facilitate the off-chain\n /// validation of the incoming proposals. Thanks to it, most\n /// of the work can be done using a single readonly contract call.\n /// Worth noting, the validation done here is not exhaustive as some\n /// conditions may not be verifiable within the on-chain function or\n /// checking them may be easier on the off-chain side. For example,\n /// this function does not check the SPV proofs and confirmations of\n /// the deposit funding transactions as this would require an\n /// integration with the difficulty relay that greatly increases\n /// complexity. Instead of that, each off-chain wallet member is\n /// supposed to do that check on their own.\n /// @param proposal The sweeping proposal to validate.\n /// @param depositsExtraInfo Deposits extra data required to perform the validation.\n /// @return True if the proposal is valid. Reverts otherwise.\n /// @dev Requirements:\n /// - The target wallet must be in the Live state,\n /// - The number of deposits included in the sweep must be in\n /// the range [1, `DEPOSIT_SWEEP_MAX_SIZE`],\n /// - The length of `depositsExtraInfo` array must be equal to the\n /// length of `proposal.depositsKeys`, i.e. each deposit must\n /// have exactly one set of corresponding extra data,\n /// - The proposed sweep tx fee must be grater than zero,\n /// - The proposed maximum per-deposit sweep tx fee must be lesser than\n /// or equal the maximum fee allowed by the Bridge (`Bridge.depositTxMaxFee`),\n /// - Each deposit must be revealed to the Bridge,\n /// - Each deposit must be old enough, i.e. at least `DEPOSIT_MIN_AGE\n /// elapsed since their reveal time,\n /// - Each deposit must not be swept yet,\n /// - Each deposit must have valid extra data (see `validateDepositExtraInfo`),\n /// - Each deposit must have the refund safety margin preserved,\n /// - Each deposit must be controlled by the same wallet,\n /// - Each deposit must target the same vault,\n /// - Each deposit must be unique.\n ///\n /// The following off-chain validation must be performed as a bare minimum:\n /// - Inputs used for the sweep transaction have enough Bitcoin confirmations,\n /// - Deposits revealed to the Bridge have enough Ethereum confirmations.\n function validateDepositSweepProposal(\n DepositSweepProposal calldata proposal,\n DepositExtraInfo[] calldata depositsExtraInfo\n ) external view returns (bool) {\n require(\n bridge.wallets(proposal.walletPubKeyHash).state ==\n Wallets.WalletState.Live,\n \"Wallet is not in Live state\"\n );\n\n require(proposal.depositsKeys.length > 0, \"Sweep below the min size\");\n\n require(\n proposal.depositsKeys.length <= DEPOSIT_SWEEP_MAX_SIZE,\n \"Sweep exceeds the max size\"\n );\n\n require(\n proposal.depositsKeys.length == depositsExtraInfo.length,\n \"Each deposit key must have matching extra data\"\n );\n\n validateSweepTxFee(proposal.sweepTxFee, proposal.depositsKeys.length);\n\n address proposalVault = address(0);\n\n uint256[] memory processedDepositKeys = new uint256[](\n proposal.depositsKeys.length\n );\n\n for (uint256 i = 0; i < proposal.depositsKeys.length; i++) {\n DepositKey memory depositKey = proposal.depositsKeys[i];\n DepositExtraInfo memory depositExtraInfo = depositsExtraInfo[i];\n\n uint256 depositKeyUint = uint256(\n keccak256(\n abi.encodePacked(\n depositKey.fundingTxHash,\n depositKey.fundingOutputIndex\n )\n )\n );\n\n // slither-disable-next-line calls-loop\n Deposit.DepositRequest memory depositRequest = bridge.deposits(\n depositKeyUint\n );\n\n require(depositRequest.revealedAt != 0, \"Deposit not revealed\");\n\n require(\n /* solhint-disable-next-line not-rely-on-time */\n block.timestamp > depositRequest.revealedAt + DEPOSIT_MIN_AGE,\n \"Deposit min age not achieved yet\"\n );\n\n require(depositRequest.sweptAt == 0, \"Deposit already swept\");\n\n validateDepositExtraInfo(\n depositKey,\n depositRequest.depositor,\n depositExtraInfo\n );\n\n uint32 depositRefundableTimestamp = BTCUtils.reverseUint32(\n uint32(depositExtraInfo.refundLocktime)\n );\n require(\n /* solhint-disable-next-line not-rely-on-time */\n block.timestamp <\n depositRefundableTimestamp - DEPOSIT_REFUND_SAFETY_MARGIN,\n \"Deposit refund safety margin is not preserved\"\n );\n\n require(\n depositExtraInfo.walletPubKeyHash == proposal.walletPubKeyHash,\n \"Deposit controlled by different wallet\"\n );\n\n // Make sure all deposits target the same vault by using the\n // vault of the first deposit as a reference.\n if (i == 0) {\n proposalVault = depositRequest.vault;\n }\n require(\n depositRequest.vault == proposalVault,\n \"Deposit targets different vault\"\n );\n\n // Make sure there are no duplicates in the deposits list.\n for (uint256 j = 0; j < i; j++) {\n require(\n processedDepositKeys[j] != depositKeyUint,\n \"Duplicated deposit\"\n );\n }\n\n processedDepositKeys[i] = depositKeyUint;\n }\n\n return true;\n }\n\n /// @notice Validates the sweep tx fee by checking if the part of the fee\n /// incurred by each deposit does not exceed the maximum value\n /// allowed by the Bridge. This function is heavily based on\n /// `DepositSweep.depositSweepTxFeeDistribution` function.\n /// @param sweepTxFee The sweep transaction fee.\n /// @param depositsCount Count of the deposits swept by the sweep transaction.\n /// @dev Requirements:\n /// - The sweep tx fee must be grater than zero,\n /// - The maximum per-deposit sweep tx fee must be lesser than or equal\n /// the maximum fee allowed by the Bridge (`Bridge.depositTxMaxFee`).\n function validateSweepTxFee(uint256 sweepTxFee, uint256 depositsCount)\n internal\n view\n {\n require(sweepTxFee > 0, \"Proposed transaction fee cannot be zero\");\n\n // Compute the indivisible remainder that remains after dividing the\n // sweep transaction fee over all deposits evenly.\n uint256 depositTxFeeRemainder = sweepTxFee % depositsCount;\n // Compute the transaction fee per deposit by dividing the sweep\n // transaction fee (reduced by the remainder) by the number of deposits.\n uint256 depositTxFee = (sweepTxFee - depositTxFeeRemainder) /\n depositsCount;\n\n (, , uint64 depositTxMaxFee, ) = bridge.depositParameters();\n\n // The transaction fee is incurred by each deposit evenly except for the last\n // deposit that has the indivisible remainder additionally incurred.\n // See `DepositSweep.submitDepositSweepProof`.\n // We must make sure the highest value of the deposit transaction fee does\n // not exceed the maximum value limited by the governable parameter.\n require(\n depositTxFee + depositTxFeeRemainder <= depositTxMaxFee,\n \"Proposed transaction fee is too high\"\n );\n }\n\n /// @notice Validates the extra data for the given deposit. This function\n /// is heavily based on `Deposit.revealDeposit` function.\n /// @param depositKey Key of the given deposit.\n /// @param depositor Depositor that revealed the deposit.\n /// @param depositExtraInfo Extra data being subject of the validation.\n /// @dev Requirements:\n /// - The transaction hash computed using `depositExtraInfo.fundingTx`\n /// must match the `depositKey.fundingTxHash`. This requirement\n /// ensures the funding transaction data provided in the extra\n /// data container actually represent the funding transaction of\n /// the given deposit.\n /// - The P2(W)SH script inferred from `depositExtraInfo` is actually\n /// used to lock funds by the `depositKey.fundingOutputIndex` output\n /// of the `depositExtraInfo.fundingTx` transaction. This requirement\n /// ensures the reveal data provided in the extra data container\n /// actually matches the given deposit.\n function validateDepositExtraInfo(\n DepositKey memory depositKey,\n address depositor,\n DepositExtraInfo memory depositExtraInfo\n ) internal view {\n bytes32 depositExtraFundingTxHash = abi\n .encodePacked(\n depositExtraInfo.fundingTx.version,\n depositExtraInfo.fundingTx.inputVector,\n depositExtraInfo.fundingTx.outputVector,\n depositExtraInfo.fundingTx.locktime\n )\n .hash256View();\n\n // Make sure the funding tx provided as part of deposit extra data\n // actually matches the deposit referred by the given deposit key.\n if (depositKey.fundingTxHash != depositExtraFundingTxHash) {\n revert(\"Extra info funding tx hash does not match\");\n }\n\n bytes memory expectedScript = abi.encodePacked(\n hex\"14\", // Byte length of depositor Ethereum address.\n depositor,\n hex\"75\", // OP_DROP\n hex\"08\", // Byte length of blinding factor value.\n depositExtraInfo.blindingFactor,\n hex\"75\", // OP_DROP\n hex\"76\", // OP_DUP\n hex\"a9\", // OP_HASH160\n hex\"14\", // Byte length of a compressed Bitcoin public key hash.\n depositExtraInfo.walletPubKeyHash,\n hex\"87\", // OP_EQUAL\n hex\"63\", // OP_IF\n hex\"ac\", // OP_CHECKSIG\n hex\"67\", // OP_ELSE\n hex\"76\", // OP_DUP\n hex\"a9\", // OP_HASH160\n hex\"14\", // Byte length of a compressed Bitcoin public key hash.\n depositExtraInfo.refundPubKeyHash,\n hex\"88\", // OP_EQUALVERIFY\n hex\"04\", // Byte length of refund locktime value.\n depositExtraInfo.refundLocktime,\n hex\"b1\", // OP_CHECKLOCKTIMEVERIFY\n hex\"75\", // OP_DROP\n hex\"ac\", // OP_CHECKSIG\n hex\"68\" // OP_ENDIF\n );\n\n bytes memory fundingOutput = depositExtraInfo\n .fundingTx\n .outputVector\n .extractOutputAtIndex(depositKey.fundingOutputIndex);\n bytes memory fundingOutputHash = fundingOutput.extractHash();\n\n // Path that checks the deposit extra data validity in case the\n // referred deposit is a P2SH.\n if (\n // slither-disable-next-line calls-loop\n fundingOutputHash.length == 20 &&\n fundingOutputHash.slice20(0) == expectedScript.hash160View()\n ) {\n return;\n }\n\n // Path that checks the deposit extra data validity in case the\n // referred deposit is a P2WSH.\n if (\n fundingOutputHash.length == 32 &&\n fundingOutputHash.toBytes32() == sha256(expectedScript)\n ) {\n return;\n }\n\n revert(\"Extra info funding output script does not match\");\n }\n\n /// @notice View function encapsulating the main rules of a valid redemption\n /// proposal. This function is meant to facilitate the off-chain\n /// validation of the incoming proposals. Thanks to it, most\n /// of the work can be done using a single readonly contract call.\n /// @param proposal The redemption proposal to validate.\n /// @return True if the proposal is valid. Reverts otherwise.\n /// @dev Requirements:\n /// - The target wallet must be in the Live state,\n /// - The number of redemption requests included in the redemption\n /// proposal must be in the range [1, `redemptionMaxSize`],\n /// - The proposed redemption tx fee must be grater than zero,\n /// - The proposed redemption tx fee must be lesser than or equal to\n /// the maximum total fee allowed by the Bridge\n /// (`Bridge.redemptionTxMaxTotalFee`),\n /// - The proposed maximum per-request redemption tx fee share must be\n /// lesser than or equal to the maximum fee share allowed by the\n /// given request (`RedemptionRequest.txMaxFee`),\n /// - Each request must be a pending request registered in the Bridge,\n /// - Each request must be old enough, i.e. at least `redemptionRequestMinAge`\n /// elapsed since their creation time,\n /// - Each request must have the timeout safety margin preserved,\n /// - Each request must be unique.\n function validateRedemptionProposal(RedemptionProposal calldata proposal)\n external\n view\n returns (bool)\n {\n require(\n bridge.wallets(proposal.walletPubKeyHash).state ==\n Wallets.WalletState.Live,\n \"Wallet is not in Live state\"\n );\n\n uint256 requestsCount = proposal.redeemersOutputScripts.length;\n\n require(requestsCount > 0, \"Redemption below the min size\");\n\n require(\n requestsCount <= REDEMPTION_MAX_SIZE,\n \"Redemption exceeds the max size\"\n );\n\n (\n ,\n ,\n ,\n uint64 redemptionTxMaxTotalFee,\n uint32 redemptionTimeout,\n ,\n\n ) = bridge.redemptionParameters();\n\n require(\n proposal.redemptionTxFee > 0,\n \"Proposed transaction fee cannot be zero\"\n );\n\n // Make sure the proposed fee does not exceed the total fee limit.\n require(\n proposal.redemptionTxFee <= redemptionTxMaxTotalFee,\n \"Proposed transaction fee is too high\"\n );\n\n // Compute the indivisible remainder that remains after dividing the\n // redemption transaction fee over all requests evenly.\n uint256 redemptionTxFeeRemainder = proposal.redemptionTxFee %\n requestsCount;\n // Compute the transaction fee per request by dividing the redemption\n // transaction fee (reduced by the remainder) by the number of requests.\n uint256 redemptionTxFeePerRequest = (proposal.redemptionTxFee -\n redemptionTxFeeRemainder) / requestsCount;\n\n uint256[] memory processedRedemptionKeys = new uint256[](requestsCount);\n\n for (uint256 i = 0; i < requestsCount; i++) {\n bytes memory script = proposal.redeemersOutputScripts[i];\n\n // As the wallet public key hash is part of the redemption key,\n // we have an implicit guarantee that all requests being part\n // of the proposal target the same wallet.\n uint256 redemptionKey = uint256(\n keccak256(\n abi.encodePacked(\n keccak256(script),\n proposal.walletPubKeyHash\n )\n )\n );\n\n // slither-disable-next-line calls-loop\n Redemption.RedemptionRequest memory redemptionRequest = bridge\n .pendingRedemptions(redemptionKey);\n\n require(\n redemptionRequest.requestedAt != 0,\n \"Not a pending redemption request\"\n );\n\n require(\n /* solhint-disable-next-line not-rely-on-time */\n block.timestamp >\n redemptionRequest.requestedAt + REDEMPTION_REQUEST_MIN_AGE,\n \"Redemption request min age not achieved yet\"\n );\n\n // Calculate the timeout the given request times out at.\n uint32 requestTimeout = redemptionRequest.requestedAt +\n redemptionTimeout;\n // Make sure we are far enough from the moment the request times out.\n require(\n /* solhint-disable-next-line not-rely-on-time */\n block.timestamp <\n requestTimeout - REDEMPTION_REQUEST_TIMEOUT_SAFETY_MARGIN,\n \"Redemption request timeout safety margin is not preserved\"\n );\n\n uint256 feePerRequest = redemptionTxFeePerRequest;\n // The last request incurs the fee remainder.\n if (i == requestsCount - 1) {\n feePerRequest += redemptionTxFeeRemainder;\n }\n // Make sure the redemption transaction fee share incurred by\n // the given request fits in the limit for that request.\n require(\n feePerRequest <= redemptionRequest.txMaxFee,\n \"Proposed transaction per-request fee share is too high\"\n );\n\n // Make sure there are no duplicates in the requests list.\n for (uint256 j = 0; j < i; j++) {\n require(\n processedRedemptionKeys[j] != redemptionKey,\n \"Duplicated request\"\n );\n }\n\n processedRedemptionKeys[i] = redemptionKey;\n }\n\n return true;\n }\n\n /// @notice View function encapsulating the main rules of a valid heartbeat\n /// proposal. This function is meant to facilitate the off-chain\n /// validation of the incoming proposals. Thanks to it, most\n /// of the work can be done using a single readonly contract call.\n /// @param proposal The heartbeat proposal to validate.\n /// @return True if the proposal is valid. Reverts otherwise.\n /// @dev Requirements:\n /// - The message to sign is a valid heartbeat message.\n function validateHeartbeatProposal(HeartbeatProposal calldata proposal)\n external\n view\n returns (bool)\n {\n require(\n Heartbeat.isValidHeartbeatMessage(proposal.message),\n \"Not a valid heartbeat message\"\n );\n\n return true;\n }\n}\n"
300
300
  },
301
301
  "contracts/bridge/Wallets.sol": {
302
302
  "content": "// SPDX-License-Identifier: GPL-3.0-only\n\n// ██████████████ ▐████▌ ██████████████\n// ██████████████ ▐████▌ ██████████████\n// ▐████▌ ▐████▌\n// ▐████▌ ▐████▌\n// ██████████████ ▐████▌ ██████████████\n// ██████████████ ▐████▌ ██████████████\n// ▐████▌ ▐████▌\n// ▐████▌ ▐████▌\n// ▐████▌ ▐████▌\n// ▐████▌ ▐████▌\n// ▐████▌ ▐████▌\n// ▐████▌ ▐████▌\n\npragma solidity 0.8.17;\n\nimport {BTCUtils} from \"@keep-network/bitcoin-spv-sol/contracts/BTCUtils.sol\";\nimport {EcdsaDkg} from \"@keep-network/ecdsa/contracts/libraries/EcdsaDkg.sol\";\nimport {Math} from \"@openzeppelin/contracts/utils/math/Math.sol\";\n\nimport \"./BitcoinTx.sol\";\nimport \"./EcdsaLib.sol\";\nimport \"./BridgeState.sol\";\n\n/// @title Wallet library\n/// @notice Library responsible for handling integration between Bridge\n/// contract and ECDSA wallets.\nlibrary Wallets {\n using BTCUtils for bytes;\n\n /// @notice Represents wallet state:\n enum WalletState {\n /// @dev The wallet is unknown to the Bridge.\n Unknown,\n /// @dev The wallet can sweep deposits and accept redemption requests.\n Live,\n /// @dev The wallet was deemed unhealthy and is expected to move their\n /// outstanding funds to another wallet. The wallet can still\n /// fulfill their pending redemption requests although new\n /// redemption requests and new deposit reveals are not accepted.\n MovingFunds,\n /// @dev The wallet moved or redeemed all their funds and is in the\n /// closing period where it is still a subject of fraud challenges\n /// and must defend against them. This state is needed to protect\n /// against deposit frauds on deposits revealed but not swept.\n /// The closing period must be greater that the deposit refund\n /// time plus some time margin.\n Closing,\n /// @dev The wallet finalized the closing period successfully and\n /// can no longer perform any action in the Bridge.\n Closed,\n /// @dev The wallet committed a fraud that was reported, did not move\n /// funds to another wallet before a timeout, or did not sweep\n /// funds moved to if from another wallet before a timeout. The\n /// wallet is blocked and can not perform any actions in the Bridge.\n /// Off-chain coordination with the wallet operators is needed to\n /// recover funds.\n Terminated\n }\n\n /// @notice Holds information about a wallet.\n struct Wallet {\n // Identifier of a ECDSA Wallet registered in the ECDSA Wallet Registry.\n bytes32 ecdsaWalletID;\n // Latest wallet's main UTXO hash computed as\n // keccak256(txHash | txOutputIndex | txOutputValue). The `tx` prefix\n // refers to the transaction which created that main UTXO. The `txHash`\n // is `bytes32` (ordered as in Bitcoin internally), `txOutputIndex`\n // an `uint32`, and `txOutputValue` an `uint64` value.\n bytes32 mainUtxoHash;\n // The total redeemable value of pending redemption requests targeting\n // that wallet.\n uint64 pendingRedemptionsValue;\n // UNIX timestamp the wallet was created at.\n // XXX: Unsigned 32-bit int unix seconds, will break February 7th 2106.\n uint32 createdAt;\n // UNIX timestamp indicating the moment the wallet was requested to\n // move their funds.\n // XXX: Unsigned 32-bit int unix seconds, will break February 7th 2106.\n uint32 movingFundsRequestedAt;\n // UNIX timestamp indicating the moment the wallet's closing period\n // started.\n // XXX: Unsigned 32-bit int unix seconds, will break February 7th 2106.\n uint32 closingStartedAt;\n // Total count of pending moved funds sweep requests targeting this wallet.\n uint32 pendingMovedFundsSweepRequestsCount;\n // Current state of the wallet.\n WalletState state;\n // Moving funds target wallet commitment submitted by the wallet. It\n // is built by applying the keccak256 hash over the list of 20-byte\n // public key hashes of the target wallets.\n bytes32 movingFundsTargetWalletsCommitmentHash;\n // This struct doesn't contain `__gap` property as the structure is stored\n // in a mapping, mappings store values in different slots and they are\n // not contiguous with other values.\n }\n\n event NewWalletRequested();\n\n event NewWalletRegistered(\n bytes32 indexed ecdsaWalletID,\n bytes20 indexed walletPubKeyHash\n );\n\n event WalletMovingFunds(\n bytes32 indexed ecdsaWalletID,\n bytes20 indexed walletPubKeyHash\n );\n\n event WalletClosing(\n bytes32 indexed ecdsaWalletID,\n bytes20 indexed walletPubKeyHash\n );\n\n event WalletClosed(\n bytes32 indexed ecdsaWalletID,\n bytes20 indexed walletPubKeyHash\n );\n\n event WalletTerminated(\n bytes32 indexed ecdsaWalletID,\n bytes20 indexed walletPubKeyHash\n );\n\n /// @notice Requests creation of a new wallet. This function just\n /// forms a request and the creation process is performed\n /// asynchronously. Outcome of that process should be delivered\n /// using `registerNewWallet` function.\n /// @param activeWalletMainUtxo Data of the active wallet's main UTXO, as\n /// currently known on the Ethereum chain.\n /// @dev Requirements:\n /// - `activeWalletMainUtxo` components must point to the recent main\n /// UTXO of the given active wallet, as currently known on the\n /// Ethereum chain. If there is no active wallet at the moment, or\n /// the active wallet has no main UTXO, this parameter can be\n /// empty as it is ignored,\n /// - Wallet creation must not be in progress,\n /// - If the active wallet is set, one of the following\n /// conditions must be true:\n /// - The active wallet BTC balance is above the minimum threshold\n /// and the active wallet is old enough, i.e. the creation period\n /// was elapsed since its creation time,\n /// - The active wallet BTC balance is above the maximum threshold.\n function requestNewWallet(\n BridgeState.Storage storage self,\n BitcoinTx.UTXO calldata activeWalletMainUtxo\n ) external {\n require(\n self.ecdsaWalletRegistry.getWalletCreationState() ==\n EcdsaDkg.State.IDLE,\n \"Wallet creation already in progress\"\n );\n\n bytes20 activeWalletPubKeyHash = self.activeWalletPubKeyHash;\n\n // If the active wallet is set, fetch this wallet's details from\n // storage to perform conditions check. The `registerNewWallet`\n // function guarantees an active wallet is always one of the\n // registered ones.\n if (activeWalletPubKeyHash != bytes20(0)) {\n uint64 activeWalletBtcBalance = getWalletBtcBalance(\n self,\n activeWalletPubKeyHash,\n activeWalletMainUtxo\n );\n uint32 activeWalletCreatedAt = self\n .registeredWallets[activeWalletPubKeyHash]\n .createdAt;\n /* solhint-disable-next-line not-rely-on-time */\n bool activeWalletOldEnough = block.timestamp >=\n activeWalletCreatedAt + self.walletCreationPeriod;\n\n require(\n (activeWalletOldEnough &&\n activeWalletBtcBalance >=\n self.walletCreationMinBtcBalance) ||\n activeWalletBtcBalance >= self.walletCreationMaxBtcBalance,\n \"Wallet creation conditions are not met\"\n );\n }\n\n emit NewWalletRequested();\n\n self.ecdsaWalletRegistry.requestNewWallet();\n }\n\n /// @notice Registers a new wallet. This function should be called\n /// after the wallet creation process initiated using\n /// `requestNewWallet` completes and brings the outcomes.\n /// @param ecdsaWalletID Wallet's unique identifier.\n /// @param publicKeyX Wallet's public key's X coordinate.\n /// @param publicKeyY Wallet's public key's Y coordinate.\n /// @dev Requirements:\n /// - The only caller authorized to call this function is `registry`,\n /// - Given wallet data must not belong to an already registered wallet.\n function registerNewWallet(\n BridgeState.Storage storage self,\n bytes32 ecdsaWalletID,\n bytes32 publicKeyX,\n bytes32 publicKeyY\n ) external {\n require(\n msg.sender == address(self.ecdsaWalletRegistry),\n \"Caller is not the ECDSA Wallet Registry\"\n );\n\n // Compress wallet's public key and calculate Bitcoin's hash160 of it.\n bytes20 walletPubKeyHash = bytes20(\n EcdsaLib.compressPublicKey(publicKeyX, publicKeyY).hash160View()\n );\n\n Wallet storage wallet = self.registeredWallets[walletPubKeyHash];\n require(\n wallet.state == WalletState.Unknown,\n \"ECDSA wallet has been already registered\"\n );\n wallet.ecdsaWalletID = ecdsaWalletID;\n wallet.state = WalletState.Live;\n /* solhint-disable-next-line not-rely-on-time */\n wallet.createdAt = uint32(block.timestamp);\n\n // Set the freshly created wallet as the new active wallet.\n self.activeWalletPubKeyHash = walletPubKeyHash;\n\n self.liveWalletsCount++;\n\n emit NewWalletRegistered(ecdsaWalletID, walletPubKeyHash);\n }\n\n /// @notice Handles a notification about a wallet redemption timeout.\n /// Triggers the wallet moving funds process only if the wallet is\n /// still in the Live state. That means multiple action timeouts can\n /// be reported for the same wallet but only the first report\n /// requests the wallet to move their funds. Executes slashing if\n /// the wallet is in Live or MovingFunds state. Allows to notify\n /// redemption timeout also for a Terminated wallet in case the\n /// redemption was requested before the wallet got terminated.\n /// @param walletPubKeyHash 20-byte public key hash of the wallet.\n /// @param walletMembersIDs Identifiers of the wallet signing group members.\n /// @dev Requirements:\n /// - The wallet must be in the `Live`, `MovingFunds`,\n /// or `Terminated` state.\n function notifyWalletRedemptionTimeout(\n BridgeState.Storage storage self,\n bytes20 walletPubKeyHash,\n uint32[] calldata walletMembersIDs\n ) internal {\n Wallet storage wallet = self.registeredWallets[walletPubKeyHash];\n WalletState walletState = wallet.state;\n\n require(\n walletState == WalletState.Live ||\n walletState == WalletState.MovingFunds ||\n walletState == WalletState.Terminated,\n \"Wallet must be in Live or MovingFunds or Terminated state\"\n );\n\n if (\n walletState == Wallets.WalletState.Live ||\n walletState == Wallets.WalletState.MovingFunds\n ) {\n // Slash the wallet operators and reward the notifier\n self.ecdsaWalletRegistry.seize(\n self.redemptionTimeoutSlashingAmount,\n self.redemptionTimeoutNotifierRewardMultiplier,\n msg.sender,\n wallet.ecdsaWalletID,\n walletMembersIDs\n );\n }\n\n if (walletState == WalletState.Live) {\n moveFunds(self, walletPubKeyHash);\n }\n }\n\n /// @notice Handles a notification about a wallet heartbeat failure and\n /// triggers the wallet moving funds process.\n /// @param publicKeyX Wallet's public key's X coordinate.\n /// @param publicKeyY Wallet's public key's Y coordinate.\n /// @dev Requirements:\n /// - The only caller authorized to call this function is `registry`,\n /// - Wallet must be in Live state.\n function notifyWalletHeartbeatFailed(\n BridgeState.Storage storage self,\n bytes32 publicKeyX,\n bytes32 publicKeyY\n ) external {\n require(\n msg.sender == address(self.ecdsaWalletRegistry),\n \"Caller is not the ECDSA Wallet Registry\"\n );\n\n // Compress wallet's public key and calculate Bitcoin's hash160 of it.\n bytes20 walletPubKeyHash = bytes20(\n EcdsaLib.compressPublicKey(publicKeyX, publicKeyY).hash160View()\n );\n\n require(\n self.registeredWallets[walletPubKeyHash].state == WalletState.Live,\n \"Wallet must be in Live state\"\n );\n\n moveFunds(self, walletPubKeyHash);\n }\n\n /// @notice Notifies that the wallet is either old enough or has too few\n /// satoshis left and qualifies to be closed.\n /// @param walletPubKeyHash 20-byte public key hash of the wallet.\n /// @param walletMainUtxo Data of the wallet's main UTXO, as currently\n /// known on the Ethereum chain.\n /// @dev Requirements:\n /// - Wallet must not be set as the current active wallet,\n /// - Wallet must exceed the wallet maximum age OR the wallet BTC\n /// balance must be lesser than the minimum threshold. If the latter\n /// case is true, the `walletMainUtxo` components must point to the\n /// recent main UTXO of the given wallet, as currently known on the\n /// Ethereum chain. If the wallet has no main UTXO, this parameter\n /// can be empty as it is ignored since the wallet balance is\n /// assumed to be zero,\n /// - Wallet must be in Live state.\n function notifyWalletCloseable(\n BridgeState.Storage storage self,\n bytes20 walletPubKeyHash,\n BitcoinTx.UTXO calldata walletMainUtxo\n ) external {\n require(\n self.activeWalletPubKeyHash != walletPubKeyHash,\n \"Active wallet cannot be considered closeable\"\n );\n\n Wallet storage wallet = self.registeredWallets[walletPubKeyHash];\n require(\n wallet.state == WalletState.Live,\n \"Wallet must be in Live state\"\n );\n\n /* solhint-disable-next-line not-rely-on-time */\n bool walletOldEnough = block.timestamp >=\n wallet.createdAt + self.walletMaxAge;\n\n require(\n walletOldEnough ||\n getWalletBtcBalance(self, walletPubKeyHash, walletMainUtxo) <\n self.walletClosureMinBtcBalance,\n \"Wallet needs to be old enough or have too few satoshis\"\n );\n\n moveFunds(self, walletPubKeyHash);\n }\n\n /// @notice Notifies about the end of the closing period for the given wallet.\n /// Closes the wallet ultimately and notifies the ECDSA registry\n /// about this fact.\n /// @param walletPubKeyHash 20-byte public key hash of the wallet.\n /// @dev Requirements:\n /// - The wallet must be in the Closing state,\n /// - The wallet closing period must have elapsed.\n function notifyWalletClosingPeriodElapsed(\n BridgeState.Storage storage self,\n bytes20 walletPubKeyHash\n ) internal {\n Wallet storage wallet = self.registeredWallets[walletPubKeyHash];\n\n require(\n wallet.state == WalletState.Closing,\n \"Wallet must be in Closing state\"\n );\n\n require(\n /* solhint-disable-next-line not-rely-on-time */\n block.timestamp >\n wallet.closingStartedAt + self.walletClosingPeriod,\n \"Closing period has not elapsed yet\"\n );\n\n finalizeWalletClosing(self, walletPubKeyHash);\n }\n\n /// @notice Notifies that the wallet completed the moving funds process\n /// successfully. Checks if the funds were moved to the expected\n /// target wallets. Closes the source wallet if everything went\n /// good and reverts otherwise.\n /// @param walletPubKeyHash 20-byte public key hash of the wallet.\n /// @param targetWalletsHash 32-byte keccak256 hash over the list of\n /// 20-byte public key hashes of the target wallets actually used\n /// within the moving funds transactions.\n /// @dev Requirements:\n /// - The caller must make sure the moving funds transaction actually\n /// happened on Bitcoin chain and fits the protocol requirements,\n /// - The source wallet must be in the MovingFunds state,\n /// - The target wallets commitment must be submitted by the source\n /// wallet,\n /// - The actual target wallets used in the moving funds transaction\n /// must be exactly the same as the target wallets commitment.\n function notifyWalletFundsMoved(\n BridgeState.Storage storage self,\n bytes20 walletPubKeyHash,\n bytes32 targetWalletsHash\n ) internal {\n Wallet storage wallet = self.registeredWallets[walletPubKeyHash];\n // Check that the wallet is in the MovingFunds state but don't check\n // if the moving funds timeout is exceeded. That should give a\n // possibility to move funds in case when timeout was hit but was\n // not reported yet.\n require(\n wallet.state == WalletState.MovingFunds,\n \"Wallet must be in MovingFunds state\"\n );\n\n bytes32 targetWalletsCommitmentHash = wallet\n .movingFundsTargetWalletsCommitmentHash;\n\n require(\n targetWalletsCommitmentHash != bytes32(0),\n \"Target wallets commitment not submitted yet\"\n );\n\n // Make sure that the target wallets where funds were moved to are\n // exactly the same as the ones the source wallet committed to.\n require(\n targetWalletsCommitmentHash == targetWalletsHash,\n \"Target wallets don't correspond to the commitment\"\n );\n\n // If funds were moved, the wallet has no longer a main UTXO.\n delete wallet.mainUtxoHash;\n\n beginWalletClosing(self, walletPubKeyHash);\n }\n\n /// @notice Called when a MovingFunds wallet has a balance below the dust\n /// threshold. Begins the wallet closing.\n /// @param walletPubKeyHash 20-byte public key hash of the wallet.\n /// @dev Requirements:\n /// - The wallet must be in the MovingFunds state.\n function notifyWalletMovingFundsBelowDust(\n BridgeState.Storage storage self,\n bytes20 walletPubKeyHash\n ) internal {\n WalletState walletState = self\n .registeredWallets[walletPubKeyHash]\n .state;\n\n require(\n walletState == Wallets.WalletState.MovingFunds,\n \"Wallet must be in MovingFunds state\"\n );\n\n beginWalletClosing(self, walletPubKeyHash);\n }\n\n /// @notice Called when the timeout for MovingFunds for the wallet elapsed.\n /// Slashes wallet members and terminates the wallet.\n /// @param walletPubKeyHash 20-byte public key hash of the wallet.\n /// @param walletMembersIDs Identifiers of the wallet signing group members.\n /// @dev Requirements:\n /// - The wallet must be in the MovingFunds state.\n function notifyWalletMovingFundsTimeout(\n BridgeState.Storage storage self,\n bytes20 walletPubKeyHash,\n uint32[] calldata walletMembersIDs\n ) internal {\n Wallets.Wallet storage wallet = self.registeredWallets[\n walletPubKeyHash\n ];\n\n require(\n wallet.state == Wallets.WalletState.MovingFunds,\n \"Wallet must be in MovingFunds state\"\n );\n\n self.ecdsaWalletRegistry.seize(\n self.movingFundsTimeoutSlashingAmount,\n self.movingFundsTimeoutNotifierRewardMultiplier,\n msg.sender,\n wallet.ecdsaWalletID,\n walletMembersIDs\n );\n\n terminateWallet(self, walletPubKeyHash);\n }\n\n /// @notice Called when a wallet which was asked to sweep funds moved from\n /// another wallet did not provide a sweeping proof before a timeout.\n /// Slashes and terminates the wallet who failed to provide a proof.\n /// @param walletPubKeyHash 20-byte public key hash of the wallet which was\n /// supposed to sweep funds.\n /// @param walletMembersIDs Identifiers of the wallet signing group members.\n /// @dev Requirements:\n /// - The wallet must be in the `Live`, `MovingFunds`,\n /// or `Terminated` state.\n function notifyWalletMovedFundsSweepTimeout(\n BridgeState.Storage storage self,\n bytes20 walletPubKeyHash,\n uint32[] calldata walletMembersIDs\n ) internal {\n Wallet storage wallet = self.registeredWallets[walletPubKeyHash];\n WalletState walletState = wallet.state;\n\n require(\n walletState == WalletState.Live ||\n walletState == WalletState.MovingFunds ||\n walletState == WalletState.Terminated,\n \"Wallet must be in Live or MovingFunds or Terminated state\"\n );\n\n if (\n walletState == Wallets.WalletState.Live ||\n walletState == Wallets.WalletState.MovingFunds\n ) {\n self.ecdsaWalletRegistry.seize(\n self.movedFundsSweepTimeoutSlashingAmount,\n self.movedFundsSweepTimeoutNotifierRewardMultiplier,\n msg.sender,\n wallet.ecdsaWalletID,\n walletMembersIDs\n );\n\n terminateWallet(self, walletPubKeyHash);\n }\n }\n\n /// @notice Called when a wallet which was challenged for a fraud did not\n /// defeat the challenge before the timeout. Slashes and terminates\n /// the wallet who failed to defeat the challenge. If the wallet is\n /// already terminated, it does nothing.\n /// @param walletPubKeyHash 20-byte public key hash of the wallet which was\n /// supposed to sweep funds.\n /// @param walletMembersIDs Identifiers of the wallet signing group members.\n /// @param challenger Address of the party which submitted the fraud\n /// challenge.\n /// @dev Requirements:\n /// - The wallet must be in the `Live`, `MovingFunds`, `Closing`\n /// or `Terminated` state.\n function notifyWalletFraudChallengeDefeatTimeout(\n BridgeState.Storage storage self,\n bytes20 walletPubKeyHash,\n uint32[] calldata walletMembersIDs,\n address challenger\n ) internal {\n Wallet storage wallet = self.registeredWallets[walletPubKeyHash];\n WalletState walletState = wallet.state;\n\n if (\n walletState == Wallets.WalletState.Live ||\n walletState == Wallets.WalletState.MovingFunds ||\n walletState == Wallets.WalletState.Closing\n ) {\n self.ecdsaWalletRegistry.seize(\n self.fraudSlashingAmount,\n self.fraudNotifierRewardMultiplier,\n challenger,\n wallet.ecdsaWalletID,\n walletMembersIDs\n );\n\n terminateWallet(self, walletPubKeyHash);\n } else if (walletState == Wallets.WalletState.Terminated) {\n // This is a special case when the wallet was already terminated\n // due to a previous deliberate protocol violation. In that\n // case, this function should be still callable for other fraud\n // challenges timeouts in order to let the challenger unlock its\n // ETH deposit back. However, the wallet termination logic is\n // not called and the challenger is not rewarded.\n } else {\n revert(\n \"Wallet must be in Live or MovingFunds or Closing or Terminated state\"\n );\n }\n }\n\n /// @notice Requests a wallet to move their funds. If the wallet balance\n /// is zero, the wallet closing begins immediately. If the move\n /// funds request refers to the current active wallet, such a wallet\n /// is no longer considered active and the active wallet slot\n /// is unset allowing to trigger a new wallet creation immediately.\n /// @param walletPubKeyHash 20-byte public key hash of the wallet.\n /// @dev Requirements:\n /// - The caller must make sure that the wallet is in the Live state.\n function moveFunds(\n BridgeState.Storage storage self,\n bytes20 walletPubKeyHash\n ) internal {\n Wallet storage wallet = self.registeredWallets[walletPubKeyHash];\n\n if (wallet.mainUtxoHash == bytes32(0)) {\n // If the wallet has no main UTXO, that means its BTC balance\n // is zero and the wallet closing should begin immediately.\n beginWalletClosing(self, walletPubKeyHash);\n } else {\n // Otherwise, initialize the moving funds process.\n wallet.state = WalletState.MovingFunds;\n /* solhint-disable-next-line not-rely-on-time */\n wallet.movingFundsRequestedAt = uint32(block.timestamp);\n\n // slither-disable-next-line reentrancy-events\n emit WalletMovingFunds(wallet.ecdsaWalletID, walletPubKeyHash);\n }\n\n if (self.activeWalletPubKeyHash == walletPubKeyHash) {\n // If the move funds request refers to the current active wallet,\n // unset the active wallet and make the wallet creation process\n // possible in order to get a new healthy active wallet.\n delete self.activeWalletPubKeyHash;\n }\n\n self.liveWalletsCount--;\n }\n\n /// @notice Begins the closing period of the given wallet.\n /// @param walletPubKeyHash 20-byte public key hash of the wallet.\n /// @dev Requirements:\n /// - The caller must make sure that the wallet is in the\n /// MovingFunds state.\n function beginWalletClosing(\n BridgeState.Storage storage self,\n bytes20 walletPubKeyHash\n ) internal {\n Wallet storage wallet = self.registeredWallets[walletPubKeyHash];\n // Initialize the closing period.\n wallet.state = WalletState.Closing;\n /* solhint-disable-next-line not-rely-on-time */\n wallet.closingStartedAt = uint32(block.timestamp);\n\n // slither-disable-next-line reentrancy-events\n emit WalletClosing(wallet.ecdsaWalletID, walletPubKeyHash);\n }\n\n /// @notice Finalizes the closing period of the given wallet and notifies\n /// the ECDSA registry about this fact.\n /// @param walletPubKeyHash 20-byte public key hash of the wallet.\n /// @dev Requirements:\n /// - The caller must make sure that the wallet is in the Closing state.\n function finalizeWalletClosing(\n BridgeState.Storage storage self,\n bytes20 walletPubKeyHash\n ) internal {\n Wallet storage wallet = self.registeredWallets[walletPubKeyHash];\n\n wallet.state = WalletState.Closed;\n\n emit WalletClosed(wallet.ecdsaWalletID, walletPubKeyHash);\n\n self.ecdsaWalletRegistry.closeWallet(wallet.ecdsaWalletID);\n }\n\n /// @notice Terminates the given wallet and notifies the ECDSA registry\n /// about this fact. If the wallet termination refers to the current\n /// active wallet, such a wallet is no longer considered active and\n /// the active wallet slot is unset allowing to trigger a new wallet\n /// creation immediately.\n /// @param walletPubKeyHash 20-byte public key hash of the wallet.\n /// @dev Requirements:\n /// - The caller must make sure that the wallet is in the\n /// Live or MovingFunds or Closing state.\n function terminateWallet(\n BridgeState.Storage storage self,\n bytes20 walletPubKeyHash\n ) internal {\n Wallet storage wallet = self.registeredWallets[walletPubKeyHash];\n\n if (wallet.state == WalletState.Live) {\n self.liveWalletsCount--;\n }\n\n wallet.state = WalletState.Terminated;\n\n // slither-disable-next-line reentrancy-events\n emit WalletTerminated(wallet.ecdsaWalletID, walletPubKeyHash);\n\n if (self.activeWalletPubKeyHash == walletPubKeyHash) {\n // If termination refers to the current active wallet,\n // unset the active wallet and make the wallet creation process\n // possible in order to get a new healthy active wallet.\n delete self.activeWalletPubKeyHash;\n }\n\n self.ecdsaWalletRegistry.closeWallet(wallet.ecdsaWalletID);\n }\n\n /// @notice Gets BTC balance for given the wallet.\n /// @param walletPubKeyHash 20-byte public key hash of the wallet.\n /// @param walletMainUtxo Data of the wallet's main UTXO, as currently\n /// known on the Ethereum chain.\n /// @return walletBtcBalance Current BTC balance for the given wallet.\n /// @dev Requirements:\n /// - `walletMainUtxo` components must point to the recent main UTXO\n /// of the given wallet, as currently known on the Ethereum chain.\n /// If the wallet has no main UTXO, this parameter can be empty as it\n /// is ignored.\n function getWalletBtcBalance(\n BridgeState.Storage storage self,\n bytes20 walletPubKeyHash,\n BitcoinTx.UTXO calldata walletMainUtxo\n ) internal view returns (uint64 walletBtcBalance) {\n bytes32 walletMainUtxoHash = self\n .registeredWallets[walletPubKeyHash]\n .mainUtxoHash;\n\n // If the wallet has a main UTXO hash set, cross-check it with the\n // provided plain-text parameter and get the transaction output value\n // as BTC balance. Otherwise, the BTC balance is just zero.\n if (walletMainUtxoHash != bytes32(0)) {\n require(\n keccak256(\n abi.encodePacked(\n walletMainUtxo.txHash,\n walletMainUtxo.txOutputIndex,\n walletMainUtxo.txOutputValue\n )\n ) == walletMainUtxoHash,\n \"Invalid wallet main UTXO data\"\n );\n\n walletBtcBalance = walletMainUtxo.txOutputValue;\n }\n\n return walletBtcBalance;\n }\n}\n"
@@ -1,4 +1,4 @@
1
1
  {
2
2
  "_format": "hh-sol-dbg-1",
3
- "buildInfo": "../../build-info/c4339f1d03c92248ba145e4a93c303ef.json"
3
+ "buildInfo": "../../build-info/24b64daac03911e82b88918e54827fc7.json"
4
4
  }
@@ -1,4 +1,4 @@
1
1
  {
2
2
  "_format": "hh-sol-dbg-1",
3
- "buildInfo": "../../../build-info/c4339f1d03c92248ba145e4a93c303ef.json"
3
+ "buildInfo": "../../../build-info/24b64daac03911e82b88918e54827fc7.json"
4
4
  }
@@ -1,4 +1,4 @@
1
1
  {
2
2
  "_format": "hh-sol-dbg-1",
3
- "buildInfo": "../../../build-info/c4339f1d03c92248ba145e4a93c303ef.json"
3
+ "buildInfo": "../../../build-info/24b64daac03911e82b88918e54827fc7.json"
4
4
  }
@@ -1,4 +1,4 @@
1
1
  {
2
2
  "_format": "hh-sol-dbg-1",
3
- "buildInfo": "../../../build-info/c4339f1d03c92248ba145e4a93c303ef.json"
3
+ "buildInfo": "../../../build-info/24b64daac03911e82b88918e54827fc7.json"
4
4
  }
@@ -1,4 +1,4 @@
1
1
  {
2
2
  "_format": "hh-sol-dbg-1",
3
- "buildInfo": "../../../build-info/c4339f1d03c92248ba145e4a93c303ef.json"
3
+ "buildInfo": "../../../build-info/24b64daac03911e82b88918e54827fc7.json"
4
4
  }
@@ -1,4 +1,4 @@
1
1
  {
2
2
  "_format": "hh-sol-dbg-1",
3
- "buildInfo": "../../../build-info/c4339f1d03c92248ba145e4a93c303ef.json"
3
+ "buildInfo": "../../../build-info/24b64daac03911e82b88918e54827fc7.json"
4
4
  }
@@ -1,4 +1,4 @@
1
1
  {
2
2
  "_format": "hh-sol-dbg-1",
3
- "buildInfo": "../../../build-info/c4339f1d03c92248ba145e4a93c303ef.json"
3
+ "buildInfo": "../../../build-info/24b64daac03911e82b88918e54827fc7.json"
4
4
  }
@@ -1,4 +1,4 @@
1
1
  {
2
2
  "_format": "hh-sol-dbg-1",
3
- "buildInfo": "../../../build-info/c4339f1d03c92248ba145e4a93c303ef.json"
3
+ "buildInfo": "../../../build-info/24b64daac03911e82b88918e54827fc7.json"
4
4
  }
@@ -1,4 +1,4 @@
1
1
  {
2
2
  "_format": "hh-sol-dbg-1",
3
- "buildInfo": "../../../build-info/c4339f1d03c92248ba145e4a93c303ef.json"
3
+ "buildInfo": "../../../build-info/24b64daac03911e82b88918e54827fc7.json"
4
4
  }
@@ -1,4 +1,4 @@
1
1
  {
2
2
  "_format": "hh-sol-dbg-1",
3
- "buildInfo": "../../../build-info/c4339f1d03c92248ba145e4a93c303ef.json"
3
+ "buildInfo": "../../../build-info/24b64daac03911e82b88918e54827fc7.json"
4
4
  }
@@ -1,4 +1,4 @@
1
1
  {
2
2
  "_format": "hh-sol-dbg-1",
3
- "buildInfo": "../../../build-info/c4339f1d03c92248ba145e4a93c303ef.json"
3
+ "buildInfo": "../../../build-info/24b64daac03911e82b88918e54827fc7.json"
4
4
  }
@@ -1,4 +1,4 @@
1
1
  {
2
2
  "_format": "hh-sol-dbg-1",
3
- "buildInfo": "../../../build-info/c4339f1d03c92248ba145e4a93c303ef.json"
3
+ "buildInfo": "../../../build-info/24b64daac03911e82b88918e54827fc7.json"
4
4
  }
@@ -1,4 +1,4 @@
1
1
  {
2
2
  "_format": "hh-sol-dbg-1",
3
- "buildInfo": "../../../build-info/c4339f1d03c92248ba145e4a93c303ef.json"
3
+ "buildInfo": "../../../build-info/24b64daac03911e82b88918e54827fc7.json"
4
4
  }
@@ -1,4 +1,4 @@
1
1
  {
2
2
  "_format": "hh-sol-dbg-1",
3
- "buildInfo": "../../../build-info/c4339f1d03c92248ba145e4a93c303ef.json"
3
+ "buildInfo": "../../../build-info/24b64daac03911e82b88918e54827fc7.json"
4
4
  }
@@ -1,4 +1,4 @@
1
1
  {
2
2
  "_format": "hh-sol-dbg-1",
3
- "buildInfo": "../../../build-info/c4339f1d03c92248ba145e4a93c303ef.json"
3
+ "buildInfo": "../../../build-info/24b64daac03911e82b88918e54827fc7.json"
4
4
  }
@@ -1,4 +1,4 @@
1
1
  {
2
2
  "_format": "hh-sol-dbg-1",
3
- "buildInfo": "../../../build-info/c4339f1d03c92248ba145e4a93c303ef.json"
3
+ "buildInfo": "../../../build-info/24b64daac03911e82b88918e54827fc7.json"
4
4
  }
@@ -1,4 +1,4 @@
1
1
  {
2
2
  "_format": "hh-sol-dbg-1",
3
- "buildInfo": "../../../build-info/c4339f1d03c92248ba145e4a93c303ef.json"
3
+ "buildInfo": "../../../build-info/24b64daac03911e82b88918e54827fc7.json"
4
4
  }
@@ -1,4 +1,4 @@
1
1
  {
2
2
  "_format": "hh-sol-dbg-1",
3
- "buildInfo": "../../../build-info/c4339f1d03c92248ba145e4a93c303ef.json"
3
+ "buildInfo": "../../../build-info/24b64daac03911e82b88918e54827fc7.json"
4
4
  }
@@ -1,4 +1,4 @@
1
1
  {
2
2
  "_format": "hh-sol-dbg-1",
3
- "buildInfo": "../../../build-info/c4339f1d03c92248ba145e4a93c303ef.json"
3
+ "buildInfo": "../../../build-info/24b64daac03911e82b88918e54827fc7.json"
4
4
  }
@@ -0,0 +1,4 @@
1
+ {
2
+ "_format": "hh-sol-dbg-1",
3
+ "buildInfo": "../../../build-info/24b64daac03911e82b88918e54827fc7.json"
4
+ }