UNPKG

@anton-seriesfi/doppler-v3-sdk

Version:
490 lines 21.9 kB
"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