Skip to content

Commit

Permalink
Staking limits in Bitcoin Depositor contract (#253)
Browse files Browse the repository at this point in the history
This PR enhances #91.
Depends on keep-network/tbtc-v2#787
Depends on keep-network/tbtc-v2#791

### Introduction

In this PR we introduce a mechanism for the dApp to throttle stake
request initialization.

The staking flow for Bitcoin is asynchronous, consisting of a couple of
stages between the user entering the staking form in the dApp and the
tBTC being deposited in the stBTC vault.

Since the stBTC vault introduced a limit for maximum total assets under
management, there is a risk that the user will initialize staking that
won't be able to finalize in stBTC, due to concurrent other users'
stakes. We need to reduce such risk by throttling stake initialization
flow in the dApp.

### Soft Cap and Hard Cap Limits

#### Hard Cap

stBTC contract defines a _maximum total assets limit_, that cannot be
exceeded with new tBTC deposits. This is considered a hard limit, that
when reached no new deposits can be made, and stake requests in the
Bitcoin Depositor contract will be queued. These queued requests will
have to wait until the limit is raised or other users withdraw their
funds, making room for new users.

#### Soft Cap

Bitcoin Depositor Contract defines a _maximum total assets soft limit_,
which is assumed to be much lower than the _hard cap_. The limit is used
to throttle stakes and use a difference between _hard cap_ and _soft
cap_ as a buffer for possible async stakes coming from the dApp.

### Stake Amount Limits

#### Minimum Stake Limit

The Bitcoin Depositor contract defines a _minimum stake limit_, that is
used to define a minimum amount of a stake. The limit has to be higher
than the _deposit dust threshold_ defined in the tBTC Bridge, which is
validated on the deposit reveal.
The _minimum stake limit_ has to take into account the _minimum deposit
limit_ defined in the stBTC contract, and consider that the amount of
tBTC deposited in the stBTC vault on stake finalization will be reduced
by tBTC Bridge fees and Depositor fee.

#### Maximum Stake Limit

The Bitcoin Depositor contract defines a _maximum stake limit_, which is
the maximum amount of a single stake request. This limit is used to
introduce granularity to the stake amount and reduce the possibility of
a big stake request using the whole soft limit shared across other
concurrent stake requests.

### Usage in dApp

The limits should be validated in the dApp staking flow to reduce the
possibility of user stakes being stuck in the queue.

The contract exposes two functions to be used in the dApp
`minStakeInSatoshi` and `maxStakeInSatoshi`, for convenience the result
is returned in the satoshi precision.

### Flow


![image](https://github.com/thesis/acre/assets/10741774/46f699d1-3607-4e27-b07a-de18b6078fbd)
[source:
Figjam](https://www.figma.com/file/5S8Wa5WudTQGbKMk7ETKq8/Bitcoin-Depositor-Staking-Limits?type=whiteboard&node-id=1-519&t=aCatffRBQpe4BCMn-4)
  • Loading branch information
dimpar authored Mar 6, 2024
2 parents 3ba69e2 + ff3af34 commit c93969e
Show file tree
Hide file tree
Showing 4 changed files with 575 additions and 11 deletions.
169 changes: 162 additions & 7 deletions core/contracts/AcreBitcoinDepositor.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ 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 {Math} from "@openzeppelin/contracts/utils/math/Math.sol";
import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol";

import "@keep-network/tbtc-v2/contracts/integrator/AbstractTBTCDepositor.sol";
Expand Down Expand Up @@ -75,6 +76,34 @@ contract AcreBitcoinDepositor is AbstractTBTCDepositor, Ownable2Step {
// slither-disable-next-line immutable-states
stBTC public stbtc;

/// @notice Minimum amount of a single stake request (in tBTC token precision).
/// @dev This parameter should be set to a value exceeding the minimum deposit
/// amount supported by tBTC Bridge.
uint256 public minStakeAmount;

/// @notice Maximum amount of a single stake request (in tBTC token precision).
/// @dev The staking flow in the dApp is asynchronous and there is a short period
/// of time between a deposit funding transaction is made on Bitcoin chain
/// and revealed to this contract. This limit is used to gain better control
/// on the stakes queue, and reduce a risk of concurrent stake requests
/// made in the dApp being blocked by another big deposit.
uint256 public maxSingleStakeAmount;

/// @notice Maximum total assets soft limit (in tBTC token precision).
/// @dev stBTC contract defines a maximum total assets limit held by the protocol
/// that new deposits cannot exceed (hard cap). Due to the asynchronous
/// manner of Bitcoin deposits process we introduce a soft limit (soft cap)
/// set to a value lower than the hard cap to let the dApp initialize
/// Bitcoin deposits only up to the soft cap limit.
uint256 public maxTotalAssetsSoftLimit;

/// @notice Total balance of pending stake requests (in tBTC token precision).
/// @dev stBTC contract introduces limits for total deposits amount. Due to
/// asynchronous manner of the staking flow, this contract needs to track
/// balance of pending stake requests to ensure new stake request are
/// not initialized if they won't be able to finalize.
uint256 public queuedStakesBalance;

/// @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:
Expand Down Expand Up @@ -154,6 +183,21 @@ contract AcreBitcoinDepositor is AbstractTBTCDepositor, Ownable2Step {
uint256 amountCancelled
);

/// @notice Emitted when a minimum single stake amount is updated.
/// @param minStakeAmount New value of the minimum single stake
/// amount (in tBTC token precision).
event MinStakeAmountUpdated(uint256 minStakeAmount);

/// @notice Emitted when a maximum single stake amount is updated.
/// @param maxSingleStakeAmount New value of the maximum single stake
/// amount (in tBTC token precision).
event MaxSingleStakeAmountUpdated(uint256 maxSingleStakeAmount);

/// @notice Emitted when a maximum total assets soft limit is updated.
/// @param maxTotalAssetsSoftLimit New value of the maximum total assets
/// soft limit (in tBTC token precision).
event MaxTotalAssetsSoftLimitUpdated(uint256 maxTotalAssetsSoftLimit);

/// @notice Emitted when a depositor fee divisor is updated.
/// @param depositorFeeDivisor New value of the depositor fee divisor.
event DepositorFeeDivisorUpdated(uint64 depositorFeeDivisor);
Expand All @@ -174,6 +218,10 @@ contract AcreBitcoinDepositor is AbstractTBTCDepositor, Ownable2Step {
StakeRequestState expectedState
);

/// @dev Attempted to initialize a stake request with a deposit amount
/// exceeding the maximum limit for a single stake amount.
error ExceededMaxSingleStake(uint256 amount, uint256 max);

/// @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
Expand Down Expand Up @@ -208,6 +256,13 @@ contract AcreBitcoinDepositor is AbstractTBTCDepositor, Ownable2Step {
/// @dev Attempted to call function by an account that is not the staker.
error CallerNotStaker();

/// @dev Attempted to set minimum stake amount to a value lower than the
/// tBTC Bridge deposit dust threshold.
error MinStakeAmountLowerThanBridgeMinDeposit(
uint256 minStakeAmount,
uint256 bridgeMinDepositAmount
);

/// @notice Acre Bitcoin Depositor contract constructor.
/// @param bridge tBTC Bridge contract instance.
/// @param tbtcVault tBTC Vault contract instance.
Expand All @@ -232,6 +287,10 @@ contract AcreBitcoinDepositor is AbstractTBTCDepositor, Ownable2Step {
tbtcToken = IERC20(_tbtcToken);
stbtc = stBTC(_stbtc);

// TODO: Revisit initial values before mainnet deployment.
minStakeAmount = 0.015 * 1e18; // 0.015 BTC
maxSingleStakeAmount = 0.5 * 1e18; // 0.5 BTC
maxTotalAssetsSoftLimit = 7 * 1e18; // 7 BTC
depositorFeeDivisor = 1000; // 1/1000 == 10bps == 0.1% == 0.001
}

Expand Down Expand Up @@ -263,7 +322,7 @@ contract AcreBitcoinDepositor is AbstractTBTCDepositor, Ownable2Step {
// 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(
(uint256 depositKey, uint256 initialDepositAmount) = _initializeDeposit(
fundingTx,
reveal,
encodeExtraData(staker, referral)
Expand All @@ -275,6 +334,12 @@ contract AcreBitcoinDepositor is AbstractTBTCDepositor, Ownable2Step {
StakeRequestState.Initialized
);

if (initialDepositAmount > maxSingleStakeAmount)
revert ExceededMaxSingleStake(
initialDepositAmount,
maxSingleStakeAmount
);

emit StakeRequestInitialized(depositKey, msg.sender, staker);
}

Expand Down Expand Up @@ -329,7 +394,10 @@ contract AcreBitcoinDepositor is AbstractTBTCDepositor, Ownable2Step {

request.queuedAmount = SafeCast.toUint88(amountToQueue);

emit StakeRequestQueued(depositKey, msg.sender, request.queuedAmount);
// Increase pending stakes balance.
queuedStakesBalance += amountToQueue;

emit StakeRequestQueued(depositKey, msg.sender, amountToQueue);
}

/// @notice This function should be called for previously queued stake
Expand All @@ -349,6 +417,9 @@ contract AcreBitcoinDepositor is AbstractTBTCDepositor, Ownable2Step {
uint256 amountToStake = request.queuedAmount;
delete (request.queuedAmount);

// Decrease pending stakes balance.
queuedStakesBalance -= amountToStake;

emit StakeRequestFinalizedFromQueue(
depositKey,
msg.sender,
Expand Down Expand Up @@ -379,17 +450,64 @@ contract AcreBitcoinDepositor is AbstractTBTCDepositor, Ownable2Step {

StakeRequest storage request = stakeRequests[depositKey];

if (request.queuedAmount == 0) revert StakeRequestNotQueued();
uint256 amount = request.queuedAmount;
if (amount == 0) revert StakeRequestNotQueued();

address staker = request.staker;
// Check if caller is the staker.
if (msg.sender != request.staker) revert CallerNotStaker();
if (msg.sender != staker) revert CallerNotStaker();

uint256 amount = request.queuedAmount;
delete (request.queuedAmount);

emit StakeRequestCancelledFromQueue(depositKey, request.staker, amount);
emit StakeRequestCancelledFromQueue(depositKey, staker, amount);

// Decrease pending stakes balance.
queuedStakesBalance -= amount;

tbtcToken.safeTransfer(request.staker, amount);
tbtcToken.safeTransfer(staker, amount);
}

/// @notice Updates the minimum stake amount.
/// @dev It requires that the new value is greater or equal to the tBTC Bridge
/// deposit dust threshold, to ensure deposit will be able to be bridged.
/// @param newMinStakeAmount New minimum stake amount (in tBTC precision).
function updateMinStakeAmount(
uint256 newMinStakeAmount
) external onlyOwner {
uint256 minBridgeDepositAmount = _minDepositAmount();

// Check if new value is at least equal the tBTC Bridge Deposit Dust Threshold.
if (newMinStakeAmount < minBridgeDepositAmount)
revert MinStakeAmountLowerThanBridgeMinDeposit(
newMinStakeAmount,
minBridgeDepositAmount
);

minStakeAmount = newMinStakeAmount;

emit MinStakeAmountUpdated(newMinStakeAmount);
}

/// @notice Updates the maximum single stake amount.
/// @param newMaxSingleStakeAmount New maximum single stake amount (in tBTC
/// precision).
function updateMaxSingleStakeAmount(
uint256 newMaxSingleStakeAmount
) external onlyOwner {
maxSingleStakeAmount = newMaxSingleStakeAmount;

emit MaxSingleStakeAmountUpdated(newMaxSingleStakeAmount);
}

/// @notice Updates the maximum total assets soft limit.
/// @param newMaxTotalAssetsSoftLimit New maximum total assets soft limit
/// (in tBTC precision).
function updateMaxTotalAssetsSoftLimit(
uint256 newMaxTotalAssetsSoftLimit
) external onlyOwner {
maxTotalAssetsSoftLimit = newMaxTotalAssetsSoftLimit;

emit MaxTotalAssetsSoftLimitUpdated(newMaxTotalAssetsSoftLimit);
}

/// @notice Updates the depositor fee divisor.
Expand All @@ -403,6 +521,43 @@ contract AcreBitcoinDepositor is AbstractTBTCDepositor, Ownable2Step {
emit DepositorFeeDivisorUpdated(newDepositorFeeDivisor);
}

/// @notice Minimum stake amount (in tBTC token precision).
/// @dev This function should be used by dApp to check the minimum amount
/// for the stake request.
/// @dev It is not enforced in the `initializeStakeRequest` function, as
/// it is intended to be used in the dApp staking form.
function minStake() external view returns (uint256) {
return minStakeAmount;
}

/// @notice Maximum stake amount (in tBTC token precision).
/// @dev It takes into consideration the maximum total assets soft limit (soft
/// cap), that is expected to be set below the stBTC maximum total assets
/// limit (hard cap).
/// @dev This function should be called before Bitcoin transaction funding
/// is made. The `initializeStakeRequest` function is not enforcing this
/// limit, not to block the reveal deposit operation of the concurrent
/// deposits made in the dApp in the short window between limit check,
/// submission of Bitcoin funding transaction and stake request
/// initialization.
/// @return Maximum allowed stake amount.
function maxStake() external view returns (uint256) {
uint256 currentTotalAssets = stbtc.totalAssets();

if (currentTotalAssets >= maxTotalAssetsSoftLimit) {
return 0;
}

uint256 availableLimit = maxTotalAssetsSoftLimit - currentTotalAssets;

if (queuedStakesBalance >= availableLimit) {
return 0;
}
availableLimit -= queuedStakesBalance;

return Math.min(availableLimit, maxSingleStakeAmount);
}

// TODO: Handle minimum deposit amount in tBTC Bridge vs stBTC.

/// @notice Encodes staker address and referral as extra data.
Expand Down
4 changes: 4 additions & 0 deletions core/contracts/test/AcreBitcoinDepositorHarness.sol
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ contract AcreBitcoinDepositorHarness is AcreBitcoinDepositor {
) external returns (uint256 amountToStake, address staker) {
return finalizeBridging(depositKey);
}

function exposed_setQueuedStakesBalance(uint256 amount) external {
queuedStakesBalance = amount;
}
}

/// @dev A test contract to stub tBTC Bridge contract.
Expand Down
Loading

0 comments on commit c93969e

Please sign in to comment.