Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
SDK: Support for depositor proxies (#776)
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()}` ) } ```
- Loading branch information