@keep-network/tbtc-v2 1.5.0-dev.3 → 1.5.0-dev.5

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 (112) 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 +8 -8
  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/WalletCoordinator.json +280 -13
  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/{62f0e623403ed4e28ae856e5d0acb6bc.json → 6cc4a0b423ac26dbfcae644b54bee6ae.json} +1 -1
  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/WalletCoordinator.sol/WalletCoordinator.dbg.json +1 -1
  70. package/build/contracts/bridge/WalletCoordinator.sol/WalletCoordinator.json +269 -2
  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 +322 -9
  86. package/export/artifacts/contracts/bridge/Bridge.sol/Bridge.json +22 -22
  87. package/export/artifacts/contracts/bridge/VendingMachine.sol/VendingMachine.json +6 -6
  88. package/export/artifacts/contracts/bridge/VendingMachineV2.sol/VendingMachineV2.json +6 -6
  89. package/export/artifacts/contracts/bridge/VendingMachineV3.sol/VendingMachineV3.json +6 -6
  90. package/export/artifacts/contracts/bridge/WalletCoordinator.sol/WalletCoordinator.json +18112 -12052
  91. package/export/artifacts/contracts/l2/L2TBTC.sol/L2TBTC.json +40 -40
  92. package/export/artifacts/contracts/l2/L2WormholeGateway.sol/L2WormholeGateway.json +49 -49
  93. package/export/artifacts/contracts/maintainer/MaintainerProxy.sol/MaintainerProxy.json +88 -88
  94. package/export/artifacts/contracts/relay/LightRelay.sol/LightRelay.json +57 -57
  95. package/export/artifacts/contracts/relay/LightRelayMaintainerProxy.sol/LightRelayMaintainerProxy.json +31 -31
  96. package/export/artifacts/contracts/test/BankStub.sol/BankStub.json +2 -2
  97. package/export/artifacts/contracts/test/BridgeStub.sol/BridgeStub.json +58 -58
  98. package/export/artifacts/contracts/test/GoerliLightRelay.sol/GoerliLightRelay.json +59 -59
  99. package/export/artifacts/contracts/test/HeartbeatStub.sol/HeartbeatStub.json +2 -2
  100. package/export/artifacts/contracts/test/LightRelayStub.sol/LightRelayStub.json +59 -59
  101. package/export/artifacts/contracts/test/ReceiveApprovalStub.sol/ReceiveApprovalStub.json +7 -7
  102. package/export/artifacts/contracts/test/SystemTestRelay.sol/SystemTestRelay.json +14 -14
  103. package/export/artifacts/contracts/test/TestERC20.sol/TestERC20.json +6 -6
  104. package/export/artifacts/contracts/test/TestERC721.sol/TestERC721.json +8 -8
  105. package/export/artifacts/contracts/test/TestEcdsaLib.sol/TestEcdsaLib.json +2 -2
  106. package/export/artifacts/contracts/test/WormholeBridgeStub.sol/WormholeBridgeStub.json +37 -37
  107. package/export/artifacts/contracts/token/TBTC.sol/TBTC.json +2 -2
  108. package/export/artifacts/contracts/vault/DonationVault.sol/DonationVault.json +11 -11
  109. package/export/artifacts/contracts/vault/TBTCVault.sol/TBTCVault.json +135 -135
  110. package/export/hardhat.config.js +7 -0
  111. package/export/typechain/factories/WalletCoordinator__factory.js +268 -1
  112. package/package.json +4 -3
@@ -1,4 +1,4 @@
1
1
  {
2
2
  "_format": "hh-sol-dbg-1",
3
- "buildInfo": "../../../build-info/dd29bc82293111de884ce85b9a6293db.json"
3
+ "buildInfo": "../../../build-info/ad2c4f91b13417738dc63e06bfaa5d8f.json"
4
4
  }
@@ -1,4 +1,4 @@
1
1
  {
2
2
  "_format": "hh-sol-dbg-1",
3
- "buildInfo": "../../../build-info/dd29bc82293111de884ce85b9a6293db.json"
3
+ "buildInfo": "../../../build-info/ad2c4f91b13417738dc63e06bfaa5d8f.json"
4
4
  }
@@ -1,4 +1,4 @@
1
1
  {
2
2
  "_format": "hh-sol-dbg-1",
3
- "buildInfo": "../../../build-info/dd29bc82293111de884ce85b9a6293db.json"
3
+ "buildInfo": "../../../build-info/ad2c4f91b13417738dc63e06bfaa5d8f.json"
4
4
  }
@@ -1,4 +1,4 @@
1
1
  {
2
2
  "_format": "hh-sol-dbg-1",
3
- "buildInfo": "../../../build-info/dd29bc82293111de884ce85b9a6293db.json"
3
+ "buildInfo": "../../../build-info/ad2c4f91b13417738dc63e06bfaa5d8f.json"
4
4
  }
@@ -1,4 +1,4 @@
1
1
  {
2
2
  "_format": "hh-sol-dbg-1",
3
- "buildInfo": "../../../build-info/dd29bc82293111de884ce85b9a6293db.json"
3
+ "buildInfo": "../../../build-info/ad2c4f91b13417738dc63e06bfaa5d8f.json"
4
4
  }
@@ -1,4 +1,4 @@
1
1
  {
2
2
  "_format": "hh-sol-dbg-1",
3
- "buildInfo": "../../../build-info/dd29bc82293111de884ce85b9a6293db.json"
3
+ "buildInfo": "../../../build-info/ad2c4f91b13417738dc63e06bfaa5d8f.json"
4
4
  }
@@ -1,4 +1,4 @@
1
1
  {
2
2
  "_format": "hh-sol-dbg-1",
3
- "buildInfo": "../../../build-info/dd29bc82293111de884ce85b9a6293db.json"
3
+ "buildInfo": "../../../build-info/ad2c4f91b13417738dc63e06bfaa5d8f.json"
4
4
  }
@@ -25,6 +25,7 @@ import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
25
25
  import "./BitcoinTx.sol";
26
26
  import "./Bridge.sol";
27
27
  import "./Deposit.sol";
28
+ import "./Redemption.sol";
28
29
  import "./Wallets.sol";
29
30
 
30
31
  /// @title Wallet coordinator.
@@ -120,6 +121,19 @@ contract WalletCoordinator is OwnableUpgradeable, Reimbursable {
120
121
  bytes4 refundLocktime;
121
122
  }
122
123
 
124
+ /// @notice Helper structure representing a redemption proposal.
125
+ struct RedemptionProposal {
126
+ // 20-byte public key hash of the target wallet.
127
+ bytes20 walletPubKeyHash;
128
+ // Array of the redeemers' output scripts that should be part of
129
+ // the redemption. Each output script MUST BE prefixed by its byte
130
+ // length, i.e. passed in the exactly same format as during the
131
+ // `Bridge.requestRedemption` transaction.
132
+ bytes[] redeemersOutputScripts;
133
+ // Proposed BTC fee for the entire transaction.
134
+ uint256 redemptionTxFee;
135
+ }
136
+
123
137
  /// @notice Mapping that holds addresses allowed to submit proposals and
124
138
  /// request heartbeats.
125
139
  mapping(address => bool) public isCoordinator;
@@ -195,6 +209,55 @@ contract WalletCoordinator is OwnableUpgradeable, Reimbursable {
195
209
  /// the current conditions.
196
210
  uint32 public depositSweepProposalSubmissionGasOffset;
197
211
 
212
+ /// @notice Determines the redemption proposal validity time. In other
213
+ /// words, this is the worst-case time for a redemption during
214
+ /// which the wallet is busy and cannot take another actions. This
215
+ /// is also the duration of the time lock applied to the wallet
216
+ /// once a new redemption proposal is submitted.
217
+ ///
218
+ /// For example, if a redemption proposal was submitted at
219
+ /// 2 pm and redemptionProposalValidity is 2 hours, the next
220
+ /// proposal (of any type) can be submitted after 4 pm.
221
+ uint32 public redemptionProposalValidity;
222
+
223
+ /// @notice The minimum time that must elapse since the redemption request
224
+ /// creation before a request becomes eligible for a processing.
225
+ ///
226
+ /// For example, if a request was created at 9 am and
227
+ /// redemptionRequestMinAge is 2 hours, the request is eligible for
228
+ /// processing after 11 am.
229
+ ///
230
+ /// @dev Forcing request minimum age ensures block finality for Ethereum.
231
+ uint32 public redemptionRequestMinAge;
232
+
233
+ /// @notice Each redemption request can be technically handled until it
234
+ /// reaches its timeout timestamp after which it can be reported
235
+ /// as timed out. However, allowing the wallet to handle requests
236
+ /// that are close to their timeout timestamp may cause a race
237
+ /// between the wallet and the redeemer. In result, the wallet may
238
+ /// redeem the requested funds even though the redeemer already
239
+ /// received back their tBTC (locked during redemption request) upon
240
+ /// reporting the request timeout. In effect, the redeemer may end
241
+ /// out with both tBTC and redeemed BTC in their hands which has
242
+ /// a negative impact on the tBTC <-> BTC peg. In order to mitigate
243
+ /// that problem, this parameter determines a safety margin that
244
+ /// puts the latest moment a request can be handled far before the
245
+ /// point after which the request can be reported as timed out.
246
+ ///
247
+ /// For example, if a request times out after 8 pm and
248
+ /// redemptionRequestTimeoutSafetyMargin is 2 hours, the request is
249
+ /// valid for processing only before 6 pm.
250
+ uint32 public redemptionRequestTimeoutSafetyMargin;
251
+
252
+ /// @notice The maximum count of redemption requests that can be processed
253
+ /// within a single redemption.
254
+ uint16 public redemptionMaxSize;
255
+
256
+ /// @notice Gas that is meant to balance the redemption proposal
257
+ /// submission overall cost. Can be updated by the owner based on
258
+ /// the current conditions.
259
+ uint32 public redemptionProposalSubmissionGasOffset;
260
+
198
261
  event CoordinatorAdded(address indexed coordinator);
199
262
 
200
263
  event CoordinatorRemoved(address indexed coordinator);
@@ -225,6 +288,19 @@ contract WalletCoordinator is OwnableUpgradeable, Reimbursable {
225
288
  address indexed coordinator
226
289
  );
227
290
 
291
+ event RedemptionProposalParametersUpdated(
292
+ uint32 redemptionProposalValidity,
293
+ uint32 redemptionRequestMinAge,
294
+ uint32 redemptionRequestTimeoutSafetyMargin,
295
+ uint16 redemptionMaxSize,
296
+ uint32 redemptionProposalSubmissionGasOffset
297
+ );
298
+
299
+ event RedemptionProposalSubmitted(
300
+ RedemptionProposal proposal,
301
+ address indexed coordinator
302
+ );
303
+
228
304
  modifier onlyCoordinator() {
229
305
  require(isCoordinator[msg.sender], "Caller is not a coordinator");
230
306
  _;
@@ -259,6 +335,12 @@ contract WalletCoordinator is OwnableUpgradeable, Reimbursable {
259
335
  depositRefundSafetyMargin = 24 hours;
260
336
  depositSweepMaxSize = 5;
261
337
  depositSweepProposalSubmissionGasOffset = 20_000; // optimized for 10 inputs
338
+
339
+ redemptionProposalValidity = 2 hours;
340
+ redemptionRequestMinAge = 600; // 10 minutes or ~50 blocks.
341
+ redemptionRequestTimeoutSafetyMargin = 2 hours;
342
+ redemptionMaxSize = 20;
343
+ redemptionProposalSubmissionGasOffset = 20_000;
262
344
  }
263
345
 
264
346
  /// @notice Adds the given address to the set of coordinator addresses.
@@ -467,7 +549,8 @@ contract WalletCoordinator is OwnableUpgradeable, Reimbursable {
467
549
  /// - Each deposit must have valid extra data (see `validateDepositExtraInfo`),
468
550
  /// - Each deposit must have the refund safety margin preserved,
469
551
  /// - Each deposit must be controlled by the same wallet,
470
- /// - Each deposit must target the same vault.
552
+ /// - Each deposit must target the same vault,
553
+ /// - Each deposit must be unique.
471
554
  ///
472
555
  /// The following off-chain validation must be performed as a bare minimum:
473
556
  /// - Inputs used for the sweep transaction have enough Bitcoin confirmations,
@@ -498,22 +581,28 @@ contract WalletCoordinator is OwnableUpgradeable, Reimbursable {
498
581
 
499
582
  address proposalVault = address(0);
500
583
 
584
+ uint256[] memory processedDepositKeys = new uint256[](
585
+ proposal.depositsKeys.length
586
+ );
587
+
501
588
  for (uint256 i = 0; i < proposal.depositsKeys.length; i++) {
502
589
  DepositKey memory depositKey = proposal.depositsKeys[i];
503
590
  DepositExtraInfo memory depositExtraInfo = depositsExtraInfo[i];
504
591
 
505
- // slither-disable-next-line calls-loop
506
- Deposit.DepositRequest memory depositRequest = bridge.deposits(
507
- uint256(
508
- keccak256(
509
- abi.encodePacked(
510
- depositKey.fundingTxHash,
511
- depositKey.fundingOutputIndex
512
- )
592
+ uint256 depositKeyUint = uint256(
593
+ keccak256(
594
+ abi.encodePacked(
595
+ depositKey.fundingTxHash,
596
+ depositKey.fundingOutputIndex
513
597
  )
514
598
  )
515
599
  );
516
600
 
601
+ // slither-disable-next-line calls-loop
602
+ Deposit.DepositRequest memory depositRequest = bridge.deposits(
603
+ depositKeyUint
604
+ );
605
+
517
606
  require(depositRequest.revealedAt != 0, "Deposit not revealed");
518
607
 
519
608
  require(
@@ -554,6 +643,16 @@ contract WalletCoordinator is OwnableUpgradeable, Reimbursable {
554
643
  depositRequest.vault == proposalVault,
555
644
  "Deposit targets different vault"
556
645
  );
646
+
647
+ // Make sure there are no duplicates in the deposits list.
648
+ for (uint256 j = 0; j < i; j++) {
649
+ require(
650
+ processedDepositKeys[j] != depositKeyUint,
651
+ "Duplicated deposit"
652
+ );
653
+ }
654
+
655
+ processedDepositKeys[i] = depositKeyUint;
557
656
  }
558
657
 
559
658
  return true;
@@ -687,4 +786,218 @@ contract WalletCoordinator is OwnableUpgradeable, Reimbursable {
687
786
 
688
787
  revert("Extra info funding output script does not match");
689
788
  }
789
+
790
+ /// @notice Updates parameters related to redemption proposal.
791
+ /// @param _redemptionProposalValidity The new value of `redemptionProposalValidity`.
792
+ /// @param _redemptionRequestMinAge The new value of `redemptionRequestMinAge`.
793
+ /// @param _redemptionRequestTimeoutSafetyMargin The new value of
794
+ /// `redemptionRequestTimeoutSafetyMargin`.
795
+ /// @param _redemptionMaxSize The new value of `redemptionMaxSize`.
796
+ /// @param _redemptionProposalSubmissionGasOffset The new value of
797
+ /// `redemptionProposalSubmissionGasOffset`.
798
+ /// @dev Requirements:
799
+ /// - The caller must be the owner.
800
+ function updateRedemptionProposalParameters(
801
+ uint32 _redemptionProposalValidity,
802
+ uint32 _redemptionRequestMinAge,
803
+ uint32 _redemptionRequestTimeoutSafetyMargin,
804
+ uint16 _redemptionMaxSize,
805
+ uint32 _redemptionProposalSubmissionGasOffset
806
+ ) external onlyOwner {
807
+ redemptionProposalValidity = _redemptionProposalValidity;
808
+ redemptionRequestMinAge = _redemptionRequestMinAge;
809
+ redemptionRequestTimeoutSafetyMargin = _redemptionRequestTimeoutSafetyMargin;
810
+ redemptionMaxSize = _redemptionMaxSize;
811
+ redemptionProposalSubmissionGasOffset = _redemptionProposalSubmissionGasOffset;
812
+
813
+ emit RedemptionProposalParametersUpdated(
814
+ _redemptionProposalValidity,
815
+ _redemptionRequestMinAge,
816
+ _redemptionRequestTimeoutSafetyMargin,
817
+ _redemptionMaxSize,
818
+ _redemptionProposalSubmissionGasOffset
819
+ );
820
+ }
821
+
822
+ /// @notice Submits a redemption proposal. Locks the target wallet
823
+ /// for a specific time, equal to the proposal validity period.
824
+ /// This function does not store the proposal in the state but
825
+ /// just emits an event that serves as a guiding light for wallet
826
+ /// off-chain members. Wallet members are supposed to validate
827
+ /// the proposal on their own, before taking any action.
828
+ /// @param proposal The redemption proposal
829
+ /// @dev Requirements:
830
+ /// - The caller is a coordinator,
831
+ /// - The wallet is not time-locked.
832
+ function submitRedemptionProposal(RedemptionProposal calldata proposal)
833
+ public
834
+ onlyCoordinator
835
+ onlyAfterWalletLock(proposal.walletPubKeyHash)
836
+ {
837
+ walletLock[proposal.walletPubKeyHash] = WalletLock(
838
+ /* solhint-disable-next-line not-rely-on-time */
839
+ uint32(block.timestamp) + redemptionProposalValidity,
840
+ WalletAction.Redemption
841
+ );
842
+
843
+ emit RedemptionProposalSubmitted(proposal, msg.sender);
844
+ }
845
+
846
+ /// @notice Wraps `submitRedemptionProposal` call and reimburses the
847
+ /// caller's transaction cost.
848
+ /// @dev See `submitRedemptionProposal` function documentation.
849
+ function submitRedemptionProposalWithReimbursement(
850
+ RedemptionProposal calldata proposal
851
+ ) external {
852
+ uint256 gasStart = gasleft();
853
+
854
+ submitRedemptionProposal(proposal);
855
+
856
+ reimbursementPool.refund(
857
+ (gasStart - gasleft()) + redemptionProposalSubmissionGasOffset,
858
+ msg.sender
859
+ );
860
+ }
861
+
862
+ /// @notice View function encapsulating the main rules of a valid redemption
863
+ /// proposal. This function is meant to facilitate the off-chain
864
+ /// validation of the incoming proposals. Thanks to it, most
865
+ /// of the work can be done using a single readonly contract call.
866
+ /// @param proposal The redemption proposal to validate.
867
+ /// @return True if the proposal is valid. Reverts otherwise.
868
+ /// @dev Requirements:
869
+ /// - The target wallet must be in the Live state,
870
+ /// - The number of redemption requests included in the redemption
871
+ /// proposal must be in the range [1, `redemptionMaxSize`],
872
+ /// - The proposed redemption tx fee must be grater than zero,
873
+ /// - The proposed redemption tx fee must be lesser than or equal to
874
+ /// the maximum total fee allowed by the Bridge
875
+ /// (`Bridge.redemptionTxMaxTotalFee`),
876
+ /// - The proposed maximum per-request redemption tx fee share must be
877
+ /// lesser than or equal to the maximum fee share allowed by the
878
+ /// given request (`RedemptionRequest.txMaxFee`),
879
+ /// - Each request must be a pending request registered in the Bridge,
880
+ /// - Each request must be old enough, i.e. at least `redemptionRequestMinAge`
881
+ /// elapsed since their creation time,
882
+ /// - Each request must have the timeout safety margin preserved,
883
+ /// - Each request must be unique.
884
+ function validateRedemptionProposal(RedemptionProposal calldata proposal)
885
+ external
886
+ view
887
+ returns (bool)
888
+ {
889
+ require(
890
+ bridge.wallets(proposal.walletPubKeyHash).state ==
891
+ Wallets.WalletState.Live,
892
+ "Wallet is not in Live state"
893
+ );
894
+
895
+ uint256 requestsCount = proposal.redeemersOutputScripts.length;
896
+
897
+ require(requestsCount > 0, "Redemption below the min size");
898
+
899
+ require(
900
+ requestsCount <= redemptionMaxSize,
901
+ "Redemption exceeds the max size"
902
+ );
903
+
904
+ (
905
+ ,
906
+ ,
907
+ ,
908
+ uint64 redemptionTxMaxTotalFee,
909
+ uint32 redemptionTimeout,
910
+ ,
911
+
912
+ ) = bridge.redemptionParameters();
913
+
914
+ require(
915
+ proposal.redemptionTxFee > 0,
916
+ "Proposed transaction fee cannot be zero"
917
+ );
918
+
919
+ // Make sure the proposed fee does not exceed the total fee limit.
920
+ require(
921
+ proposal.redemptionTxFee <= redemptionTxMaxTotalFee,
922
+ "Proposed transaction fee is too high"
923
+ );
924
+
925
+ // Compute the indivisible remainder that remains after dividing the
926
+ // redemption transaction fee over all requests evenly.
927
+ uint256 redemptionTxFeeRemainder = proposal.redemptionTxFee %
928
+ requestsCount;
929
+ // Compute the transaction fee per request by dividing the redemption
930
+ // transaction fee (reduced by the remainder) by the number of requests.
931
+ uint256 redemptionTxFeePerRequest = (proposal.redemptionTxFee -
932
+ redemptionTxFeeRemainder) / requestsCount;
933
+
934
+ uint256[] memory processedRedemptionKeys = new uint256[](requestsCount);
935
+
936
+ for (uint256 i = 0; i < requestsCount; i++) {
937
+ bytes memory script = proposal.redeemersOutputScripts[i];
938
+
939
+ // As the wallet public key hash is part of the redemption key,
940
+ // we have an implicit guarantee that all requests being part
941
+ // of the proposal target the same wallet.
942
+ uint256 redemptionKey = uint256(
943
+ keccak256(
944
+ abi.encodePacked(
945
+ keccak256(script),
946
+ proposal.walletPubKeyHash
947
+ )
948
+ )
949
+ );
950
+
951
+ // slither-disable-next-line calls-loop
952
+ Redemption.RedemptionRequest memory redemptionRequest = bridge
953
+ .pendingRedemptions(redemptionKey);
954
+
955
+ require(
956
+ redemptionRequest.requestedAt != 0,
957
+ "Not a pending redemption request"
958
+ );
959
+
960
+ require(
961
+ /* solhint-disable-next-line not-rely-on-time */
962
+ block.timestamp >
963
+ redemptionRequest.requestedAt + redemptionRequestMinAge,
964
+ "Redemption request min age not achieved yet"
965
+ );
966
+
967
+ // Calculate the timeout the given request times out at.
968
+ uint32 requestTimeout = redemptionRequest.requestedAt +
969
+ redemptionTimeout;
970
+ // Make sure we are far enough from the moment the request times out.
971
+ require(
972
+ /* solhint-disable-next-line not-rely-on-time */
973
+ block.timestamp <
974
+ requestTimeout - redemptionRequestTimeoutSafetyMargin,
975
+ "Redemption request timeout safety margin is not preserved"
976
+ );
977
+
978
+ uint256 feePerRequest = redemptionTxFeePerRequest;
979
+ // The last request incurs the fee remainder.
980
+ if (i == requestsCount - 1) {
981
+ feePerRequest += redemptionTxFeeRemainder;
982
+ }
983
+ // Make sure the redemption transaction fee share incurred by
984
+ // the given request fits in the limit for that request.
985
+ require(
986
+ feePerRequest <= redemptionRequest.txMaxFee,
987
+ "Proposed transaction per-request fee share is too high"
988
+ );
989
+
990
+ // Make sure there are no duplicates in the requests list.
991
+ for (uint256 j = 0; j < i; j++) {
992
+ require(
993
+ processedRedemptionKeys[j] != redemptionKey,
994
+ "Duplicated request"
995
+ );
996
+ }
997
+
998
+ processedRedemptionKeys[i] = redemptionKey;
999
+ }
1000
+
1001
+ return true;
1002
+ }
690
1003
  }