Overview
Kodiak Islands are concentrated liquidity management vaults built on top of Kodiak V3 pools. This guide covers the technical aspects of integrating with Kodiak Islands from a smart contract perspective.
1. Usage with existing/Deployed Islands
What can be achieved
Depositing into existing Islands
Withdrawing from existing Islands
Checking current island position
Checking current fee earned
Calling rebalance to invest any pending fees
Prerequisites
Before integrating with Kodiak Islands, you should understand:
Kodiak V3 concentrated liquidity concepts
ERC20 token standards and approvals
Basic understanding of what's slippage and slippage protection mechanisms
Deployed contract addresses
Understanding of token ratio that is needed to deposit tokens into the island.
Finding the correct deposit params for double sided token deposits.
Finding the correct deposit and swap params for single sided token deposits.
Refer the section on Understanding Token Deposit Ratio for in dept guide.
Core Contracts
The main contracts involved for integration with existing islands are:
Island Router Contract : Handles complex operations like zaps and slippage protection
Island Contract : The actual vault contract managing the Kodiak V3 position
Integration Examples
1. Basic Position Information
Copy interface IKodiakIsland {
function getUnderlyingBalances() external view returns (uint256 amount0Current, uint256 amount1Current);
function token0() external view returns (IERC20);
function token1() external view returns (IERC20);
function pool() external view returns (IUniswapV3Pool);
function lowerTick() external view returns (int24);
function upperTick() external view returns (int24);
}
2. Depositing with Both Tokens
Copy interface IKodiakIslandRouter {
function addLiquidity(
IKodiakIsland island,
uint256 amount0Max,
uint256 amount1Max,
uint256 amount0Min,
uint256 amount1Min,
uint256 amountSharesMin,
address receiver
) external returns (uint256 amount0, uint256 amount1, uint256 mintAmount);
}
contract IslandDepositor {
IKodiakIslandRouter public immutable router;
constructor(address _router) {
router = IKodiakIslandRouter(_router);
}
function deposit(
IKodiakIsland island,
uint256 amount0,
uint256 amount1,
uint256 slippageBPS,
uint minShares
) external returns (uint256 mintAmount) {
// Transfer tokens to this contract
IERC20(island.token0()).transferFrom(msg.sender, address(this), amount0);
IERC20(island.token1()).transferFrom(msg.sender, address(this), amount1);
// Approve router
IERC20(island.token0()).approve(address(router), amount0);
IERC20(island.token1()).approve(address(router), amount1);
// Calculate minimum amounts with slippage
uint256 amount0Min = amount0 * (10000 - slippageBPS) / 10000;
uint256 amount1Min = amount1 * (10000 - slippageBPS) / 10000;
// Add liquidity
(,, mintAmount) = router.addLiquidity(
island,
amount0,
amount1,
amount0Min,
amount1Min,
minShares, // Min shares, can be calculated based on simulation before calling this
msg.sender
);
}
}
3. Single Token Deposits (Zaps)
For single token deposits, the swap calldata needs to be prepared off-chain using the Kodiak Quoter API. The process involves:
Getting the optimal swap amount using the formula:
Copy // Pseudo-code for swap amount calculation
function calculateSwapAmount(
uint256 totalAmount,
uint256 price,
uint256 ratio0,
uint256 ratio1
) pure returns (uint256) {
// If depositing token0
return (ratio1 * totalAmount * 1e18) / (ratio0 * price + (ratio1 * 1e18));
// If depositing token1
// return (ratio0 * totalAmount * 1e18) / (ratio1 * price + ratio0 * 1e18);
}
Implementing the deposit:
Copy interface IKodiakIslandRouter {
struct RouterSwapParams {
bool zeroForOne;
uint256 amountIn;
uint256 minAmountOut;
bytes routeData;
}
function addLiquiditySingle(
IKodiakIsland island,
uint256 totalAmountIn,
uint256 amountSharesMin,
uint256 maxStakingSlippageBPS,
RouterSwapParams calldata swapData,
address receiver
) external returns (uint256 amount0, uint256 amount1, uint256 mintAmount);
}
contract IslandZapper {
IKodiakIslandRouter public immutable router;
constructor(address _router) {
router = IKodiakIslandRouter(_router);
}
function zapIn(
IKodiakIsland island,
uint256 amountIn,
RouterSwapParams calldata swapData,
uint256 slippageBPS,
uint256 minShares
) external returns (uint256 mintAmount) {
// Transfer input token
IERC20 inputToken = swapData.zeroForOne ? island.token0() : island.token1();
inputToken.transferFrom(msg.sender, address(this), amountIn);
// Approve router
inputToken.approve(address(router), amountIn);
// Execute zap
(,, mintAmount) = router.addLiquiditySingle(
island,
amountIn,
minShares, // Min shares, should be calculated based on simulation
slippageBPS, // This ensures the target pool's price does not move dramatically before your deposit goes through
swapData,
msg.sender
);
}
}
4. Withdrawing Liquidity
Copy interface IKodiakIslandRouter {
function removeLiquidity(
IKodiakIsland island,
uint256 burnAmount,
uint256 amount0Min,
uint256 amount1Min,
address receiver
) external returns (uint256 amount0, uint256 amount1, uint128 liquidityBurned);
}
contract IslandWithdrawer {
IKodiakIslandRouter public immutable router;
constructor(address _router) {
router = IKodiakIslandRouter(_router);
}
function withdraw(
IKodiakIsland island,
uint256 burnAmount,
uint256 slippageBPS
) external returns (uint256 amount0, uint256 amount1) {
// Get expected amounts
(uint256 expectedAmount0, uint256 expectedAmount1) = island.getUnderlyingBalances();
expectedAmount0 = expectedAmount0 * burnAmount / island.totalSupply();
expectedAmount1 = expectedAmount1 * burnAmount / island.totalSupply();
// Calculate minimum amounts with slippage
uint256 amount0Min = expectedAmount0 * (10000 - slippageBPS) / 10000;
uint256 amount1Min = expectedAmount1 * (10000 - slippageBPS) / 10000;
// Transfer LP tokens and approve router
IERC20(address(island)).transferFrom(msg.sender, address(this), burnAmount);
IERC20(address(island)).approve(address(router), burnAmount);
// Remove liquidity
(amount0, amount1,) = router.removeLiquidity(
island,
burnAmount,
amount0Min,
amount1Min,
msg.sender
);
}
}
5. Rebalancing and Fee Collection
Copy contract IslandRebalancer {
function rebalanceIsland(IKodiakIsland island) external {
// Anyone can call rebalance to reinvest fees
island.rebalance();
}
function collectManagerFees(IKodiakIsland island) external {
// Only manager can collect fees
require(island.manager() == msg.sender, "Not manager");
island.withdrawManagerBalance();
}
}
2. Deploying new Islands
What can be achieved
Deploying new managed Islands
Deploying new Permissionless Islands
Enabling mint for everyone for deployed Islands
Managing deployed Island parameters and position
Giving up ownership of deployed Islands
Prerequisites
Before deploying new Islands, you should understand:
Kodiak V3 concentrated liquidity concepts
Openzeppelin Minimal Proxy Contracts
Island management parameters and fee structures
Core Contracts
The main contracts involved in deployment are:
Island Factory Contract : Creates new Island instances
Island Implementation Contract : The base contract that gets cloned
Deployment Examples
1. Deploying a Managed Island
Copy interface IKodiakIslandFactory {
function deployVault(
address tokenA,
address tokenB,
uint24 uniFee,
address manager,
address managerTreasury,
uint16 managerFee,
int24 lowerTick,
int24 upperTick
) external returns (address island);
}
contract IslandDeployer {
IKodiakIslandFactory public immutable factory;
constructor(address _factory) {
factory = IKodiakIslandFactory(_factory);
}
function deployManagedIsland(
address tokenA,
address tokenB,
uint24 uniFee,
address manager,
address treasury,
uint16 managerFeeBPS,
int24 lowerTick,
int24 upperTick
) external returns (address) {
// Validate parameters
require(manager != address(0), "Invalid manager"); // this is address zero for permissionless islands only
require(treasury != address(0), "Invalid treasury"); // You need to set a treasury for managed islands to collect fees
require(managerFeeBPS <= 10000, "Invalid fee"); // Manager fee cannot exceed 10000 BPS (100%)
// Deploy island
return factory.deployVault(
tokenA,
tokenB,
uniFee,
manager,
treasury,
managerFeeBPS,
lowerTick,
upperTick
);
}
}
2. Deploying a Permissionless Island
Copy contract PermissionlessIslandDeployer {
IKodiakIslandFactory public immutable factory;
constructor(address _factory) {
factory = IKodiakIslandFactory(_factory);
}
function deployPermissionlessIsland(
address tokenA,
address tokenB,
uint24 uniFee,
int24 lowerTick,
int24 upperTick
) external returns (address) {
// For permissionless islands:
// - manager must be address(0)
// - treasury must be address(0)
// - managerFee must be 0
return factory.deployVault(
tokenA,
tokenB,
uniFee,
address(0), // No manager
address(0), // No treasury
0, // No manager fee
lowerTick,
upperTick
);
}
}
Important Notes
Tick Spacing : Ensure ticks align with pool's tick spacing otherwise an error will be thrown
Token Ordering : Factory handles token ordering internally
Fee Limits : Manager fees cannot exceed 10000 BPS (100%)
Permissionless vs Managed : Understand the differences in parameters to deploy managed vs permissionless islands
Enabling Minting : You need to enable minting for everyone for managed islands
Rebalance : You need to call rebalance on the island for compounding earned fee back into the position.
Permissionless Islands Frontend listing : You need to contact the Kodiak team to get your permissionless island listed on the kodiak frontend.
Security Considerations
Always use the router for deposits/withdrawals to ensure proper slippage protection
Validate all parameters before deployment
Test with small amounts first
Be cautious with manager permissions, use islands only from trusted managers.
Implement proper access control in integrating contracts
Monitor gas costs, especially for operations involving swaps