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

SDK: Support for depositor proxies #776

Merged
merged 8 commits into from
Jan 19, 2024
Merged

SDK: Support for depositor proxies #776

merged 8 commits into from
Jan 19, 2024

Conversation

lukasz-zimnoch
Copy link
Member

@lukasz-zimnoch lukasz-zimnoch commented Jan 18, 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:

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:

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()}`
  )
}

Here we introduce the concept of a `DepositorProxy` contract. A `DepositProxy`
can reveal deposits to the Bridge, on behalf of the user
(i.e. original depositor). Once minting is completed, the `DepositProxy`
receives minted TBTC and can provide additional services to the user, such as
routing the minted TBTC tokens to another protocols, in an automated way.

To expose this feature to the SDK users, we are adding a new
`initiateDepositWithProxy` function. This function triggers the deposit flow
with support of the given `DepositProxy`.
If extra data is set, the deposit should be revealed using the
`revealDepositWithExtraData` function of the Ethereum `Bridge`.
Otherwise, the regular `revealDeposit` should be used.
The `EthereumDepositorProxy` is a base class meant to facilitate
integration with Ethereum depositor proxies.
@lukasz-zimnoch lukasz-zimnoch marked this pull request as ready for review January 19, 2024 13:02
@nkuba nkuba merged commit 4b61439 into main Jan 19, 2024
38 checks passed
@nkuba nkuba deleted the sdk-deposit-extra-data branch January 19, 2024 15:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
🔌 typescript TypeScript library
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants