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

The AbstractTBTCDepositor contract #778

Merged
merged 23 commits into from
Feb 1, 2024
Merged

The AbstractTBTCDepositor contract #778

merged 23 commits into from
Feb 1, 2024

Conversation

lukasz-zimnoch
Copy link
Member

@lukasz-zimnoch lukasz-zimnoch commented Jan 26, 2024

Refs: #750

Here we introduce the AbstractTBTCDepositor contract meant to facilitate integration of protocols aiming to use tBTC as an underlying Bitcoin bridge.

Usage of the AbstractTBTCDepositor contract

An integrator willing to leverage the AbstractTBTCDepositor contract is supposed to:

  • Create a child contract inheriting from this abstract contract
  • Call the __AbstractTBTCDepositor_initialize initializer function
  • Use the _initializeDeposit and _finalizeDeposit as part of their business logic in order to initialize and finalize deposits.

Example code:

// Example upgradeable integrator contract.                   
contract ExampleTBTCIntegrator is AbstractTBTCDepositor, Initializable {          
    /// @custom:oz-upgrades-unsafe-allow constructor                           
    constructor() {                                                            
        // Prevents the contract from being initialized again.                 
        _disableInitializers();                                                
    }                                                                          
                                                                               
    function initialize(                                                       
        address _bridge,                                                       
        address _tbtcVault                                                     
    ) external initializer {                                                   
        __AbstractTBTCDepositor_initialize(_bridge, _tbtcVault);                  
    }                                                                          
                                                                               
    function startProcess(                                                     
        IBridgeTypes.BitcoinTxInfo calldata fundingTx,                         
        IBridgeTypes.DepositRevealInfo calldata reveal                         
    ) external {                                                               
        // Embed necessary context as extra data.                              
        bytes32 extraData = ...;                                               
                                                                               
        uint256 depositKey = _initializeDeposit(                               
            fundingTx,                                                         
            reveal,                                                            
            extraData                                                          
        );                                                                     
                                                                               
        // Use the depositKey to track the process.                            
    }                                                                          
                                                                               
    function finalizeProcess(uint256 depositKey) external {                    
        (uint256 tbtcAmount, bytes32 extraData) = _finalizeDeposit(depositKey);
                                                                               
        // Do something with the minted TBTC using context                     
        // embedded in the extraData.                                          
    }                                                                          
}                                                                                                                                           

The tbtcAmount caveat

The tbtcAmount computed by _calculateTbtcAmount and returned by _finalizeDeposit may not correspond to the actual amount of TBTC minted for the deposit. Although the treasury fee cut upon minting is known precisely, this is not the case for the optimistic minting fee and the Bitcoin transaction fee. To overcome that problem, this contract just takes the current maximum values of both fees, at the moment of deposit finalization. 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 integrator is responsible for handling such cases.

@lukasz-zimnoch lukasz-zimnoch added the ⛓️ solidity Solidity contracts label Jan 26, 2024
@lukasz-zimnoch lukasz-zimnoch requested a review from nkuba January 26, 2024 16:12
@lukasz-zimnoch lukasz-zimnoch force-pushed the abstract-depositor-proxy branch from 7116e02 to 7e347dc Compare January 26, 2024 16:20
solidity/contracts/bridge/DepositorProxy.sol Outdated Show resolved Hide resolved
solidity/contracts/bridge/DepositorProxy.sol Outdated Show resolved Hide resolved
solidity/contracts/bridge/DepositorProxy.sol Outdated Show resolved Hide resolved
Here we extract the tbtc amount calculation logic to a separate
`calculateTbtcAmount` function. We are making it `virtual` to allow
child contracts override it if they need to do so.
This reference is not needed at this level as it will be used directly
by child contracts.
Returning deposit key can be useful for child contracts using this function.
@lukasz-zimnoch lukasz-zimnoch force-pushed the abstract-depositor-proxy branch from 291bead to 8d183bf Compare January 29, 2024 14:36
Here we make the abstract contract upgrade-friendly by replacing the
constructor with an initializer and adding a storage gap.
@lukasz-zimnoch lukasz-zimnoch changed the title Abstract DepositorProxy contract Abstract TBTCDepositorProxy contract Jan 29, 2024
Copy link

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

Copy link

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

@lukasz-zimnoch lukasz-zimnoch self-assigned this Jan 29, 2024
Copy link

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

Copy link

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

Copy link

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

pragma solidity 0.8.17;

import {BTCUtils} from "@keep-network/bitcoin-spv-sol/contracts/BTCUtils.sol";
import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
Copy link
Member

Choose a reason for hiding this comment

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

I guess we're not likely to switch to the latest @openzeppelin/contracts-upgradeable v5?

Copy link
Member Author

Choose a reason for hiding this comment

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

I think we can do it just for this abstract contract. ERC7201 seems to be worth it. Let me see.

Copy link
Member Author

@lukasz-zimnoch lukasz-zimnoch Jan 30, 2024

Choose a reason for hiding this comment

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

Unfortunately, this won't be so simple. Migration from OZ v4 to v5 is non-trivial and will incur the upgrade of the OZ Upgrade plugin. This may break things while dealing with existing tBTC contract deployments. I don't want to take this risk right now.

However, to make TBTCDepositorProxy usable with both OZ v4 and v5, I decided to remove the dependency on Initializable in favor of a custom guard in the __TBTCDepositorProxy_initialize initializer. Regarding the storage layout protection, I think we should keep using storage gaps in this case. That gives the best flexibility as child contracts of TBTCDepositorProxy can still inherit from OZ v5. This is safe because ERC-7201 namespaces used by OZ v5 are safe against collisions with the standard storage layout used by Solidity (source: ERC-7201 abstract). I have also explored the possibility of using ERC-7201 in TBTCDepositorProxy. Unfortunately, this is not an option because the OZ Upgrade Plugin raises an error for Solidity versions <0.8.20 (source) and TBTCDepositorProxy must support all 0.8.x versions.

Please see the change: 2c3c3c6

@lukasz-zimnoch lukasz-zimnoch force-pushed the abstract-depositor-proxy branch from 20c60b3 to 5744adb Compare January 30, 2024 08:41
Copy link

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

Here we remove the `onDepositFinalized` function and return `tbtcAmount`
and `extraData` from `finalizeDeposit` in a direct way. This should make
writing child contracts even easier. Moreover, we are prefixing all
functions with `_` to explicitly denote that all inherited functions
are `internal` and reduce problems around name clashes.
Copy link

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

Copy link

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

This way, the `integrator` subpackage does not need to import
anything from the `bridge` subpackage and explicitly depend on it.
This simplifies the dependency graph for integrators.
Copy link

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

nkuba added a commit to thesis/acre that referenced this pull request Feb 1, 2024
Use TBTCDepositorProxy abstract contract implemented in keep-network/tbtc-v2#778
as a base for the depositor contract.

The abstract contract handles calculation of the approximate bridged
tBTC amount.

The tbtcAmount caveat:

The tbtcAmount computed by _calculateTbtcAmount and returned by
_finalizeDeposit may not correspond to the actual amount of TBTC
minted for the deposit. Although the treasury fee cut upon minting
is known precisely, this is not the case for the optimistic minting
fee and the Bitcoin transaction fee. To overcome that problem,
this contract just takes the current maximum values of both fees,
at the moment of deposit finalization. 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 integrator is responsible for handling such cases.
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 did a high-level review. The contract looks good to me with some potential minor tweaks already noted. I was not reviewing tests.

/// // embedded in the extraData.
/// }
/// }
abstract contract TBTCDepositorProxy {
Copy link
Member

Choose a reason for hiding this comment

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

Why not call it just TBTCDepositor or AbstractTBTCDepositor? The proxy suffix could suggest it is some sort of an upgradeable proxy contract.

Copy link
Member

Choose a reason for hiding this comment

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

I vote for AbstractTBTCDepositor, as the implementation could be named TBTCDepositor

Copy link
Member Author

Choose a reason for hiding this comment

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

I wanted to avoid pure "depositor" to not mix it with the concept of "depositor" already present in the Bridge. I don't have strong preferences here though. AbstractTBTCDepositor is completely fine.

Copy link
Member Author

Choose a reason for hiding this comment

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

See 68d02e8

internal
returns (uint256 tbtcAmount, bytes32 extraData)
{
require(pendingDeposits[depositKey], "Deposit not initialized");
Copy link
Member

Choose a reason for hiding this comment

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

Do we need pendingDeposits mapping? I think it should be enough to check whether the deposit.depositor value is non-zero or set to address(this). We retrieve IBridgeTypes.DepositRequest anyway. Also, the implementation of the TBTCDepositorProxy is already tightly coupled with the implementation of Bridge and TBTCVault. We could save some gas especially since those functions will be called by a relayer bot and not by the users.

EDIT: No, the current version is correct. We do not want _finalizeDeposit to be called twice for the same deposit.

Copy link
Member Author

Choose a reason for hiding this comment

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

EDIT: No, the current version is correct. We do not want _finalizeDeposit to be called twice for the same deposit.

Exactly. Moreover, we don't want to push this responsibility to the child contract.

/// Although the treasury fee cut upon minting is known precisely,
/// this is not the case for the optimistic minting fee and the Bitcoin
/// transaction fee. To overcome that problem, this function just takes
/// the current maximum values of both fees, at the moment of deposit
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
/// the current maximum values of both fees, at the moment of deposit
/// the current maximum allowed values of both fees, at the moment of deposit

Copy link
Member Author

Choose a reason for hiding this comment

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

See a6f4a92

solidity/contracts/integrator/TBTCDepositorProxy.sol Outdated Show resolved Hide resolved
Copy link

github-actions bot commented Feb 1, 2024

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

Copy link

github-actions bot commented Feb 1, 2024

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

@lukasz-zimnoch lukasz-zimnoch changed the title Abstract TBTCDepositorProxy contract The AbstractTBTCDepositor contract Feb 1, 2024
Copy link

github-actions bot commented Feb 1, 2024

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

@nkuba nkuba merged commit dd4bd02 into main Feb 1, 2024
38 checks passed
@nkuba nkuba deleted the abstract-depositor-proxy branch February 1, 2024 17:41
lukasz-zimnoch added a commit that referenced this pull request Feb 2, 2024
In this PR I propose some modifications to the initial implementation of
`AbstractTBTCDepositor` contract
(#778).

### Return initial deposit amount from _initializeDeposit function

Integrators may be interested in adding more fees to the deposit amount.
For this reason, it may be cleanest to calculate it based on the initial
deposit amount. By returning the value here they will avoid the need to
read the funding deposit amount from the storage again.

### Mock contracts tweaks

I added modifications to the MockBridge and MockTBTCVault contracts,
that were useful in tests of the integrator's implementation
(thesis/acre#91).
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
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">
@lukasz-zimnoch lukasz-zimnoch added this to the solidity/v1.6.0 milestone Mar 19, 2024
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