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

Expose revealDepositWithExtraData in the Bridge contract #760

Merged
merged 12 commits into from
Jan 9, 2024

Conversation

lukasz-zimnoch
Copy link
Member

@lukasz-zimnoch lukasz-zimnoch commented Dec 15, 2023

Refs: #749

Here we expose the revealDepositWithExtraData function in the Bridge contract. This function allows revealing a deposit whose funding transaction embeds arbitrary 32-byte extra data.

The new function opens a variety of use cases. An EOA depositor can craft a Bitcoin deposit transaction that allows a third-party smart contract to manage the deposit and provide additional services on top. In this case, 32-byte extra data can be used to securely attribute the deposit to the original EOA depositor.

The revealDepositWithExtraData function

The revealDepositWithExtraData was added next to the existing revealDeposit function to provide backward compatibility of the Bridge contract API. The Deposit library that does the heavy lifting was refactored to handle both regular deposits without extra data and the new deposits with extra data. The common logic was extracted to the _revealDeposit internal function to avoid duplication.

Worth noting, the extra data passed in the new flow has to be externalized so tBTC wallets can reconstruct the deposit script and sweep those deposits, just as the regular ones. To do so, we decided to store the extra data in the Bridge storage (as part of the DepositRequest structure). This approach is not ideal but has some advantages:

  • Negligible gas overhead for regular deposits without extra data (~2000 of gas)
  • Easier integration as extra data can be easily fetched from storage by tBTC wallets and third-party smart contracts
  • Making the Bridge aware of the extra data may open some use cases in the future

Alternative approaches rely on emitting the extra data using contract events. However:

  • Extending the existing DepositReveal event is not backward compatible. The event signature would change and clients trying to fetch past events that don't contain the extra data field would fail. This is especially problematic for tBTC wallets, SPV maintainers, and block explorers
  • Using a new event just for emitting the extra data is clunky and makes the integration experience significantly worse

Changes in the WalletProposalValidator contract

The WalletProposalValidator contract is used by tBTC off-chain wallet clients to validate incoming deposit sweep proposals. This contract needs to reconstruct deposit scripts so must be aware of the new alternative format with 32-byte extra data. As part of this pull request, we are adjusting this contract to properly validate deposit sweep proposals that include deposits with extra data.

Impact on gas costs and contract size

Before the presented change, the gas costs of functions and size of affected contract/libraries was as follows:

·------------------------------|---------------------------|--------------|-----------------------------·
|     Solc version: 0.8.17     ·  Optimizer enabled: true  ·  Runs: 1000  ·  Block limit: 30000000 gas  │
·······························|···························|··············|······························
|  Methods                                                                                              │
·············|·················|·············|·············|··············|···············|··············
|  Contract  ·  Method         ·  Min        ·  Max        ·  Avg         ·  # calls      ·  eur (avg)  │
·············|·················|·············|·············|··············|···············|··············
|  Bridge    ·  revealDeposit  ·  82767      ·  105463     ·  102057      ·  170          ·  -          │
·············|·················|·············|·············|··············|···············|··············
·-------------------------------|-------------|---------------·
|  Contract Name                ·  Size (KB)  ·  Change (KB)  │
································|·············|················
|  Bridge                       ·     21.264  ·               │
································|·············|················
|  Deposit                      ·      6.327  ·               │
································|·············|················
|  WalletProposalValidator      ·     11.266  ·               │
································|·············|················

After introducing revealDepositWithExtraData, those are as follows:

·------------------------------·············|---------------------------|--------------|-----------------------------·
|           Solc version: 0.8.17            ·  Optimizer enabled: true  ·  Runs: 1000  ·  Block limit: 30000000 gas  │
············································|···························|··············|······························
|  Methods                                                                                                           │
·············|······························|·············|·············|··············|···············|··············
|  Contract  ·  Method                      ·  Min        ·  Max        ·  Avg         ·  # calls      ·  eur (avg)  │
·············|······························|·············|·············|··············|···············|··············
|  Bridge    ·  revealDeposit               ·  85270      ·  107966     ·  104562      ·  170          ·  -          │
·············|······························|·············|·············|··············|···············|··············
|  Bridge    ·  revealDepositWithExtraData  ·  124176     ·  126773     ·  125872      ·  12           ·  -          │
·············|······························|·············|·············|··············|···············|··············
·-------------------------------|-------------|---------------·
|  Contract Name                ·  Size (KB)  ·  Change (KB)  │
································|·············|················
|  Bridge                       ·     21.563  ·               │
································|·············|················
|  Deposit                      ·      6.894  ·               │
································|·············|················
|  WalletProposalValidator      ·     11.520  ·               │
································|·············|················

Next steps

This change is the most important one towards full support of deposits with extra data. However, comprehensive support requires some additional steps:

  • Add support in the tBTC off-chain client
  • Add support in the tBTC SDK

Here we expose the `revealDepositWithExtraData` function that
allows revealing deposits with embedded 32-byte extra data.
Copy link

Solidity API documentation preview available in the artifacts of the https://github.com/keep-network/tbtc-v2/actions/runs/7248203327 check.

…error

The number of local variables used in the `_revealDeposit` functions
exceeds the allowed limit and causes the `stack too deep` error during
compilation. To mitigate it, we are extracting `_emitDepositRevealedEvent`
whose responsibility is to emit the `DepositRevealed` event.
Copy link

Solidity API documentation preview available in the artifacts of the https://github.com/keep-network/tbtc-v2/actions/runs/7248310783 check.

It turns out that emitting `extraData` as part of the existing `DepositRevealed`
event is problematic as it is not compatible backward. Adding a new field to
the event changes its signature (it is computed as keccak256 of the event name
and argument types) so off-chain clients will not be able to fetch past
`DepositRevealed` events (without the `extraData` field) using the new
signature of the `DepositRevealed` (with added `extraData` field) event.
Copy link

Solidity API documentation preview available in the artifacts of the https://github.com/keep-network/tbtc-v2/actions/runs/7251523421 check.

Copy link

Solidity API documentation preview available in the artifacts of the https://github.com/keep-network/tbtc-v2/actions/runs/7251539839 check.

Copy link

Solidity API documentation preview available in the artifacts of the https://github.com/keep-network/tbtc-v2/actions/runs/7252467306 check.

…tra data

A wallet that is about to sweep some deposits calls the
`WalletProposalValidator.validateDepositSweepProposal` to ensure the
received sweep proposal is valid. This function must be aware of deposits
with extra data as it checks deposit scripts. Here, we are ensuring that.
Copy link

Solidity API documentation preview available in the artifacts of the https://github.com/keep-network/tbtc-v2/actions/runs/7262389788 check.

Copy link

Solidity API documentation preview available in the artifacts of the https://github.com/keep-network/tbtc-v2/actions/runs/7264077087 check.

Here we provide unit tests covering the new `revealDepositWithExtraData`
function. Those tests are heavily based on the existing `revealDeposit`
tests. Moreover, we are improving unit tests for `revealDeposit` to cover
new cases that emerge due to the addition of `revealDepositWithExtraData`.
Copy link

Solidity API documentation preview available in the artifacts of the https://github.com/keep-network/tbtc-v2/actions/runs/7278535619 check.

Copy link

Solidity API documentation preview available in the artifacts of the https://github.com/keep-network/tbtc-v2/actions/runs/7286985596 check.

The `WalletProposalValidator` contract uses a concept of `deposit extra info`
which is an additional data required to perform validation of a deposit
sweep proposal. However, most of the comments refer that as `deposit extra data`
which is problematic in the light of the recently introduced 32-byte
deposit extra data which is a different thing.

To fix that, we are improving the applicable comments and refer to `deposit
extra info` explicitly.
Copy link

Solidity API documentation preview available in the artifacts of the https://github.com/keep-network/tbtc-v2/actions/runs/7287106539 check.

Copy link

Solidity API documentation preview available in the artifacts of the https://github.com/keep-network/tbtc-v2/actions/runs/7287350385 check.

@lukasz-zimnoch lukasz-zimnoch marked this pull request as ready for review December 21, 2023 12:34
Copy link

Solidity API documentation preview available in the artifacts of the https://github.com/keep-network/tbtc-v2/actions/runs/7288333818 check.

Copy link
Member

@pdyraga pdyraga left a comment

Choose a reason for hiding this comment

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

I focused on the Solidity code, I was not reviewing tests.

/// @param extraData 32-byte deposit extra data.
/// @dev Requirements:
/// - All requirements from `revealDeposit` function must be met,
/// - `extraData` must not be bytes32(0),
Copy link
Member

Choose a reason for hiding this comment

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

Why? My first thought was that it is to protect users but at this point, the Bitcoin transaction with 0x0 extra data is probably already in the Bitcoin mempool. I am not saying this condition is wrong, just curious about the reasons.

Copy link
Member Author

@lukasz-zimnoch lukasz-zimnoch Dec 22, 2023

Choose a reason for hiding this comment

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

My reasoning was: I don't see a reason to use revealDepositWithExtraData with empty extra data. Allowing to do so is a corner case. Such corner cases are often vectors of attack. This brought me to the security vs flexibility dilemma. In this case, I choose security. If there is a strong need to allow this case, we can always lift it in the future. Moreover, this requirement nicely separates revealDepositWithExtraData from revealDeposit.

Also, if someone does such a Bitcoin transaction with 0x0 extra data on purpose, there is always the refund option.

/// If any of these requirements is not met, the wallet _must_ refuse
/// to sweep the deposit and the depositor has to wait until the
/// deposit script unlocks to receive their BTC back.
function revealDepositWithExtraData(
Copy link
Member

Choose a reason for hiding this comment

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

I am wondering if this is not a good opportunity to allow anyone to reveal the deposit. In revealDeposit, the requirements would be just as they are now because the vault address is not a part of the P2(W)SH script. In revealDepositWithExtraData, we use another version of the script so we could expect the vault address to be inside the script.

We could also lift the requirements of not having extra data empty (see my previous comment) and potentially allow to use revealDepositWithExtraData for tBTC vault. In this model, revealDeposit is the cheaper version that has come constraints allowing to make it cheaper. For anyone who needs more flexibility at the expense of paying more for gas, revealDepositWithExtraData can be used.

I think we had this discussion before but I do not remember why we did not decide on this option. Having a separate depositor contract may not be needed in all cases and I am not even sure if it is needed for Solana/Base, cases.

Copy link
Member Author

Choose a reason for hiding this comment

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

It's not only about the vault. Most importantly, the depositor embedded in the script is the receiver of minted TBTC (or Bank's balance). If something has to happen with minted TBTC automatically, the depositor must be aware of the deposit. In most cases, the depositor will also need to record some information upon deposit reveal (e.g. current fee values) to properly do the automation on minted TBTC (e.g. compute the net amount of the deposit). Requiring that only the depositor can reveal guarantees that the deposit always passes through the depositor before reaching the Bridge.

Let's use Base as an example: While making a deposit, we are setting the BaseDepositor contract as depositor and the Base EOA as the original depositor in extra data. If we allow anyone to reveal, a third party can just reveal that deposit to the Bridge and bypass the BaseDepositor contract. The BaseDepositor will receive minted TBTC but will not be aware of the deposit from the very beginning. Although it's probably possible to deliver that information later, this can be very tricky in some cases and complicates the relayer bot. Moreover, BaseDepositor hasn't had a chance to capture the Bridge state upon deposit reveal so doing the right thing with minted TBTC can be hard. This example also shows why we need the depositor contract for Base: it's not only about revealing the deposit but also about moving back minted TBTC back to Base.

Last but not least, I'd say that not having the vault in the script is more flexible and safe. What if the vault is set as untrusted due to a critical vulnerability? If the vault is embedded in the script, the deposit can be revealed there at any time by anyone, far before the refund. The vault cannot be changed in that case. If we don't embed, the deposit can always be revealed with vault address(0).

That said, I see more problems than advantages with letting anyone reveal the deposit. The revealDepositWithExtraData covers the flexibility gap that was caused by this requirement in the original revealDeposit and still provides strong security guarantees.

bytes32 extraData
) external {
// Strong requirement in order to differentiate from the regular
// reveal flow and reduce potential attack surface.
Copy link
Member

Choose a reason for hiding this comment

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

Is there any particular attack surface? If the vault doesn't use extra data as in the example of tBTC, this is just about mapping the revealed deposit to the Bitcoin script to unlock UTXO.

Copy link
Member Author

Choose a reason for hiding this comment

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

Please see: #760 (comment)

Copy link

Solidity API documentation preview available in the artifacts of the https://github.com/keep-network/tbtc-v2/actions/runs/7298334361 check.

nkuba added a commit to thesis/acre that referenced this pull request Dec 22, 2023
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.
@nkuba nkuba enabled auto-merge January 9, 2024 10:59
Copy link

github-actions bot commented Jan 9, 2024

Solidity API documentation preview available in the artifacts of the https://github.com/keep-network/tbtc-v2/actions/runs/7460183079 check.

@nkuba nkuba merged commit 44d9fc8 into main Jan 9, 2024
38 checks passed
@nkuba nkuba deleted the deposit-extra-data branch January 9, 2024 11:04
nkuba added a commit that referenced this pull request Jan 19, 2024
Refs: #749

Pull request #760 introduced
a possibility to embed 32-byte extra data within the deposit script.
This simple change opens multiple possibilities. Notably, third-party
smart contracts can now act as depositor proxies and reveal deposits on
depositors' behalf. This way, proxy contracts receive minted TBTC and
can provide extra services without requiring additional actions from the
depositor (e.g., deposit it to a yield protocol or bridge it to an L2
chain). This, in turn, empowers third-party protocols to use tBTC as a
foundation and propose additional value on top of it. The goal of this
pull request is to facilitate the integration of such third-party
protocols through tBTC SDK.

### The `DepositorProxy` interface

First of all, we are introducing the `DepositorProxy` interface that
represents a third-party depositor proxy contract in a chain-agnostic
way. A third-party integrator willing to relay deposits is expected to
provide an implementation of this interface and inject it into the tBTC
SDK.

The SDK uses the instance of the `DepositorProxy` to prepare the right
deposit script (thus deposit BTC address) and notifies that instance
once the deposit is funded and ready for minting. How minting is
initialized depends on the proxy implementation thus this logic is
abstracted as the `revealDeposit` function exposed by the
`DepositorProxy` interface. This way, the SDK is responsible for the
heavy lifting around deposit construction while the depositor proxy must
only finalize the process by doing their own logic and, reveal the
deposit to the `Bridge` contract.

To facilitate the job for Ethereum-based proxies, we are also exposing
the `EthereumDepositorProxy` abstract class. This component can serve as
a base for classes interacting with Ethereum depositor proxy contracts
that relay deposit data to the Ethereum `Bridge`. The
`EthereumDepositorProxy` aims to make that part easier.

### The `initiateDepositWithProxy` function

To provide a single point of entrance to the depositor proxy flow, we
are exposing the `initiateDepositWithProxy` function. This function is
available from the top-level SDK interface, alongside the regular
`initiateDeposit` function triggering the non-proxy deposit flow. The
signature of the `initiateDepositWithProxy` function is similar to
`initiateDeposit`. The difference is that it expects an instance of the
`DepositProxy` interface. It also accepts optional 32-byte `extraData`
that can be used to embed additional data within the deposit script
(e.g. data allowing to attribute the deposit to the original depositor).
The `initiateDepositWithProxy` returns a `Deposit` object that
represents the initiated deposit process.

### Usage

Here is a brief example illustrating what a third-party integrator
should do to use their contract as a deposit intermediary.
Let's suppose the integrator implemented an `ExampleDepositor` contract
that exposes a `revealDepositOnBehalf` function which takes a deposit
and reveals it to the `Bridge` on behalf of the original depositor:
```typescript
contract ExampleDepositor {
    Bridge public bridge;

    function revealDepositOnBehalf(
        BitcoinTx.Info calldata fundingTx,
        DepositRevealInfo calldata reveal,
        address originalDepositor,
    ) external {
        // Do some additional logic, e.g. attribute the deposit to the original depositor.

        bytes32 extraData = bytes32(abi.encodePacked(originalDepositor));
        bridge.revealDepositWithExtraData(fundingTx, reveal, extraData);
    }
}
```
In that case, the off-chain part leveraging tBTC SDK can be as follows:
```typescript
import {
  BitcoinRawTxVectors,
  ChainIdentifier,
  DepositReceipt,
  EthereumDepositorProxy,
  Hex,
  TBTC,
} from "@keep-network/tbtc-v2.ts"

import { Contract as EthersContract } from "@ethersproject/contracts"
import { JsonRpcProvider, Provider } from "@ethersproject/providers"
import { Signer, Wallet } from "ethers"

// Address of the ExampleDepositor contract.
const contractAddress = "..."
// ABI of the ExampleDepositor contract.
const contractABI = "..."

class ExampleDepositor extends EthereumDepositorProxy {
  // Ethers handle pointing to the ExampleDepositor contract.
  private contractHandle: EthersContract

  constructor(signer: Signer) {
    super(contractAddress)

    this.contractHandle = new EthersContract(
      contractAddress,
      contractABI,
      signer
    )
  }

  revealDeposit(
    depositTx: BitcoinRawTxVectors,
    depositOutputIndex: number,
    deposit: DepositReceipt,
    vault?: ChainIdentifier
  ): Promise<Hex> {
    // Additional logic, if necessary.

    // Prepare parameters for the contract call.
    const { fundingTx, reveal, extraData } = this.packRevealDepositParameters(
      depositTx,
      depositOutputIndex,
      deposit,
      vault
    )

    // Call the depositor contract function that reveals the deposit to the
    // Bridge.
    return this.contractHandle.revealDepositOnBehalf(
      fundingTx,
      reveal,
      this.decodeExtraData(extraData) // Contract function expects originalDepositor as third parameter
    )
  }

  private decodeExtraData(extraData: string): string {
    // Extract the originalDepositor address from extraData.
    // This should be a reverse operation to extra data encoding
    // implemented in the revealDepositOnBehalf function of
    // the ExampleDepositor contract.
    return "..."
  }
}

async function main() {
  // Create a readonly Ethers provider.
  const provider: Provider = new JsonRpcProvider("...")
  // Create an instance of the ExampleDepositor class. Pass an Ethers
  // signer as constructor parameter. This is needed because the
  // ExampleDepositor issues transactions using an Ethers contract handle.
  const exampleDepositor: ExampleDepositor = new ExampleDepositor(
    new Wallet("...", provider)
  )
  // Create an instance of the tBTC SDK. It is enough to pass a readonly
  // Ethers provider as parameter. In this example, the SDK does not issue
  // transactions directly but relies on the ExampleDepositor
  // instead.
  const sdk: TBTC = await TBTC.initializeMainnet(provider)

  // Get BTC recovery address for the deposit.
  const bitcoinRecoveryAddress: string = "..."
  // Determine the address of the original depositor who will actually
  // own the deposit.
  const originalDepositor: string = "..."
  // Encode the original depositor as 32-byte deposit extra data. This
  // must be done in the same way as in the ExampleDepositor solidity contract
  // (see revealDepositOnBehalf function).
  const extraData: Hex = encodeExtraData(originalDepositor)

  // Initiate the deposit with the proxy.
  const deposit = await sdk.deposits.initiateDepositWithProxy(
    bitcoinRecoveryAddress,
    exampleDepositor,
    extraData
  )
  // Get BTC deposit address and send funds to it.
  const depositAddress: string = await deposit.getBitcoinAddress()
  // Initiate minting once BTC transaction is made. This will call
  // revealDepositOnBehalf function of the ExampleDepositor contract
  // under the hood.
  const ethTxHash: Hex = await deposit.initiateMinting()

  console.log(
    `Minting initiated. ETH transaction hash: ${ethTxHash.toPrefixedString()}`
  )
}
```
dimpar added a commit to thesis/acre that referenced this pull request Mar 6, 2024
The Acre Bitcoin 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 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:

<details>
<summary>
Fee changes simulation
</summary>

**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.

</details>

### 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
@lukasz-zimnoch lukasz-zimnoch added this to the solidity/v1.6.0 milestone Mar 8, 2024
tomaszslabon added a commit that referenced this pull request Mar 13, 2024
Refs: #750

Here we present the smart contracts necessary to enable the L2 direct
bridging (a.k.a native bridging) feature. This mechanism allows getting
canonical TBTC on the given supported L2 chain, without the need to
touch the L1 Ethereum chain the tBTC protocol is deployed on.

Changes made as part of this pull request introduce a generic mechanism
that can be deployed on all supported L2 EVM-based chains and deploy the
mechanism on Ethereum Sepolia and Base Sepolia chains for testing
purposes.

### Motivation

Right now, a user of the supported L2 chain willing to obtain canonical
L2 TBTC has to go the following path:
1. Generate a Bitcoin deposit address (using tBTC dApp or SDK)
2. Make a deposit transaction on Bitcoin
3. Reveal the Bitcoin transaction to the tBTC Bridge to start the
minting process (Ethereum transaction)
4. Once TBTC is minted on Ethereum, go to the [Wormhole Token
Portal](https://portalbridge.com) and bridge minted TBTC to the given L2
(another Ethereum transaction)
5. Canonical L2 TBTC lands on the user account automatically

This flow is unwieldy and has major drawbacks:
- It's complicated and requires multiple transactions
- It requires L2 users to interact with the L1 Ethereum chain
- It requires interacting with the Wormhole Token Portal

The idea behind direct bridging is simplifying the above flow to
something like:
1. Generate a Bitcoin deposit address (using dApp or SDK)
2. Make a deposit transaction on Bitcoin
3. Reveal the Bitcoin transaction to the tBTC Bridge **using a single
transaction on the L2 chain**
4. Canonical L2 TBTC lands on the user account automatically

Although this flow still relies on Wormhole underneath, the advantages
are:
- Simpler flow with fewer transactions
- L2 users interact only with the given L2 chain
- No need to touch the Wormhole Token Portal
- As a next iteration, we can even get rid of the reveal deposit
transaction on L2 and use Bitcoin deposit transactions as a trigger.
This will improve UX even more. See the **Next iteration: Gasless
bridging** section for details.

### High-level architecture

The high-level architecture of the direct briding mechanism is presented
on the chart below:

<img width="2144" alt="bob-i1"
src="https://github.com/keep-network/tbtc-v2/assets/11180469/6a32050d-6bc4-44cb-a299-1bc3e8009364">

- The **green** contracts are existing tBTC contracts that form the
current bridging flow based on the Wormhole Token Portal (see [RFC
8](https://github.com/keep-network/tbtc-v2/blob/main/docs/rfc/rfc-8.adoc#37-smart-contracts)).
The `TBTC Bridge` component is the `Bridge` contract deployed on L1
Ethereum responsible for minting the L1 TBTC token. The
`L2WormholeGateway` contract has the authority to mint canonical L2 TBTC
on the given L2, based on received Wormhole L2 TBTC tokens. The `L2TBTC`
component is the canonical L2 TBTC token contract.
- The **blue** contracts are the new contracts that enable the new
direct bridging flow. The `AbstractTBTCDepositor` contract (introduced
by #778) provides some
useful tooling facilitating integration with the tBTC `Bridge` and its
new **deposit with extra data** function (developed in
#760) which is the
foundation of the L2 direct bridging mechanism. The `L1BitcoinDepositor`
and `L2BitcoinDepositor` components are smart contracts handling the
actual direct bridging actions on the L1 and L2 chains respectively.
Those two contracts are introduced by this pull request.
- The **red** contracts belong to the Wormhole protocol that handles
cross-chain operations. In the context of the direct bridging mechanism,
that means the transfer of minted L1 TBTC to the L2 chain. The
`TokenBridge` contract handles the bookkeeping part of the transfer. The
`Relayer` contract handles the actual execution of it.
- The **yellow** off-chain relayer bot is the planned component
(implemented as an immediate next step) that will "turn the crank" of
the direct bridging mechanism. It will initialize deposits on the L1
chain (based on L2 events in the first iteration) and finalize them once
L1 TBTC is minted by the tBTC `Bridge` contract.

The above components interact with each other in the following way:
1. The user makes the BTC deposit funding transaction and calls the
`L2BitcoinDepositor.initializeDeposit` function to initiate the deposit
process on L2 (the call is made through a dApp and tBTC SDK).
2. The off-chain relayer bot observes the `DepositInitialized` event
emitted by the `L2BitcoinDepositor` contract.
3. After assessing the deposit validity, the off-chain relayer bot calls
the `L1BitcoinDepositor.initializeDeposit` function on L1.
4. The `L1BitcoinDepositor` contract calls the
`Bridge.revealDepositWithExtra` function under the hood (through the
`AbstractTBTCDepositor` abstract contract).
5. After some time (several hours), the `Bridge` contract mints L1 TBTC
to the `L1BitcoinDepositor` contract.
6. The off-chain bot observes the mint event and calls
`L1BitcoinDepositor.finalizeDeposit` to start the finalization process
and move L1 TBTC to the L2 chain.
7. The `L1BitcoinDepositor` calls Wormhole's
`TokenBridge.transferTokensWithPayload` function to initiate the
cross-chain transfer of L1 TBTC. This call pulls out the L1 TBTC from
the `L1BitcoinDepositor` contract and locks it in the `TokenBridge`.
8. The `L1BitcoinDepositor` calls Wormhole's `Relay.sendVaasToEvm` to
send a cross-chain message to `L2BitcoinDepositor` and notify it about a
pending cross-chain transfer.
9. The Wormhole protocol calls the
`L2BitcoinDepositor.receiveWormholeMessages` function to deliver the
cross-chain message.
10. The `L2BitcoinDepositor` contract calls the
`L2WormholeGateway.receiveTbtc` function under the hood. It passes the
VAA representing the cross-chain transfer as an argument of the call.
11. The `L2WormholeGateway` uses the obtained VAA to finalize the
cross-chain transfer by calling the Wormhole's
`TokenBridge.completeTransferWithPayload` function. This call redeems
Wormhole-wrapped L2 TBTC from the `TokenBridge`.
12. The `L2WormholeGateway` uses obtained Wormhole-wrapped L2 TBTC to
call `L2TBTC.mint` and mint canonical L2 TBTC.
13. Minted canonical L2 TBTC is transferred to the L2 user.

### Immediate next steps

Changes presented in this pull request introduce the on-chain components
of the direct bridging mechanism. To make the mechanism complete, the
following steps need to take place:
- Expose the L2 direct bridging feature in the tBTC Typescript SDK. This
feature will be incrementally exposed for specific L2 chains the
mechanism will be deployed for. The first L2 chain will be Base.
- Implement the off-chain relayer bot

### Next iteration: Gasless bridging

The plans for the future include some improvements to the first
iteration of the direct bridging mechanism described above. Namely, the
next iteration will bring gasless direct bridging that will not require
any L2 transaction to initiate the process and will rely on a single
Bitcoin funding transaction issued by the L2 user. On the technical
level, the first two steps of the flow will be replaced by a direct call
to the off-chain relayer's REST endpoint:

<img width="2144" alt="bob-i2"
src="https://github.com/keep-network/tbtc-v2/assets/11180469/f6a01138-0354-4b41-b3f1-4f4a38be7f91">
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.

3 participants