Skip to content

Commit

Permalink
Track pending stake requests balance
Browse files Browse the repository at this point in the history
Acre contract has a max deposit limit as a cap for new deposits.
tBTC Depositor contract is a contract that performs stakes
asynchronously, requiring tBTC Bridge to mint tBTC for deposited
Bitcoin.
We cannot rely solely on the `Acre.maxDeposit` function for the
maximum amount of stake users can make, as the limit may be
reached while initiating the stake request and finalizing tBTC minting.

In the tBTC Depositor contract, we define a `maxStake` function
that will consider the `Acre.maxDeposit` and the balance of pending
stakes requested by the tBTC Depositor contract. The dApp should
use this function to check the maximum allowed stake limit.

Deposit details revealed in the stake request may be invalid,
and a Bitcoin funding transaction was not made. In such a case,
we don't want the amount for this request to be included in the
pending stakes balance forever. We introduce a timeout, after
which the requested amount will be deducted from the balance of
the pending stake.

When the stake request is finalized, it will be released from
the pending stakes balance, as it will be deposited and included
in the `Acre.maxDeposit` amount.

When the stake request is recalled and tBTC is withdrawn by the
staker, the pending stakes balance will also be reduced.
  • Loading branch information
nkuba committed Jan 22, 2024
1 parent 4d5cdbb commit 82f3fd8
Showing 1 changed file with 141 additions and 1 deletion.
142 changes: 141 additions & 1 deletion core/contracts/tbtc/TbtcDepositor.sol
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ contract TbtcDepositor is Ownable {
// tBTC token amount to stake after deducting tBTC minting fees and the
// Depositor fee.
uint256 amountToStake;
// Marks stake request that has already been deducted from the pending
// stakes balance.
bool excludedFromPendingStakesBalanceTracker;
}

/// @notice tBTC Bridge contract.
Expand Down Expand Up @@ -82,6 +85,21 @@ contract TbtcDepositor is Ownable {
/// `1/50 = 0.02 = 2%`.
uint64 public depositorFeeDivisor;

/// @notice Balance of stake requests that were initiated. The balance is
/// used to track asynchronous stake requests, before they are
/// finalized and included in the Acre contract max deposit cap.
/// @dev Value in the tBTC token precision (1e18).
/// A stake request amount is added to the pending stakes balance when
/// a stake request is initiated. It will be deducted once the request
/// is finalized. It may happen that a stake request is initialized with
/// invalid funding transaction details. In such case the balance is
/// reduced when this stake request is notified to be timed out.
uint256 public pendingStakesBalance;

// TODO: Confirm the value to include time needed for optimistic minting or sweeping
/// @notice A timeout duration to wait for the tBTC Bridge to initiate minting.
uint32 public tbtcBridgingInitializationTimeout = 3 days;

/// @notice Emitted when a stake request is initialized.
/// @dev Deposit details can be fetched from {{ Bridge.DepositRevealed }}
/// event emitted in the same transaction.
Expand All @@ -96,6 +114,10 @@ contract TbtcDepositor is Ownable {
uint16 referral
);

/// @notice Emitted when bridging timeout has been notified.
/// @param depositKey Deposit identifier.
event BridgingTimedOut(uint256 depositKey);

/// @notice Emitted when bridging completion has been notified.
/// @param depositKey Deposit identifier.
/// @param caller Address that notified about bridging completion.
Expand Down Expand Up @@ -131,6 +153,18 @@ contract TbtcDepositor is Ownable {
/// @dev Attempted to finalize a stake request that has not been initialized.
error StakeRequestNotInitialized();

/// @dev Attempted to notify bridging timeout, while bridging has not timed
/// out yet.
error StakeRequestNotTimedOut(uint256 timeoutTimestamp);

/// @dev Attempted to notify bridging timeout, while optimistic minting
/// was initiated.
error OptimisticMintingInProgress();

/// @dev Attempted to notify bridging timeout, while the deposit was already
/// swept.
error DepositAlreadySwept();

/// @dev Attempted to notify about completed bridging while the notification
/// was already submitted.
error BridgingAlreadyCompleted();
Expand Down Expand Up @@ -258,6 +292,62 @@ contract TbtcDepositor is Ownable {
// tBTC Vault supported by this contract.
if (bridgeDepositRequest.vault != address(tbtcVault))
revert UnexpectedTbtcVault(bridgeDepositRequest.vault);

// Increase pending stakes balance by the funds that stake request was
// initiated for.
pendingStakesBalance +=
bridgeDepositRequest.amount *
SATOSHI_MULTIPLIER;
}

/// @notice This function should be called for previously initialized stake
/// request, when tBTC Bridge cannot confirm validity of the funding
/// Bitcoin transaction, meaning the revealed deposit request is
/// most likely invalid.
/// @dev When a deposit is revealed to the tBTC Bridge contract, it is expected
/// that funding Bitcoin transaction is confirmed and optimistic minting
/// to be initiated. If optimistic minting is not initiated there is
/// a second path where tBTC nodes sweep the deposit to get tBTC minted.
/// If none of this happens within a bridging timeout duration it
/// means that most likely the revealed deposit was invalid and bridging
/// won't be completed.
/// @param depositKey Deposit key computed as
/// `keccak256(fundingTxHash | fundingOutputIndex)`.
function notifyBridgingTimeout(uint256 depositKey) external {
StakeRequest storage request = stakeRequests[depositKey];

if (request.requestedAt == 0) revert StakeRequestNotInitialized();
if (request.amountToStake > 0) revert BridgingAlreadyCompleted();

// Check if timeout has passed.
uint64 expectedTimeoutAt = request.requestedAt +
tbtcBridgingInitializationTimeout;
// solhint-disable-next-line not-rely-on-time
if (expectedTimeoutAt > block.timestamp)
revert StakeRequestNotTimedOut(expectedTimeoutAt);

// Check if Optimistic Minting is in progress. If optimistic minting of
// the deposit was requested it's very likely the deposit request will be
// completed soon, as it is already being processed by tBTC Minters
// and Guardians.
ITBTCVault.OptimisticMintingRequest
memory optimisticMintingRequest = tbtcVault
.optimisticMintingRequests(depositKey);

if (optimisticMintingRequest.requestedAt > 0)
revert OptimisticMintingInProgress();

// Check if the Deposit was swept.
IBridge.DepositRequest memory bridgeDepositRequest = bridge.deposits(
depositKey
);
if (bridgeDepositRequest.sweptAt > 0) revert DepositAlreadySwept();

emit BridgingTimedOut(depositKey);

// Release the pending bridging amount, as it is unlikely the bridging
// will be completed.
reducePendingStakesBalance(depositKey);
}

/// @notice This function should be called for previously initialized stake
Expand Down Expand Up @@ -389,6 +479,10 @@ contract TbtcDepositor is Ownable {
// solhint-disable-next-line not-rely-on-time
request.finalizedAt = uint64(block.timestamp);

// Free pending stakes amount as the funds are being deposited to the Acre
// contract and will be included in Acre.maxDeposit limit.
reducePendingStakesBalance(depositKey);

// Get deposit details from tBTC Bridge.
IBridge.DepositRequest memory bridgeDepositRequest = bridge.deposits(
depositKey
Expand Down Expand Up @@ -424,6 +518,10 @@ contract TbtcDepositor is Ownable {
// solhint-disable-next-line not-rely-on-time
request.finalizedAt = uint64(block.timestamp);

// Free pending stakes amount as the funds are being withdraw from the
// Depositor contract and stake request will be marked as finalized.
reducePendingStakesBalance(depositKey);

// Get deposit details from tBTC Bridge and Vault contracts.
IBridge.DepositRequest memory bridgeDepositRequest = bridge.deposits(
depositKey
Expand Down Expand Up @@ -453,7 +551,21 @@ contract TbtcDepositor is Ownable {
emit DepositorFeeDivisorUpdated(newDepositorFeeDivisor);
}

// TODO: Handle minimum deposit amount in tBTC Bridge vs Acre.
/// @notice Maximum stake amount
/// @dev The value is returned in tBTC token precision (1e18).
/// This function should be called before Bitcoin transaction funding
/// is made.
/// @param receiver The address to which the stBTC shares will be minted.
/// @return Maximum allowed stake amount.
function maxStake(address receiver) external view returns (uint256) {
uint256 acreMaxDeposit = acre.maxDeposit(receiver);

if (acreMaxDeposit == type(uint256).max) {
return type(uint256).max;
}

return acreMaxDeposit - pendingStakesBalance;
}

/// @notice Calculates deposit key the same way as the Bridge contract.
/// @dev The deposit key is computed as
Expand Down Expand Up @@ -499,4 +611,32 @@ contract TbtcDepositor is Ownable {
// Next 2 bytes of extra data is referral info.
referral = uint16(bytes2(extraData << (8 * 20)));
}

/// @notice Deduct stake request's amount from the pending stakes balance
/// tracker.
/// @dev The function is intended to be called when stake request is finalized
/// or recalled, but also when the bridging request is timed out.
/// It makes sure it is called just once for a given stake request,
/// to avoid reducing the balance multiple times for the same stake
/// request. It may happen that bridging times out due to tBTC Bridge
/// related issues and timeout is notified to the stake request in this
/// contract. But when the deposit in tBTC Bridge is later minted
/// the finalize stake function may be called, so we don't want the balance
/// to be reduced twice.
/// @param depositKey Deposit key computed as
/// `keccak256(fundingTxHash | fundingOutputIndex)`.
function reducePendingStakesBalance(uint256 depositKey) internal {
StakeRequest storage request = stakeRequests[depositKey];

if (!request.excludedFromPendingStakesBalanceTracker) {
request.excludedFromPendingStakesBalanceTracker = true;

IBridge.DepositRequest memory bridgeDepositRequest = bridge
.deposits(depositKey);

pendingStakesBalance -=
bridgeDepositRequest.amount *
SATOSHI_MULTIPLIER;
}
}
}

0 comments on commit 82f3fd8

Please sign in to comment.