@anton-seriesfi/doppler-v3-sdk
Version:
SDK for interacting with Doppler v3 protocol
490 lines • 21.9 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ReadWriteFactory = exports.DEAD_ADDRESS = exports.WAD = exports.DEFAULT_INITIAL_PROPOSAL_THRESHOLD = exports.DEFAULT_INITIAL_VOTING_PERIOD = exports.DEFAULT_INITIAL_VOTING_DELAY = exports.DEFAULT_MAX_SHARE_TO_BE_SOLD = exports.DEFAULT_PRE_MINT_WAD = exports.DEFAULT_YEARLY_MINT_RATE_WAD = exports.DEFAULT_NUM_TOKENS_TO_SELL_WAD = exports.DEFAULT_INITIAL_SUPPLY_WAD = exports.DEFAULT_VESTING_DURATION = exports.DEFAULT_FEE = exports.DEFAULT_NUM_POSITIONS = exports.DEFAULT_END_TICK = exports.DEFAULT_START_TICK = exports.ONE_YEAR_IN_SECONDS = void 0;
const drift_1 = require("@delvtech/drift");
const ReadFactory_1 = require("./ReadFactory");
const abis_1 = require("../../abis");
const viem_1 = require("viem");
// Constants for default configuration values
exports.ONE_YEAR_IN_SECONDS = 365 * 24 * 60 * 60;
exports.DEFAULT_START_TICK = 175000;
exports.DEFAULT_END_TICK = 225000;
exports.DEFAULT_NUM_POSITIONS = 15;
exports.DEFAULT_FEE = 10000; // 1% fee tier
exports.DEFAULT_VESTING_DURATION = BigInt(exports.ONE_YEAR_IN_SECONDS);
exports.DEFAULT_INITIAL_SUPPLY_WAD = (0, viem_1.parseEther)("1000000000");
exports.DEFAULT_NUM_TOKENS_TO_SELL_WAD = (0, viem_1.parseEther)("900000000");
exports.DEFAULT_YEARLY_MINT_RATE_WAD = (0, viem_1.parseEther)("0.02");
exports.DEFAULT_PRE_MINT_WAD = (0, viem_1.parseEther)("9000000"); // 0.9% of the total supply
exports.DEFAULT_MAX_SHARE_TO_BE_SOLD = (0, viem_1.parseEther)("0.35");
exports.DEFAULT_INITIAL_VOTING_DELAY = 172800;
exports.DEFAULT_INITIAL_VOTING_PERIOD = 1209600;
exports.DEFAULT_INITIAL_PROPOSAL_THRESHOLD = BigInt(0);
exports.WAD = BigInt(10 ** 18);
exports.DEAD_ADDRESS = "0x000000000000000000000000000000000000dEaD";
/**
* Factory class for creating and managing Doppler V3 pools with read/write capabilities
*/
class ReadWriteFactory extends ReadFactory_1.ReadFactory {
/**
* Create a new ReadWriteFactory instance
* @param address Contract address
* @param drift Drift instance for blockchain interaction
* @param defaultConfigs Optional default configurations
*/
constructor(address, bundlerAddress, drift = (0, drift_1.createDrift)(), defaultConfigs) {
super(address, drift);
/**
* Generate a random salt
* @param account User address to incorporate into salt
* @returns Hex string of generated salt
*/
this.generateRandomSalt = (account) => {
const array = new Uint8Array(32);
// Sequential byte generation
for (let i = 0; i < 32; i++) {
array[i] = i;
}
if (account) {
const addressBytes = account.slice(2).padStart(40, "0");
for (let i = 0; i < 20; i++) {
const addressByte = parseInt(addressBytes.slice(i * 2, (i + 1) * 2), 16);
array[i] ^= addressByte;
}
}
return `0x${Array.from(array)
.map((b) => b.toString(16).padStart(2, "0"))
.join("")}`;
};
this.drift = drift;
this.bundler = drift.contract({
abi: abis_1.BundlerAbi,
address: bundlerAddress,
});
// Initialize default configurations with fallback values
this.defaultV3PoolConfig = defaultConfigs?.defaultV3PoolConfig ?? {
startTick: exports.DEFAULT_START_TICK,
endTick: exports.DEFAULT_END_TICK,
numPositions: exports.DEFAULT_NUM_POSITIONS,
maxShareToBeSold: exports.DEFAULT_MAX_SHARE_TO_BE_SOLD,
fee: exports.DEFAULT_FEE,
};
this.defaultVestingConfig = defaultConfigs?.defaultVestingConfig ?? {
yearlyMintRate: exports.DEFAULT_YEARLY_MINT_RATE_WAD,
vestingDuration: exports.DEFAULT_VESTING_DURATION,
recipients: [],
amounts: [],
};
this.defaultSaleConfig = defaultConfigs?.defaultSaleConfig ?? {
initialSupply: exports.DEFAULT_INITIAL_SUPPLY_WAD,
numTokensToSell: exports.DEFAULT_NUM_TOKENS_TO_SELL_WAD,
};
this.defaultGovernanceConfig = defaultConfigs?.defaultGovernanceConfig ?? {
initialVotingDelay: exports.DEFAULT_INITIAL_VOTING_DELAY,
initialVotingPeriod: exports.DEFAULT_INITIAL_VOTING_PERIOD,
initialProposalThreshold: exports.DEFAULT_INITIAL_PROPOSAL_THRESHOLD,
};
}
/**
* Merge user configuration with defaults
* @param config User-provided partial configuration
* @param defaults Full default configuration
* @returns Merged configuration object
*/
mergeWithDefaults(config, defaults) {
return { ...defaults, ...config };
}
/**
* Get merged sale configuration
* @param saleConfig Optional partial sale config
* @returns Complete SaleConfig
*/
getMergedSaleConfig(saleConfig) {
return this.mergeWithDefaults(saleConfig, this.defaultSaleConfig);
}
/**
* Get merged pool configuration
* @param v3PoolConfig Optional partial pool config
* @returns Complete V3PoolConfig
*/
getMergedV3PoolConfig(v3PoolConfig) {
return this.mergeWithDefaults(v3PoolConfig, this.defaultV3PoolConfig);
}
/**
* Get merged governance configuration
* @param governanceConfig Optional partial governance config
* @returns Complete GovernanceConfig
*/
getMergedGovernanceConfig(governanceConfig) {
return this.mergeWithDefaults(governanceConfig, this.defaultGovernanceConfig);
}
/**
* Get merged vesting configuration
* @param config Vesting config or "default" preset
* @param userAddress User address for default recipient
* @returns Complete VestingConfig
*/
getMergedVestingConfig(config, userAddress) {
const base = config === "default" ? this.defaultVestingConfig : config;
return {
...base,
recipients: config === "default" ? [userAddress] : [...base.recipients],
amounts: config === "default" ? [exports.DEFAULT_PRE_MINT_WAD] : [...base.amounts],
};
}
/**
* Encode lockable pool initializer data
* @param v3PoolConfig Complete pool configuration
* @returns ABI-encoded initialization data
*/
encodeLockablePoolInitializerData(v3PoolConfig) {
if (!v3PoolConfig.beneficiaries) {
throw new Error("Beneficiaries are required for lockable pool initialization");
}
const totalShares = v3PoolConfig.beneficiaries.reduce((acc, beneficiary) => acc + beneficiary.shares, 0n);
if (totalShares !== exports.WAD) {
throw new Error("Total shares must be equal to 1e18");
}
// Wrapping all the components in a tuple since the data is decoded as a InitData struct
return (0, viem_1.encodeAbiParameters)([
{
type: "tuple",
components: [
{ name: "fee", type: "uint24" },
{ name: "startTick", type: "int24" },
{ name: "endTick", type: "int24" },
{ name: "numPositions", type: "uint16" },
{ name: "maxShareToBeSold", type: "uint256" },
{
name: "beneficiaries",
type: "tuple[]",
components: [
{ type: "address", name: "beneficiary" },
{ type: "uint96", name: "shares" }
]
},
]
},
], [
{
fee: v3PoolConfig.fee,
startTick: v3PoolConfig.startTick,
endTick: v3PoolConfig.endTick,
numPositions: v3PoolConfig.numPositions,
maxShareToBeSold: v3PoolConfig.maxShareToBeSold,
beneficiaries: v3PoolConfig.beneficiaries,
}
]);
}
/**
* Encode pool initialization data for contract calls
* @param v3PoolConfig Complete pool configuration
* @returns ABI-encoded initialization data
*/
encodePoolInitializerData(v3PoolConfig) {
return (0, viem_1.encodeAbiParameters)([
{ type: "uint24" },
{ type: "int24" },
{ type: "int24" },
{ type: "uint16" },
{ type: "uint256" },
], [
v3PoolConfig.fee,
v3PoolConfig.startTick,
v3PoolConfig.endTick,
v3PoolConfig.numPositions,
v3PoolConfig.maxShareToBeSold,
]);
}
/**
* Encode token factory initialization data
* @param tokenConfig Token metadata
* @param vestingConfig Vesting schedule
* @returns ABI-encoded token factory data
*/
encodeTokenFactoryData(tokenConfig, vestingConfig) {
return (0, viem_1.encodeAbiParameters)([
{ type: "string" },
{ type: "string" },
{ type: "uint256" },
{ type: "uint256" },
{ type: "address[]" },
{ type: "uint256[]" },
{ type: "string" },
], [
tokenConfig.name,
tokenConfig.symbol,
vestingConfig.yearlyMintRate,
vestingConfig.vestingDuration,
vestingConfig.recipients,
vestingConfig.amounts,
tokenConfig.tokenURI,
]);
}
/**
* Encode governance factory initialization data
* @param tokenConfig Token metadata
* @returns ABI-encoded governance data
*/
encodeGovernanceFactoryData(tokenConfig, governanceConfig) {
return (0, viem_1.encodeAbiParameters)([
{ type: "string" },
{ type: "uint48" },
{ type: "uint32" },
{ type: "uint256" },
], [
tokenConfig.name,
Number(governanceConfig.initialVotingDelay),
Number(governanceConfig.initialVotingPeriod),
governanceConfig.initialProposalThreshold,
]);
}
/**
* Encode all parameters for pool creation
* @param params CreateV3PoolParams input parameters
* @returns Object containing create parameters and final pool config
* @throws Error if user address is missing or invalid tick range
*/
encode(params) {
const { userAddress, numeraire, integrator, contracts, tokenConfig } = params;
if (!userAddress) {
throw new Error("User address is required. Is a wallet connected?");
}
// Merge configurations with defaults
const vestingConfig = this.getMergedVestingConfig(params.vestingConfig, userAddress);
const v3PoolConfig = this.getMergedV3PoolConfig(params.v3PoolConfig);
const saleConfig = this.getMergedSaleConfig(params.saleConfig);
const governanceConfig = this.getMergedGovernanceConfig(params.governanceConfig);
// Validate tick configuration
if (v3PoolConfig.startTick > v3PoolConfig.endTick) {
throw new Error("Invalid start and end ticks. Start tick must be less than end tick.");
}
// Generate unique salt and encode contract data
const salt = this.generateRandomSalt(userAddress);
const governanceFactoryData = this.encodeGovernanceFactoryData(tokenConfig, governanceConfig);
const tokenFactoryData = this.encodeTokenFactoryData(tokenConfig, vestingConfig);
const poolInitializerData = v3PoolConfig.beneficiaries ? this.encodeLockablePoolInitializerData(v3PoolConfig) : this.encodePoolInitializerData(v3PoolConfig);
const liquidityMigratorData = params.liquidityMigratorData ?? "0x";
// Prepare final arguments
const { tokenFactory, governanceFactory, v3Initializer: poolInitializer, liquidityMigrator, } = contracts;
const { initialSupply, numTokensToSell } = saleConfig;
const args = {
initialSupply,
numTokensToSell,
numeraire,
tokenFactory,
tokenFactoryData,
governanceFactory,
governanceFactoryData,
poolInitializer,
poolInitializerData,
liquidityMigrator,
liquidityMigratorData,
integrator,
salt,
};
return {
createParams: args,
v3PoolConfig,
};
}
/**
* Encode creation data with token order validation
* @param params CreateV3PoolParams input parameters
* @returns Finalized create parameters with adjusted ticks if needed
*/
async encodeCreateData(params) {
// First, perform validation before any encoding
const saleConfig = this.getMergedSaleConfig(params.saleConfig);
const vestingConfig = this.getMergedVestingConfig(params.vestingConfig, params.userAddress);
const totalVestedAmount = vestingConfig.amounts.reduce((sum, amount) => sum + amount, 0n);
// Validation Rule #1: Supply Integrity Constraint
if (saleConfig.initialSupply < saleConfig.numTokensToSell + totalVestedAmount) {
throw new Error(`Configuration Error: Vesting and sale amounts (${saleConfig.numTokensToSell + totalVestedAmount}) exceed the initial supply (${saleConfig.initialSupply}). Please adjust your vesting schedule or increase the initial supply.`);
}
if (params.v3PoolConfig?.beneficiaries) {
params.v3PoolConfig.beneficiaries = this.sortBeneficiaries(params.v3PoolConfig.beneficiaries);
this.validateBeneficiaries(params.v3PoolConfig.beneficiaries);
// assert that the beneficiaries are sorted and give 0.05 ether to the airlock owner
const airlockOwner = await this.airlock.read("owner");
const airlockOwnerIndex = params.v3PoolConfig.beneficiaries.findIndex(b => b.beneficiary.toLowerCase() === airlockOwner.toLowerCase());
if (airlockOwnerIndex === -1) {
throw new Error("Airlock owner is not a beneficiary");
}
if (params.v3PoolConfig.beneficiaries[airlockOwnerIndex].shares !== (0, viem_1.parseEther)("0.05")) {
throw new Error("Airlock owner must have 0.05 ether");
}
}
// Validation Rule #2: No-Op Governance Constraint
// Check if the governance factory is a no-op governance factory
// const chainId = await this.drift.getChainId();
// const addresses = DOPPLER_V3_ADDRESSES[chainId];
// const isNoOp = addresses?.noOpGovernanceFactory &&
// params.contracts.governanceFactory.toLowerCase() ===
// addresses.noOpGovernanceFactory.toLowerCase();
// if (isNoOp) {
// const excess = saleConfig.initialSupply - (saleConfig.numTokensToSell + totalVestedAmount);
// if (excess !== 0n) {
// throw new Error(
// `Configuration Error: No-op governance requires zero excess tokens. ` +
// `The current configuration creates an excess of ${excess} tokens. ` +
// `Please set initialSupply to be exactly the sum of numTokensToSell and vested amounts.`
// );
// }
// }
// If validation passes, proceed with the original logic
let isToken0 = true;
let createParams;
let asset;
let i = 0n;
while (isToken0) {
const encoded = this.encode(params);
createParams = encoded.createParams;
createParams.salt = this.generateRandomSalt((0, viem_1.toHex)(BigInt(params.userAddress) + BigInt(i)));
const simulateResult = await this.simulateCreate(createParams);
asset = simulateResult.asset;
isToken0 = Number(asset) < Number(params.numeraire);
i++;
}
return createParams;
}
/**
* Execute pool creation transaction
* @param params Finalized create parameters
* @param options Write options and mined handlers
* @returns Transaction hash
*/
async create(params, options) {
return this.airlock.write("create", { createData: params }, options);
}
/**
* Simulate pool creation transaction
* @param params Create parameters
* @returns Simulation results
*/
async simulateCreate(params) {
return this.airlock.simulateWrite("create", { createData: params });
}
async simulateBundleExactOutput(createData, params) {
return this.bundler.simulateWrite("simulateBundleExactOut", {
createData,
params: { ...params },
});
}
async simulateBundleExactInput(createData, params) {
return this.bundler.simulateWrite("simulateBundleExactIn", {
createData,
params: { ...params },
});
}
async bundle(createData, commands, inputs, options) {
return this.bundler.write("bundle", { createData, commands, inputs }, options);
}
/**
* Update default configurations
* @param configs Partial configuration overrides
*/
updateDefaultConfigs(configs) {
this.defaultV3PoolConfig = this.mergeWithDefaults(configs.defaultV3PoolConfig || {}, this.defaultV3PoolConfig);
this.defaultVestingConfig = this.mergeWithDefaults(configs.defaultVestingConfig || {}, this.defaultVestingConfig);
this.defaultSaleConfig = this.mergeWithDefaults(configs.defaultSaleConfig || {}, this.defaultSaleConfig);
this.defaultGovernanceConfig = this.mergeWithDefaults(configs.defaultGovernanceConfig || {}, this.defaultGovernanceConfig);
}
/**
* Sort beneficiaries by address in ascending order
* @param beneficiaries Array of beneficiary data
* @returns Sorted array of beneficiaries
*/
sortBeneficiaries(beneficiaries) {
return [...beneficiaries].sort((a, b) => {
const aNum = BigInt(a.beneficiary);
const bNum = BigInt(b.beneficiary);
return aNum < bNum ? -1 : aNum > bNum ? 1 : 0;
});
}
/**
* Validate beneficiary data
* @param beneficiaries Array of beneficiary data to validate
* @throws Error if validation fails
*/
validateBeneficiaries(beneficiaries) {
if (beneficiaries.length === 0) {
throw new Error("At least one beneficiary is required");
}
// Check that beneficiaries are sorted
for (let i = 1; i < beneficiaries.length; i++) {
if (BigInt(beneficiaries[i].beneficiary) <=
BigInt(beneficiaries[i - 1].beneficiary)) {
throw new Error("Beneficiaries must be sorted in ascending order by address");
}
}
// Check that all shares are positive
let totalShares = BigInt(0);
for (const beneficiary of beneficiaries) {
if (beneficiary.shares <= 0) {
throw new Error("All beneficiary shares must be positive");
}
totalShares += beneficiary.shares;
}
// Check that shares sum to WAD
if (totalShares !== exports.WAD) {
throw new Error(`Total shares must equal ${exports.WAD} (100%), but got ${totalShares}`);
}
}
/**
* Encode V4 migrator data for Uniswap V4 migration with StreamableFeesLocker
* @param data V4 migrator configuration
* @param includeDefaultBeneficiary Whether to include the airlock owner as a default 5% beneficiary
* @returns Encoded hex data
*/
async encodeV4MigratorData(data, includeDefaultBeneficiary = true) {
let beneficiaries = [...data.beneficiaries];
if (includeDefaultBeneficiary) {
// Get the airlock owner address
const airlockOwner = await this.owner();
// Check if airlock owner is already in the beneficiaries list
const existingOwnerIndex = beneficiaries.findIndex(b => b.beneficiary.toLowerCase() === airlockOwner.toLowerCase());
if (existingOwnerIndex === -1) {
// Add airlock owner as 5% beneficiary
const ownerShares = BigInt(0.05e18); // 5% in WAD
// Scale down other beneficiaries proportionally
const remainingShares = exports.WAD - ownerShares; // 95% remaining
const currentTotal = beneficiaries.reduce((sum, b) => sum + b.shares, BigInt(0));
beneficiaries = beneficiaries.map(b => ({
...b,
shares: (b.shares * remainingShares) / currentTotal
}));
// Add the owner beneficiary
beneficiaries.push({
beneficiary: airlockOwner,
shares: ownerShares
});
// Sort beneficiaries by address
beneficiaries = this.sortBeneficiaries(beneficiaries);
}
}
// Validate beneficiaries before encoding
this.validateBeneficiaries(beneficiaries);
return (0, viem_1.encodeAbiParameters)([
{ type: "uint24" }, // fee
{ type: "int24" }, // tickSpacing
{ type: "uint32" }, // lockDuration
{
type: "tuple[]", components: [
{ type: "address", name: "beneficiary" },
{ type: "uint96", name: "shares" }
]
}
], [
data.fee,
data.tickSpacing,
data.lockDuration,
beneficiaries.map(b => ({
beneficiary: b.beneficiary,
shares: b.shares
}))
]);
}
}
exports.ReadWriteFactory = ReadWriteFactory;
//# sourceMappingURL=ReadWriteFactory.js.map