Implementing Cross Chain Contract Calls Using Squid: A Step-by-Step Tutorial
Overview
In recent years, blockchain networks have grown exponentially. Looking at the Ethereum ecosystem alone, there are now more than 50 different chains. While this growth brings more opportunities, it also creates challenges for users. Managing funds across multiple chains and maintaining sufficient gas fees for transactions on each network has become increasingly complex.
This is where Squid comes to the rescue. As a cross-chain platform, Squid specializes in enabling token transfers between the Ethereum and Cosmos ecosystems. Their recently released SDK takes this functionality a step further by allowing cross-chain smart contract calls. This means developers can now program specific actions to occur before or after tokens are bridged, unlocking new possibilities in the DeFi ecosystem.
Understanding Hooks in Squid
Before we start writing the code, let’s look at how this works -
At the heart of Squid's system are "hooks" - specialized functions that enable smart contract execution. These hooks can be triggered either before or after a token swap/bridging occurs.
Pre Hooks are executed before the swap/bridging occurs and Post Hooks are executed after the swap/bridging has occured.
For example - Pre Hooks can come in handy when you need to perform some operations on chain before performing the swap such as withdrawing any asset from aave by supplying the aToken and receiving the underlying token and then you can bridge the underlying token using squid, all in one transaction. Crazy, right?
Implementation
Okay, enough talking. Let’s see it in action. We will be implementing depositing into aave protocol in this tutorial.
In order to enable cross-chain transactions, we obviously need to install squid-sdk into our next project. You can do so by running the following command.
npm install @0xsquid/sdk @0xsquid/squid-types
We will also need ethers.js library, so let’s download that as well.
npm i ethers@5
Ok, let’s setup the squid config first of all. Create a file config/squid.ts
to setup the squid config by pasting the below code.
import { Squid } from '@0xsquid/sdk';
export const squidConfig = async () => {
const squid = new Squid({
baseUrl: "https://v2.api.squidrouter.com",
integratorId: process.env.NEXT_PUBLIC_SQUID_INTEGRATOR_ID as string,
executionSettings: {
infiniteApproval: false,
},
});
await squid.init();
return squid;
};
In order to get the INTEGRATOR_ID, you can fill up the form here.
Now, let’s declare the argument type for our function. Paste the code below in types/squid.ts
.
import { Hook } from "@0xsquid/squid-types";
export type TSquidQouteArgs = {
fromChain: string;
toChain: string;
fromAmount: string;
fromToken: string;
toToken: string;
fromAddress: string;
toAddress: string;
receiveGasOnDestination: boolean;
slippage?: number;
preHook?: Hook;
postHook?: Hook;
};
Next up, create a file tools/squid.ts
inside your src folder, this is where we will use squid to fetch the route of the transaction.
import { squidConfig } from "@/config/squid";
import { TSquidQouteArgs } from "@/types/squid";
import { RouteRequest } from "@0xsquid/squid-types";
export async function createQuote(squidQuoteArgs: TSquidQouteArgs) {
try {
const squid = await squidConfig();
const config: RouteRequest = {
fromChain: squidQuoteArgs.fromChain,
fromAmount: squidQuoteArgs.fromAmount,
fromToken: squidQuoteArgs.fromToken,
toChain: squidQuoteArgs.toChain,
toToken: squidQuoteArgs.toToken,
fromAddress: squidQuoteArgs.fromAddress,
toAddress: squidQuoteArgs.toAddress,
receiveGasOnDestination: squidQuoteArgs.receiveGasOnDestination,
enableBoost: true,
};
if (squidQuoteArgs?.slippage) {
config.slippage = squidQuoteArgs.slippage;
}
if (squidQuoteArgs.preHook) {
config.preHook = squidQuoteArgs.preHook;
}
if (squidQuoteArgs.postHook) {
config.postHook = squidQuoteArgs.postHook;
}
const { route } = await squid.getRoute(config);
return route;
} catch (error) {
console.log(error);
}
}
Although most of these fields are pretty self explanatory, I will explain them one by one for a better understanding.
First of all, we initialize Squid’s SDK by calling the squidConfig
function which returns the squid instance.
Now, we need to setup the parameters which are needed in order to get the route, these parameters are -
fromChain
- the chain Id of the chain from which we will be sending the transaction.
fromAmount
- the amount which will be sent from the source chain or fromChain. Remember this amount should be in wei.
fromToken
- the token that user will send from the source chain.
toChain
- the chain Id of the destination chain
fromAddress
- the address of the EOA wallet that will be sending the funds from the source chain.
toAddress
- the address of the EOA wallet that will receive the funds on destination chain.
receiveGasOnDestination
- if toAddress should recieve some gas on the destination chain, defaults to false.
slippage
- slippage is an optional field and specifies how much slippage should occur at max while swapping/bridging
preHook
- preHook is also optional and specifies the code that should be executed onChain before the swap/bridging happens.
postHook
- postHook is another optional field same as preHook that specifies the code that should be executed after the swap/bridging has happened.
Ok, now that we have setup the getRoute function, let’s see how we can create preHook and postHook where the real magic actually happens.
We will create a function called hookBuilder
which will be responsibe for creating the hook i.e postHook or preHook. So, first of all let’s create type for the arguments that we will be passing into the function. Modify types/squid.ts
with the following code.
/*......*/
import {SquidCallType} from "@0xsquid/squid-types";
/*......*/
export type HookBuilderArgs = {
fundToken: string;
fundAmount: string;
description: string;
calls: {
target: string;
callType: SquidCallType;
callData: string;
payload?: {
tokenAddress: string;
inputPos: number;
};
estimatedGas?: string;
}[];
};
Now, create a file utils/hook-builder/index.ts
and paste the following code.
import { ChainType, Hook } from "@0xsquid/squid-types";
import { HookBuilderArgs } from "@/types/squid";
export const hookBuilder = (hookBuilderArgs: HookBuilderArgs): Hook => {
const hook: Hook = {
chainType: ChainType.EVM,
fundToken: hookBuilderArgs.fundToken,
fundAmount: hookBuilderArgs.fundAmount,
description: hookBuilderArgs.description,
calls: hookBuilderArgs.calls.map((call) => {
return {
chainType: ChainType.EVM,
callType: call.callType,
target: call.target,
callData: call.callData,
estimatedGas: "4000000",
value: "0",
payload: call.payload,
};
}),
provider: "Your app name",
logoURI: "your logo url",
};
return hook;
};
Notice, that this file is written based on our specific use case and you are free to modify the parameteres such as value
or estimatedGas
as per your needs.
Let’s understand the parameters one by one -
chainType
- chainType mentions the type of transaction, is it of type cosmos or evm.
fundToken
- fundToken is the token that we take from user, whereas fromToken is the one which will be swapped or bridged. This comes in handy when you use preHook and you want to send aToken to squid, perform some preHook operation and then bridge the underlying token. In that case aToken will be the fundToken and underlying token will be the fromToken.
fundAmount
- the amount of fundToken that should be sent.
description
- description of the transaction.
calls
- calls is basically an array of smart contract calls that you want to execute before or after the swap/bridging happens.
callType
- Well, basically there are 3 types of callType that you will be using. DEFAULT
, FULL_TOKEN_BALANCE
and FULL_NATIVE_BALANCE
. Let’s take an example to understand them better - Let’s say I am sending 10 USDC from base and I want USDT on arbitrum and I want to use postHook to deposit all the amount to aave. But I don’t really know how much USDT I will receive on destination chain because of the slippage. So, I can’t really make a smart contract call out of it mentioning that I want to deposit x amount of USDT. To solve this, we can use FULL_TOKEN_BALANCE
to alter the callData in flight with the amount that I will receive after bridging/swapping. This is used in relation with payload that we will see later.
target
- target is the address of the smart contract or EOA that I want to make this smart contract call to.
callData
- callData is the encoded function data in the form of bytes that we want to execute.
estimatedGas
- the gas required to execute this function call.
value
- value in terms of ETH that we want to pass to the function call.
payload
- payload is used when we are using FULL_TOKEN_BALANCE
or FULL_NATIVE_BALANCE
. payload needs two things - the token that we want to modify the value of and the argument index value where the value should be modified.
With that done, we are down to our last steps which is setting up the deposit call for aave. So, let’s do this.
We will be interacting with aave pool contract. So, we need to ABI of that and the ABI of erc20 token. You can fetch those details from aave official docs. So, we will be skipping that.
You also need the contract address of the aave pool contract. Okay, so with that being done. Create a file for type of the deposit function arguments named types/transcation.ts
import { TSquidQouteArgs } from "./squid";
export type TTransactionPayload = TSquidQouteArgs & {
fundToken: string;
fundAmount: string;
};
Ok, let’s setup supplyHandler
in libs/aave/index.ts
.
import { POOL_ADDRESS } from "@/constants";
import { AAVE_POOL_ABI, ERC20_ABI } from "@/constants/abi";
import { HookBuilderArgs } from "@/types/squid";
import { TTransactionPayload } from "@/types/transaction";
import { hookBuilder } from "@/utils/hook-builder";
import { Hook, SquidCallType } from "@0xsquid/squid-types";
import { ethers } from "ethers";
export const supplyHandler = async (txDetails: TTransactionPayload): Promise<Hook> => {
const calls: HookBuilderArgs["calls"] = [];
const erc20Interface = new ethers.utils.Interface(ERC20_ABI);
const approveCall = erc20Interface.encodeFunctionData(
"approve",
[POOL_ADDRESS, 1] //* the value at first index gets overwritten by payload
);
calls.push({
target: txDetails.toToken,
callType: SquidCallType.FULL_TOKEN_BALANCE,
callData: approveCall,
payload: {
tokenAddress: txDetails.toToken,
inputPos: 1,
},
});
const aaveSupplyInterface = new ethers.utils.Interface(AAVE_POOL_ABI);
const supplyCall = aaveSupplyInterface.encodeFunctionData("supply", [txDetails.toToken, 1, txDetails.fromAddress, 0]);
calls.push({
target: POOL_ADDRESS,
callType: SquidCallType.FULL_TOKEN_BALANCE,
callData: supplyCall,
payload: {
tokenAddress: txDetails.toToken,
inputPos: 1,
},
});
const hooks = hookBuilder({
fundToken: txDetails.fromToken,
fundAmount: txDetails.fromAmount,
description: "Supply",
calls,
});
return hooks;
};
Ok, everything done. Now we just have to call the methods to get the route and execute it. You can do it as per your needs but for demo purposes, I will be doing it in the home page itself.
"use client";
import { squidConfig } from "@/config/squid";
import { supplyHandler } from "@/libs/aave";
import { createQuote } from "@/tools/squid";
import { TTransactionPayload } from "@/types/transaction";
import { ethers } from "ethers";
export default function Home() {
const supply = async () => {
const transactionPayload: TTransactionPayload = {
fromChain: "42161",
toChain: "8453",
fundToken: "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1", //WETH on arbitrum
fundAmount: "10000000000000000", //0.01 WETH
fromAmount: "10000000000000000", //0.01 WETH
fromToken: "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1", //WETH on arbitrum
toToken: "0x4200000000000000000000000000000000000006", //WETH on base
fromAddress: "0x0f4e3b1f3d6a9f9c8d3f7d0a3c0c3a2b1a090807", //user address
toAddress: "0x0f4e3b1f3d6a9f9c8d3f7d0a3c0c3a2b1a090807", //user address
receiveGasOnDestination: false,
};
const postHook = await supplyHandler(transactionPayload);
const route = await createQuote({ ...transactionPayload, postHook });
if (!route) return;
const squid = await squidConfig();
const provider = new ethers.providers.Web3Provider(window.ethereum, "any");
const signer = provider.getSigner();
await squid.executeRoute({ signer, route });
};
return (
<>
<button onClick={supply}>
Execute
</button>
</>
);
}
And that’s it🎉🎉. Now you can successfully execute any smart contract from any chain and build anything you want.
Support me ⭐
If you found this tutorial helpful, consider sharing it with your friends. Thank you for reading, and I wish you a great time!
Feel free to connect with me on Twitter, Github or Linkedin. I Would love to hear your feedback.
Happy learning 👋