Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Staking limits in Bitcoin Depositor contract #253

Merged
merged 19 commits into from
Mar 6, 2024
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
163 changes: 163 additions & 0 deletions core/contracts/AcreBitcoinDepositor.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ 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 {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import {Math} from "@openzeppelin/contracts/utils/math/Math.sol";

import "@keep-network/tbtc-v2/contracts/integrator/AbstractTBTCDepositor.sol";

Expand Down Expand Up @@ -70,6 +71,34 @@ contract AcreBitcoinDepositor is AbstractTBTCDepositor, Ownable2Step {
/// @dev The key is a deposit key identifying the deposit.
mapping(uint256 => StakeRequest) public stakeRequests;

/// @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 @@ -149,6 +178,21 @@ contract AcreBitcoinDepositor is AbstractTBTCDepositor, Ownable2Step {
uint256 amountToStake
);

/// @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 @@ -169,6 +213,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 notify a bridging completion, while it was already
/// notified.
error BridgingCompletionAlreadyNotified();
Expand Down Expand Up @@ -197,6 +245,13 @@ contract AcreBitcoinDepositor is AbstractTBTCDepositor, Ownable2Step {
/// @dev Attempted to recover tBTC token.
error RecoverTbtcNotAllowed();

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

/// @notice Acre Bitcoin Depositor contract constructor.
/// @param bridge tBTC Bridge contract instance.
/// @param tbtcVault tBTC Vault contract instance.
Expand All @@ -221,6 +276,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 @@ -264,6 +323,12 @@ contract AcreBitcoinDepositor is AbstractTBTCDepositor, Ownable2Step {
StakeRequestState.Initialized
);

uint256 depositAmount = bridge.deposits(depositKey).amount *
SATOSHI_MULTIPLIER;

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

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

Expand Down Expand Up @@ -315,6 +380,9 @@ contract AcreBitcoinDepositor is AbstractTBTCDepositor, Ownable2Step {

(request.queuedAmount, request.staker) = finalizeBridging(depositKey);

// Increase pending stakes balance.
queuedStakesBalance += request.queuedAmount;

emit StakeRequestQueued(depositKey, msg.sender, request.queuedAmount);
}

Expand All @@ -335,6 +403,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 @@ -375,9 +446,101 @@ contract AcreBitcoinDepositor is AbstractTBTCDepositor, Ownable2Step {

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

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

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

/// @notice Minimum stake amount in satoshi 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 minStakeInSatoshi() external view returns (uint256) {
return minStakeAmount / SATOSHI_MULTIPLIER;
}

/// @notice Maximum stake amount in satoshi 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 maxStakeInSatoshi() external view returns (uint256) {
uint256 currentTotalAssets = stbtc.totalAssets();

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

uint256 availableLimit = maxTotalAssetsSoftLimit - currentTotalAssets;

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

uint256 result = Math.min(availableLimit, maxSingleStakeAmount);

return result / SATOSHI_MULTIPLIER;
}

/// @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 {
// Read tBTC Bridge Deposit Dust Threshold in satoshi precision.
(uint64 bridgeDepositDustThresholdSat, , , ) = bridge
.depositParameters();

// Convert tBTC Bridge Deposit Dust Threshold to tBTC token precision.
uint256 bridgeDepositDustThreshold = uint256(
bridgeDepositDustThresholdSat
) * SATOSHI_MULTIPLIER;

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

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.
/// @param newDepositorFeeDivisor New depositor fee divisor value.
function updateDepositorFeeDivisor(
Expand Down
4 changes: 4 additions & 0 deletions core/contracts/test/AcreBitcoinDepositor.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,8 @@ contract AcreBitcoinDepositorHarness is AcreBitcoinDepositor {
) external returns (uint256 amountToStake, address staker) {
return finalizeBridging(depositKey);
}

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