doppler-v4-sdk
v0.2.21
Published
A TypeScript SDK for interacting with the Doppler V4 protocol - a liquidity bootstrapping system built on Uniswap V4.
Keywords
Readme
Doppler V4 SDK
A TypeScript SDK for interacting with the Doppler V4 protocol - a liquidity bootstrapping system built on Uniswap V4.
Overview
The Doppler V4 SDK provides a comprehensive interface for:
- Token Creation & Management: Deploy ERC-20 tokens with vesting schedules
- Pool Creation: Create Uniswap V4 pools with custom hooks for price discovery
- Price Discovery: Automated gradual dutch auctions with customizable parameters
- Governance: Deploy and manage governance contracts for token communities (optional)
- Liquidity Migration: Move liquidity between discovery and trading pools
Installation
npm install doppler-v4-sdkQuick Start
import { ReadWriteFactory, DOPPLER_V4_ADDRESSES } from 'doppler-v4-sdk';
import { createPublicClient, createWalletClient, http } from 'viem';
import { base } from 'viem/chains';
import { privateKeyToAccount } from 'viem/accounts';
import { createDrift } from '@delvtech/drift';
// Setup clients
const publicClient = createPublicClient({
chain: base,
transport: http(),
});
const account = privateKeyToAccount('0x...');
const walletClient = createWalletClient({
account,
chain: base,
transport: http(),
});
// Create drift instance
const drift = createDrift({ publicClient, walletClient });
// Get contract addresses for Base
const addresses = DOPPLER_V4_ADDRESSES[8453];
// Initialize factory
const factory = new ReadWriteFactory(addresses.airlock, drift);Core Concepts
Price Discovery Mechanism
Doppler V4 implements a gradual dutch auction where:
- Token prices move along a predefined curve over time
- Price movements occur in discrete epochs
- The
gammaparameter controls price movement per epoch - Liquidity is concentrated around the current price
Pool Configuration
Each Doppler pool requires:
- Asset Token: The token being sold
- Quote Token: The token being received (e.g., ETH, USDC)
- Price Range: Starting and ending ticks defining the price curve
- Time Parameters: Start time, duration, and epoch length
- Proceeds Thresholds: Minimum and maximum proceeds targets
API Reference
ReadWriteFactory
The main class for creating and managing Doppler pools.
Creating a Pool with Governance
const config: DopplerPreDeploymentConfig = {
name: tokenName,
symbol: tokenSymbol,
totalSupply: parseEther('1000000000'),
numTokensToSell: parseEther('600000000'),
tokenURI,
blockTimestamp: Math.floor(Date.now() / 1000),
startTimeOffset: 1,
duration: 1 / 4,
epochLength: 200,
gamma: 800,
tickRange: {
startTick: 174_312,
endTick: 186_840,
},
tickSpacing: 2,
fee: 20_000, // 2%
minProceeds: parseEther('2'),
maxProceeds: parseEther('4'),
yearlyMintRate: 0n,
vestingDuration: BigInt(24 * 60 * 60 * 365),
recipients: [wallet.account.address],
amounts: [parseEther('50000000')],
numPdSlugs: 15,
integrator,
};
// Build configuration (uses governance by default)
const { createParams, hook, token } = factory.buildConfig(config, addresses);
// Simulate transaction
const simulation = await factory.simulateCreate(createParams);
console.log(`Estimated gas: ${simulation.request.gas}`);
// Execute creation
const txHash = await factory.create(createParams);
console.log(`Pool created: ${txHash}`);Creating a Pool without Governance
To deploy tokens without governance (using NoOpGovernanceFactory), pass the optional useGovernance: false parameter:
// Build configuration without governance
const { createParams, hook, token } = factory.buildConfig(
config,
addresses,
{ useGovernance: false }
);
// Deploy the token (no governance contracts will be created)
const txHash = await factory.create(createParams);When using useGovernance: false:
- The NoOpGovernanceFactory will be used instead of the regular governance factory
- No actual governance or timelock contracts will be deployed
- The governance and timelock addresses will be set to
0xdead - This saves gas and simplifies deployment for tokens that don't need governance
Key Methods
buildConfig(params, addresses, options?)- Build complete pool configurationcreate(createParams, options?)- Deploy the poolsimulateCreate(createParams)- Simulate deploymentmigrate(asset, options?)- Migrate liquidity after price discovery
ReadDoppler
Read-only interface for querying pool state.
import { ReadDoppler } from 'doppler-v4-sdk';
const doppler = new ReadDoppler(
dopplerAddress,
addresses.stateView,
drift,
poolId
);
// Get current price
const price = await doppler.getCurrentPrice();
// Get pool configuration
const poolKey = await doppler.getPoolKey();
// Get strategy parameters
const startTime = await doppler.getStartingTime();
const endTime = await doppler.getEndingTime();
const gamma = await doppler.getGamma();
// Get token instances
const assetToken = await doppler.getAssetToken();
const quoteToken = await doppler.getQuoteToken();Token Interfaces
ReadDerc20 / ReadWriteDerc20
const token = await doppler.getAssetToken();
// Read operations
const name = await token.getName();
const symbol = await token.getSymbol();
const decimals = await token.getDecimals();
const totalSupply = await token.getTotalSupply();
const balance = await token.getBalanceOf(userAddress);
// Write operations (ReadWriteDerc20 only)
await token.transfer(recipient, amount);
await token.approve(spender, amount);Quoter
Get price quotes for swaps:
import { ReadQuoter } from 'doppler-v4-sdk';
const quoter = new ReadQuoter(addresses.v4Quoter, drift);
const quote = await quoter.quoteExactInputSingle({
tokenIn: assetTokenAddress,
tokenOut: quoteTokenAddress,
fee: 3000,
amountIn: parseEther('100'),
sqrtPriceLimitX96: BigInt(0),
});
console.log(`Input: ${quote.amountIn}`);
console.log(`Output: ${quote.amountOut}`);Supported Networks
| Network | Chain ID | Status | | ------------ | -------- | ------------- | | Base | 8453 | ✅ Production | | Base Sepolia | 84532 | 🧪 Testnet | | Unichain | 130 | ✅ Production | | Ink | 57073 | ✅ Production |
Get network addresses:
import { DOPPLER_V4_ADDRESSES } from 'doppler-v4-sdk';
const baseAddresses = DOPPLER_V4_ADDRESSES[8453];
const sepoliaAddresses = DOPPLER_V4_ADDRESSES[84532];Advanced Usage
Multicurve Initializer (V4)
Create a pool with the multicurve initializer by encoding curve ranges and optional lockable fee beneficiaries. Below are two examples:
- Compact 2-curve example
import { ReadWriteFactory } from 'doppler-v4-sdk'
import { parseEther } from 'viem'
const factory = new ReadWriteFactory(addresses.airlock, drift)
const multicurve = {
name: 'MyToken',
symbol: 'MTK',
totalSupply: parseEther('1000000'),
numTokensToSell: parseEther('600000'),
tokenURI: 'ipfs://.../metadata.json',
numeraire: quoteTokenAddress,
pool: {
fee: 3000,
tickSpacing: 60,
curves: [
{ tickLower: 170_000, tickUpper: 175_000, numPositions: 16, shares: parseEther('1') },
{ tickLower: 175_000, tickUpper: 180_000, numPositions: 16, shares: parseEther('2') },
],
// Optional lockable fee beneficiaries (shares in WAD)
lockableBeneficiaries: [
{ beneficiary: someAddress, shares: parseEther('1.0') },
],
},
integrator: integratorAddress,
}
const { createParams } = factory.buildMulticurveCreateParams(multicurve, addresses, { useGovernance: true })
const sim = await factory.simulateCreate(createParams)
const tx = await factory.create(createParams)- 10-curve config (matches our Base Sepolia fork test)
import { ReadWriteFactory } from 'doppler-v4-sdk'
import { parseEther } from 'viem'
const factory = new ReadWriteFactory(addresses.airlock, drift)
const multicurve10 = {
name: 'ForkTest Multicurve',
symbol: 'FTMC',
totalSupply: parseEther('1000000'),
numTokensToSell: parseEther('1000000'),
tokenURI: 'ipfs://example/token.json',
numeraire: quoteTokenAddress,
pool: {
// fee = 0, tickSpacing = 8, 10 curves stepping by 16_000 ticks
fee: 0,
tickSpacing: 8,
curves: Array.from({ length: 10 }, (_, i) => ({
tickLower: i * 16_000,
tickUpper: 240_000,
numPositions: 10,
shares: parseEther('0.1'),
})),
lockableBeneficiaries: [],
},
integrator: integratorAddress,
}
const { createParams } = factory.buildMulticurveCreateParams(multicurve10, addresses, { useGovernance: true })
const sim = await factory.simulateCreate(createParams)
const tx = await factory.create(createParams)Notes:
- Ensure
addresses.v4MulticurveInitializeris set for your target chain (Base Sepolia is prefilled in this SDK). - Use
useGovernance: falseto targetnoOpGovernanceFactorywhen desired.
Custom Liquidity Migration
// Configure custom LP migration
const migrationData = factory.encodeCustomLPLiquidityMigratorData({
customLPWad: parseEther('1000'), // LP tokens to mint
customLPRecipient: lpRecipientAddress,
lockupPeriod: 86400, // 1 day lockup
});
const config = {
// ... other parameters
liquidityMigratorData: migrationData,
};Gamma Calculation
The SDK automatically calculates optimal gamma (price movement per epoch):
// Manual gamma calculation
const totalEpochs = (durationDays * 86400) / epochLength;
const tickDelta = Math.abs(endTick - startTick);
const gamma = Math.ceil(tickDelta / totalEpochs) * tickSpacing;Error Handling
The SDK provides detailed error messages for common issues:
- Missing or invalid configuration parameters
- NoOpGovernanceFactory not deployed when
useGovernance: false - Invalid price ranges or tick ranges
- Insufficient permissions
Building
bun run buildExamples
See the examples/ directory for complete implementation examples:
- Basic pool creation
- Multi-chain deployment
- Custom governance setup
- Liquidity migration strategies
// (For a simple usage example, see
examples/multicurve-create.ts.)
Important Notes
- The
useGovernanceparameter defaults totrueto maintain backward compatibility - NoOpGovernanceFactory must be deployed on the target chain before using
useGovernance: false - If NoOpGovernanceFactory is not deployed, the SDK will throw an error when attempting to use it
Contributing
- Fork the repository
- Create a feature branch
- Make your changes
- Add tests
- Submit a pull request
License
MIT License - see LICENSE file for details.
