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 and EoaExecutorWithoutDataProvider from the Router Protocol Intents package.
IERC20 and SafeERC20 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 and wnative tokens at first two places remains fixed).
Adjust the parsing according to the needed arguments in the execute function and parseInputs 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 the IPancakeswapNonfungiblePositionManager 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 calling nonFungiblePositionManager.mint(mintParams).
The parseInputs function decodes the MintParams for the PancakeSwap position manager.
function parseInputs(
bytes memory data
) public pure returns (IPancakeswapNonfungiblePositionManager.MintParams memory) {
return abi.decode(data, (IPancakeswapNonfungiblePositionManager.MintParams));
}
Depending on the protocol, you need to replace this with the appropriate input structure that matches the action being performed.
Ensure that the constructor is updated to reflect the new protocol's requirements. In the provided PancakeswapMint contract, the constructor accepts the address for nonFungiblePositionManager:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
import {IPancakeswapNonfungiblePositionManager} 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 "../../../utils/SafeERC20.sol";
/**
* @title PancakeswapMint
* @notice Minting a new position on Pancakeswap.
*/
contract PancakeswapMint is RouterIntentEoaAdapterWithoutDataProvider, PancakeswapHelpers {
using SafeERC20 for IERC20;
IPancakeswapNonfungiblePositionManager
public immutable nonFungiblePositionManager;
constructor(
address __native,
address __wnative,
address __nonFungiblePositionManager
)
RouterIntentEoaAdapterWithoutDataProvider(__native, __wnative)
PancakeswapHelpers(__nonFungiblePositionManager)
// solhint-disable-next-line no-empty-blocks
{
nonFungiblePositionManager = IPancakeswapNonfungiblePositionManager(
__nonFungiblePositionManager
);
}
function name() public pure override returns (string memory) {
return "PancakeswapMint";
}
/**
* @inheritdoc EoaExecutorWithoutDataProvider
*/
function execute(
bytes calldata data
) external payable override returns (address[] memory tokens) {
IPancakeswapNonfungiblePositionManager.MintParams
memory mintParams = parseInputs(data);
// If the adapter is called using `call` and not `delegatecall`
if (address(this) == self()) {
if (mintParams.token0 != native())
IERC20(mintParams.token0).safeTransferFrom(
msg.sender,
self(),
mintParams.amount0Desired
);
else
require(
msg.value == mintParams.amount0Desired,
Errors.INSUFFICIENT_NATIVE_FUNDS_PASSED
);
if (mintParams.token1 != native())
IERC20(mintParams.token1).safeTransferFrom(
msg.sender,
self(),
mintParams.amount1Desired
);
else
require(
msg.value == mintParams.amount1Desired,
Errors.INSUFFICIENT_NATIVE_FUNDS_PASSED
);
} else {
if (mintParams.amount0Desired == type(uint256).max)
mintParams.amount0Desired = getBalance(
mintParams.token0,
address(this)
);
if (mintParams.amount1Desired == type(uint256).max)
mintParams.amount1Desired = getBalance(
mintParams.token1,
address(this)
);
}
if (mintParams.token0 == native()) {
convertNativeToWnative(mintParams.amount0Desired);
mintParams.token0 = wnative();
}
if (mintParams.token1 == native()) {
convertNativeToWnative(mintParams.amount1Desired);
mintParams.token1 = wnative();
}
IERC20(mintParams.token0).safeIncreaseAllowance(
address(nonFungiblePositionManager),
mintParams.amount0Desired
);
IERC20(mintParams.token1).safeIncreaseAllowance(
address(nonFungiblePositionManager),
mintParams.amount1Desired
);
bytes memory logData;
(tokens, logData) = _mint(mintParams);
emit ExecutionEvent(name(), logData);
return tokens;
}
//////////////////////////// ACTION LOGIC ////////////////////////////
function _mint(
IPancakeswapNonfungiblePositionManager.MintParams memory mintParams
) internal returns (address[] memory tokens, bytes memory logData) {
(uint256 tokenId, , , ) = nonFungiblePositionManager.mint(mintParams);
tokens = new address[](2);
tokens[0] = mintParams.token0;
tokens[1] = mintParams.token1;
logData = abi.encode(mintParams, tokenId);
}
/**
* @dev function to parse input data.
* @param data input data.
*/
function parseInputs(
bytes memory data
)
public
pure
returns (IPancakeswapNonfungiblePositionManager.MintParams memory)
{
return
abi.decode(
data,
(IPancakeswapNonfungiblePositionManager.MintParams)
);
}
// solhint-disable-next-line no-empty-blocks
receive() external payable {}
}
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:
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 the internal 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.