diff --git a/core/.eslintrc b/core/.eslintrc index 781127ef7..00fc354f3 100644 --- a/core/.eslintrc +++ b/core/.eslintrc @@ -12,11 +12,12 @@ "test/**" ] } - ] + ], + "@typescript-eslint/no-use-before-define": "off" }, "overrides": [ { - "files": ["deploy/*.ts"], + "files": ["deploy/**/*.ts", "test/**/*.ts"], "rules": { "@typescript-eslint/unbound-method": "off" } diff --git a/core/contracts/AcreBitcoinDepositor.sol b/core/contracts/AcreBitcoinDepositor.sol new file mode 100644 index 000000000..2bf4da21f --- /dev/null +++ b/core/contracts/AcreBitcoinDepositor.sol @@ -0,0 +1,524 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity ^0.8.21; + +import "@openzeppelin/contracts/access/Ownable2Step.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; + +import "@keep-network/tbtc-v2/contracts/integrator/AbstractTBTCDepositor.sol"; + +import {stBTC} from "./stBTC.sol"; + +// TODO: Make Upgradable +// TODO: Make Pausable + +/// @title Acre Bitcoin Depositor contract. +/// @notice The contract integrates Acre staking with tBTC minting. +/// User who wants to stake BTC in Acre should submit a Bitcoin transaction +/// to the most recently created off-chain ECDSA wallets of the tBTC Bridge +/// using pay-to-script-hash (P2SH) or pay-to-witness-script-hash (P2WSH) +/// containing hashed information about this Depositor contract address, +/// and staker's Ethereum address. +/// Then, the staker initiates tBTC minting by revealing their Ethereum +/// address along with their deposit blinding factor, refund public key +/// hash and refund locktime on the tBTC Bridge through this Depositor +/// contract. +/// The off-chain ECDSA wallet and Optimistic Minting bots listen for these +/// sorts of messages and when they get one, they check the Bitcoin network +/// to make sure the deposit lines up. Majority of tBTC minting is finalized +/// by the Optimistic Minting process, where Minter bot initializes +/// minting process and if there is no veto from the Guardians, the +/// process is finalized and tBTC minted to the Depositor address. If +/// the revealed deposit is not handled by the Optimistic Minting process +/// the off-chain ECDSA wallet may decide to pick the deposit transaction +/// for sweeping, and when the sweep operation is confirmed on the Bitcoin +/// network, the tBTC Bridge and tBTC vault mint the tBTC token to the +/// Depositor address. After tBTC is minted to the Depositor, on the stake +/// finalization tBTC is staked in Acre and stBTC shares are emitted +/// to the staker. +contract AcreBitcoinDepositor is AbstractTBTCDepositor, Ownable2Step { + using SafeERC20 for IERC20; + + /// @notice State of the stake request. + enum StakeRequestState { + Unknown, + Initialized, + Finalized, + Queued, + FinalizedFromQueue, + CancelledFromQueue + } + + struct StakeRequest { + // State of the stake request. + StakeRequestState state; + // The address to which the stBTC shares will be minted. Stored only when + // request is queued. + address staker; + // tBTC token amount to stake after deducting tBTC minting fees and the + // Depositor fee. Stored only when request is queued. + uint88 queuedAmount; + } + + /// @notice Mapping of stake requests. + /// @dev The key is a deposit key identifying the deposit. + mapping(uint256 => StakeRequest) public stakeRequests; + + /// @notice tBTC Token contract. + // TODO: Remove slither disable when introducing upgradeability. + // slither-disable-next-line immutable-states + IERC20 public tbtcToken; + + /// @notice stBTC contract. + // TODO: Remove slither disable when introducing upgradeability. + // slither-disable-next-line immutable-states + stBTC public stbtc; + + /// @notice Divisor used to compute the depositor fee taken from each deposit + /// and transferred to the treasury upon stake request finalization. + /// @dev That fee is computed as follows: + /// `depositorFee = depositedAmount / depositorFeeDivisor` + /// for example, if the depositor fee needs to be 2% of each deposit, + /// the `depositorFeeDivisor` should be set to `50` because + /// `1/50 = 0.02 = 2%`. + uint64 public depositorFeeDivisor; + + /// @notice Emitted when a stake request is initialized. + /// @dev Deposit details can be fetched from {{ Bridge.DepositRevealed }} + /// event emitted in the same transaction. + /// @param depositKey Deposit key identifying the deposit. + /// @param caller Address that initialized the stake request. + /// @param staker The address to which the stBTC shares will be minted. + event StakeRequestInitialized( + uint256 indexed depositKey, + address indexed caller, + address indexed staker + ); + + /// @notice Emitted when bridging completion has been notified. + /// @param depositKey Deposit key identifying the deposit. + /// @param caller Address that notified about bridging completion. + /// @param referral Identifier of a partner in the referral program. + /// @param bridgedAmount Amount of tBTC tokens that was bridged by the tBTC bridge. + /// @param depositorFee Depositor fee amount. + event BridgingCompleted( + uint256 indexed depositKey, + address indexed caller, + uint16 indexed referral, + uint256 bridgedAmount, + uint256 depositorFee + ); + + /// @notice Emitted when a stake request is finalized. + /// @dev Deposit details can be fetched from {{ ERC4626.Deposit }} + /// event emitted in the same transaction. + /// @param depositKey Deposit key identifying the deposit. + /// @param caller Address that finalized the stake request. + /// @param stakedAmount Amount of staked tBTC tokens. + event StakeRequestFinalized( + uint256 indexed depositKey, + address indexed caller, + uint256 stakedAmount + ); + + /// @notice Emitted when a stake request is queued. + /// @param depositKey Deposit key identifying the deposit. + /// @param caller Address that finalized the stake request. + /// @param queuedAmount Amount of queued tBTC tokens. + event StakeRequestQueued( + uint256 indexed depositKey, + address indexed caller, + uint256 queuedAmount + ); + + /// @notice Emitted when a stake request is finalized from the queue. + /// @dev Deposit details can be fetched from {{ ERC4626.Deposit }} + /// event emitted in the same transaction. + /// @param depositKey Deposit key identifying the deposit. + /// @param caller Address that finalized the stake request. + /// @param stakedAmount Amount of staked tBTC tokens. + event StakeRequestFinalizedFromQueue( + uint256 indexed depositKey, + address indexed caller, + uint256 stakedAmount + ); + + /// @notice Emitted when a queued stake request is cancelled. + /// @param depositKey Deposit key identifying the deposit. + /// @param staker Address of the staker. + /// @param amountCancelled Amount of queued tBTC tokens that got cancelled. + event StakeRequestCancelledFromQueue( + uint256 indexed depositKey, + address indexed staker, + uint256 amountCancelled + ); + + /// @notice Emitted when a depositor fee divisor is updated. + /// @param depositorFeeDivisor New value of the depositor fee divisor. + event DepositorFeeDivisorUpdated(uint64 depositorFeeDivisor); + + /// Reverts if the tBTC Token address is zero. + error TbtcTokenZeroAddress(); + + /// Reverts if the stBTC address is zero. + error StbtcZeroAddress(); + + /// @dev Staker address is zero. + error StakerIsZeroAddress(); + + /// @dev Attempted to execute function for stake request in unexpected current + /// state. + error UnexpectedStakeRequestState( + StakeRequestState currentState, + StakeRequestState expectedState + ); + + /// @dev Attempted to finalize bridging with depositor's contract tBTC balance + /// lower than the calculated bridged tBTC amount. This error means + /// that Governance should top-up the tBTC reserve for bridging fees + /// approximation. + error InsufficientTbtcBalance( + uint256 amountToStake, + uint256 currentBalance + ); + + /// @dev Attempted to notify a bridging completion, while it was already + /// notified. + error BridgingCompletionAlreadyNotified(); + + /// @dev Attempted to finalize a stake request, while bridging completion has + /// not been notified yet. + error BridgingNotCompleted(); + + /// @dev Calculated depositor fee exceeds the amount of minted tBTC tokens. + error DepositorFeeExceedsBridgedAmount( + uint256 depositorFee, + uint256 bridgedAmount + ); + + /// @dev Attempted to call bridging finalization for a stake request for + /// which the function was already called. + error BridgingFinalizationAlreadyCalled(); + + /// @dev Attempted to finalize or cancel a stake request that was not added + /// to the queue, or was already finalized or cancelled. + error StakeRequestNotQueued(); + + /// @dev Attempted to call function by an account that is not the staker. + error CallerNotStaker(); + + /// @notice Acre Bitcoin Depositor contract constructor. + /// @param bridge tBTC Bridge contract instance. + /// @param tbtcVault tBTC Vault contract instance. + /// @param _tbtcToken tBTC token contract instance. + /// @param _stbtc stBTC contract instance. + // TODO: Move to initializer when making the contract upgradeable. + constructor( + address bridge, + address tbtcVault, + address _tbtcToken, + address _stbtc + ) Ownable(msg.sender) { + __AbstractTBTCDepositor_initialize(bridge, tbtcVault); + + if (address(_tbtcToken) == address(0)) { + revert TbtcTokenZeroAddress(); + } + if (address(_stbtc) == address(0)) { + revert StbtcZeroAddress(); + } + + tbtcToken = IERC20(_tbtcToken); + stbtc = stBTC(_stbtc); + + depositorFeeDivisor = 1000; // 1/1000 == 10bps == 0.1% == 0.001 + } + + /// @notice This function allows staking process initialization for a Bitcoin + /// deposit made by an user with a P2(W)SH transaction. It uses the + /// supplied information to reveal a deposit to the tBTC Bridge contract. + /// @dev Requirements: + /// - The revealed vault address must match the TBTCVault address, + /// - All requirements from {Bridge#revealDepositWithExtraData} + /// function must be met. + /// - `staker` must be the staker address used in the P2(W)SH BTC + /// deposit transaction as part of the extra data. + /// - `referral` must be the referral info used in the P2(W)SH BTC + /// deposit transaction as part of the extra data. + /// - BTC deposit for the given `fundingTxHash`, `fundingOutputIndex` + /// can be revealed only one time. + /// @param fundingTx Bitcoin funding transaction data, see `IBridgeTypes.BitcoinTxInfo`. + /// @param reveal Deposit reveal data, see `IBridgeTypes.DepositRevealInfo`. + /// @param staker The address to which the stBTC shares will be minted. + /// @param referral Data used for referral program. + function initializeStake( + IBridgeTypes.BitcoinTxInfo calldata fundingTx, + IBridgeTypes.DepositRevealInfo calldata reveal, + address staker, + uint16 referral + ) external { + if (staker == address(0)) revert StakerIsZeroAddress(); + + // We don't check if the request was already initialized, as this check + // is enforced in `_initializeDeposit` when calling the + // `Bridge.revealDepositWithExtraData` function. + uint256 depositKey = _initializeDeposit( + fundingTx, + reveal, + encodeExtraData(staker, referral) + ); + + transitionStakeRequestState( + depositKey, + StakeRequestState.Unknown, + StakeRequestState.Initialized + ); + + emit StakeRequestInitialized(depositKey, msg.sender, staker); + } + + /// @notice This function should be called for previously initialized stake + /// request, after tBTC bridging process was finalized. + /// It stakes the tBTC from the given deposit into stBTC, emitting the + /// stBTC shares to the staker specified in the deposit extra data + /// and using the referral provided in the extra data. + /// @dev In case depositing in stBTC vault fails (e.g. because of the + /// maximum deposit limit being reached), the `queueStake` function + /// should be called to add the stake request to the staking queue. + /// @param depositKey Deposit key identifying the deposit. + function finalizeStake(uint256 depositKey) external { + transitionStakeRequestState( + depositKey, + StakeRequestState.Initialized, + StakeRequestState.Finalized + ); + + (uint256 amountToStake, address staker) = finalizeBridging(depositKey); + + emit StakeRequestFinalized(depositKey, msg.sender, amountToStake); + + // Deposit tBTC in stBTC. + tbtcToken.safeIncreaseAllowance(address(stbtc), amountToStake); + // slither-disable-next-line unused-return + stbtc.deposit(amountToStake, staker); + } + + /// @notice This function should be called for previously initialized stake + /// request, after tBTC bridging process was finalized, in case the + /// `finalizeStake` failed due to stBTC vault deposit limit + /// being reached. + /// @dev It queues the stake request, until the stBTC vault is ready to + /// accept the deposit. The request must be finalized with `finalizeQueuedStake` + /// after the limit is increased or other user withdraws their funds + /// from the stBTC contract to make place for another deposit. + /// The staker has a possibility to submit `cancelQueuedStake` that + /// will withdraw the minted tBTC token and abort staking process. + /// @param depositKey Deposit key identifying the deposit. + function queueStake(uint256 depositKey) external { + transitionStakeRequestState( + depositKey, + StakeRequestState.Initialized, + StakeRequestState.Queued + ); + + StakeRequest storage request = stakeRequests[depositKey]; + + uint256 amountToQueue; + (amountToQueue, request.staker) = finalizeBridging(depositKey); + + request.queuedAmount = SafeCast.toUint88(amountToQueue); + + emit StakeRequestQueued(depositKey, msg.sender, request.queuedAmount); + } + + /// @notice This function should be called for previously queued stake + /// request, when stBTC vault is able to accept a deposit. + /// @param depositKey Deposit key identifying the deposit. + function finalizeQueuedStake(uint256 depositKey) external { + transitionStakeRequestState( + depositKey, + StakeRequestState.Queued, + StakeRequestState.FinalizedFromQueue + ); + + StakeRequest storage request = stakeRequests[depositKey]; + + if (request.queuedAmount == 0) revert StakeRequestNotQueued(); + + uint256 amountToStake = request.queuedAmount; + delete (request.queuedAmount); + + emit StakeRequestFinalizedFromQueue( + depositKey, + msg.sender, + amountToStake + ); + + // Deposit tBTC in stBTC. + tbtcToken.safeIncreaseAllowance(address(stbtc), amountToStake); + // slither-disable-next-line unused-return + stbtc.deposit(amountToStake, request.staker); + } + + /// @notice Cancel queued stake. + /// The function can be called by the staker to recover tBTC that cannot + /// be finalized to stake in stBTC contract due to a deposit limit being + /// reached. + /// @dev This function can be called only after the stake request was added + /// to queue. + /// @dev Only staker provided in the extra data of the stake request can + /// call this function. + /// @param depositKey Deposit key identifying the deposit. + function cancelQueuedStake(uint256 depositKey) external { + transitionStakeRequestState( + depositKey, + StakeRequestState.Queued, + StakeRequestState.CancelledFromQueue + ); + + StakeRequest storage request = stakeRequests[depositKey]; + + if (request.queuedAmount == 0) revert StakeRequestNotQueued(); + + // Check if caller is the staker. + if (msg.sender != request.staker) revert CallerNotStaker(); + + uint256 amount = request.queuedAmount; + delete (request.queuedAmount); + + emit StakeRequestCancelledFromQueue(depositKey, request.staker, amount); + + tbtcToken.safeTransfer(request.staker, amount); + } + + /// @notice Updates the depositor fee divisor. + /// @param newDepositorFeeDivisor New depositor fee divisor value. + function updateDepositorFeeDivisor( + uint64 newDepositorFeeDivisor + ) external onlyOwner { + // TODO: Introduce a parameters update process. + depositorFeeDivisor = newDepositorFeeDivisor; + + emit DepositorFeeDivisorUpdated(newDepositorFeeDivisor); + } + + // TODO: Handle minimum deposit amount in tBTC Bridge vs stBTC. + + /// @notice Encodes staker address and referral as extra data. + /// @dev Packs the data to bytes32: 20 bytes of staker address and + /// 2 bytes of referral, 10 bytes of trailing zeros. + /// @param staker The address to which the stBTC shares will be minted. + /// @param referral Data used for referral program. + /// @return Encoded extra data. + function encodeExtraData( + address staker, + uint16 referral + ) public pure returns (bytes32) { + return bytes32(abi.encodePacked(staker, referral)); + } + + /// @notice Decodes staker address and referral from extra data. + /// @dev Unpacks the data from bytes32: 20 bytes of staker address and + /// 2 bytes of referral, 10 bytes of trailing zeros. + /// @param extraData Encoded extra data. + /// @return staker The address to which the stBTC shares will be minted. + /// @return referral Data used for referral program. + function decodeExtraData( + bytes32 extraData + ) public pure returns (address staker, uint16 referral) { + // First 20 bytes of extra data is staker address. + staker = address(uint160(bytes20(extraData))); + // Next 2 bytes of extra data is referral info. + referral = uint16(bytes2(extraData << (8 * 20))); + } + + /// @notice This function is used for state transitions. It ensures the current + /// state matches expected, and updates the stake request to a new + /// state. + /// @param depositKey Deposit key identifying the deposit. + /// @param expectedState Expected current stake request state. + /// @param newState New stake request state. + function transitionStakeRequestState( + uint256 depositKey, + StakeRequestState expectedState, + StakeRequestState newState + ) internal { + // Validate current stake request state. + if (stakeRequests[depositKey].state != expectedState) + revert UnexpectedStakeRequestState( + stakeRequests[depositKey].state, + expectedState + ); + + // Transition to a new state. + stakeRequests[depositKey].state = newState; + } + + /// @notice This function should be called for previously initialized stake + /// request, after tBTC minting process completed, meaning tBTC was + /// minted to this contract. + /// @dev It calculates the amount to stake based on the approximate minted + /// tBTC amount reduced by the depositor fee. + /// @dev IMPORTANT NOTE: The minted tBTC amount used by this function is an + /// approximation. See documentation of the + /// {{AbstractTBTCDepositor#_calculateTbtcAmount}} responsible for calculating + /// this value for more details. + /// @dev In case balance of tBTC tokens in this contract doesn't meet the + /// calculated tBTC amount, the function reverts with `InsufficientTbtcBalance` + /// error. This case requires Governance's validation, as tBTC Bridge minting + /// fees might changed in the way that reserve mentioned in + /// {{AbstractTBTCDepositor#_calculateTbtcAmount}} needs a top-up. + /// @param depositKey Deposit key identifying the deposit. + /// @return amountToStake tBTC token amount to stake after deducting tBTC bridging + /// fees and the depositor fee. + /// @return staker The address to which the stBTC shares will be minted. + function finalizeBridging( + uint256 depositKey + ) internal returns (uint256, address) { + ( + uint256 initialDepositAmount, + uint256 tbtcAmount, + bytes32 extraData + ) = _finalizeDeposit(depositKey); + + // Check if current balance is sufficient to finalize bridging of `tbtcAmount`. + uint256 currentBalance = tbtcToken.balanceOf(address(this)); + if (tbtcAmount > tbtcToken.balanceOf(address(this))) { + revert InsufficientTbtcBalance(tbtcAmount, currentBalance); + } + + // Compute depositor fee. The fee is calculated based on the initial funding + // transaction amount, before the tBTC protocol network fees were taken. + uint256 depositorFee = depositorFeeDivisor > 0 + ? (initialDepositAmount / depositorFeeDivisor) + : 0; + + // Ensure the depositor fee does not exceed the approximate minted tBTC + // amount. + if (depositorFee >= tbtcAmount) { + revert DepositorFeeExceedsBridgedAmount(depositorFee, tbtcAmount); + } + + uint256 amountToStake = tbtcAmount - depositorFee; + + (address staker, uint16 referral) = decodeExtraData(extraData); + + // Emit event for accounting purposes to track partner's referral ID and + // depositor fee taken. + emit BridgingCompleted( + depositKey, + msg.sender, + referral, + tbtcAmount, + depositorFee + ); + + // Transfer depositor fee to the treasury wallet. + if (depositorFee > 0) { + tbtcToken.safeTransfer(stbtc.treasury(), depositorFee); + } + + return (amountToStake, staker); + } +} diff --git a/core/contracts/Dispatcher.sol b/core/contracts/Dispatcher.sol index cac8fa317..ec1696239 100644 --- a/core/contracts/Dispatcher.sol +++ b/core/contracts/Dispatcher.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-only pragma solidity ^0.8.21; -import "@openzeppelin/contracts/access/Ownable.sol"; +import "@openzeppelin/contracts/access/Ownable2Step.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "@openzeppelin/contracts/interfaces/IERC4626.sol"; import "./Router.sol"; @@ -11,7 +11,7 @@ import "./stBTC.sol"; /// @notice Dispatcher is a contract that routes tBTC from stBTC to /// yield vaults and back. Vaults supply yield strategies with tBTC that /// generate yield for Bitcoin holders. -contract Dispatcher is Router, Ownable { +contract Dispatcher is Router, Ownable2Step { using SafeERC20 for IERC20; /// Struct holds information about a vault. diff --git a/core/contracts/TbtcDepositor.sol b/core/contracts/TbtcDepositor.sol deleted file mode 100644 index 732d289c0..000000000 --- a/core/contracts/TbtcDepositor.sol +++ /dev/null @@ -1,13 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -pragma solidity ^0.8.21; - -import "@keep-network/tbtc-v2/contracts/integrator/AbstractTBTCDepositor.sol"; - -contract TbtcDepositor is AbstractTBTCDepositor { - function initializeStakeRequest( - IBridgeTypes.BitcoinTxInfo calldata fundingTx, - IBridgeTypes.DepositRevealInfo calldata reveal, - address receiver, - uint16 referral - ) external {} -} diff --git a/core/contracts/stBTC.sol b/core/contracts/stBTC.sol index 9b5c186a4..b3c112869 100644 --- a/core/contracts/stBTC.sol +++ b/core/contracts/stBTC.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.21; import "@openzeppelin/contracts/token/ERC20/extensions/ERC4626.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import "@openzeppelin/contracts/access/Ownable.sol"; +import "@openzeppelin/contracts/access/Ownable2Step.sol"; import "./Dispatcher.sol"; /// @title stBTC @@ -17,7 +17,7 @@ import "./Dispatcher.sol"; /// of yield-bearing vaults. This contract facilitates the minting and /// burning of shares (stBTC), which are represented as standard ERC20 /// tokens, providing a seamless exchange with tBTC tokens. -contract stBTC is ERC4626, Ownable { +contract stBTC is ERC4626, Ownable2Step { using SafeERC20 for IERC20; /// Dispatcher contract that routes tBTC from stBTC to a given vault and back. @@ -26,8 +26,13 @@ contract stBTC is ERC4626, Ownable { /// Address of the treasury wallet, where fees should be transferred to. address public treasury; - /// Minimum amount for a single deposit operation. + /// Minimum amount for a single deposit operation. The value should be set + /// low enough so the deposits routed through Bitcoin Depositor contract won't + /// be rejected. It means that minimumDepositAmount should be lower than + /// tBTC protocol's depositDustThreshold reduced by all the minting fees taken + /// before depositing in the Acre contract. uint256 public minimumDepositAmount; + /// Maximum total amount of tBTC token held by Acre protocol. uint256 public maximumTotalAssets; diff --git a/core/contracts/test/AcreBitcoinDepositorHarness.sol b/core/contracts/test/AcreBitcoinDepositorHarness.sol new file mode 100644 index 000000000..9a57f61e0 --- /dev/null +++ b/core/contracts/test/AcreBitcoinDepositorHarness.sol @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* solhint-disable func-name-mixedcase */ +pragma solidity ^0.8.21; + +import {AcreBitcoinDepositor} from "../AcreBitcoinDepositor.sol"; +import {MockBridge, MockTBTCVault} from "@keep-network/tbtc-v2/contracts/test/TestTBTCDepositor.sol"; +import {IBridge} from "@keep-network/tbtc-v2/contracts/integrator/IBridge.sol"; +import {IBridgeTypes} from "@keep-network/tbtc-v2/contracts/integrator/IBridge.sol"; + +import {TestERC20} from "./TestERC20.sol"; + +/// @dev A test contract to expose internal function from AcreBitcoinDepositor contract. +/// This solution follows Foundry recommendation: +/// https://book.getfoundry.sh/tutorials/best-practices#internal-functions +contract AcreBitcoinDepositorHarness is AcreBitcoinDepositor { + constructor( + address bridge, + address tbtcVault, + address tbtcToken, + address stbtc + ) AcreBitcoinDepositor(bridge, tbtcVault, tbtcToken, stbtc) {} + + function exposed_finalizeBridging( + uint256 depositKey + ) external returns (uint256 amountToStake, address staker) { + return finalizeBridging(depositKey); + } +} + +/// @dev A test contract to stub tBTC Bridge contract. +contract BridgeStub is MockBridge {} + +/// @dev A test contract to stub tBTC Vault contract. +contract TBTCVaultStub is MockTBTCVault { + TestERC20 public immutable tbtc; + IBridge public immutable bridge; + + /// @notice Multiplier to convert satoshi to TBTC token units. + uint256 public constant SATOSHI_MULTIPLIER = 10 ** 10; + + constructor(TestERC20 _tbtc, IBridge _bridge) { + tbtc = _tbtc; + bridge = _bridge; + } + + function finalizeOptimisticMintingRequest( + uint256 depositKey + ) public override { + IBridgeTypes.DepositRequest memory deposit = bridge.deposits( + depositKey + ); + + uint256 amountSubTreasury = (deposit.amount - deposit.treasuryFee) * + SATOSHI_MULTIPLIER; + + uint256 omFee = optimisticMintingFeeDivisor > 0 + ? (amountSubTreasury / optimisticMintingFeeDivisor) + : 0; + + // The deposit transaction max fee is in the 1e8 satoshi precision. + // We need to convert them to the 1e18 TBTC precision. + // slither-disable-next-line unused-return + (, , uint64 depositTxMaxFee, ) = bridge.depositParameters(); + uint256 txMaxFee = depositTxMaxFee * SATOSHI_MULTIPLIER; + + uint256 amountToMint = amountSubTreasury - omFee - txMaxFee; + + finalizeOptimisticMintingRequestWithAmount(depositKey, amountToMint); + } + + function finalizeOptimisticMintingRequestWithAmount( + uint256 depositKey, + uint256 amountToMint + ) public { + MockTBTCVault.finalizeOptimisticMintingRequest(depositKey); + + tbtc.mint(bridge.deposits(depositKey).depositor, amountToMint); + } +} diff --git a/core/contracts/test/TestERC20.sol b/core/contracts/test/TestERC20.sol index 44e5e14dc..c6b25896d 100644 --- a/core/contracts/test/TestERC20.sol +++ b/core/contracts/test/TestERC20.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.21; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; contract TestERC20 is ERC20 { - constructor() ERC20("Test Token", "TEST") {} + constructor(string memory name, string memory symbol) ERC20(name, symbol) {} function mint(address account, uint256 value) external { _mint(account, value); diff --git a/core/deploy/00_resolve_tbtc_bridge.ts b/core/deploy/00_resolve_tbtc_bridge.ts new file mode 100644 index 000000000..4150b696a --- /dev/null +++ b/core/deploy/00_resolve_tbtc_bridge.ts @@ -0,0 +1,35 @@ +import type { DeployFunction } from "hardhat-deploy/types" +import type { + HardhatNetworkConfig, + HardhatRuntimeEnvironment, +} from "hardhat/types" +import { isNonZeroAddress } from "../helpers/address" +import { waitConfirmationsNumber } from "../helpers/deployment" + +const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { + const { getNamedAccounts, deployments } = hre + const { log } = deployments + const { deployer } = await getNamedAccounts() + + const bridge = await deployments.getOrNull("Bridge") + + if (bridge && isNonZeroAddress(bridge.address)) { + log(`using Bridge contract at ${bridge.address}`) + } else if ((hre.network.config as HardhatNetworkConfig)?.forking?.enabled) { + throw new Error("deployed Bridge contract not found") + } else { + log("deploying Bridge contract stub") + + await deployments.deploy("Bridge", { + contract: "BridgeStub", + args: [], + from: deployer, + log: true, + waitConfirmations: waitConfirmationsNumber(hre), + }) + } +} + +export default func + +func.tags = ["TBTC", "Bridge"] diff --git a/core/deploy/00_resolve_tbtc.ts b/core/deploy/00_resolve_tbtc_token.ts similarity index 63% rename from core/deploy/00_resolve_tbtc.ts rename to core/deploy/00_resolve_tbtc_token.ts index dc1bfeff5..ef00bcdef 100644 --- a/core/deploy/00_resolve_tbtc.ts +++ b/core/deploy/00_resolve_tbtc_token.ts @@ -1,6 +1,10 @@ -import type { HardhatRuntimeEnvironment } from "hardhat/types" import type { DeployFunction } from "hardhat-deploy/types" +import type { + HardhatNetworkConfig, + HardhatRuntimeEnvironment, +} from "hardhat/types" import { isNonZeroAddress } from "../helpers/address" +import { waitConfirmationsNumber } from "../helpers/deployment" const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { const { getNamedAccounts, deployments } = hre @@ -11,20 +15,24 @@ const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { if (tbtc && isNonZeroAddress(tbtc.address)) { log(`using TBTC contract at ${tbtc.address}`) - } else if (!hre.network.tags.allowStubs) { + } else if ( + !hre.network.tags.allowStubs || + (hre.network.config as HardhatNetworkConfig)?.forking?.enabled + ) { throw new Error("deployed TBTC contract not found") } else { log("deploying TBTC contract stub") await deployments.deploy("TBTC", { contract: "TestERC20", + args: ["Test tBTC", "TestTBTC"], from: deployer, log: true, - waitConfirmations: 1, + waitConfirmations: waitConfirmationsNumber(hre), }) } } export default func -func.tags = ["TBTC"] +func.tags = ["TBTC", "TBTCToken"] diff --git a/core/deploy/00_resolve_tbtc_vault.ts b/core/deploy/00_resolve_tbtc_vault.ts new file mode 100644 index 000000000..46edbd919 --- /dev/null +++ b/core/deploy/00_resolve_tbtc_vault.ts @@ -0,0 +1,42 @@ +import type { DeployFunction } from "hardhat-deploy/types" +import type { + HardhatNetworkConfig, + HardhatRuntimeEnvironment, +} from "hardhat/types" +import { isNonZeroAddress } from "../helpers/address" +import { waitConfirmationsNumber } from "../helpers/deployment" + +const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { + const { getNamedAccounts, deployments } = hre + const { log } = deployments + const { deployer } = await getNamedAccounts() + + const tbtcVault = await deployments.getOrNull("TBTCVault") + + if (tbtcVault && isNonZeroAddress(tbtcVault.address)) { + log(`using TBTCVault contract at ${tbtcVault.address}`) + } else if ( + !hre.network.tags.allowStubs || + (hre.network.config as HardhatNetworkConfig)?.forking?.enabled + ) { + throw new Error("deployed TBTCVault contract not found") + } else { + log("deploying TBTCVault contract stub") + + const tbtc = await deployments.get("TBTC") + const bridge = await deployments.get("Bridge") + + await deployments.deploy("TBTCVault", { + contract: "TBTCVaultStub", + args: [tbtc.address, bridge.address], + from: deployer, + log: true, + waitConfirmations: waitConfirmationsNumber(hre), + }) + } +} + +export default func + +func.tags = ["TBTC", "TBTCVault"] +func.dependencies = ["TBTCToken", "Bridge"] diff --git a/core/deploy/02_deploy_dispatcher.ts b/core/deploy/02_deploy_dispatcher.ts index 8104d906f..4473531e9 100644 --- a/core/deploy/02_deploy_dispatcher.ts +++ b/core/deploy/02_deploy_dispatcher.ts @@ -9,7 +9,7 @@ const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { const tbtc = await deployments.get("TBTC") const stbtc = await deployments.get("stBTC") - await deployments.deploy("Dispatcher", { + const dispatcher = await deployments.deploy("Dispatcher", { from: deployer, args: [stbtc.address, tbtc.address], log: true, @@ -17,7 +17,7 @@ const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { }) if (hre.network.tags.etherscan) { - await helpers.etherscan.verify(stbtc) + await helpers.etherscan.verify(dispatcher) } // TODO: Add Tenderly verification diff --git a/core/deploy/03_deploy_acre_bitcoin_depositor.ts b/core/deploy/03_deploy_acre_bitcoin_depositor.ts new file mode 100644 index 000000000..4863dd77b --- /dev/null +++ b/core/deploy/03_deploy_acre_bitcoin_depositor.ts @@ -0,0 +1,35 @@ +import type { DeployFunction } from "hardhat-deploy/types" +import type { HardhatRuntimeEnvironment } from "hardhat/types" +import { waitConfirmationsNumber } from "../helpers/deployment" + +const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { + const { getNamedAccounts, deployments, helpers } = hre + const { deployer } = await getNamedAccounts() + + const bridge = await deployments.get("Bridge") + const tbtcVault = await deployments.get("TBTCVault") + const tbtc = await deployments.get("TBTC") + const stbtc = await deployments.get("stBTC") + + const depositor = await deployments.deploy("AcreBitcoinDepositor", { + contract: + process.env.HARDHAT_TEST === "true" + ? "AcreBitcoinDepositorHarness" + : "AcreBitcoinDepositor", + from: deployer, + args: [bridge.address, tbtcVault.address, tbtc.address, stbtc.address], + log: true, + waitConfirmations: waitConfirmationsNumber(hre), + }) + + if (hre.network.tags.etherscan) { + await helpers.etherscan.verify(depositor) + } + + // TODO: Add Tenderly verification +} + +export default func + +func.tags = ["AcreBitcoinDepositor"] +func.dependencies = ["TBTC", "stBTC"] diff --git a/core/deploy/21_transfer_ownership_stbtc.ts b/core/deploy/21_transfer_ownership_stbtc.ts index d3554032e..fa2ad6c47 100644 --- a/core/deploy/21_transfer_ownership_stbtc.ts +++ b/core/deploy/21_transfer_ownership_stbtc.ts @@ -14,8 +14,17 @@ const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { "transferOwnership", governance, ) + + if (hre.network.name !== "mainnet") { + await deployments.execute( + "stBTC", + { from: governance, log: true, waitConfirmations: 1 }, + "acceptOwnership", + ) + } } export default func func.tags = ["TransferOwnershipStBTC"] +func.dependencies = ["stBTC"] diff --git a/core/deploy/22_transfer_ownership_dispatcher.ts b/core/deploy/22_transfer_ownership_dispatcher.ts index 5e85a15d0..5b8e930dc 100644 --- a/core/deploy/22_transfer_ownership_dispatcher.ts +++ b/core/deploy/22_transfer_ownership_dispatcher.ts @@ -14,6 +14,14 @@ const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { "transferOwnership", governance, ) + + if (hre.network.name !== "mainnet") { + await deployments.execute( + "Dispatcher", + { from: governance, log: true, waitConfirmations: 1 }, + "acceptOwnership", + ) + } } export default func diff --git a/core/deploy/23_transfer_ownership_acre_bitcoin_depositor.ts b/core/deploy/23_transfer_ownership_acre_bitcoin_depositor.ts new file mode 100644 index 000000000..c04f6f679 --- /dev/null +++ b/core/deploy/23_transfer_ownership_acre_bitcoin_depositor.ts @@ -0,0 +1,32 @@ +import type { HardhatRuntimeEnvironment } from "hardhat/types" +import type { DeployFunction } from "hardhat-deploy/types" + +const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { + const { getNamedAccounts, deployments } = hre + const { deployer, governance } = await getNamedAccounts() + const { log } = deployments + + log( + `transferring ownership of AcreBitcoinDepositor contract to ${governance}`, + ) + + await deployments.execute( + "AcreBitcoinDepositor", + { from: deployer, log: true, waitConfirmations: 1 }, + "transferOwnership", + governance, + ) + + if (hre.network.name !== "mainnet") { + await deployments.execute( + "AcreBitcoinDepositor", + { from: governance, log: true, waitConfirmations: 1 }, + "acceptOwnership", + ) + } +} + +export default func + +func.tags = ["TransferOwnershipAcreBitcoinDepositor"] +func.dependencies = ["AcreBitcoinDepositor"] diff --git a/core/package.json b/core/package.json index 46baa37f3..58568dc37 100644 --- a/core/package.json +++ b/core/package.json @@ -26,7 +26,7 @@ "lint:sol:fix": "solhint 'contracts/**/*.sol' --fix && prettier --write 'contracts/**/*.sol'", "lint:config": "prettier --check '**/*.@(json)'", "lint:config:fix": "prettier --write '**/*.@(json)'", - "test": "hardhat test" + "test": "HARDHAT_TEST=true hardhat test" }, "devDependencies": { "@keep-network/hardhat-helpers": "^0.7.1", diff --git a/core/slither.config.json b/core/slither.config.json index fed211ee1..46c3cb424 100644 --- a/core/slither.config.json +++ b/core/slither.config.json @@ -1,5 +1,5 @@ { "detectors_to_exclude": "assembly,naming-convention,timestamp,pragma,solc-version", "hardhat_artifacts_directory": "build", - "filter_paths": "node_modules/.*" + "filter_paths": "contracts/test|node_modules" } diff --git a/core/test/AcreBitcoinDepositor.test.ts b/core/test/AcreBitcoinDepositor.test.ts new file mode 100644 index 000000000..f87ab02ac --- /dev/null +++ b/core/test/AcreBitcoinDepositor.test.ts @@ -0,0 +1,1521 @@ +/* eslint-disable func-names */ +import { loadFixture } from "@nomicfoundation/hardhat-toolbox/network-helpers" + +import { ethers, helpers } from "hardhat" +import { expect } from "chai" +import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers" +import { ContractTransactionResponse, ZeroAddress } from "ethers" + +import { StakeRequestState } from "../types" + +import type { + StBTC, + BridgeStub, + TBTCVaultStub, + AcreBitcoinDepositorHarness, + TestERC20, +} from "../typechain" +import { deployment } from "./helpers" +import { beforeAfterSnapshotWrapper } from "./helpers/snapshot" +import { tbtcDepositData } from "./data/tbtc" +import { to1ePrecision } from "./utils" + +async function fixture() { + const { bitcoinDepositor, tbtcBridge, tbtcVault, stbtc, tbtc } = + await deployment() + + return { bitcoinDepositor, tbtcBridge, tbtcVault, stbtc, tbtc } +} + +const { lastBlockTime } = helpers.time +const { getNamedSigners, getUnnamedSigners } = helpers.signers + +describe("AcreBitcoinDepositor", () => { + const defaultDepositTreasuryFeeDivisor = 2000 // 1/2000 = 0.05% = 0.0005 + const defaultDepositTxMaxFee = 1000 // 1000 satoshi = 0.00001 BTC + const defaultOptimisticFeeDivisor = 500 // 1/500 = 0.002 = 0.2% + const defaultDepositorFeeDivisor = 1000 // 1/1000 = 0.001 = 0.1% + + // Funding transaction amount: 10000 satoshi + // tBTC Deposit Treasury Fee: 0.05% = 10000 * 0.05% = 5 satoshi + // tBTC Optimistic Minting Fee: 0.2% = (10000 - 5) * 0.2% = 19,99 satoshi + // tBTC Deposit Transaction Max Fee: 1000 satoshi + // Depositor Fee: 1.25% = 10000 satoshi * 0.01% = 10 satoshi + const initialDepositAmount = to1ePrecision(10000, 10) // 10000 satoshi + const bridgedTbtcAmount = to1ePrecision(897501, 8) // 8975,01 satoshi + const depositorFee = to1ePrecision(10, 10) // 10 satoshi + const amountToStake = to1ePrecision(896501, 8) // 8965,01 satoshi + + let bitcoinDepositor: AcreBitcoinDepositorHarness + let tbtcBridge: BridgeStub + let tbtcVault: TBTCVaultStub + let stbtc: StBTC + let tbtc: TestERC20 + + let governance: HardhatEthersSigner + let treasury: HardhatEthersSigner + let thirdParty: HardhatEthersSigner + let staker: HardhatEthersSigner + + before(async () => { + ;({ bitcoinDepositor, tbtcBridge, tbtcVault, stbtc, tbtc } = + await loadFixture(fixture)) + ;({ governance, treasury } = await getNamedSigners()) + ;[thirdParty] = await getUnnamedSigners() + + staker = await helpers.account.impersonateAccount(tbtcDepositData.staker, { + from: thirdParty, + }) + + await stbtc.connect(governance).updateDepositParameters( + 10000000000000, // 0.00001 + await stbtc.maximumTotalAssets(), + ) + + tbtcDepositData.reveal.vault = await tbtcVault.getAddress() + + await tbtcBridge + .connect(governance) + .setDepositTreasuryFeeDivisor(defaultDepositTreasuryFeeDivisor) + await tbtcBridge + .connect(governance) + .setDepositTxMaxFee(defaultDepositTxMaxFee) + await tbtcVault + .connect(governance) + .setOptimisticMintingFeeDivisor(defaultOptimisticFeeDivisor) + await bitcoinDepositor + .connect(governance) + .updateDepositorFeeDivisor(defaultDepositorFeeDivisor) + }) + + describe("initializeStake", () => { + describe("when staker is zero address", () => { + it("should revert", async () => { + await expect( + bitcoinDepositor.initializeStake( + tbtcDepositData.fundingTxInfo, + tbtcDepositData.reveal, + ZeroAddress, + 0, + ), + ).to.be.revertedWithCustomError(bitcoinDepositor, "StakerIsZeroAddress") + }) + }) + + describe("when staker is non zero address", () => { + describe("when stake is not in progress", () => { + describe("when tbtc vault address is incorrect", () => { + beforeAfterSnapshotWrapper() + + it("should revert", async () => { + const invalidTbtcVault = + await ethers.Wallet.createRandom().getAddress() + + await expect( + bitcoinDepositor + .connect(thirdParty) + .initializeStake( + tbtcDepositData.fundingTxInfo, + { ...tbtcDepositData.reveal, vault: invalidTbtcVault }, + tbtcDepositData.staker, + tbtcDepositData.referral, + ), + ).to.be.revertedWith("Vault address mismatch") + }) + }) + + describe("when tbtc vault address is correct", () => { + describe("when referral is non-zero", () => { + beforeAfterSnapshotWrapper() + + let tx: ContractTransactionResponse + + before(async () => { + tx = await bitcoinDepositor + .connect(thirdParty) + .initializeStake( + tbtcDepositData.fundingTxInfo, + tbtcDepositData.reveal, + tbtcDepositData.staker, + tbtcDepositData.referral, + ) + }) + + it("should emit StakeRequestInitialized event", async () => { + await expect(tx) + .to.emit(bitcoinDepositor, "StakeRequestInitialized") + .withArgs( + tbtcDepositData.depositKey, + thirdParty.address, + tbtcDepositData.staker, + ) + }) + + it("should update stake state", async () => { + const stakeRequest = await bitcoinDepositor.stakeRequests( + tbtcDepositData.depositKey, + ) + + expect(stakeRequest.state).to.be.equal( + StakeRequestState.Initialized, + ) + }) + + it("should not store stake data in queue", async () => { + const stakeRequest = await bitcoinDepositor.stakeRequests( + tbtcDepositData.depositKey, + ) + + expect(stakeRequest.staker, "invalid staker").to.be.equal( + ZeroAddress, + ) + expect( + stakeRequest.queuedAmount, + "invalid queuedAmount", + ).to.be.equal(0) + }) + + it("should reveal the deposit to the bridge contract with extra data", async () => { + const storedRevealedDeposit = await tbtcBridge.deposits( + tbtcDepositData.depositKey, + ) + + expect( + storedRevealedDeposit.revealedAt, + "invalid revealedAt", + ).to.be.equal(await lastBlockTime()) + + expect( + storedRevealedDeposit.extraData, + "invalid extraData", + ).to.be.equal(tbtcDepositData.extraData) + }) + }) + + describe("when referral is zero", () => { + beforeAfterSnapshotWrapper() + + it("should succeed", async () => { + await expect( + bitcoinDepositor + .connect(thirdParty) + .initializeStake( + tbtcDepositData.fundingTxInfo, + tbtcDepositData.reveal, + tbtcDepositData.staker, + 0, + ), + ).to.be.not.reverted + }) + }) + }) + }) + + describe("when stake is already in progress", () => { + beforeAfterSnapshotWrapper() + + before(async () => { + await initializeStake() + }) + + it("should revert", async () => { + await expect( + bitcoinDepositor + .connect(thirdParty) + .initializeStake( + tbtcDepositData.fundingTxInfo, + tbtcDepositData.reveal, + tbtcDepositData.staker, + tbtcDepositData.referral, + ), + ).to.be.revertedWith("Deposit already revealed") + }) + }) + + describe("when stake is already finalized", () => { + beforeAfterSnapshotWrapper() + + before(async () => { + await initializeStake() + + // Simulate deposit request finalization. + await finalizeMinting(tbtcDepositData.depositKey) + + await bitcoinDepositor + .connect(thirdParty) + .finalizeStake(tbtcDepositData.depositKey) + }) + + it("should revert", async () => { + await expect( + bitcoinDepositor + .connect(thirdParty) + .initializeStake( + tbtcDepositData.fundingTxInfo, + tbtcDepositData.reveal, + tbtcDepositData.staker, + tbtcDepositData.referral, + ), + ).to.be.revertedWith("Deposit already revealed") + }) + }) + + describe("when stake is already queued", () => { + beforeAfterSnapshotWrapper() + + before(async () => { + await initializeStake() + + // Simulate deposit request finalization. + await finalizeMinting(tbtcDepositData.depositKey) + + await bitcoinDepositor + .connect(thirdParty) + .queueStake(tbtcDepositData.depositKey) + }) + + it("should revert", async () => { + await expect( + bitcoinDepositor + .connect(thirdParty) + .initializeStake( + tbtcDepositData.fundingTxInfo, + tbtcDepositData.reveal, + tbtcDepositData.staker, + tbtcDepositData.referral, + ), + ).to.be.revertedWith("Deposit already revealed") + }) + }) + + describe("when stake is already cancelled", () => { + beforeAfterSnapshotWrapper() + + before(async () => { + await initializeStake() + + // Simulate deposit request finalization. + await finalizeMinting(tbtcDepositData.depositKey) + + await bitcoinDepositor + .connect(thirdParty) + .queueStake(tbtcDepositData.depositKey) + + await bitcoinDepositor + .connect(staker) + .cancelQueuedStake(tbtcDepositData.depositKey) + }) + + it("should revert", async () => { + await expect( + bitcoinDepositor + .connect(thirdParty) + .initializeStake( + tbtcDepositData.fundingTxInfo, + tbtcDepositData.reveal, + tbtcDepositData.staker, + tbtcDepositData.referral, + ), + ).to.be.revertedWith("Deposit already revealed") + }) + }) + }) + }) + + describe("finalizeBridging", () => { + describe("when stake has not been initialized", () => { + it("should revert", async () => { + await expect( + bitcoinDepositor + .connect(thirdParty) + .exposed_finalizeBridging(tbtcDepositData.depositKey), + ).to.be.revertedWith("Deposit not initialized") + }) + }) + + describe("when stake has been initialized", () => { + beforeAfterSnapshotWrapper() + + before(async () => { + await initializeStake() + }) + + describe("when deposit was not bridged", () => { + it("should revert", async () => { + await expect( + bitcoinDepositor + .connect(thirdParty) + .exposed_finalizeBridging(tbtcDepositData.depositKey), + ).to.be.revertedWith("Deposit not finalized by the bridge") + }) + }) + + describe("when deposit was bridged", () => { + beforeAfterSnapshotWrapper() + + describe("when depositor contract balance is lower than bridged amount", () => { + beforeAfterSnapshotWrapper() + + // The minted value should be less than calculated `bridgedTbtcAmount`. + const mintedAmount = to1ePrecision(7455, 10) // 7455 satoshi + + before(async () => { + // Simulate deposit request finalization. + await finalizeMinting(tbtcDepositData.depositKey, mintedAmount) + }) + + it("should revert", async () => { + await expect( + bitcoinDepositor + .connect(thirdParty) + .exposed_finalizeBridging(tbtcDepositData.depositKey), + ) + .to.be.revertedWithCustomError( + bitcoinDepositor, + "InsufficientTbtcBalance", + ) + .withArgs(bridgedTbtcAmount, mintedAmount) + }) + }) + + describe("when depositor contract balance is higher than bridged amount", () => { + beforeAfterSnapshotWrapper() + + before(async () => { + // Simulate deposit request finalization. + await finalizeMinting(tbtcDepositData.depositKey) + }) + + describe("when bridging finalization has not been called", () => { + describe("when depositor fee divisor is not zero", () => { + beforeAfterSnapshotWrapper() + + let returnedValue: bigint + let tx: ContractTransactionResponse + + before(async () => { + returnedValue = await bitcoinDepositor + .connect(thirdParty) + .exposed_finalizeBridging.staticCall( + tbtcDepositData.depositKey, + ) + + tx = await bitcoinDepositor + .connect(thirdParty) + .exposed_finalizeBridging(tbtcDepositData.depositKey) + }) + + it("should emit BridgingCompleted event", async () => { + await expect(tx) + .to.emit(bitcoinDepositor, "BridgingCompleted") + .withArgs( + tbtcDepositData.depositKey, + thirdParty.address, + tbtcDepositData.referral, + bridgedTbtcAmount, + depositorFee, + ) + }) + + it("should return amount to stake", () => { + expect(returnedValue[0]).to.be.equal(amountToStake) + }) + + it("should return staker", () => { + expect(returnedValue[1]).to.be.equal(tbtcDepositData.staker) + }) + + it("should transfer depositor fee", async () => { + await expect(tx).to.changeTokenBalances( + tbtc, + [treasury], + [depositorFee], + ) + }) + }) + + describe("when depositor fee divisor is zero", () => { + beforeAfterSnapshotWrapper() + + let returnedValue: bigint + let tx: ContractTransactionResponse + + before(async () => { + await bitcoinDepositor + .connect(governance) + .updateDepositorFeeDivisor(0) + + returnedValue = await bitcoinDepositor + .connect(thirdParty) + .exposed_finalizeBridging.staticCall( + tbtcDepositData.depositKey, + ) + + tx = await bitcoinDepositor + .connect(thirdParty) + .exposed_finalizeBridging(tbtcDepositData.depositKey) + }) + + it("should emit BridgingCompleted event", async () => { + await expect(tx) + .to.emit(bitcoinDepositor, "BridgingCompleted") + .withArgs( + tbtcDepositData.depositKey, + thirdParty.address, + tbtcDepositData.referral, + bridgedTbtcAmount, + 0, + ) + }) + + it("should return amount to stake", () => { + expect(returnedValue[0]).to.be.equal(bridgedTbtcAmount) + }) + + it("should return staker", () => { + expect(returnedValue[1]).to.be.equal(tbtcDepositData.staker) + }) + + it("should not transfer depositor fee", async () => { + await expect(tx).to.changeTokenBalances(tbtc, [treasury], [0]) + }) + }) + + describe("when depositor fee exceeds bridged amount", () => { + beforeAfterSnapshotWrapper() + + before(async () => { + await bitcoinDepositor + .connect(governance) + .updateDepositorFeeDivisor(1) + }) + + it("should revert", async () => { + await expect( + bitcoinDepositor + .connect(thirdParty) + .exposed_finalizeBridging(tbtcDepositData.depositKey), + ) + .to.be.revertedWithCustomError( + bitcoinDepositor, + "DepositorFeeExceedsBridgedAmount", + ) + .withArgs(initialDepositAmount, bridgedTbtcAmount) + }) + }) + }) + }) + }) + }) + }) + + describe("finalizeStake", () => { + beforeAfterSnapshotWrapper() + + describe("when stake has not been initialized", () => { + it("should revert", async () => { + await expect( + bitcoinDepositor + .connect(thirdParty) + .finalizeStake(tbtcDepositData.depositKey), + ) + .to.be.revertedWithCustomError( + bitcoinDepositor, + "UnexpectedStakeRequestState", + ) + .withArgs(StakeRequestState.Unknown, StakeRequestState.Initialized) + }) + }) + + describe("when stake has been initialized", () => { + beforeAfterSnapshotWrapper() + + before(async () => { + await initializeStake() + }) + + describe("when deposit was not bridged", () => { + it("should revert", async () => { + await expect( + bitcoinDepositor + .connect(thirdParty) + .finalizeStake(tbtcDepositData.depositKey), + ).to.be.revertedWith("Deposit not finalized by the bridge") + }) + }) + + describe("when deposit was bridged", () => { + beforeAfterSnapshotWrapper() + + before(async () => { + // Simulate deposit request finalization. + await finalizeMinting(tbtcDepositData.depositKey) + }) + + describe("when stake has not been finalized", () => { + beforeAfterSnapshotWrapper() + + const expectedAssetsAmount = amountToStake + const expectedReceivedSharesAmount = amountToStake + + let tx: ContractTransactionResponse + + before(async () => { + tx = await bitcoinDepositor + .connect(thirdParty) + .finalizeStake(tbtcDepositData.depositKey) + }) + + it("should emit BridgingCompleted event", async () => { + await expect(tx) + .to.emit(bitcoinDepositor, "BridgingCompleted") + .withArgs( + tbtcDepositData.depositKey, + thirdParty.address, + tbtcDepositData.referral, + bridgedTbtcAmount, + depositorFee, + ) + }) + + it("should transfer depositor fee", async () => { + await expect(tx).to.changeTokenBalances( + tbtc, + [treasury], + [depositorFee], + ) + }) + + it("should update stake state", async () => { + const stakeRequest = await bitcoinDepositor.stakeRequests( + tbtcDepositData.depositKey, + ) + + expect(stakeRequest.state).to.be.equal(StakeRequestState.Finalized) + }) + + it("should emit StakeRequestFinalized event", async () => { + await expect(tx) + .to.emit(bitcoinDepositor, "StakeRequestFinalized") + .withArgs( + tbtcDepositData.depositKey, + thirdParty.address, + expectedAssetsAmount, + ) + }) + + it("should emit Deposit event", async () => { + await expect(tx) + .to.emit(stbtc, "Deposit") + .withArgs( + await bitcoinDepositor.getAddress(), + tbtcDepositData.staker, + expectedAssetsAmount, + expectedReceivedSharesAmount, + ) + }) + + it("should stake in Acre contract", async () => { + await expect( + tx, + "invalid minted stBTC amount", + ).to.changeTokenBalances( + stbtc, + [tbtcDepositData.staker], + [expectedReceivedSharesAmount], + ) + + await expect( + tx, + "invalid staked tBTC amount", + ).to.changeTokenBalances(tbtc, [stbtc], [expectedAssetsAmount]) + }) + }) + + describe("when stake has been queued", () => { + beforeAfterSnapshotWrapper() + + before(async () => { + await bitcoinDepositor + .connect(thirdParty) + .queueStake(tbtcDepositData.depositKey) + }) + + describe("when stake is still in the queue", () => { + it("should revert", async () => { + await expect( + bitcoinDepositor + .connect(thirdParty) + .finalizeStake(tbtcDepositData.depositKey), + ) + .to.be.revertedWithCustomError( + bitcoinDepositor, + "UnexpectedStakeRequestState", + ) + .withArgs( + StakeRequestState.Queued, + StakeRequestState.Initialized, + ) + }) + }) + + describe("when stake is finalized from the queue", () => { + beforeAfterSnapshotWrapper() + + before(async () => { + await bitcoinDepositor + .connect(thirdParty) + .finalizeQueuedStake(tbtcDepositData.depositKey) + }) + + it("should revert", async () => { + await expect( + bitcoinDepositor + .connect(thirdParty) + .finalizeStake(tbtcDepositData.depositKey), + ) + .to.be.revertedWithCustomError( + bitcoinDepositor, + "UnexpectedStakeRequestState", + ) + .withArgs( + StakeRequestState.FinalizedFromQueue, + StakeRequestState.Initialized, + ) + }) + }) + + describe("when stake has been cancelled", () => { + beforeAfterSnapshotWrapper() + + before(async () => { + await bitcoinDepositor + .connect(staker) + .cancelQueuedStake(tbtcDepositData.depositKey) + }) + + it("should revert", async () => { + await expect( + bitcoinDepositor + .connect(thirdParty) + .finalizeStake(tbtcDepositData.depositKey), + ) + .to.be.revertedWithCustomError( + bitcoinDepositor, + "UnexpectedStakeRequestState", + ) + .withArgs( + StakeRequestState.CancelledFromQueue, + StakeRequestState.Initialized, + ) + }) + }) + }) + + describe("when stake has been finalized", () => { + beforeAfterSnapshotWrapper() + + before(async () => { + // Finalize stake. + await bitcoinDepositor + .connect(thirdParty) + .finalizeStake(tbtcDepositData.depositKey) + }) + + it("should revert", async () => { + await expect( + bitcoinDepositor + .connect(thirdParty) + .finalizeStake(tbtcDepositData.depositKey), + ) + .to.be.revertedWithCustomError( + bitcoinDepositor, + "UnexpectedStakeRequestState", + ) + .withArgs( + StakeRequestState.Finalized, + StakeRequestState.Initialized, + ) + }) + }) + }) + }) + }) + + describe("queueStake", () => { + beforeAfterSnapshotWrapper() + + describe("when stake has not been initialized", () => { + it("should revert", async () => { + await expect( + bitcoinDepositor + .connect(thirdParty) + .queueStake(tbtcDepositData.depositKey), + ) + .to.be.revertedWithCustomError( + bitcoinDepositor, + "UnexpectedStakeRequestState", + ) + .withArgs(StakeRequestState.Unknown, StakeRequestState.Initialized) + }) + }) + + describe("when stake has been initialized", () => { + beforeAfterSnapshotWrapper() + + before(async () => { + await initializeStake() + }) + + describe("when deposit was not bridged", () => { + it("should revert", async () => { + await expect( + bitcoinDepositor + .connect(thirdParty) + .queueStake(tbtcDepositData.depositKey), + ).to.be.revertedWith("Deposit not finalized by the bridge") + }) + }) + + describe("when deposit was bridged", () => { + beforeAfterSnapshotWrapper() + + before(async () => { + // Simulate deposit request finalization. + await finalizeMinting(tbtcDepositData.depositKey) + }) + + describe("when stake has not been finalized", () => { + beforeAfterSnapshotWrapper() + + let tx: ContractTransactionResponse + + before(async () => { + tx = await bitcoinDepositor + .connect(thirdParty) + .queueStake(tbtcDepositData.depositKey) + }) + + it("should emit BridgingCompleted event", async () => { + await expect(tx) + .to.emit(bitcoinDepositor, "BridgingCompleted") + .withArgs( + tbtcDepositData.depositKey, + thirdParty.address, + tbtcDepositData.referral, + bridgedTbtcAmount, + depositorFee, + ) + }) + + it("should transfer depositor fee", async () => { + await expect(tx).to.changeTokenBalances( + tbtc, + [treasury], + [depositorFee], + ) + }) + + it("should update stake state", async () => { + const stakeRequest = await bitcoinDepositor.stakeRequests( + tbtcDepositData.depositKey, + ) + + expect(stakeRequest.state).to.be.equal(StakeRequestState.Queued) + }) + + it("should set staker", async () => { + expect( + (await bitcoinDepositor.stakeRequests(tbtcDepositData.depositKey)) + .staker, + ).to.be.equal(tbtcDepositData.staker) + }) + + it("should set queuedAmount", async () => { + expect( + (await bitcoinDepositor.stakeRequests(tbtcDepositData.depositKey)) + .queuedAmount, + ).to.be.equal(amountToStake) + }) + + it("should not emit StakeRequestQueued event", async () => { + await expect(tx) + .to.emit(bitcoinDepositor, "StakeRequestQueued") + .withArgs( + tbtcDepositData.depositKey, + thirdParty.address, + amountToStake, + ) + }) + + it("should not emit StakeRequestFinalized event", async () => { + await expect(tx).to.not.emit( + bitcoinDepositor, + "StakeRequestFinalized", + ) + }) + + it("should not emit Deposit event", async () => { + await expect(tx).to.not.emit(stbtc, "Deposit") + }) + + it("should not stake in Acre contract", async () => { + await expect( + tx, + "invalid minted stBTC amount", + ).to.changeTokenBalances(stbtc, [tbtcDepositData.staker], [0]) + + await expect( + tx, + "invalid staked tBTC amount", + ).to.changeTokenBalances(tbtc, [stbtc], [0]) + }) + }) + + describe("when stake has been queued", () => { + beforeAfterSnapshotWrapper() + + before(async () => { + await bitcoinDepositor + .connect(thirdParty) + .queueStake(tbtcDepositData.depositKey) + }) + + describe("when stake is still in the queue", () => { + it("should revert", async () => { + await expect( + bitcoinDepositor + .connect(thirdParty) + .queueStake(tbtcDepositData.depositKey), + ) + .to.be.revertedWithCustomError( + bitcoinDepositor, + "UnexpectedStakeRequestState", + ) + .withArgs( + StakeRequestState.Queued, + StakeRequestState.Initialized, + ) + }) + }) + + describe("when stake is finalized from the queue", () => { + beforeAfterSnapshotWrapper() + + before(async () => { + await bitcoinDepositor + .connect(thirdParty) + .finalizeQueuedStake(tbtcDepositData.depositKey) + }) + + it("should revert", async () => { + await expect( + bitcoinDepositor + .connect(thirdParty) + .queueStake(tbtcDepositData.depositKey), + ) + .to.be.revertedWithCustomError( + bitcoinDepositor, + "UnexpectedStakeRequestState", + ) + .withArgs( + StakeRequestState.FinalizedFromQueue, + StakeRequestState.Initialized, + ) + }) + }) + + describe("when stake has been cancelled", () => { + beforeAfterSnapshotWrapper() + + before(async () => { + await bitcoinDepositor + .connect(staker) + .cancelQueuedStake(tbtcDepositData.depositKey) + }) + + it("should revert", async () => { + await expect( + bitcoinDepositor + .connect(thirdParty) + .queueStake(tbtcDepositData.depositKey), + ) + .to.be.revertedWithCustomError( + bitcoinDepositor, + "UnexpectedStakeRequestState", + ) + .withArgs( + StakeRequestState.CancelledFromQueue, + StakeRequestState.Initialized, + ) + }) + }) + }) + + describe("when stake has been finalized", () => { + beforeAfterSnapshotWrapper() + + before(async () => { + // Finalize stake. + await bitcoinDepositor + .connect(thirdParty) + .finalizeStake(tbtcDepositData.depositKey) + }) + + it("should revert", async () => { + await expect( + bitcoinDepositor + .connect(thirdParty) + .queueStake(tbtcDepositData.depositKey), + ) + .to.be.revertedWithCustomError( + bitcoinDepositor, + "UnexpectedStakeRequestState", + ) + .withArgs( + StakeRequestState.Finalized, + StakeRequestState.Initialized, + ) + }) + }) + }) + }) + }) + + describe("finalizeQueuedStake", () => { + describe("when stake has not been initialized", () => { + it("should revert", async () => { + await expect( + bitcoinDepositor + .connect(staker) + .finalizeQueuedStake(tbtcDepositData.depositKey), + ) + .to.be.revertedWithCustomError( + bitcoinDepositor, + "UnexpectedStakeRequestState", + ) + .withArgs(StakeRequestState.Unknown, StakeRequestState.Queued) + }) + }) + + describe("when stake has been initialized", () => { + beforeAfterSnapshotWrapper() + + before(async () => { + await initializeStake() + }) + + describe("when stake has not been queued", () => { + it("should revert", async () => { + await expect( + bitcoinDepositor + .connect(thirdParty) + .finalizeQueuedStake(tbtcDepositData.depositKey), + ) + .to.be.revertedWithCustomError( + bitcoinDepositor, + "UnexpectedStakeRequestState", + ) + .withArgs(StakeRequestState.Initialized, StakeRequestState.Queued) + }) + }) + + describe("when stake has been queued", () => { + beforeAfterSnapshotWrapper() + + before(async () => { + await finalizeMinting(tbtcDepositData.depositKey) + + await bitcoinDepositor + .connect(thirdParty) + .queueStake(tbtcDepositData.depositKey) + }) + + describe("when stake has not been finalized", () => { + beforeAfterSnapshotWrapper() + + const expectedAssetsAmount = amountToStake + const expectedReceivedSharesAmount = amountToStake + + let tx: ContractTransactionResponse + + before(async () => { + tx = await bitcoinDepositor + .connect(thirdParty) + .finalizeQueuedStake(tbtcDepositData.depositKey) + }) + + it("should update stake state", async () => { + const stakeRequest = await bitcoinDepositor.stakeRequests( + tbtcDepositData.depositKey, + ) + + expect(stakeRequest.state).to.be.equal( + StakeRequestState.FinalizedFromQueue, + ) + }) + + it("should set queuedAmount to zero", async () => { + expect( + (await bitcoinDepositor.stakeRequests(tbtcDepositData.depositKey)) + .queuedAmount, + ).to.be.equal(0) + }) + + it("should emit StakeRequestFinalizedFromQueue event", async () => { + await expect(tx) + .to.emit(bitcoinDepositor, "StakeRequestFinalizedFromQueue") + .withArgs( + tbtcDepositData.depositKey, + thirdParty.address, + expectedAssetsAmount, + ) + }) + + it("should emit Deposit event", async () => { + await expect(tx) + .to.emit(stbtc, "Deposit") + .withArgs( + await bitcoinDepositor.getAddress(), + tbtcDepositData.staker, + expectedAssetsAmount, + expectedReceivedSharesAmount, + ) + }) + + it("should stake in Acre contract", async () => { + await expect( + tx, + "invalid minted stBTC amount", + ).to.changeTokenBalances( + stbtc, + [tbtcDepositData.staker], + [expectedReceivedSharesAmount], + ) + + await expect( + tx, + "invalid staked tBTC amount", + ).to.changeTokenBalances(tbtc, [stbtc], [expectedAssetsAmount]) + }) + }) + + describe("when stake has been finalized", () => { + beforeAfterSnapshotWrapper() + + before(async () => { + await bitcoinDepositor + .connect(thirdParty) + .finalizeQueuedStake(tbtcDepositData.depositKey) + }) + + it("should revert", async () => { + await expect( + bitcoinDepositor + .connect(thirdParty) + .finalizeQueuedStake(tbtcDepositData.depositKey), + ) + .to.be.revertedWithCustomError( + bitcoinDepositor, + "UnexpectedStakeRequestState", + ) + .withArgs( + StakeRequestState.FinalizedFromQueue, + StakeRequestState.Queued, + ) + }) + }) + + describe("when stake has been cancelled", () => { + beforeAfterSnapshotWrapper() + + before(async () => { + await bitcoinDepositor + .connect(staker) + .cancelQueuedStake(tbtcDepositData.depositKey) + }) + + it("should revert", async () => { + await expect( + bitcoinDepositor + .connect(thirdParty) + .finalizeQueuedStake(tbtcDepositData.depositKey), + ) + .to.be.revertedWithCustomError( + bitcoinDepositor, + "UnexpectedStakeRequestState", + ) + .withArgs( + StakeRequestState.CancelledFromQueue, + StakeRequestState.Queued, + ) + }) + }) + }) + }) + }) + + describe("cancelQueuedStake", () => { + describe("when stake has not been initialized", () => { + it("should revert", async () => { + await expect( + bitcoinDepositor + .connect(staker) + .cancelQueuedStake(tbtcDepositData.depositKey), + ) + .to.be.revertedWithCustomError( + bitcoinDepositor, + "UnexpectedStakeRequestState", + ) + .withArgs(StakeRequestState.Unknown, StakeRequestState.Queued) + }) + }) + + describe("when stake has been initialized", () => { + beforeAfterSnapshotWrapper() + + before(async () => { + await initializeStake() + }) + + describe("when stake has not been queued", () => { + beforeAfterSnapshotWrapper() + + it("should revert", async () => { + await expect( + bitcoinDepositor + .connect(staker) + .cancelQueuedStake(tbtcDepositData.depositKey), + ) + .to.be.revertedWithCustomError( + bitcoinDepositor, + "UnexpectedStakeRequestState", + ) + .withArgs(StakeRequestState.Initialized, StakeRequestState.Queued) + }) + }) + + describe("when stake has been queued", () => { + beforeAfterSnapshotWrapper() + + before(async () => { + await finalizeMinting(tbtcDepositData.depositKey) + + await bitcoinDepositor + .connect(thirdParty) + .queueStake(tbtcDepositData.depositKey) + }) + + describe("when stake has not been cancelled", () => { + describe("when caller is non-staker", () => { + it("should revert", async () => { + await expect( + bitcoinDepositor + .connect(thirdParty) + .cancelQueuedStake(tbtcDepositData.depositKey), + ).to.be.revertedWithCustomError( + bitcoinDepositor, + "CallerNotStaker", + ) + }) + }) + + describe("when caller is staker", () => { + beforeAfterSnapshotWrapper() + + let tx: ContractTransactionResponse + + before(async () => { + tx = await bitcoinDepositor + .connect(staker) + .cancelQueuedStake(tbtcDepositData.depositKey) + }) + + it("should update stake state", async () => { + const stakeRequest = await bitcoinDepositor.stakeRequests( + tbtcDepositData.depositKey, + ) + + expect(stakeRequest.state).to.be.equal( + StakeRequestState.CancelledFromQueue, + ) + }) + + it("should set queuedAmount to zero", async () => { + expect( + ( + await bitcoinDepositor.stakeRequests( + tbtcDepositData.depositKey, + ) + ).queuedAmount, + ).to.be.equal(0) + }) + + it("should emit StakeRequestCancelledFromQueue event", async () => { + await expect(tx) + .to.emit(bitcoinDepositor, "StakeRequestCancelledFromQueue") + .withArgs( + tbtcDepositData.depositKey, + staker.address, + amountToStake, + ) + }) + + it("should transfer tbtc to staker", async () => { + await expect(tx).to.changeTokenBalances( + tbtc, + [bitcoinDepositor, staker], + [-amountToStake, amountToStake], + ) + }) + }) + }) + + describe("when stake has been finalized", () => { + beforeAfterSnapshotWrapper() + + before(async () => { + await bitcoinDepositor + .connect(thirdParty) + .finalizeQueuedStake(tbtcDepositData.depositKey) + }) + + it("should revert", async () => { + await expect( + bitcoinDepositor + .connect(staker) + .cancelQueuedStake(tbtcDepositData.depositKey), + ) + .to.be.revertedWithCustomError( + bitcoinDepositor, + "UnexpectedStakeRequestState", + ) + .withArgs( + StakeRequestState.FinalizedFromQueue, + StakeRequestState.Queued, + ) + }) + }) + + describe("when stake has been cancelled", () => { + beforeAfterSnapshotWrapper() + + before(async () => { + await bitcoinDepositor + .connect(staker) + .cancelQueuedStake(tbtcDepositData.depositKey) + }) + + it("should revert", async () => { + await expect( + bitcoinDepositor + .connect(staker) + .cancelQueuedStake(tbtcDepositData.depositKey), + ) + .to.be.revertedWithCustomError( + bitcoinDepositor, + "UnexpectedStakeRequestState", + ) + .withArgs( + StakeRequestState.CancelledFromQueue, + StakeRequestState.Queued, + ) + }) + }) + }) + }) + }) + + describe("updateDepositorFeeDivisor", () => { + beforeAfterSnapshotWrapper() + + describe("when caller is not governance", () => { + it("should revert", async () => { + await expect( + bitcoinDepositor.connect(thirdParty).updateDepositorFeeDivisor(1234), + ) + .to.be.revertedWithCustomError( + bitcoinDepositor, + "OwnableUnauthorizedAccount", + ) + .withArgs(thirdParty.address) + }) + }) + + describe("when caller is governance", () => { + const testUpdateDepositorFeeDivisor = (newValue: bigint) => + function () { + beforeAfterSnapshotWrapper() + + let tx: ContractTransactionResponse + + before(async () => { + tx = await bitcoinDepositor + .connect(governance) + .updateDepositorFeeDivisor(newValue) + }) + + it("should emit DepositorFeeDivisorUpdated event", async () => { + await expect(tx) + .to.emit(bitcoinDepositor, "DepositorFeeDivisorUpdated") + .withArgs(newValue) + }) + + it("should update value correctly", async () => { + expect(await bitcoinDepositor.depositorFeeDivisor()).to.be.eq( + newValue, + ) + }) + } + + describe( + "when new value is non-zero", + testUpdateDepositorFeeDivisor(47281n), + ) + + describe("when new value is zero", testUpdateDepositorFeeDivisor(0n)) + + describe( + "when new value is max uint64", + testUpdateDepositorFeeDivisor(18446744073709551615n), + ) + }) + }) + + const extraDataValidTestData = new Map< + string, + { + staker: string + referral: number + extraData: string + } + >([ + [ + "staker has leading zeros", + { + staker: "0x000055d85E80A49B5930C4a77975d44f012D86C1", + referral: 6851, // hex: 0x1ac3 + extraData: + "0x000055d85e80a49b5930c4a77975d44f012d86c11ac300000000000000000000", + }, + ], + [ + "staker has trailing zeros", + { + staker: "0x2d2F8BC7923F7F806Dc9bb2e17F950b42CfE0000", + referral: 6851, // hex: 0x1ac3 + extraData: + "0x2d2f8bc7923f7f806dc9bb2e17f950b42cfe00001ac300000000000000000000", + }, + ], + [ + "referral is zero", + { + staker: "0xeb098d6cDE6A202981316b24B19e64D82721e89E", + referral: 0, + extraData: + "0xeb098d6cde6a202981316b24b19e64d82721e89e000000000000000000000000", + }, + ], + [ + "referral has leading zeros", + { + staker: "0xeb098d6cDE6A202981316b24B19e64D82721e89E", + referral: 31, // hex: 0x001f + extraData: + "0xeb098d6cde6a202981316b24b19e64d82721e89e001f00000000000000000000", + }, + ], + [ + "referral has trailing zeros", + { + staker: "0xeb098d6cDE6A202981316b24B19e64D82721e89E", + referral: 19712, // hex: 0x4d00 + extraData: + "0xeb098d6cde6a202981316b24b19e64d82721e89e4d0000000000000000000000", + }, + ], + [ + "referral is maximum value", + { + staker: "0xeb098d6cDE6A202981316b24B19e64D82721e89E", + referral: 65535, // max uint16 + extraData: + "0xeb098d6cde6a202981316b24b19e64d82721e89effff00000000000000000000", + }, + ], + ]) + + describe("encodeExtraData", () => { + extraDataValidTestData.forEach( + // eslint-disable-next-line @typescript-eslint/no-shadow + ({ staker, referral, extraData: expectedExtraData }, testName) => { + it(testName, async () => { + expect( + await bitcoinDepositor.encodeExtraData(staker, referral), + ).to.be.equal(expectedExtraData) + }) + }, + ) + }) + + describe("decodeExtraData", () => { + extraDataValidTestData.forEach( + ( + { staker: expectedStaker, referral: expectedReferral, extraData }, + testName, + ) => { + it(testName, async () => { + const [actualStaker, actualReferral] = + await bitcoinDepositor.decodeExtraData(extraData) + + expect(actualStaker, "invalid staker").to.be.equal(expectedStaker) + expect(actualReferral, "invalid referral").to.be.equal( + expectedReferral, + ) + }) + }, + ) + + it("with unused bytes filled with data", async () => { + // Extra data uses address (20 bytes) and referral (2 bytes), leaving the + // remaining 10 bytes unused. This test fills the unused bytes with a random + // value. + const extraData = + "0xeb098d6cde6a202981316b24b19e64d82721e89e1ac3105f9919321ea7d75f58" + const expectedStaker = "0xeb098d6cDE6A202981316b24B19e64D82721e89E" + const expectedReferral = 6851 // hex: 0x1ac3 + + const [actualStaker, actualReferral] = + await bitcoinDepositor.decodeExtraData(extraData) + + expect(actualStaker, "invalid staker").to.be.equal(expectedStaker) + expect(actualReferral, "invalid referral").to.be.equal(expectedReferral) + }) + }) + + async function initializeStake() { + await bitcoinDepositor + .connect(thirdParty) + .initializeStake( + tbtcDepositData.fundingTxInfo, + tbtcDepositData.reveal, + tbtcDepositData.staker, + tbtcDepositData.referral, + ) + } + + async function finalizeMinting(depositKey: bigint, amountToMint?: bigint) { + await tbtcVault.createOptimisticMintingRequest(depositKey) + + // Simulate deposit request finalization via optimistic minting. + if (amountToMint) { + await tbtcVault.finalizeOptimisticMintingRequestWithAmount( + depositKey, + amountToMint, + ) + } else { + await tbtcVault.finalizeOptimisticMintingRequest(depositKey) + } + } +}) diff --git a/core/test/Deployment.test.ts b/core/test/Deployment.test.ts index 4c0d9233b..5914e98bb 100644 --- a/core/test/Deployment.test.ts +++ b/core/test/Deployment.test.ts @@ -1,16 +1,18 @@ import { loadFixture } from "@nomicfoundation/hardhat-toolbox/network-helpers" import { expect } from "chai" import { MaxUint256 } from "ethers" +import { helpers } from "hardhat" import type { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers" import { deployment } from "./helpers/context" -import { getNamedSigner } from "./helpers/signer" import type { StBTC as stBTC, Dispatcher, TestERC20 } from "../typechain" +const { getNamedSigners } = helpers.signers + async function fixture() { const { tbtc, stbtc, dispatcher } = await deployment() - const { governance, maintainer, treasury } = await getNamedSigner() + const { governance, maintainer, treasury } = await getNamedSigners() return { stbtc, dispatcher, tbtc, governance, maintainer, treasury } } diff --git a/core/test/Dispatcher.test.ts b/core/test/Dispatcher.test.ts index 8328ffbef..4e1e8a987 100644 --- a/core/test/Dispatcher.test.ts +++ b/core/test/Dispatcher.test.ts @@ -1,4 +1,4 @@ -import { ethers } from "hardhat" +import { ethers, helpers } from "hardhat" import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers" import { expect } from "chai" import { loadFixture } from "@nomicfoundation/hardhat-toolbox/network-helpers" @@ -8,8 +8,6 @@ import { beforeAfterEachSnapshotWrapper, beforeAfterSnapshotWrapper, deployment, - getNamedSigner, - getUnnamedSigner, } from "./helpers" import { @@ -21,10 +19,12 @@ import { import { to1e18 } from "./utils" +const { getNamedSigners, getUnnamedSigners } = helpers.signers + async function fixture() { const { tbtc, stbtc, dispatcher, vault } = await deployment() - const { governance, maintainer } = await getNamedSigner() - const [thirdParty] = await getUnnamedSigner() + const { governance, maintainer } = await getNamedSigners() + const [thirdParty] = await getUnnamedSigners() return { dispatcher, governance, thirdParty, maintainer, vault, tbtc, stbtc } } diff --git a/core/test/data/tbtc.ts b/core/test/data/tbtc.ts new file mode 100644 index 000000000..fd491081f --- /dev/null +++ b/core/test/data/tbtc.ts @@ -0,0 +1,49 @@ +/* eslint-disable import/prefer-default-export */ + +import { ethers } from "hardhat" + +// TODO: Revisit the data once full integration is tested on testnet with valid +// contracts integration. +// Fixture used for revealDepositWithExtraData test scenario. +// source: https://github.com/keep-network/tbtc-v2/blob/103411a595c33895ff6bff8457383a69eca4963c/solidity/test/bridge/Bridge.Deposit.test.ts#L132 +export const tbtcDepositData = { + // Data of a proper P2SH deposit funding transaction embedding some + // extra data. Little-endian hash is: + // 0x6383cd1829260b6034cd12bad36171748e8c3c6a8d57fcb6463c62f96116dfbc. + fundingTxInfo: { + version: "0x01000000", + inputVector: + "0x018348cdeb551134fe1f19d378a8adec9b146671cb67b945b71bf56b20d" + + "c2b952f0100000000ffffffff", + outputVector: + "0x02102700000000000017a9149fe6615a307aa1d7eee668c1227802b2fbc" + + "aa919877ed73b00000000001600147ac2d9378a1c47e589dfb8095ca95ed2" + + "140d2726", + locktime: "0x00000000", + }, + fundingTxHash: + "0x6383cd1829260b6034cd12bad36171748e8c3c6a8d57fcb6463c62f96116dfbc", + // Data matching the redeem script locking the funding output of + // P2SHFundingTx and P2WSHFundingTx. + depositorAddress: "0x934B98637cA318a4D6E7CA6ffd1690b8e77df637", + reveal: { + fundingOutputIndex: 0, + blindingFactor: "0xf9f0c90d00039523", + // HASH160 of 03989d253b17a6a0f41838b84ff0d20e8898f9d7b1a98f2564da4cc29dcf8581d9. + walletPubKeyHash: "0x8db50eb52063ea9d98b3eac91489a90f738986f6", + // HASH160 of 0300d6f28a2f6bf9836f57fcda5d284c9a8f849316119779f0d6090830d97763a9. + refundPubKeyHash: "0x28e081f285138ccbe389c1eb8985716230129f89", + refundLocktime: "0x60bcea61", + vault: "0x594cfd89700040163727828AE20B52099C58F02C", + }, + // 20-bytes of extraData + staker: "0xa9B38eA6435c8941d6eDa6a46b68E3e211719699", + // 2-bytes of extraData + referral: "0x5bd1", + extraData: + "0xa9b38ea6435c8941d6eda6a46b68e3e2117196995bd100000000000000000000", + // Deposit key is keccak256(fundingTxHash | fundingOutputIndex). + depositKey: ethers.getBigInt( + "0x8dde6118338ae2a046eb77a4acceb0521699275f9cc8e9b50057b29d9de1e844", + ), +} diff --git a/core/test/helpers/context.ts b/core/test/helpers/context.ts index 81cece769..239721235 100644 --- a/core/test/helpers/context.ts +++ b/core/test/helpers/context.ts @@ -5,17 +5,35 @@ import type { StBTC as stBTC, Dispatcher, TestERC20, + BridgeStub, TestERC4626, + TBTCVaultStub, + AcreBitcoinDepositorHarness, } from "../../typechain" // eslint-disable-next-line import/prefer-default-export export async function deployment() { await deployments.fixture() - const tbtc: TestERC20 = await getDeployedContract("TBTC") const stbtc: stBTC = await getDeployedContract("stBTC") + const bitcoinDepositor: AcreBitcoinDepositorHarness = + await getDeployedContract("AcreBitcoinDepositor") + + const tbtc: TestERC20 = await getDeployedContract("TBTC") + const tbtcBridge: BridgeStub = await getDeployedContract("Bridge") + const tbtcVault: TBTCVaultStub = await getDeployedContract("TBTCVault") + const dispatcher: Dispatcher = await getDeployedContract("Dispatcher") + const vault: TestERC4626 = await getDeployedContract("Vault") - return { tbtc, stbtc, dispatcher, vault } + return { + tbtc, + stbtc, + bitcoinDepositor, + tbtcBridge, + tbtcVault, + dispatcher, + vault, + } } diff --git a/core/test/helpers/contract.ts b/core/test/helpers/contract.ts index 6ba7b36ae..9233529d4 100644 --- a/core/test/helpers/contract.ts +++ b/core/test/helpers/contract.ts @@ -1,7 +1,8 @@ -import { deployments, ethers } from "hardhat" +import { deployments, ethers, helpers } from "hardhat" import type { BaseContract } from "ethers" -import { getUnnamedSigner } from "./signer" + +const { getUnnamedSigners } = helpers.signers /** * Get instance of a contract from Hardhat Deployments. @@ -15,7 +16,7 @@ export async function getDeployedContract( const { address, abi } = await deployments.get(deploymentName) // Use default unnamed signer from index 0 to initialize the contract runner. - const [defaultSigner] = await getUnnamedSigner() + const [defaultSigner] = await getUnnamedSigners() return new ethers.BaseContract(address, abi, defaultSigner) as T } diff --git a/core/test/helpers/index.ts b/core/test/helpers/index.ts index e4df2196a..40da2ef89 100644 --- a/core/test/helpers/index.ts +++ b/core/test/helpers/index.ts @@ -1,4 +1,3 @@ export * from "./context" export * from "./contract" -export * from "./signer" export * from "./snapshot" diff --git a/core/test/helpers/signer.ts b/core/test/helpers/signer.ts deleted file mode 100644 index 0ae57f35e..000000000 --- a/core/test/helpers/signer.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { ethers, getNamedAccounts, getUnnamedAccounts } from "hardhat" - -import type { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers" - -/** - * Get named Hardhat Ethers Signers. - * @returns Map of named Hardhat Ethers Signers. - */ -export async function getNamedSigner(): Promise<{ - [name: string]: HardhatEthersSigner -}> { - const namedSigners: { [name: string]: HardhatEthersSigner } = {} - - await Promise.all( - Object.entries(await getNamedAccounts()).map(async ([name, address]) => { - namedSigners[name] = await ethers.getSigner(address) - }), - ) - - return namedSigners -} - -/** - * Get unnamed Hardhat Ethers Signers. - * @returns Array of unnamed Hardhat Ethers Signers. - */ -export async function getUnnamedSigner(): Promise { - const accounts = await getUnnamedAccounts() - - return Promise.all(accounts.map(ethers.getSigner)) -} diff --git a/core/test/stBTC.test.ts b/core/test/stBTC.test.ts index 3bde615b6..6b6d33261 100644 --- a/core/test/stBTC.test.ts +++ b/core/test/stBTC.test.ts @@ -4,26 +4,23 @@ import { } from "@nomicfoundation/hardhat-toolbox/network-helpers" import { expect } from "chai" import { ContractTransactionResponse, MaxUint256, ZeroAddress } from "ethers" -import { ethers } from "hardhat" +import { ethers, helpers } from "hardhat" import type { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers" import type { SnapshotRestorer } from "@nomicfoundation/hardhat-toolbox/network-helpers" -import { - beforeAfterSnapshotWrapper, - deployment, - getNamedSigner, - getUnnamedSigner, -} from "./helpers" +import { beforeAfterSnapshotWrapper, deployment } from "./helpers" import { to1e18 } from "./utils" import type { StBTC as stBTC, TestERC20, Dispatcher } from "../typechain" +const { getNamedSigners, getUnnamedSigners } = helpers.signers + async function fixture() { const { tbtc, stbtc, dispatcher } = await deployment() - const { governance, treasury } = await getNamedSigner() + const { governance, treasury } = await getNamedSigners() - const [depositor1, depositor2, thirdParty] = await getUnnamedSigner() + const [depositor1, depositor2, thirdParty] = await getUnnamedSigners() const amountToMint = to1e18(100000) await tbtc.mint(depositor1, amountToMint) diff --git a/core/tsconfig.json b/core/tsconfig.json index f9167f325..0350f4062 100644 --- a/core/tsconfig.json +++ b/core/tsconfig.json @@ -9,5 +9,5 @@ "resolveJsonModule": true }, "files": ["./hardhat.config.ts"], - "include": ["./deploy", "./test", "./typechain", "./helpers"] + "include": ["./deploy", "./test", "./typechain", "./helpers", "./types"] } diff --git a/core/types/index.ts b/core/types/index.ts new file mode 100644 index 000000000..8365a2fc1 --- /dev/null +++ b/core/types/index.ts @@ -0,0 +1,9 @@ +/* eslint-disable import/prefer-default-export */ +export enum StakeRequestState { + Unknown, + Initialized, + Finalized, + Queued, + FinalizedFromQueue, + CancelledFromQueue, +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4f8c58ba1..32bc0c4a0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -25,7 +25,7 @@ importers: version: 3.4.0-solc-0.8 '@keep-network/tbtc-v2': specifier: development - version: 1.6.0-dev.18(@keep-network/keep-core@1.8.1-dev.0) + version: 1.6.0-dev.21(@keep-network/keep-core@1.8.1-dev.0) '@openzeppelin/contracts': specifier: ^5.0.0 version: 5.0.0 @@ -143,7 +143,7 @@ importers: version: 1.3.0(react@18.2.0) '@reduxjs/toolkit': specifier: ^2.2.0 - version: 2.2.0(react-redux@9.1.0)(react@18.2.0) + version: 2.2.1(react-redux@9.1.0)(react@18.2.0) '@sentry/react': specifier: ^7.98.0 version: 7.98.0(react@18.2.0) @@ -179,10 +179,10 @@ importers: version: 9.1.0(@types/react@18.2.38)(react@18.2.0)(redux@5.0.1) react-router-dom: specifier: ^6.22.0 - version: 6.22.0(react-dom@18.2.0)(react@18.2.0) + version: 6.22.1(react-dom@18.2.0)(react@18.2.0) recharts: specifier: ^2.12.0 - version: 2.12.0(react-dom@18.2.0)(react@18.2.0) + version: 2.12.1(react-dom@18.2.0)(react@18.2.0) devDependencies: '@thesis-co/eslint-config': specifier: github:thesis/eslint-config#7b9bc8c @@ -4354,15 +4354,15 @@ packages: - '@keep-network/keep-core' dev: false - /@keep-network/ecdsa@2.1.0-dev.18(@keep-network/keep-core@1.8.1-dev.0): - resolution: {integrity: sha512-VjgQL5wROhUHrVnu2glkLi0x6wj3Q0AW4f843cr/PgMhQoJ6LG6WQoE6OANbg4WbNIx5Tcf9/9FZ2m1k+IYQXQ==} + /@keep-network/ecdsa@2.1.0-dev.19(@keep-network/keep-core@1.8.1-dev.0): + resolution: {integrity: sha512-cyqRqK/sOqyaXZWY/O9ij6EINQuJ+bHLMiuufOFyP5YCj4GCuNqOcCytGAZPT+mED/0J/xn0vm+fgiCBq/uJkQ==} engines: {node: '>= 14.0.0'} dependencies: - '@keep-network/random-beacon': 2.1.0-dev.17(@keep-network/keep-core@1.8.1-dev.0) + '@keep-network/random-beacon': 2.1.0-dev.18(@keep-network/keep-core@1.8.1-dev.0) '@keep-network/sortition-pools': 2.0.0 '@openzeppelin/contracts': 4.9.5 '@openzeppelin/contracts-upgradeable': 4.9.5 - '@threshold-network/solidity-contracts': 1.3.0-dev.11(@keep-network/keep-core@1.8.1-dev.0) + '@threshold-network/solidity-contracts': 1.3.0-dev.12(@keep-network/keep-core@1.8.1-dev.0) transitivePeerDependencies: - '@keep-network/keep-core' dev: false @@ -4500,12 +4500,12 @@ packages: - utf-8-validate dev: false - /@keep-network/tbtc-v2@1.6.0-dev.18(@keep-network/keep-core@1.8.1-dev.0): - resolution: {integrity: sha512-62QBEPAsE3dju5Hk+yqeLhE5ohmVg8ZdFE+x+lmkDPbVz6DrHx0NqF6a1TLC/yH826zhnVCTNpXoPuOv3QUnvA==} + /@keep-network/tbtc-v2@1.6.0-dev.21(@keep-network/keep-core@1.8.1-dev.0): + resolution: {integrity: sha512-/p2PCm0lWEnVbILrBhSO9MWjv6PvJ0lThEzUat85h6SkrGiIojqIMiOlI+pCIi8rhPtZ1xY+9jG6AbDs7XrupQ==} engines: {node: '>= 14.0.0'} dependencies: '@keep-network/bitcoin-spv-sol': 3.4.0-solc-0.8 - '@keep-network/ecdsa': 2.1.0-dev.18(@keep-network/keep-core@1.8.1-dev.0) + '@keep-network/ecdsa': 2.1.0-dev.19(@keep-network/keep-core@1.8.1-dev.0) '@keep-network/random-beacon': 2.1.0-dev.18(@keep-network/keep-core@1.8.1-dev.0) '@keep-network/tbtc': 1.1.2-dev.1 '@openzeppelin/contracts': 4.9.5 @@ -5862,8 +5862,8 @@ packages: resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} dev: false - /@reduxjs/toolkit@2.2.0(react-redux@9.1.0)(react@18.2.0): - resolution: {integrity: sha512-ZvPYKfu4kDnAqPhJ1bsis8QFbiQRz3Q2HxW3tw9tVGusPzYKRG7ju1FA+34PGcwCoemjGGv+f/7fEygcRZIwmA==} + /@reduxjs/toolkit@2.2.1(react-redux@9.1.0)(react@18.2.0): + resolution: {integrity: sha512-8CREoqJovQW/5I4yvvijm/emUiCCmcs4Ev4XPWd4mizSO+dD3g5G6w34QK5AGeNrSH7qM8Fl66j4vuV7dpOdkw==} peerDependencies: react: ^16.9.0 || ^17.0.0 || ^18 react-redux: ^7.2.1 || ^8.1.3 || ^9.0.0 @@ -5881,8 +5881,8 @@ packages: reselect: 5.1.0 dev: false - /@remix-run/router@1.15.0: - resolution: {integrity: sha512-HOil5aFtme37dVQTB6M34G95kPM3MMuqSmIRVCC52eKV+Y/tGSqw9P3rWhlAx6A+mz+MoX+XxsGsNJbaI5qCgQ==} + /@remix-run/router@1.15.1: + resolution: {integrity: sha512-zcU0gM3z+3iqj8UX45AmWY810l3oUmXM7uH4dt5xtzvMhRtYVhKGOmgOd1877dOPPepfCjUv57w+syamWIYe7w==} engines: {node: '>=14.0.0'} dev: false @@ -6426,6 +6426,17 @@ packages: '@thesis/solidity-contracts': github.com/thesis/solidity-contracts/4985bcf dev: false + /@threshold-network/solidity-contracts@1.3.0-dev.12(@keep-network/keep-core@1.8.1-dev.0): + resolution: {integrity: sha512-06EF583uEwko3ik7qjnMOg+sJ+Vb7YWkqag4a9xZq8Mmy8rifpmLjnfDKCVGeKbUis3uI++pTGC9U/EfvVOrlQ==} + peerDependencies: + '@keep-network/keep-core': '>1.8.1-dev <1.8.1-goerli' + dependencies: + '@keep-network/keep-core': 1.8.1-dev.0 + '@openzeppelin/contracts': 4.5.0 + '@openzeppelin/contracts-upgradeable': 4.5.2 + '@thesis/solidity-contracts': github.com/thesis/solidity-contracts/4985bcf + dev: false + /@threshold-network/solidity-contracts@1.3.0-dev.8(@keep-network/keep-core@1.8.1-dev.0): resolution: {integrity: sha512-s6SFZyf1xXgOdMK1zYnjsURnVz7Xxzf0z/34vH+hDg8n/G8L0jPR6Iz4laWSSL2y1P3ffFAFTUMvwfJMJitfVw==} peerDependencies: @@ -7842,16 +7853,6 @@ packages: transitivePeerDependencies: - debug - /axios@1.6.7: - resolution: {integrity: sha512-/hDJGff6/c7u0hDkvkGxR/oy6CbCs8ziCsC7SqmhjfozqiJGc8Z11wrv9z9lYfY4K8l+H9TpjcMDX0xOZmx+RA==} - dependencies: - follow-redirects: 1.15.5 - form-data: 4.0.0 - proxy-from-env: 1.1.0 - transitivePeerDependencies: - - debug - dev: false - /axobject-query@3.2.1: resolution: {integrity: sha512-jsyHu61e6N4Vbz/v18DHwWYKK0bSWLqn47eeDSKPB7m8tqMHF9YJ+mhIk2lVteyZrY8tnSj/jHOv4YiTCuCJgg==} dependencies: @@ -8430,8 +8431,8 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true dependencies: - caniuse-lite: 1.0.30001574 - electron-to-chromium: 1.4.622 + caniuse-lite: 1.0.30001585 + electron-to-chromium: 1.4.659 node-releases: 2.0.14 update-browserslist-db: 1.0.13(browserslist@4.22.2) @@ -8658,8 +8659,8 @@ packages: /caniuse-lite@1.0.30001564: resolution: {integrity: sha512-DqAOf+rhof+6GVx1y+xzbFPeOumfQnhYzVnZD6LAXijR77yPtm9mfOcqOnT3mpnJiZVT+kwLAFnRlZcIz+c6bg==} - /caniuse-lite@1.0.30001574: - resolution: {integrity: sha512-BtYEK4r/iHt/txm81KBudCUcTy7t+s9emrIaHqjYurQ10x71zJ5VQ9x1dYPcz/b+pKSp4y/v1xSI67A+LzpNyg==} + /caniuse-lite@1.0.30001585: + resolution: {integrity: sha512-yr2BWR1yLXQ8fMpdS/4ZZXpseBgE7o4g41x3a6AJOqZuOi+iE/WdJYAuZ6Y95i4Ohd2Y+9MzIWRR+uGABH4s3Q==} /capital-case@1.0.4: resolution: {integrity: sha512-ds37W8CytHgwnhGGTi88pcPyR15qoNkOpYwmMMfnWqqWgESapLqvDx6huFjQ5vqWSn2Z06173XNA7LtMOeUh1A==} @@ -10127,8 +10128,8 @@ packages: /electron-to-chromium@1.4.592: resolution: {integrity: sha512-D3NOkROIlF+d5ixnz7pAf3Lu/AuWpd6AYgI9O67GQXMXTcCP1gJQRotOq35eQy5Sb4hez33XH1YdTtILA7Udww==} - /electron-to-chromium@1.4.622: - resolution: {integrity: sha512-GZ47DEy0Gm2Z8RVG092CkFvX7SdotG57c4YZOe8W8qD4rOmk3plgeNmiLVRHP/Liqj1wRiY3uUUod9vb9hnxZA==} + /electron-to-chromium@1.4.659: + resolution: {integrity: sha512-sRJ3nV3HowrYpBtPF9bASQV7OW49IgZC01Xiq43WfSE3RTCkK0/JidoCmR73Hyc1mN+l/H4Yqx0eNiomvExFZg==} /elliptic@6.3.3: resolution: {integrity: sha512-cIky9SO2H8W2eU1NOLySnhOYJnuEWCq9ZJeHvHd/lXzEL9vyraIMfilZSn57X3aVX+wkfYmqkch2LvmTzkjFpA==} @@ -16760,26 +16761,26 @@ packages: use-sidecar: 1.1.2(@types/react@18.2.38)(react@18.2.0) dev: false - /react-router-dom@6.22.0(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-z2w+M4tH5wlcLmH3BMMOMdrtrJ9T3oJJNsAlBJbwk+8Syxd5WFJ7J5dxMEW0/GEXD1BBis4uXRrNIz3mORr0ag==} + /react-router-dom@6.22.1(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-iwMyyyrbL7zkKY7MRjOVRy+TMnS/OPusaFVxM2P11x9dzSzGmLsebkCvYirGq0DWB9K9hOspHYYtDz33gE5Duw==} engines: {node: '>=14.0.0'} peerDependencies: react: '>=16.8' react-dom: '>=16.8' dependencies: - '@remix-run/router': 1.15.0 + '@remix-run/router': 1.15.1 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - react-router: 6.22.0(react@18.2.0) + react-router: 6.22.1(react@18.2.0) dev: false - /react-router@6.22.0(react@18.2.0): - resolution: {integrity: sha512-q2yemJeg6gw/YixRlRnVx6IRJWZD6fonnfZhN1JIOhV2iJCPeRNSH3V1ISwHf+JWcESzLC3BOLD1T07tmO5dmg==} + /react-router@6.22.1(react@18.2.0): + resolution: {integrity: sha512-0pdoRGwLtemnJqn1K0XHUbnKiX0S4X8CgvVVmHGOWmofESj31msHo/1YiqcJWK7Wxfq2a4uvvtS01KAQyWK/CQ==} engines: {node: '>=14.0.0'} peerDependencies: react: '>=16.8' dependencies: - '@remix-run/router': 1.15.0 + '@remix-run/router': 1.15.1 react: 18.2.0 dev: false @@ -16905,8 +16906,8 @@ packages: decimal.js-light: 2.5.1 dev: false - /recharts@2.12.0(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-rVNcdNQ5b7+40Ue7mcEKZJyEv+3SUk2bDEVvOyXPDXXVE7TU3lrvnJUgAvO36hSzhRP2DnAamKXvHLFIFOU0Ww==} + /recharts@2.12.1(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-35vUCEBPf+pM+iVgSgVTn86faKya5pc4JO6cYJL63qOK2zDEyzDn20Tdj+CDI/3z+VcpKyQ8ZBQ9OiQ+vuAbjg==} engines: {node: '>=14'} peerDependencies: react: ^16.0.0 || ^17.0.0 || ^18.0.0 diff --git a/sdk/src/lib/ethereum/tbtc-depositor.ts b/sdk/src/lib/ethereum/tbtc-depositor.ts index d46ddd310..7ce185d09 100644 --- a/sdk/src/lib/ethereum/tbtc-depositor.ts +++ b/sdk/src/lib/ethereum/tbtc-depositor.ts @@ -1,5 +1,5 @@ import { packRevealDepositParameters } from "@keep-network/tbtc-v2.ts" -import { TbtcDepositor as TbtcDepositorTypechain } from "@acre-btc/core/typechain/contracts/TbtcDepositor" +import { AcreBitcoinDepositor as AcreBitcoinDepositorTypechain } from "@acre-btc/core/typechain/contracts/AcreBitcoinDepositor" import { ZeroAddress, dataSlice, @@ -26,14 +26,16 @@ import { EthereumNetwork } from "./network" import SepoliaTbtcDepositor from "./artifacts/sepolia/TbtcDepositor.json" +// TODO: Rename TBTCDepositor to AcreBitcoinDepositor + /** * Ethereum implementation of the TBTCDepositor. */ class EthereumTBTCDepositor // @ts-expect-error TODO: Figure out why type generated by typechain does not // satisfy the constraint `Contract`. Error: `Property '[internal]' is missing - // in type 'TbtcDepositor' but required in type 'Contract'`. - extends EthersContractWrapper + // in type 'AcreBitcoinDepositor' but required in type 'Contract'`. + extends EthersContractWrapper implements TBTCDepositor { constructor(config: EthersContractConfig, network: EthereumNetwork) { @@ -86,7 +88,7 @@ class EthereumTBTCDepositor const { staker, referral } = this.decodeExtraData(extraData) - const tx = await this.instance.initializeStakeRequest( + const tx = await this.instance.initializeStake( fundingTx, reveal, `0x${staker.identifierHex}`, diff --git a/sdk/test/lib/ethereum/tbtc-depositor.test.ts b/sdk/test/lib/ethereum/tbtc-depositor.test.ts index 2edca897f..a631bbefe 100644 --- a/sdk/test/lib/ethereum/tbtc-depositor.test.ts +++ b/sdk/test/lib/ethereum/tbtc-depositor.test.ts @@ -21,7 +21,7 @@ describe("TBTCDepositor", () => { const mockedContractInstance = { tbtcVault: jest.fn().mockImplementation(() => vaultAddress.identifierHex), - initializeStakeRequest: jest.fn(), + initializeStake: jest.fn(), } let depositor: EthereumTBTCDepositor let depositorAddress: EthereumAddress @@ -103,7 +103,7 @@ describe("TBTCDepositor", () => { let result: Hex beforeAll(async () => { - mockedContractInstance.initializeStakeRequest.mockReturnValue({ + mockedContractInstance.initializeStake.mockReturnValue({ hash: mockedTx.toPrefixedString(), }) @@ -154,9 +154,7 @@ describe("TBTCDepositor", () => { vault: `0x${vaultAddress.identifierHex}`, } - expect( - mockedContractInstance.initializeStakeRequest, - ).toHaveBeenCalledWith( + expect(mockedContractInstance.initializeStake).toHaveBeenCalledWith( btcTxInfo, revealInfo, `0x${staker.identifierHex}`,