Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/rhinestonewtf/warp-router/llms.txt

Use this file to discover all available pages before exploring further.

This guide walks you through building custom adapters to integrate new protocols with the Warp Router. You’ll learn the requirements, best practices, and implementation patterns for creating production-ready adapters.

Adapter Architecture

Adapters are protocol-specific contracts that handle settlement logic via delegatecall from the Router. They inherit the Router’s storage context and permissions during execution. Key Principles from AdapterBase.sol:8-61:
  • Always executed via delegatecall from the Router
  • Access Router’s storage and balance during execution
  • Must return function selector for validation
  • Must implement ERC165 interface detection
  • Only interact with trusted, audited protocols

Implementation Requirements

Every adapter must meet these critical requirements:

1. Return Function Selector

All fill and claim functions must return their own selector:
// From AdapterBase.sol:25
function myFillOperation(...) external returns (bytes4) {
    // Settlement logic here
    return this.myFillOperation.selector;
}

2. Implement ERC165

Support interface detection for all settlement functions:
// From AdapterBase.sol:43-50
function supportsInterface(bytes4 interfaceId) public pure override returns (bool) {
    return interfaceId == this.myFillOperation.selector ||
           interfaceId == this.myClaimOperation.selector ||
           super.supportsInterface(interfaceId);
}

3. Use onlyViaRouter Modifier

Protect all external functions from direct calls:
// From AdapterBase.sol:99-102
modifier onlyViaRouter() {
    _onlyRouterAdapter();
    _;
}

function myFillOperation(...) external onlyViaRouter returns (bytes4) {
    // Only callable via Router delegatecall
}

4. Security Constraints

From AdapterBase.sol:18-21:
  • Never make direct calls to untrusted contracts
  • Use only trusted, well-audited protocols
  • Remember you’re executing in Router’s context
  • Storage writes affect Router’s storage

Building Your First Adapter

Let’s build a simple adapter for a custom DEX protocol:
1

Inherit from AdapterBase

Start with the base contract:
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.28;

import { AdapterBase, SemVer } from "../../base/adapter/AdapterBase.sol";

contract MyDexAdapter is AdapterBase {
    // Your implementation
}
2

Set up constructor

Initialize with Router and Arbiter addresses:
// From AdapterBase.sol:82-89
constructor(
    address router,
    address arbiter  // Or address(0) for self-arbitration
) 
    AdapterBase(router, arbiter)
    SemVer(0, 0)  // Major version 0, minor version 0
{ }
The constructor sets:
  • _ROUTER: Immutable router address for security checks
  • ARBITER: Contract that validates settlements (from AdapterBase.sol:69)
3

Define data structures

Create structs for your fill operations:
struct FillData {
    address tokenIn;
    address tokenOut;
    uint256 amountIn;
    uint256 minAmountOut;
    address recipient;
    bytes dexData;
}
4

Implement fill function

Add your settlement logic:
function mydex_handleSwapFill(FillData calldata fillData)
    external
    payable
    onlyViaRouter
    returns (bytes4)
{
    // 1. Extract solver context
    address solverRecipient = _extractSolverRecipient();
    
    // 2. Pre-fund user with output tokens
    _transferTokens(msg.sender, fillData.recipient, fillData.tokenOut, fillData.minAmountOut);
    
    // 3. Execute DEX swap
    uint256 amountOut = _executeSwap(fillData);
    
    // 4. Ensure slippage requirements met
    require(amountOut >= fillData.minAmountOut, "Slippage too high");
    
    // 5. Return selector for validation
    return this.mydex_handleSwapFill.selector;
}
5

Extract solver context

Implement context extraction:
function _extractSolverRecipient() internal pure returns (address) {
    (uint256 contextLength, bytes calldata context) = _loadRelayerContext();
    require(contextLength == 20, InvalidRelayerContext());
    return address(bytes20(context[:20]));
}
The _loadRelayerContext() helper is provided by AdapterBase.sol:137-144.
6

Add interface detection

Implement ERC165 support:
function supportsInterface(bytes4 selector) 
    public 
    pure 
    override 
    returns (bool) 
{
    return selector == this.mydex_handleSwapFill.selector ||
           super.supportsInterface(selector);
}

Advanced Patterns

Using AdapterBasePrefund

For adapters that need to prefund recipients, inherit from AdapterBasePrefund:
import { AdapterBasePrefund } from "../../base/adapter/AdapterBasePrefund.sol";

contract MyAdapter is AdapterBasePrefund {
    constructor(address router, address arbiter) 
        AdapterBasePrefund(router, arbiter) 
        SemVer(0, 0)
    { }
    
    function myFill(FillData calldata data) 
        external 
        payable 
        onlyViaRouter 
        returns (bytes4) 
    {
        // Prefund recipient with output tokens
        _prefundRecipient({
            from: msg.sender,
            to: data.recipient,
            tokenOut: data.tokenOut,
            amountOut: data.amountOut
        });
        
        // Then execute settlement
        _executeSettlement(data);
        
        return this.myFill.selector;
    }
}
From AdapterBasePrefund.sol:42-50, the helper handles both native ETH and ERC20 tokens:
function _prefundRecipient(
    address from,
    address to,
    address tokenOut,
    uint256 amountOut
) internal {
    if (tokenOut == Constants.NATIVE_TOKEN) {
        to.safeTransferETH(amountOut);
    } else {
        tokenOut.safeTransferFrom(from, to, amountOut);
    }
}

Multi-Token Support

For adapters handling multiple tokens (from AdapterBasePrefund.sol:22-31):
function _prefundRecipient(
    address from,
    address to,
    uint256[2][] calldata tokenOut
) internal {
    uint256 length = tokenOut.length;
    for (uint256 i; i < length;) {
        _prefundRecipient(from, to, tokenOut[i][0].toAddress(), tokenOut[i][1]);
        unchecked { ++i; }
    }
}

Custom Solver Context

Define complex context structures:
struct MyRelayerContext {
    address tokenInRecipient;
    uint256 maxSlippageBps;
    address feeRecipient;
    uint256 feeBps;
}

function _loadMyContext() internal pure returns (MyRelayerContext memory) {
    (uint256 length, bytes calldata context) = _loadRelayerContext();
    
    // Validate length: 20 + 32 + 20 + 32 = 104 bytes
    require(length == 104, InvalidRelayerContext());
    
    return MyRelayerContext({
        tokenInRecipient: address(bytes20(context[0:20])),
        maxSlippageBps: uint256(bytes32(context[20:52])),
        feeRecipient: address(bytes20(context[52:72])),
        feeBps: uint256(bytes32(context[72:104]))
    });
}

// Provide encoding helper for solvers
function encodeRelayerContext(
    address tokenInRecipient,
    uint256 maxSlippageBps,
    address feeRecipient,
    uint256 feeBps
) external pure returns (bytes memory) {
    return abi.encodePacked(
        tokenInRecipient,
        maxSlippageBps,
        feeRecipient,
        feeBps
    );
}

Skip Relayer Context

For adapters that don’t need solver context (from IntentExecutorAdapter.sol:227-229):
import { AdapterTagLib } from "@rhinestone/compact-utils/src/router/lib/v1/AdapterTagLib.sol";
import { Constants } from "@rhinestone/compact-utils/src/types/Constants.sol";

function ADAPTER_TAG() external pure override returns (bytes12) {
    return Constants.DEFAULT_ADAPTER_TAG.setSkipRelayerContext();
}
This tells the Router not to consume a context entry for this adapter in batch operations.

Claim Operations

Implement claim functions for resource unlocking:
function mydex_handleClaim(ClaimData calldata claimData)
    external
    payable
    onlyViaRouter
    returns (bytes4)
{
    // Extract solver context
    address recipient = _extractSolverRecipient();
    
    // Call protocol to unlock funds
    uint256 claimed = IMyProtocol(ARBITER).claimFunds(
        claimData.user,
        claimData.token,
        claimData.amount,
        recipient
    );
    
    // Validate claim succeeded
    require(claimed == claimData.amount, "Claim failed");
    
    return this.mydex_handleClaim.selector;
}

Real-World Example: SameChainAdapter

Let’s examine the production SameChainAdapter from SameChainAdapter.sol:

Structure

// From SameChainAdapter.sol:58
contract SameChainAdapter is AdapterBasePrefund, SameChainArbiter {
    
    // Data structure for Compact fills
    struct FillDataCompact {
        Types.Order order;
        Types.Signatures userSigs;
        bytes32[] otherElements;
        bytes allocatorData;
    }
    
    constructor(
        address router,
        address compact,
        address arbiter,
        address addressBook
    )
        AdapterBasePrefund(router, arbiter)
        SemVer(Version.SAMECHAIN_VERSION_MINOR, Version.SAMECHAIN_VERSION_PATCH)
        SameChainArbiter(router, compact, addressBook)
    { }
}

Context Extraction

// From SameChainAdapter.sol:115-120
function _tokenInRecipient() internal pure returns (address tokenInRecipient) {
    (uint256 relayerContextLength, bytes calldata relayerContext) = _loadRelayerContext();
    require(relayerContextLength == 20, InvalidRelayerContext());
    return address(bytes20(relayerContext[:20]));
}

Fill Implementation

// From SameChainAdapter.sol:172-177
function samechain_compact_handleFill(FillDataCompact calldata fillData) 
    external 
    payable 
    onlyViaRouter 
    returns (bytes4 selector) 
{
    _handleCompactFill(fillData, _tokenInRecipient());
    return this.samechain_compact_handleFill.selector;
}

// From SameChainAdapter.sol:233-247
function _handleCompactFill(
    FillDataCompact calldata fillData, 
    address tokenInRecipient
) internal {
    // 1. Pre-fund the recipient
    _prefundRecipient({ 
        from: msg.sender, 
        to: fillData.order.recipient, 
        tokenOut: fillData.order.tokenOut 
    });
    
    // 2. Call arbiter to process settlement
    (address sponsor, uint256 nonce) = SameChainArbiter(ARBITER)
        .handleCompact_NotarizedChain({
            order: fillData.order,
            sigs: fillData.userSigs,
            otherElements: fillData.otherElements,
            allocatorData: fillData.allocatorData,
            relayer: tokenInRecipient
        });

    // 3. Emit event
    emit RouterFilled(sponsor, nonce);
}

Interface Support

// From SameChainAdapter.sol:295-298
function supportsInterface(bytes4 selector) 
    public 
    pure 
    override(AdapterBase, ArbiterBase) 
    returns (bool supported) 
{
    return selector == this.samechain_compact_handleFill.selector ||
           selector == this.samechain_permit2_handleFill.selector ||
           AdapterBase.supportsInterface(selector) ||
           ArbiterBase.supportsInterface(selector);
}

Testing Your Adapter

Unit Tests

contract MyAdapterTest is Test {
    MyAdapter adapter;
    Router router;
    
    function setUp() public {
        router = new Router(atomicSigner, adder, remover);
        adapter = new MyAdapter(address(router), arbiter);
    }
    
    function testFillOperation() public {
        // Prepare fill data
        FillData memory fillData = FillData({
            tokenIn: address(usdc),
            tokenOut: address(dai),
            amountIn: 1000e6,
            minAmountOut: 999e18,
            recipient: user,
            dexData: hex"..."
        });
        
        // Prepare solver context
        bytes memory context = abi.encodePacked(solverAddress);
        
        // Prepare calldata with context appended
        bytes memory adapterCalldata = abi.encodeWithSelector(
            adapter.mydex_handleSwapFill.selector,
            fillData
        );
        bytes memory fullCalldata = abi.encodePacked(
            adapterCalldata,
            context,
            uint256(context.length)
        );
        
        // Execute via delegatecall (simulating Router)
        vm.prank(solver);
        (bool success, bytes memory result) = address(adapter).delegatecall(fullCalldata);
        
        assertTrue(success);
        assertEq(bytes4(result), adapter.mydex_handleSwapFill.selector);
    }
    
    function testOnlyViaRouter() public {
        FillData memory fillData = /* ... */;
        
        // Direct call should fail
        vm.expectRevert(AdapterBase.OnlyDelegateCall.selector);
        adapter.mydex_handleSwapFill(fillData);
    }
    
    function testSupportsInterface() public {
        assertTrue(adapter.supportsInterface(
            adapter.mydex_handleSwapFill.selector
        ));
        assertTrue(adapter.supportsInterface(
            type(IAdapter).interfaceId
        ));
    }
}

Integration Tests

function testRouterIntegration() public {
    // Install adapter in router
    vm.prank(adder);
    router.installFillAdapter(
        0x0000,  // Version 0.0
        adapter.mydex_handleSwapFill.selector,
        address(adapter)
    );
    
    // Prepare fill
    bytes memory context = abi.encodePacked(solverAddress);
    bytes memory calldata = abi.encodeWithSelector(
        adapter.mydex_handleSwapFill.selector,
        fillData
    );
    
    // Execute through router
    vm.prank(solver);
    router.routeFill(context, calldata);
    
    // Verify results
    assertEq(token.balanceOf(user), expectedAmount);
}

Deployment and Registration

See the Deployment Guide for details on deploying and registering your adapter with the Router.

Best Practices

Security

  • Validate inputs: Always check addresses, amounts, and context data
  • Use SafeTransferLib: Never use raw transfer() or transferFrom()
  • Trusted protocols only: Only integrate with audited, production-ready protocols
  • Reentrancy protection: Be aware of the Router’s reentrancy guard

Gas Optimization

  • Minimize storage: Remember you’re in Router’s context - avoid unnecessary SSTORE operations
  • Use assembly carefully: For calldata extraction, but maintain memory safety
  • Cache storage reads: If reading Router storage, cache values
  • Efficient encoding: Use encodePacked for solver context

Maintainability

  • Clear documentation: Document context format and requirements
  • Helper functions: Provide encoding helpers for solvers
  • Versioning: Use semantic versioning properly
  • Events: Emit events for off-chain tracking

Testing

  • Unit tests: Test each function in isolation
  • Integration tests: Test with actual Router
  • Fuzz testing: Test with random inputs
  • Gas benchmarks: Measure and optimize gas usage

Next Steps

Solver Context

Learn about solver context formats and patterns

Deployment

Deploy and register your adapter

Build docs developers (and LLMs) love