Operating Multichain tokens on a unilateral token bridge (BSC <> SOLANA)

abdhafizahmed

Abdulhafiz

Posted on March 14, 2024

Operating Multichain tokens on a unilateral token bridge (BSC <> SOLANA)

It's almost like we hear of new blockchains every other day now and they are often architecturally incompatible. It could be overwhelming to keep up with the technological innovation.

Luckily, tools are constantly being created to match the requirements of clients so that developers can make their technical wishes come true (at least before we are all replaced by AI 🤖 👨🏾‍💻)

In this article, I am going to show you can build a conceptual token bridge that achieves 1:1 parity between tokens on Binance Smart Chain (BSC) and Solana Blockchain.

Considerations:

- Token Name and Token Symbol
Since we will work with the assumption that this token is owned by the same project, I've the same name and symbol to represent both. The token name is "Useless Token" and symbol is "USELESS".

- Token Standard
USELESS (BSC) version will be BEP20
USELESS (Solana) token will be SPL-20

- Smart contracts

- Token Interaction
USELESS (BSC) will be interacted with using Ethers.js

USELESS (Solana) will be interacted with using Solana Web3 Javascript SDK and Solana Token Program Library

Steps

  1. Creating USELESS (Solana) token deploying it on solana chain.

The first step to do this is to install solana-cli.
I used the command
solana-keygen grind --starts-with use:1 --ignore-case to create a vanity address keypair with the "use" prefix because I think it's sounds cool since our token will be "USE-less". It generated the address uSEZ...5zSo and its keypair will be saved in the working directory, I sent some SOL to it from another wallet which will be used for transaction fees later.

I created my token metadata and stored it on gitlab as a snippet, then stored it on gitlab as a snippet although it is advisable to store metadata on immutable storages like arweave. Regarding the token metadata json structure, you can read more about the different token standards well-explained on metaplex's website

By using metaboss, you need to create another metadata file in the directory you are working from with the fields "name", "symbol" and "uri". This uri must point to the raw token standard metadata file hosted on arweave or in this case, gitlab's snippet.

{
    "name": "Useless Token",
    "symbol": "USELESS",
    "uri": "https://gitlab.com/-/snippets/3686959/raw/main/useless.json"
}
Enter fullscreen mode Exit fullscreen mode

AGAIN: You will create two metadata files: one that conforms with that of the spl-token token standard that would be stored in an immutable storage, and another one (above) to be used while creating our token below.

Since, I have both keypair and the "metadata pointer" files in the same directory, I used this command to mint a new token with 100m supply and 9 decimals.

metaboss create fungible -k uSEZwk76nxzBCbbPzD9Yc191wYdkQu5wN8GmV7p5zSo.json -d 9 --initial-supply 100000000 -m token.json -r https://api.mainnet-beta.solana.com
Enter fullscreen mode Exit fullscreen mode

The token minted successfully.

Note: If you get the error Error: RPC response error -32002: Transaction simulation failed: Blockhash not found, just keep trying or try other node endpoints like getblock.io with my affiliate link (thank you).

  1. Scripting with NodeJS Bun I am beginning to prefer Bun because of its native typescript support, so I used bun to initialize my project and installed necessary dependencies
bunx init
bun add @solana/web3.js
bun add @solana/spl-token
Enter fullscreen mode Exit fullscreen mode

I created a file "useless.ts" and added the following code

// useless.ts
import { PublicKey, Connection, clusterApiUrl, Keypair, Transaction, sendAndConfirmTransaction } from '@solana/web3.js'
import { getOrCreateAssociatedTokenAccount, TOKEN_PROGRAM_ID, createTransferCheckedInstruction } from '@solana/spl-token'

const connection = new Connection(clusterApiUrl('mainnet-beta'), 'confirmed') // connection to mainnet
const secretKeyArray = await Bun.file('./uSEZwk76nxzBCbbPzD9Yc191wYdkQu5wN8GmV7p5zSo.json').json() // generated key is in the same directory as this file
const secretKey = Uint8Array.from(secretKeyArray);
const signer = Keypair.fromSecretKey(secretKey); // derived signer https://solana.com/docs/clients/javascript#quickstart


const decimals = 9 // token decimals during token creation
const uselessTokenAddressSPL = '6QWe8neoMdYUcv7LRnaBguVHTEKxC59j6Fhw8XT2pxqW' // address generated after token was minted
const mySOLWalletAddress = 'uSEZwk76nxzBCbbPzD9Yc191wYdkQu5wN8GmV7p5zSo'

// function to send solana token to given receiver and amount 
const sendSolanaToken = async (solReceiver: string, amount: number) => {
  const uselessToken = new PublicKey(uselessTokenAddressSPL)
  const mySOLWallet = new PublicKey(mySOLWalletAddress)
  const receiver = new PublicKey(solReceiver)

  // this is important because solana spl-tokens use different accounts
  // to hold tokens for different wallets, so if the receiving account
  // doesn't already have an associated account for this token, we will have to create it for them
  // so that they can receive it (Read more at https://spl.solana.com/token#example-transferring-tokens-to-another-user)
  const fromAccount = await getOrCreateAssociatedTokenAccount(connection, signer, uselessToken, mySOLWallet)
  const toAccount = await getOrCreateAssociatedTokenAccount(connection, signer, uselessToken, receiver)

  try {
    // to ensure we are attempting to add our transaction after a previously finalized block
    const latestBlock = await connection.getLatestBlockhash('finalized');
    const tx = new Transaction().add(
      createTransferCheckedInstruction(
        fromAccount.address, uselessToken, toAccount.address, mySOLWallet, amount, decimals, undefined, TOKEN_PROGRAM_ID
      )
    )
    tx.recentBlockhash = latestBlock.blockhash
    tx.lastValidBlockHeight = latestBlock.lastValidBlockHeight

    // broadcast the transaction
    const txHash = await sendAndConfirmTransaction(connection, tx, [signer])
    // log transaction hash
    console.log(txHash)
    return txHash;
  } catch (e) {
    // log error
    console.log('error occurred here', e)
  }
}
Enter fullscreen mode Exit fullscreen mode

My script can now send "USELESS" token to any valid solana address but I wanted it to send whenever I receive a similar amount on BSC. And to achieve this, it will lead us to...

  1. Create USELESS (BSC) token and deploy it on Binance Smart Chain network.

Useless Token on BSC

As you see in the image above, I have imported the ERC20 libraries I wanted. (ERC20Permit is optional but recommended) then deployed it on the network at 0x84109ff145Df1F2f3df12AD57BC104d96E034f58.

But this is not enough as I need the users to submit their solana address whenever they are making a deposit so that my bridge script can pick it up automatically and send then the equivalent spl-token. I deployed the contract for this successfully at UselessPurseBSC

UselessPurseBSC

And then I modified my "useless.ts" script accordingly by including the new lines

// useless.ts
...
import { Contract, JsonRpcProvider } from 'ethers'
const uselessPurseABI = [   {
  "anonymous": false,
  "inputs": [
    {
      "indexed": false,
      "internalType": "address",
      "name": "bscSender",
      "type": "address"
    },
    {
      "indexed": false,
      "internalType": "uint256",
      "name": "amount",
      "type": "uint256"
    },
    {
      "indexed": false,
      "internalType": "string",
      "name": "solReceiver",
      "type": "string"
    }
  ],
  "name": "Deposit",
  "type": "event"
}]
const uselessTokenPurseBSC = '0x3B156913F104287eb1cA166C3cf5F8F0f59e561E'
const bscConnection = new JsonRpcProvider('https://go.getblock.io/<api-key>')
const uselessPurse = new Contract(uselessTokenPurseBSC, uselessPurseABI, bscConnection)
...

// this will watch for the deposit event on our UselessPurse contract 
// this is the entry point to the bridge
uselessPurse.on('Deposit', async(_: string, amount: string, receiver: string) =>  {
  // a good idea would be to queue responses first or store them in a database and then fulfill them automatically at intervals later on
  // because of many potential problems including as rate-limiting on the node, invalid solana address,
// exhausted funds for solana token transfer, 
// any other unforeseen technical error

  // fulfill the token transfer
  await sendSolanaToken(receiver, Number(amount))
})
Enter fullscreen mode Exit fullscreen mode

With the above done, I could now run my bridge script using

bun --watch useless.ts
Enter fullscreen mode Exit fullscreen mode

Using a client side dApp or bscscan explorer, users can deposit tokens into the UselessPurse contract and they will get the equivalent in their provided solana wallet.

Conclusion:
The use of crypto bridge is important because of the consistent evolution of blockchain technology and the need for projects and startups to make their utility tokens interoperable with multiple chains. In this article, we built a simple unilateral bridge that accepts tokens deposits on BSC network and automatically transfer equivalent amount on solana network.

IMPORTANT None of the contracts in this tutorial has been audited and therefore cannot be guaranteed to be safe to use but serve well as good reference for educational material.

Further reading:
A crash course in crypto currency bridges
Wormhole Blog

References:
Solana Clients
SPL Token
Metaboss

Node (Affiliate Link)
Getblock

Repo:
cross chain token repo

💖 💪 🙅 🚩
abdhafizahmed
Abdulhafiz

Posted on March 14, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related