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

Initial implementation of Bitcoin depositor (tBTC Depositor) #91

Merged
merged 139 commits into from
Mar 6, 2024

Conversation

nkuba
Copy link
Member

@nkuba nkuba commented Dec 22, 2023

The Acre Bitcoin Depositor contract is an implementation of a depositor contract mentioned in RFC-11.

This contract serves as an integrator of tBTC Bridge and Acre stBTC staking contract.

Bitcoin deposits revealed via the tBTC Depositor contract will be automatically staked in Acre after the tBTC minting process is completed on the tBTC network side.

The contract is based on the AbstractTBTCDepositor (keep-network/tbtc-v2#778).

The staking process consists of two steps:

  • initializeStake - this step reveals a deposit to tBTC Bridge to start the minting process
  • finalizeStake - this step should be called after tBTC minting on the tBTC Bridge side is completed, and tBTC token is transferred to the tBTC Depositor contract, this step calculates the approximate amount of minted tBTC tokens, and stakes them in stBTC contract.

The functions are unprotected, meaning anyone can call them (e.g. bots). This solution will be used to enable a gasless minting experience for the users, where only a funding Bitcoin transaction will be required from them.

tBTC Minting Fees

The complexity comes with the actual minted tBTC amount calculation as the tBTC network fees are not unambiguously predictable.
The stake finalization step calculates the amount to stake in Acre by deducting approximate tBTC network minting fees from the initial funding transaction amount.

The calculation is performed in AbstractTBTCDepositor._calculateTbtcAmount function.

The amount to stake is calculated d:
amount = depositAmount - depositTreasuryFee - optimisticMintingFee - depositTxMaxFee

These calculations are approximate and can leave some imbalance in the Depositor contract, as:

  • depositTreasuryFee - this is a precise value, snapshotted at the moment of deposit reveal,
  • optimisticMintingFee - this is an optimistic minting fee calculated at the moment of
    completion notification, there is a very low possibility that the fee was updated in the tBTC Vault contract between tBTC was minted and completion notification was submitted,
  • depositTxMaxFee - this is the maximum transaction fee that can be deducted on Bitcoin transaction sweeping, in most cases it will be higher than the actual deducted amount, and will grow the reserve in the depositor contract.

For the great majority of the deposits, such an algorithm will return a tbtcAmount slightly lesser than the actual amount of TBTC minted for the deposit. This will cause some TBTC to be left in the contract and ensure there is enough liquidity to finalize the deposit. However, in some rare cases, where the actual values of those fees change between the deposit minting and finalization, the tbtcAmount returned by this function may be greater than the actual amount of TBTC minted for the deposit.
If this happens and the reserve coming from previous deposits leftovers does not provide enough liquidity, the deposit will have to wait for finalization until the reserve is refilled by subsequent deposits or a manual top-up.
The Acre governance is responsible for handling such cases.

Fee changes simulation

Please see the simulation performed by @lukasz-zimnoch for fee changes analysis:

Fee changes simulation

Case 1 - Deposit is optimistically minted (currently 98.6% of mainnet deposits)

Let's say a deposit is 100 BTC, treasury_fee = 5%, opt_minting_fee = 3% and max_tx_fee = 0.01 BTC. The actually minted amount will be (100 BTC * 0.95) * 0.97 = 92.15 TBTC and this will be in direct control of the depositor contract. The actual tx_fee remains as debt after the deposit is swept and we know it is lower than max_tx_fee = 0.01 BTC

The depositor contract does the finalization AFTER the deposit is optimistically minted. If it uses the tbtc_net_amount = btc_gross_amount - treasury_fee - opt_minting_fee - max_tx_fee equation, it will compute tbtc_net_amount = 100 BTC - 5 BTC - 2.85 BTC - 0.01 BTC = 92.14 TBTC. Note that 92.15 - 92.14 = 0.01 TBTC is in reserve to cover the tx_fee debt which is more than enough.

Now, consider fee changes. Fee changes matters only if they occur between deposit mint and deposit finalization. Let's suppose the depositor contract received the aforementioned 92.15 TBTC. This is the outcome if particular fees change between deposit mint and finalization:

  • If treasury_fee changes, it doesn't matter as we used the real snapshotted value

  • If opt_minting_fee increases, let's say to 5%, the tbtc_net_amount = 100 BTC - 5 BTC - 4.75 BTC - 0.01 BTC = 90.24 TBTC. That means 92.15 - 90.24 = 1.91 TBTC stays in reserve

  • If opt_minting_fee decreases, let's say to 1%, the tbtc_net_amount = 100 BTC - 5 BTC - 0.95 BTC - 0.01 BTC = 94.04 TBTC. The loss is 92.15 - 94.04 = -1.89 TBTC. If there is a reserve, we take from there. If not, we can either pay the full balance 92.15 or wait for the gov or next deposits to fill the gap (new deposits won't have this error)

  • If max_tx_fee increases to 0.05 BTC, the tbtc_net_amount = 100 BTC - 5 BTC - 2.85 BTC - 0.05 BTC = 92.10 BTC. That means 92.15 - 90.10 = 0.05 TBTC stays in reserve. We know that actual tx_fee accrued as debt will be lower so we are covered. The corner case here is max_tx_fee increases AFTER deposit finalization. This can make the real debt slightly uncovered but only if the actual tx_fee exceeds the old value of max_tx_fee

  • If max_tx_fee decreases to 0.002 BTC, the tbtc_net_amount = 100 BTC - 5 BTC - 2.85 BTC - 0.002 BTC = 92.148 BTC. That means 92.15 - 92.148 = 0.002 TBTC stays in reserve. We know that actual tx_fee accrued as debt will be lower so we are covered. The corner case here is max_tx_fee decreases AFTER deposit finalization. However, this is not a problem as the reserve will still be greater than the debt (we used old max_tx_fee to cut the reserve while the debt is below the new max_tx_fee value)


As you can see, almost all cases cause the positive imbalance and reserve increase. There are only two cases that cause negative imbalance:

  • opt_minting_fee decreases between deposit minting and finalization
  • max_tx_fee increases after deposit finalization

The first case requires a very unfortunate timing. It becomes a real problem only when the reserve is not enough to cover the loss. We can decrease the probability by keeping the delay between minting and finalization as short as possible.

The second case is similar. In practice, max_tx_fee never changed and the only reason to do so would be a global change of fee levels on Bitcoin. Moreover, the max_tx_fee is just a cap and the actual fee is always lower as tBTC clients estimate it according to the network circumstances. Last but not least, this becomes a real problem only in case a deposit is not optimistically minted but swept instead (currently 1.4% of mainnet deposits went this way) so part of their minted amount is used to repaid the accrued debt. It also requires that the reserve is not enough to cover the loss.

Case 2 - Deposit is not optimistically minted but swept (currently 1.4% of mainnet deposits)

Let's say a deposit is 100 BTC, treasury_fee = 5%, opt_minting_fee = 3% and max_tx_fee = 0.01 BTC but the actual tx_fee = 0.005 BTC. The actually minted amount will be 100 BTC * 0.95 - 0.005 BTC = 94.995 TBTC (opt_minting_fee is not accrued here) and this will be in direct control of the depositor contract.

The depositor contract does the finalization AFTER the deposit is swept. If it uses the tbtc_net_amount = btc_gross_amount - treasury_fee - opt_minting_fee - max_tx_fee equation, it will compute tbtc_net_amount = 100 BTC - 5 BTC - 2.85 BTC - 0.01 BTC = 92.14 TBTC. Note that 94.995 - 92.14 = 2.855 TBTC stays in reserve.

Now, consider fee changes. Fee changes matter only if they occur between deposit sweep and deposit finalization. Let's suppose the depositor contract received the aforementioned 94.995 TBTC. This is the outcome if particular fees change between deposit sweep and finalization:

  • If treasury_fee changes, it doesn't matter as we used the real snapshotted value

  • If opt_minting_fee increases, let's say to 5%, the tbtc_net_amount = 100 BTC - 5 BTC - 4.75 BTC - 0.01 BTC = 90.24 TBTC. That means 94.995 - 90.24 = 4.755 TBTC stays in reserve

  • If opt_minting_fee decreases, let's say to 1%, the tbtc_net_amount = 100 BTC - 5 BTC - 0.95 BTC - 0.01 BTC = 94.04 TBTC. That means 94.995 - 94.04 = 0.955 TBTC stays in reserve

  • If max_tx_fee increases to 0.05 BTC, the tbtc_net_amount = 100 BTC - 5 BTC - 2.85 BTC - 0.05 BTC = 92.10 BTC. That means 94.995 - 92.10 = 2.895 TBTC stays in reserve.

  • If max_tx_fee decreases to 0.002 BTC, the tbtc_net_amount = 100 BTC - 5 BTC - 2.85 BTC - 0.002 BTC = 92.148 BTC. That means 94.995 - 92.148 = 2.847 TBTC stays in reserve.

As you can see, fee changes in this case do not cause a negative imbalance at all. The only risk is that this deposit is partially used to repay previous debts. However, we are balancing this unlikely case by always deducting the opt_minting_fee fee which commits to the reserve. The risk that the reserve won't be enough is low here.

Maximum Stake Limit

The Acre contract has a limit for the maximum amount of deposits it can accept. Due to an asynchronous manner of staking where Bitcoin has to be first bridged to tBTC via tBTC Bridge, the limit for deposits may be reached in the meantime of stake request initialization and finalization. In such cases, the stake request has to wait until more deposits are accepted by the Acre contract (the maximum limit is increased, or funds are withdrawn from the Acre contract, making space for new deposits).

In this PR we added a path where stake requests that are unable to be finalized will be added to a queue and finalized later. If the user is not willing to wait anymore for the stake request to be finalized, they can recall the tBTC stake request and withdraw to their wallet the liquid tBTC that got minted.

This solution should be improved further to not allow the user to reduce the possibility of such a situation happening. Different possibilities are being explored as part of #191.


Refs: #60
Depends on: keep-network/tbtc-v2#760

We will integrate with tBTC Bridge and TBTCVault contracts. Here we
define interfaces with the functions from external contracts that we
will use.
The interfaces are based on the changes proposed in
keep-network/tbtc-v2#760.
The tBTC Depositor contract is an implementation of a depositor contract
mentioned in
[RFC-11](https://github.com/keep-network/tbtc-v2/blob/main/docs/rfc/rfc-11.adoc).

This contract serves as an integrator of tBTC Bridge and Acre staking
contract.

Bitcoin deposits revealed via tBTC Depositor contract will be
automatically staked in Acre after tBTC minting process is completed on
the tBTC network side.

The process consists of two steps:
- initializeStake - this step reveals a deposit to tBTC Bridge to start
  minting process
- finalizeStake - this step should be called after tBTC minting was
  completed to stake the tBTC tokens in Acre

The functions are unprotected, it means that anyone can call them (e.g.
bots). This solution will be used to enable gasless minting experience
to the users, where only funding Bitcoin transaction will be required
from them.

The complexity comes with the actual minted tBTC amount calculation, as
the fees are not unambiguously predictable.
The stake finalization step calculates the amount to stake in Acre contract by deducting
tBTC network minting fees from the initial funding transaction amount.
The amount to stake is calculated depending on the process the tBTC
was minted in:
- for swept deposits:
  `amount = depositAmount - depositTreasuryFee - depositTxMaxFee`
- for optimistically minted deposits:
  ```
  amount = depositAmount - depositTreasuryFee - depositTxMaxFee
           - optimisticMintingFee
  ```
These calculation are simplified and can leave some positive
imbalance in the Depositor contract.
- depositTxMaxFee - this is a maximum transaction fee that can be deducted
  on Bitcoin transaction sweeping,
- optimisticMintingFee - this is a optimistic minting fee snapshotted
  at the moment of the deposit reveal, there is a chance that the fee
  parameter is updated in the tBTC Vault contract before the optimistic
  minting is finalized.
The imbalance should be donated to the Acre staking contract by the
Governance.
The dependency is used in TbtcDepositor contract to calculate
bitcoin transaction hash.
uint16 should be enough to track partners as the max value is 65535.
@nkuba nkuba self-assigned this Dec 22, 2023
core/contracts/tbtc/TbtcDepositor.sol Outdated Show resolved Hide resolved
core/contracts/tbtc/TbtcDepositor.sol Outdated Show resolved Hide resolved
core/contracts/external/tbtc/IBridge.sol Outdated Show resolved Hide resolved
core/contracts/external/tbtc/IBridge.sol Outdated Show resolved Hide resolved
core/contracts/tbtc/TbtcDepositor.sol Outdated Show resolved Hide resolved
core/contracts/tbtc/TbtcDepositor.sol Outdated Show resolved Hide resolved
@dimpar
Copy link
Member

dimpar commented Jan 3, 2024

I reviewed the contracts high level. I'm not very familiar with tBTC integration things, so leaving the deeper review for Łukasz / Piotr.

nkuba added 7 commits January 4, 2024 10:59
The contract is Ownable with governance as the owner.
Define a fee for tBTC Depositor contract that will be taken before
staking tBTC in Acre contract.
The fee should cover costs of running maintainer bots that will execute
initializeStake and finalizeStake methods.

The fee is calculated based on the divisor.
Initial value for the fee is set to zero.
The treasury address will be used as a destination to transfer fees.
The value for minimum deposit amount in Acre contract should consider
the amount that will be a result of TbtcDepositor stake.
It means that it is dependend on tBTC minimum deposit
(`depositDustThreshold`) and all the fees taken by tBTC minting process
and TbtcDepositor fee.
The function estimates fees that will be taken for a deposit of a given
amount by:
- tBTC protocol minting
- tBTC Depositor contract
- Acre contract's deposit function
r-czajkowski added a commit that referenced this pull request Feb 29, 2024
To match the contract API from #91.
nkuba added a commit that referenced this pull request Mar 1, 2024
Depends on: #91

In this PR we adjust SDK and dApp to reflect changes introduced in
#91.

The contract was renamed from TbtcDepositor to AcreBitcoinDepositor.
Copy link

netlify bot commented Mar 4, 2024

Deploy Preview for acre-dapp-testnet canceled.

Name Link
🔨 Latest commit e4d3062
🔍 Latest deploy log https://app.netlify.com/sites/acre-dapp-testnet/deploys/65e8494617b3eb0008dfe680

core/contracts/AcreBitcoinDepositor.sol Outdated Show resolved Hide resolved
core/contracts/AcreBitcoinDepositor.sol Outdated Show resolved Hide resolved

await deployments.deploy("AcreBitcoinDepositor", {
contract:
process.env.HARDHAT_TEST === "true"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup, I know what you're trying to achieve. My proposition was having two deployment files: 03_deploy_acre_bitcoin_depositor.ts and 03_test_deploy_acre_bitcoin_depositor.ts. In the first one we add skip if the network is hardhat. For the latter script we can add skip in case the network is other than hardhat. This way the logic from deployment script and package.json will go away. Anyway, this is just another option, non blocker and up to you.

core/contracts/AcreBitcoinDepositor.sol Show resolved Hide resolved
core/contracts/AcreBitcoinDepositor.sol Outdated Show resolved Hide resolved
core/contracts/AcreBitcoinDepositor.sol Show resolved Hide resolved
core/contracts/AcreBitcoinDepositor.sol Show resolved Hide resolved
core/contracts/AcreBitcoinDepositor.sol Outdated Show resolved Hide resolved
core/contracts/test/AcreBitcoinDepositor.t.sol Outdated Show resolved Hide resolved
core/deploy/03_deploy_acre_bitcoin_depositor.ts Outdated Show resolved Hide resolved
nkuba added 4 commits March 6, 2024 10:07
To reduce confusion we put all the test contracts related to tBTC
bridging into one file.
@thesis thesis deleted a comment from cloudflare-workers-and-pages bot Mar 6, 2024
@nkuba nkuba requested a review from dimpar March 6, 2024 09:36
nkuba added 3 commits March 6, 2024 10:58
The contract defines `exposed_finalizeBridging` which is a naming
pattern recommended by Foundry. We need to disable the rule for the
file.
@dimpar dimpar merged commit 3ba69e2 into main Mar 6, 2024
20 checks passed
@dimpar dimpar deleted the tbtc-depositor branch March 6, 2024 11:25
dimpar added a commit that referenced this pull request Mar 6, 2024
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)
r-czajkowski added a commit that referenced this pull request Mar 11, 2024
Depends on: #91

In this PR we adjust SDK and dApp to reflect changes introduced in
#91.

The contract was renamed from TbtcDepositor to AcreBitcoinDepositor.

We also add artifacts of the contract deployed to Sepolia.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
🔗 Solidity Solidity contracts
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants