Kodiak Island Router
A helper contract that enables easy, secure liquidity provisioning and withdrawals
Overview
The IslandRouter contract serves as a helper contract for users to easily provide liquidity to Kodiak Islands. It handles the complexities of depositing tokens, slippage protection, and token swaps when needed, while ensuring optimal liquidity provision.
Kodiak Islands require liquidity providers to deposit tokens in specific ratios that match the Island's underlying Kodiak V3 position. The router provides several key protections:
Minimum output amount protection for swaps when providing liquidity with single token.
Deposit ratio slippage protection
Minimum LP token (shares) protection
Managing Liquidity
Token Deposit Ratio
The router handles liquidity addition by:
Calculating the optimal deposit amounts using
island.getMintAmounts()
Ensuring the amounts meet minimum requirements
Transferring tokens and minting LP tokens
Steps to Add Liquidity
Approve the router to spend your tokens (both tokens or one token in case of singleSided
Choose the appropriate liquidity addition method based on your tokens
If the liquidity addition is using a single token, find the appropriate swap data and use Kodiak Quoter api to get the calldata according to the swap params.
Set reasonable slippage parameters
Execute transaction
Kodiak Island LP tokens sent to the receiver as passed in params
In case of zaps msg.sender receives back any unused token0 or token1
Adding liquidity with both tokens
1. Standard Two Token Deposit
Prerequisites
Token0 and Token1 balances sufficient for desired liquidity.
Approved router contract to spend both tokens.
function addLiquidity(
IKodiakIsland island, // Address of the Kodiak Island
uint256 amount0Max, // Maximum amount of token0 willing to deposit
uint256 amount1Max, // Maximum amount of token1 willing to deposit
uint256 amount0Min, // Minimum acceptable token0 deposit (slippage protection)
uint256 amount1Min, // Minimum acceptable token1 deposit (slippage protection)
uint256 amountSharesMin, // Minimum IslandTokens to receive
address receiver // Address to receive LP tokens
) external returns (
uint256 amount0, // Actual token0 amount deposited
uint256 amount1, // Actual token1 amount deposited
uint256 mintAmount // LP tokens received
)
Implementation Example
// 1. Start with maximum amounts to deposit, for example 10 token0 and 10 token1
const amount0Max = ethers.parseUnits("10", token0Decimals);
const amount1Max = ethers.parseUnits("10", token1Decimals);
// 2. Find the appropriate ratio of tokens to deposit
(amount0Used, amount1Used, ) = await island.getMintAmounts(amount0Max, amount1Max)
amount0Max = amount0Used
amount1Max = amount1Used
// 3. Set slippage tolerance for the minimum amounts of token0 and token1
// (e.g. 1% slippage). This means that atleast 99% of these tokens should
// be deposited in the pool. This gives you a protection from the pool price
// deviating a lot before your transaction goes through
// which affects the tokens and ratio they are deposited in
const amount0Min = amount0Max.mul(99).div(100);
const amount1Min = amount1Max.mul(99).div(100);
const amountSharesMin = 0; // Set based on expected shares
// 4. Approve router
await token0.approve(routerAddress, amount0Max);
await token1.approve(routerAddress, amount1Max);
// 5. Callstatic deposit to get the amountShares minted
const simulationResult = await router.callStatic.addLiquidity(
islandAddress,
amount0Max,
amount1Max,
amount0Min,
amount1Min,
amountSharesMin,
receiverAddress
);
// 6. Mint Island tokens, Use the mintAmount from above
// add a comfortable slippage (ex 1%) to this and use this for amountSharesMin
// Use BPS for higher precision.
amountSharesMin = simulationResult[2]
.mul(99).div(100).toString()
const tx = await router.addLiquidity(
islandAddress,
amount0Max,
amount1Max,
amount0Min,
amount1Min,
amountSharesMin,
receiverAddress
);
2. Native BERA + Token Deposit
It is important that one of the tokens in the underlying pool is WBERA for depositing with native BERA.
Prerequisites
Sender must have sufficient NativeToken and Token1 balances for desired liquidity.
Approve router contract to spend Token1 tokens.
Send required bera as msg.value
// Assuming token0 is WBERA
function addLiquidityNative(
IKodiakIsland island, // Address of the Kodiak Island
uint256 amount0Max, // Maximum BERA amount
uint256 amount1Max, // Maximum token amount
uint256 amount0Min, // Minimum BERA deposit
uint256 amount1Min, // Minimum token deposit
uint256 amountSharesMin, // Minimum LP tokens to receive
address receiver // Address to receive LP tokens
) external payable returns (
uint256 amount0, // Actual BERA amount deposited
uint256 amount1, // Actual token amount deposited
uint256 mintAmount // LP tokens received
)
Implementation is same as above except for sending native token BERA as msg.value
const tx = await router.addLiquidityNative(
islandAddress,
amount0Max,
amount1Max,
amount0Min,
amount1Min,
amountSharesMin,
receiverAddress,
{
value: amount0Max // assuming island.token0() is WBERA
}
);
Adding Liquidity with a single token
When depositing into a concentrated liquidity position like an Island, it's crucial to understand that the underlying tokens need to be in a specific ratio to maximize the value of the position. If a user wants to deposit a single token, a swap is required to balance the tokens before adding liquidity to the position. This process involves calculating the ideal amount of the input token to swap for the other token in the pair.
The IslandRouter uses a external swap Routers to swap this token to achieve maximal efficiency during the swap. The kodiak router is whitelisted to begin with and other routers will be whitelisted later to further increase this swap efficiency.
Prerequisites
Sufficient balance of input token or native token.
Approved router for input token amount
Understanding on how to find the swap params such that after the swap the token0 and token1 are in correct ratio to deposit into the island
Understanding on how to generate swap calldata using Kodiak Router.
Single Token Deposit Implementation
function addLiquiditySingle(
IKodiakIsland island, // Island address
uint256 totalAmountIn, // Total input token amount
uint256 amountSharesMin, // Minimum LP tokens to receive
uint256 maxStakingSlippageBPS, // Max slippage in basis points (100 = 1%)
RouterSwapParams calldata swapData, // Swap parameters for converting portion of input
address receiver // LP token recipient
) external returns (
uint256 amount0, // Final token0 amount deposited
uint256 amount1, // Final token1 amount deposited
uint256 mintAmount // LP tokens received
)
struct RouterSwapParams {
bool zeroForOne; // Swap direction
uint256 amountIn; // Amount to swap
uint256 minAmountOut; // Minimum output from swap
bytes routeData; // Encoded swap route data
}
Implementation Example
// 1. Prepare swap parameters. refer the section on finding the swap amounts
const swapParams = {
zeroForOne: true, // true if swapping token0 for token1
amountIn: swapAmount,
minAmountOut: minimumSwapOutput,
routeData: swapCalldata
};
// 2. Set slippage parameters
const maxStakingSlippageBPS = 100; // 1% slippage
const minShares = minShares; // find the min shares by making a static call
// and adding a 1% slippage to it as demonstrated in previous examples
// 3. Approve island router to spend your inputToken
await token0.approve(islandRouterAddress, totalAmount);
// 3. Execute single token deposit
const tx = await islandRouter.addLiquiditySingle(
islandAddress,
totalAmount,
minShares,
maxStakingSlippageBPS,
swapParams,
receiverAddress
);
Adding Liquidity with Native BERA
When you want to provide liquidity to an island with native token BERA.
Prerequisites
Native BERA balance
Island must have WBERA as one of its tokens
Single Native Token Deposit
function addLiquiditySingleNative(
IKodiakIsland island, // Island address (must include WBERA)
uint256 amountSharesMin, // Minimum LP tokens to receive
uint256 maxStakingSlippageBPS, // Max slippage in basis points
RouterSwapParams calldata swapData, // Swap parameters
address receiver // LP token recipient
) external payable returns (
uint256 amount0, // Final token0 amount deposited
uint256 amount1, // Final token1 amount deposited
uint256 mintAmount // LP tokens received
)
Implementation Example
// 1. Calculate BERA amount to send
const beraAmount = ethers.parseEther("1.0");
// 2. Prepare swap parameters
const swapParams = {
zeroForOne: true,
amountIn: swapAmount,
minAmountOut: minOutput,
routeData: swapCalldata
};
// 3. Execute native deposit
const tx = await islandRouter.addLiquiditySingleNative(
islandAddress,
minimumShares,
100, // 1% max slippage
swapParams,
receiverAddress,
{ value: beraAmount }
);
Important Notes
For native BERA deposits, one token in the Island pair must be WBERA
Unused BERA is automatically returned to sender as native tokens when performing single sided deposits with native BERA
If the msg.sender is a contract, it must handle both type of unused tokens i.e ERC20 tokens and native tokens when depositing with single token or native token
Set appropriate slippage parameters based on market conditions
Monitor gas costs, especially for operations involving swaps
Verify all addresses and amounts before execution
Consider using view functions to estimate outputs before executing transactions
Removing Liquidity
The router exposes two functions for removing liquidity with slippage control
removeLiquidity
function removeLiquidity(
IKodiakIsland island,
uint256 burnAmount,
uint256 amount0Min,
uint256 amount1Min,
address receiver
) external returns (uint256 amount0, uint256 amount1, uint128 liquidityBurned)
Purpose: Removes liquidity from a Kodiak Island position by burning LP tokens and getting the underlying tokens.
Parameters:
island
: The address of the Kodiak Island position to withdraw fromburnAmount
: The quantity of Kodiak Island LP tokens to burnamount0Min
: Minimum amount of token0 that must be received (slippage protection)amount1Min
: Minimum amount of token1 that must be received (slippage protection)receiver
: The address that will receive the withdrawn tokens
Returns:
amount0
: The actual amount of token0 receivedamount1
: The actual amount of token1 receivedliquidityBurned
: The amount of liquidity removed from the underlying Kodiak V3 position
Important Notes:
Caller must approve the router contract to spend their Kodiak Island LP tokens
Transaction will revert if received amounts are less than specified minimums
Useful for standard token pairs where wrapped native token conversion isn't needed
removeLiquidityNative
function removeLiquidityNative(
IKodiakIsland island,
uint256 burnAmount,
uint256 amount0Min,
uint256 amount1Min,
address payable receiver
) external returns (uint256 amount0, uint256 amount1, uint128 liquidityBurned)
Purpose: Similar to removeLiquidity
but specifically handles positions involving WBERA (wrapped BERA), automatically unwrapping it to native BERA before returning it to the user.
Parameters:
island
: The address of the Kodiak Island position to withdraw fromburnAmount
: The quantity of Kodiak Island LP tokens to burnamount0Min
: Minimum amount of token0 that must be received (slippage protection)amount1Min
: Minimum amount of token1 that must be received (slippage protection)receiver
: The address that will receive the withdrawn tokens (must be payable to receive native BERA)
Returns:
amount0
: The actual amount of token0 receivedamount1
: The actual amount of token1 receivedliquidityBurned
: The amount of liquidity removed from the underlying Kodiak V3 position
Important Notes:
Caller must approve the router contract to spend their Kodiak Island LP tokens
One of the tokens in the pair must be WBERA
WBERA portion will be automatically unwrapped and sent as native BERA to the receiver
The non-WBERA token will be sent as is to the receiver
Transaction will revert if received amounts are less than specified minimums
Receiver address must be payable to receive native BERA
Particularly useful for positions involving native BERA pairs
For both functions, it's recommended to:
Calculate expected output amounts before calling
Include reasonable slippage protection via
amount0Min
andamount1Min
Ensure sufficient approvals are in place
Handle both success and failure cases in your integration
Verify received amounts match expectations after the transaction
ABI
```json
[
{
"type": "constructor",
"inputs": [
{
"name": "_wBera",
"type": "address",
"internalType": "contract IWETH"
},
{
"name": "_kodiakRouter",
"type": "address",
"internalType": "address"
}
],
"stateMutability": "nonpayable"
},
{
"type": "receive",
"stateMutability": "payable"
},
{
"type": "function",
"name": "addLiquidity",
"inputs": [
{
"name": "island",
"type": "address",
"internalType": "contract IKodiakIsland"
},
{
"name": "amount0Max",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "amount1Max",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "amount0Min",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "amount1Min",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "amountSharesMin",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "receiver",
"type": "address",
"internalType": "address"
}
],
"outputs": [
{
"name": "amount0",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "amount1",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "mintAmount",
"type": "uint256",
"internalType": "uint256"
}
],
"stateMutability": "nonpayable"
},
{
"type": "function",
"name": "addLiquidityNative",
"inputs": [
{
"name": "island",
"type": "address",
"internalType": "contract IKodiakIsland"
},
{
"name": "amount0Max",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "amount1Max",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "amount0Min",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "amount1Min",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "amountSharesMin",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "receiver",
"type": "address",
"internalType": "address"
}
],
"outputs": [
{
"name": "amount0",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "amount1",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "mintAmount",
"type": "uint256",
"internalType": "uint256"
}
],
"stateMutability": "payable"
},
{
"type": "function",
"name": "addLiquiditySingle",
"inputs": [
{
"name": "island",
"type": "address",
"internalType": "contract IKodiakIsland"
},
{
"name": "totalAmountIn",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "amountSharesMin",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "maxStakingSlippageBPS",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "swapData",
"type": "tuple",
"internalType": "struct RouterSwapParams",
"components": [
{
"name": "amountIn",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "minAmountOut",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "zeroForOne",
"type": "bool",
"internalType": "bool"
},
{
"name": "routeData",
"type": "bytes",
"internalType": "bytes"
}
]
},
{
"name": "receiver",
"type": "address",
"internalType": "address"
}
],
"outputs": [
{
"name": "amount0",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "amount1",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "mintAmount",
"type": "uint256",
"internalType": "uint256"
}
],
"stateMutability": "nonpayable"
},
{
"type": "function",
"name": "addLiquiditySingleNative",
"inputs": [
{
"name": "island",
"type": "address",
"internalType": "contract IKodiakIsland"
},
{
"name": "amountSharesMin",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "maxStakingSlippageBPS",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "swapData",
"type": "tuple",
"internalType": "struct RouterSwapParams",
"components": [
{
"name": "amountIn",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "minAmountOut",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "zeroForOne",
"type": "bool",
"internalType": "bool"
},
{
"name": "routeData",
"type": "bytes",
"internalType": "bytes"
}
]
},
{
"name": "receiver",
"type": "address",
"internalType": "address"
}
],
"outputs": [
{
"name": "amount0",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "amount1",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "mintAmount",
"type": "uint256",
"internalType": "uint256"
}
],
"stateMutability": "payable"
},
{
"type": "function",
"name": "kodiakRouter",
"inputs": [],
"outputs": [
{
"name": "",
"type": "address",
"internalType": "address"
}
],
"stateMutability": "view"
},
{
"type": "function",
"name": "removeLiquidity",
"inputs": [
{
"name": "island",
"type": "address",
"internalType": "contract IKodiakIsland"
},
{
"name": "burnAmount",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "amount0Min",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "amount1Min",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "receiver",
"type": "address",
"internalType": "address"
}
],
"outputs": [
{
"name": "amount0",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "amount1",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "liquidityBurned",
"type": "uint128",
"internalType": "uint128"
}
],
"stateMutability": "nonpayable"
},
{
"type": "function",
"name": "removeLiquidityNative",
"inputs": [
{
"name": "island",
"type": "address",
"internalType": "contract IKodiakIsland"
},
{
"name": "burnAmount",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "amount0Min",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "amount1Min",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "receiver",
"type": "address",
"internalType": "address payable"
}
],
"outputs": [
{
"name": "amount0",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "amount1",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "liquidityBurned",
"type": "uint128",
"internalType": "uint128"
}
],
"stateMutability": "nonpayable"
},
{
"type": "function",
"name": "wBera",
"inputs": [],
"outputs": [
{
"name": "",
"type": "address",
"internalType": "contract IWETH"
}
],
"stateMutability": "view"
}
]
```
Last updated