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
- RouterIntentEoaAdapterWithoutDataProviderand- EoaExecutorWithoutDataProviderfrom the Router Protocol Intents package.
- IERC20and- SafeERC20for 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 - nativeand- wnativetokens at first two places remains fixed).
- Adjust the parsing according to the needed arguments in the - executefunction and- parseInputsfunction.
- Modify the name of the internal function and logic in - _staketo interact with protocol’s staking, lending, or swapping functions.
- Adjust the ERC20 token interactions as necessary (use - SafeERC20for 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 - PancakeswapMintadapter, the contract interacts with the- IPancakeswapNonfungiblePositionManagerinterface 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).
    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 - executefunction handles transferring tokens to the contract and managing approvals before minting a position:
IERC20(mintParams.token0).safeIncreaseAllowance(
  address(nonFungiblePositionManager),
  mintParams.amount0Desired
);- The - parseInputsfunction decodes the- MintParamsfor 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 - PancakeswapMintcontract, the constructor accepts the address for- nonFungiblePositionManager:
    constructor(
    address __native,
    address __wnative,
    address __nonFungiblePositionManager
    )Step 5: Compile the contract
npx hardhat compileEnsure 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 - executefunction and logic inside the- internalfunction.
- 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
