How to Create an Intent Adapter using Router's CCIF
This guide will walk you through the steps for creating an intent adapter using Router's Intent library. We will cover the steps from setting up dependencies, creating the contract, defining functions, and invoking them.
Prerequisites
Before proceeding further, make sure, you have the following:
Node.js and npm installed in your system
Hardhat
A basic understanding of Solidity
Step 1: Set Up the Project
Initialize the project directory:
Open your terminal and create a project directory:
mkdir my-intent-adapter cd my-intent-adapter
Initialize a Node.js project:
npm init -y
Install Hardhat:
npm install --save-dev hardhat
Create a new Hardhat project:
npx hardhat
Step 2: Install Required Dependencies
Install Router Protocol Intents packages:
npm install @routerprotocol/intents-core
or
yarn add @routerprotocol/intents-core
Install other dependencies: Install the dependencies needed for SafeERC20.
npm install @openzeppelin/contracts
or
yarn add @openzeppelin/contracts
Step 3: Define the Adapter Contract
Create a Solidity file for your adapter in the contracts
folder.
Example: StaderStakeEth Adapter: This adapter uses the Router Intents framework to enable users that have funds on any compatible chain to stake their funds on Stader on Ethereum. (Modify as per the needs of your own protocol)
Interface
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
interface IStaderPool {
function deposit(address receiver) external payable returns (uint256);
function swapMaticForMaticXViaInstantPool() external payable;
}
Adapter Contract
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
import {IStaderPool} from "./Interfaces.sol";
import {RouterIntentEoaAdapterWithoutDataProvider, EoaExecutorWithoutDataProvider} from "@routerprotocol/intents-core/contracts/RouterIntentEoaAdapter.sol";
import {Errors} from "@routerprotocol/intents-core/contracts/utils/Errors.sol";
import {IERC20, SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
/**
* @title StaderStakeEth
* @notice Example adapter for staking ETH on Stader protocol.
*/
contract StaderStakeEth is RouterIntentEoaAdapterWithoutDataProvider {
using SafeERC20 for IERC20;
address public immutable ethx;
IStaderPool public immutable staderPool;
constructor(
address __native,
address __wnative,
address __ethx,
address __staderPool
) RouterIntentEoaAdapterWithoutDataProvider(__native, __wnative) {
ethx = __ethx;
staderPool = IStaderPool(__staderPool);
}
function name() public pure override returns (string memory) {
return "StaderStakeEth";
}
function execute(
bytes calldata data
) external payable override returns (address[] memory tokens) {
(address _recipient, uint256 _amount) = parseInputs(data);
if (address(this) == self()) {
require(
msg.value == _amount,
Errors.INSUFFICIENT_NATIVE_FUNDS_PASSED
);
} else if (_amount == type(uint256).max)
_amount = address(this).balance;
bytes memory logData;
(tokens, logData) = _stake(_recipient, _amount);
emit ExecutionEvent(name(), logData);
return tokens;
}
function _stake(
address _recipient,
uint256 _amount
) internal returns (address[] memory tokens, bytes memory logData) {
staderPool.deposit{value: _amount}(_recipient);
tokens = new address ;
tokens[0] = native();
tokens[1] = ethx;
logData = abi.encode(_recipient, _amount);
}
function parseInputs(
bytes memory data
) public pure returns (address, uint256) {
return abi.decode(data, (address, uint256));
}
receive() external payable {}
}
Imports
RouterIntentEoaAdapterWithoutDataProvider
andEoaExecutorWithoutDataProvider
from the Router Protocol Intents package.IERC20
andSafeERC20
for token transfers.
Constructor
The constructor accepts addresses for native token, wnative token and protocol-specific contracts or the contracts containing entry points for the protocol. (e.g., ethx, staderPool).
Functions
execute
: This function has to be present in every adapter contract and is expected to handle the data received from the multi caller contract (BatchTransaction contract). It parses the input, ensures the correct value is passed, and executes the _stake function._stake
: This is an internal function that interacts with the protocol (in this case, the StaderPool) to deposit funds.
Step 4: Customize for Your Protocol
To adapt the contract to your own protocol, follow these steps:
Replace Protocol-Specific Logic
Change the external contract interfaces (e.g.,
IStaderPool
) to match your protocol's interface.Change the constructor arguments according to the protocol. (the address of
native
andwnative
tokens at first two places remains fixed).Adjust the parsing according to the needed arguments in the
execute
function andparseInputs
function.Modify the name of the internal function and logic in
_stake
to interact with protocol’s staking, lending, or swapping functions.Adjust the ERC20 token interactions as necessary (use
SafeERC20
for safety).
In this step, we explain how to customize the PancakeswapMint adapter for PancakeSwap or any other protocol. The goal is to replace protocol-specific logic, imports, and interfaces with the equivalent components of the new protocol you're targeting.
Example: Adapting for a new Protocol (PancakeSwap Mint)
In
PancakeswapMint
adapter, the contract interacts with theIPancakeswapNonfungiblePositionManager
interface for minting positions.
import { IPancakeswapNonfungiblePositionManager } from "./Interfaces.sol";
The core function is
_mint
, which handles the logic for minting a new position on PancakeSwap. This function interacts with the PancakeSwap contract to mint a position by callingnonFungiblePositionManager.mint(mintParams)
.
function _mint(
IPancakeswapNonfungiblePositionManager.MintParams memory mintParams
) internal returns (address[] memory tokens, bytes memory logData) {
(uint256 tokenId, , , ) = nonFungiblePositionManager.mint(mintParams);
tokens = new address ;
tokens[0] = mintParams.token0;
tokens[1] = mintParams.token1;
logData = abi.encode(mintParams, tokenId);
}
The
execute
function handles transferring tokens to the contract and managing approvals before minting a position:
IERC20(mintParams.token0).safeIncreaseAllowance(
address(nonFungiblePositionManager),
mintParams.amount0Desired
);
The
parseInputs
function decodes theMintParams
for the PancakeSwap position manager.
function parseInputs(
bytes memory data
) public pure returns (IPancakeswapNonfungiblePositionManager.MintParams memory) {
return abi.decode(data, (IPancakeswapNonfungiblePositionManager.MintParams));
}
Ensure that the constructor is updated to reflect the new protocol's requirements. In the provided
PancakeswapMint
contract, the constructor accepts the address fornonFungiblePositionManager
:
constructor(
address __native,
address __wnative,
address __nonFungiblePositionManager
)
Step 5: Compile the contract
npx hardhat compile
Ensure there are no compilation errors.
Step 6: Deploy the Adapter
Create a deployment script in scripts/deploy.js:
const hre = require("hardhat");
async function main() {
const StaderStakeEth = await hre.ethers.getContractFactory("StaderStakeEth");
const staderStakeEth = await StaderStakeEth.deploy(
"0xNativeTokenAddress", // FIXED: Replace with actual native token address
"0xWNativeTokenAddress", // FIXED: Replace with actual wrapped native token address
"0xEthxTokenAddress", // PROTOCOL SPECIFIC NEED 1: Replace with the token address or pool address
"0xStaderPoolAddress" // PROTOCOL SPECIFIC NEED 2: if any
);
await staderStakeEth.deployed();
console.log("StaderStakeEth deployed to:", staderStakeEth.address);
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
Deploy the contract:
npx hardhat run scripts/deploy.js --network <your-network>
Step 7: Interact with the Adapter
Once deployed, you can interact with the adapter using the following steps:
Contact us for getting the adapter whitelisted on our BatchTransaction Contract in order to use the intents framework.
Any one can interact with the whitelisted intent adapters via Router's BatchTransaction. Deployed addressed can be found here.
/**
* @dev function to execute batch calls on the same chain
* @param appId Application Id
* @param tokens Addresses of the tokens to fetch from the user
* @param amounts amounts of the tokens to fetch from the user
* @param feeData data for fee deduction
* @param target Addresses of the contracts to call
* @param value Amounts of native tokens to send along with the transactions
* @param callType Type of call. 1: call, 2: delegatecall
* @param data Data of the transactions
*/
function executeBatchCallsSameChain(
uint256 appId,
address[] calldata tokens,
uint256[] calldata amounts,
bytes calldata feeData,
address[] calldata target,
uint256[] calldata value,
uint256[] calldata callType,
bytes[] calldata data
) external payable {}
The data can be created in the following way in order to call any adapter. Note that the protocol data is subject to change according to the needs of the action, here we are just giving an example for how to create data for Stader. The fee data contains following:
(
uint256[] memory _appId,
uint96[] memory _fee,
address[] memory _tokens,
uint256[] memory _amounts,
bool _isActive
)
_appId
: Array of application ids for the specific protocols that you want to invoke as adapters in the transaction._fee
: Array of fee amounts that has to be charged by the protocols in the same order._tokens
: Array of the tokens needed for the protocols in that order._amounts
: Array of amounts for the above tokens._isActive
: If true , the adapters are subject to fees charged by Router.
const amount = ethers.utils.parseEther("1");
const staderData = defaultAbiCoder.encode(
["address", "uint256"],
[deployer.address, amount]
);
const feedata = defaultAbiCoder.encode(
["uint256[]", "uint96[]", "address[]", "uint256[]", "bool"],
[0], [0], [NATIVE_TOKEN], [amount], [true]
);
const tokens = [NATIVE_TOKEN];
const amounts = [amount];
const targets = [staderStakeEthAdapter.address];
const data = [staderData];
const value = [0];
const callType = [2];
// const feeInfo = [{ fee: 0, recipient: zeroAddress() }];
const balBefore = await ethers.provider.getBalance(deployer.address);
const ethxBalBefore = await ethx.balanceOf(deployer.address);
await batchTransaction.executeBatchCallsSameChain(
0,
tokens,
amounts,
feeData,
targets,
value,
callType,
data,
{ value: amount }
);
Conclusion
This guide demonstrates how to create a generic intent adapter using Router Protocol's Intents package. Adapt the contract logic to the protocol by modifying the constructor, external interfaces, and the core functionality (e.g., staking, swapping, adding liquidity, lending). Happy building!
Notes:
You can modify the contract to suit different protocols by adjusting the external contract interface, parsing in
execute
function and logic inside theinternal
function.Make sure to update the deployment addresses (like
native
,wnative
, and protocol-specific contract addresses) in the deployment script.Contact us for getting the adapter whitelisted on our BatchTransaction Contract.
Last updated