Operating Multichain tokens on a unilateral token bridge (BSC <> SOLANA)
Abdulhafiz
Posted on March 14, 2024
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
- USELESS (BSC) will be designed using Openzeppelin's library and deployed via remix
- USELESS (Solana) will be deployed to the blockchain using Metaboss tool
- 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
- 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"
}
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
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).
- Scripting with
NodeJSBun 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
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)
}
}
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...
- Create USELESS (BSC) token and deploy it on Binance Smart Chain network.
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
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))
})
With the above done, I could now run my bridge script using
bun --watch useless.ts
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
Posted on March 14, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.