Bridging BTC to a canister with post execution

Hi, I am currently building a pool staking application for native bitcoin users, so users are not expected to use any ICP wallets. Each pool has several locktime options: 90, 180, and 360 days. The ideal user flow should look like this:

  1. The user opens the dApp and selects a staking pool.
  2. The user configures the staking terms.
  3. The dApp generates a BTC address for the user to send Bitcoin to.
  4. The user sends their BTC to the address.

After the 4th step, the user can quit the app and come back in a few months. Meanwhile, in the background, these things should happen: BTC is bridged to ckBTC, ckBTC is transferred to a pool canister, and ckBTC is deposited with the right timelock terms.

However, I can’t find a smooth ICP way to implement this flow. Currently, it’s possible to bridge the user’s BTC directly to a canister, but it’s not possible to get the canister to know what the deposit terms for the funds are.

Is there a way to implement this flow? Are there any similar projects to grasp an idea from?

Thanks

2 Likes

Interesting dApp idea! I’m just think out loud here. In your step 3, you can design your own rules for generating a BTC address.

The ckBTC minter provides a method called get_btc_address that generates a Bitcoin deposit address that a user can send Bitcoin to:

get_btc_address : (record { owner: opt principal; subaccount : opt blob }) -> (text);

Since it’ll be your dApp canister that receives the converted ckBTC, the owner here can be your dApp’s canister id. You may also supply a 32-byte subaccount of your own choosing, which can help encode additional logic you need.

Now, your dApp can store the staking terms indexed by subaccount. It can generate a fresh subaccount for each new deposit to receive, store (subaccount, staking terms) in this table, and then generate the deposit address by calling get_btc_address with this subaccount.

Note that this owner and subaccount pair is exactly the Account being used by ICRC tokens, such as ckBTC.

Later your canister (or anyone else) may call the update_balance method of ckBTC minter to confirm if any balance has been received. This method also takes an Account (owner and subaccount) as argument. If new UTXOs have been received on this address, it means new ckBTC has been minted to the corresponding Account. Your dApp can then lookup the above table for the staking terms.

In fact, your dApp can use the same table to keep track of ongoing deposits. Iterate through this table and call ckBTC minter’s update_balance with each subaccount. On a successful deposit, it may move ckBTC tokens to a central account and remove the corresponding entry in this table.

There are a few things to note though:

  1. If a user sends more BTC to the generated address, new ckBTC will not be minted unless get_balance is called again. So you may want to keep checking until all expected funds are received.
  2. Calling get_balance is actually expensive. You can call get_known_utxos method first, compare with a previous result of get_known_utxos to check if there is any new UTXO on this address, and only call update_balance when there is.

Subaccounts need to be unique, and you probably should also think of how users may retrieve their staking in the future, which btw can be part of the staking terms.

If your users are authenticated, the subaccount can be their principal id + nonce, where nonce can be just a counter that increases when the user creates a new stake. If your users are not authenticated, the subaccount can be just a plain counter. Either way, you probably need some way to prevent spamming.

3 Likes

Thanks for the answer @PaulLiu!
So, if I understood correctly, I should go with these high-level steps:

  1. The user opens the dApp and selects a staking pool.
  2. The user configures the staking terms.
  3. The dApp makes an ICP transaction to get an associated subaccount to the principal and timelock.
  4. The dApp generates a BTC address for the subaccount.
  5. The user sends their BTC to the address.

In this case, as you already mentioned, there could be a problem with spamming, because both the 3rd and 4th steps require transactions from the dApp side. Can you elaborate a little bit on user authentication and the classical ICP way to prevent spamming?

Also, it would be great if there are similar ICP applications which can be used as examples.

Thanks in advance!

Hey @Norfolks,

thanks for joining the office hours today. I am trying to elaborate on this matter again and hope to be able to clarify all the questions/concerns you have here.

My summary would be like this:

  1. The user opens the dApp and selects a staking pool.
  2. The user configures the staking terms.
  3. The dapp defines a 32-byte subaccount for the user
  4. The dapp stores the staking terms of the user in a table which is indexed by subaccount
  5. The dapp calls get_btc_address for the principal (canister id of your dapp) along with the subaccount associated with the staking conditions
  6. The dapp displays the user the BTC deposit address for the subaccount with the associated staking conditions
  7. The user sends BTC to the deposit address
  8. The dapp calls update_balance of the ckBTC minter after 6 confirmations of the deposit tx’s on the Bitcoin network
    • note: this can potentially be done by any canister on the network and requires principal (canister id of your dapp) as well as the subaccount to be provided
    • you could think about setting up a timer in your dapp to check get_known_utxos every 10-15 minutes until and then call update_balance for the subaccount once a new UTXO is observed
      • your dapp could run the timer e.g. for 24 hours and if no deposit happened, stop the timer
      • the user should be informed to check back and let the dapp know if 24h passed and a deposit has been made in the meantime
  9. The dapp might want to keep track of the users deposits and perform whatever logic is required based on the configured staking terms that can easily be looked up using the table indexed by subaccount

Regarding spam prevention I recommend to read following resources:

Can anybody else share some more insights in how to prevent spamming? What would you recommend @PaulLiu ?

@THLO I also got asked whether we are considering to allow an arbitrary derivation path to be used with the ckBTC minter. I assume that was a specific design decision to use icrc1Account (principal + subaccount) here to actually make sure that the recipient of the minted ckBTC on the ckBTC ledger actually is a valid icrc1Account. but as of writing this, I notice that this absolutely makes sense.

@Norfolks are your questions/concerns addressed or is there still sth. unclear? maybe you want to elaborate a little bit more on your concern with collision resistance for the subaccounts that you have. I am not sure if I understood this correctly and if this concern is valid.

Hi, thank you for your answer. I think I should elaborate a bit more on my concerns about subaccount size.

  1. The subaccount size is a 32-bit unsigned integer, which equals 4,294,967,295 (a little over 4 billion). Since 4 billion is already less than the global population of 8 billion, it’s theoretically impossible to have a unique subaccount for every person. But let’s continue.

  2. Each of our pools may have 4 different staking options. So, if we use a principal ID + nonce formula to generate subaccounts for staking terms, the number of possible users shrinks by a factor of 4 — from 4 billion to 1 billion. While I don’t expect to have that many users anytime soon, let’s continue.

  3. There’s the birthday attack (for some reason I cannot include a link to a wiki page here), or paradox, which states that the probability of a collision between two randomly chosen values reaches 50% at around the square root of the total number of possibilities. So, with 1 billion possible subaccounts, the probability of a collision becomes significant at around √1,000,000,000 ≈ 32,000 users. This worries me because I can imagine the application having 30k+ users. And even if we use the entire 32-bit space (4 billion), a 50% probability of collision is expected at around 64k users.

I see that subaccounts are widely used in ICP despite this seemingly small address space, so I assume there’s something I might be misunderstanding.

Also, it would be great to know if a user from his side can get the bitcoin address based on canister principal and his principal as a subaccount without calling get_btc_address on a canister as it mentioned for direct UTXO integration of the bitcoin.
So, we can get the address user should send his BTC to, before having any transaction on the ICP side.
Thanks.

IMO this should be possible if you know the public master key of the ckBTC minter. the logic behind this is can be looked up in following places:

I am not sure about the cost efficiency in that regard. I assume you would want to avoid calling get_btc_address for your users, right?

@THLO do we have a snippet to compute the btc address off-chain based on the public master key of the ckBTC minter? (I am not sure what the public master key of the ckBTC minter is :thinking:) when I remember correctly, Bogdan from liquidium was recently also asking about this. (cc @cryptoschindler)

The subaccount size is 32 bytes. I think this size is large enough for your purposes, right?

You can find a code snippet to compute public keys of canisters (and therefore also Bitcoin addresses) here. I hope this helps!

1 Like

Yes, thanks!
I was completly brain fogged on bit/bytes.

1 Like