diff --git a/libs/model/src/services/commonProtocol/abi/LPBondingCurve.ts b/libs/model/src/services/commonProtocol/abi/LPBondingCurve.ts new file mode 100644 index 00000000000..77ffade18ca --- /dev/null +++ b/libs/model/src/services/commonProtocol/abi/LPBondingCurve.ts @@ -0,0 +1,343 @@ +export const LPBondingCurveAbi = [ + { + type: 'constructor', + inputs: [ + { + name: '_protocolFeeDestination', + type: 'address', + internalType: 'address', + }, + { name: '_launchpad', type: 'address', internalType: 'address' }, + { + name: '_protocolFeePercent', + type: 'uint256', + internalType: 'uint256', + }, + { name: '_LPCurveManager', type: 'address', internalType: 'address' }, + { + name: '_curveActionHook', + type: 'address', + internalType: 'address', + }, + ], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'LPCurveManager', + inputs: [], + outputs: [{ name: '', type: 'address', internalType: 'address' }], + stateMutability: 'view', + }, + { + type: 'function', + name: '_getFloatingTokenSupply', + inputs: [ + { name: 'tokenAddress', type: 'address', internalType: 'address' }, + ], + outputs: [{ name: '', type: 'uint256', internalType: 'uint256' }], + stateMutability: 'view', + }, + { + type: 'function', + name: '_getTotalTokenSupply', + inputs: [ + { name: 'tokenAddress', type: 'address', internalType: 'address' }, + ], + outputs: [{ name: '', type: 'uint256', internalType: 'uint256' }], + stateMutability: 'view', + }, + { + type: 'function', + name: '_isFunded', + inputs: [ + { name: 'tokenAddress', type: 'address', internalType: 'address' }, + ], + outputs: [{ name: '', type: 'bool', internalType: 'bool' }], + stateMutability: 'view', + }, + { + type: 'function', + name: '_launchpadLiquidity', + inputs: [ + { name: 'tokenAddress', type: 'address', internalType: 'address' }, + ], + outputs: [{ name: '', type: 'uint256', internalType: 'uint256' }], + stateMutability: 'view', + }, + { + type: 'function', + name: '_poolLiquidity', + inputs: [ + { name: 'tokenAddress', type: 'address', internalType: 'address' }, + ], + outputs: [{ name: '', type: 'uint256', internalType: 'uint256' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'buyToken', + inputs: [ + { name: 'tokenAddress', type: 'address', internalType: 'address' }, + { + name: 'minAmountOut', + type: 'uint256', + internalType: 'uint256', + }, + ], + outputs: [{ name: '', type: 'bool', internalType: 'bool' }], + stateMutability: 'payable', + }, + { + type: 'function', + name: 'curveActionHook', + inputs: [], + outputs: [ + { name: '', type: 'address', internalType: 'contract ICurveActionHook' }, + ], + stateMutability: 'view', + }, + { + type: 'function', + name: 'getPrice', + inputs: [ + { name: 'tokenAddress', type: 'address', internalType: 'address' }, + { + name: 'amountIn', + type: 'uint256', + internalType: 'uint256', + }, + { name: 'isBuy', type: 'bool', internalType: 'bool' }, + ], + outputs: [{ name: '', type: 'uint256', internalType: 'uint256' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'launchpad', + inputs: [], + outputs: [{ name: '', type: 'address', internalType: 'address' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'liquidity', + inputs: [{ name: '', type: 'address', internalType: 'address' }], + outputs: [{ name: '', type: 'uint256', internalType: 'uint256' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'protocolFeeDestination', + inputs: [], + outputs: [{ name: '', type: 'address', internalType: 'address' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'protocolFeePercent', + inputs: [], + outputs: [{ name: '', type: 'uint256', internalType: 'uint256' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'registerToken', + inputs: [ + { + name: 'params', + type: 'tuple', + internalType: 'struct LPBondingCurve.RegisterTokenParams', + components: [ + { name: '_tokenAddress', type: 'address', internalType: 'address' }, + { + name: '_curveId', + type: 'uint256', + internalType: 'uint256', + }, + { name: '_scalar', type: 'uint256', internalType: 'uint256' }, + { + name: '_reserveRatio', + type: 'uint32', + internalType: 'uint32', + }, + { name: 'totalSupply', type: 'uint256', internalType: 'uint256' }, + { + name: 'holders', + type: 'address[]', + internalType: 'address[]', + }, + { name: 'shares', type: 'uint256[]', internalType: 'uint256[]' }, + { + name: 'LPHook', + type: 'address', + internalType: 'address', + }, + ], + }, + ], + outputs: [], + stateMutability: 'payable', + }, + { + type: 'function', + name: 'sellToken', + inputs: [ + { name: 'tokenAddress', type: 'address', internalType: 'address' }, + { + name: 'amount', + type: 'uint256', + internalType: 'uint256', + }, + { name: 'minAmountOut', type: 'uint256', internalType: 'uint256' }, + ], + outputs: [], + stateMutability: 'payable', + }, + { + type: 'function', + name: 'tokens', + inputs: [{ name: '', type: 'address', internalType: 'address' }], + outputs: [ + { name: 'launchpadLiquidity', type: 'uint256', internalType: 'uint256' }, + { + name: 'poolLiquidity', + type: 'uint256', + internalType: 'uint256', + }, + { name: 'curveId', type: 'uint256', internalType: 'uint256' }, + { + name: 'scalar', + type: 'uint256', + internalType: 'uint256', + }, + { name: 'reserveRatio', type: 'uint32', internalType: 'uint32' }, + { + name: 'LPhook', + type: 'address', + internalType: 'address', + }, + { name: 'funded', type: 'bool', internalType: 'bool' }, + ], + stateMutability: 'view', + }, + { + type: 'function', + name: 'transferLiquidity', + inputs: [ + { name: 'tokenAddress', type: 'address', internalType: 'address' }, + { + name: 'minAmountOut', + type: 'uint256', + internalType: 'uint256', + }, + ], + outputs: [], + stateMutability: 'payable', + }, + { + type: 'event', + name: 'LiquidityTransferred', + inputs: [ + { + name: 'tokenAddress', + type: 'address', + indexed: true, + internalType: 'address', + }, + { + name: 'LPHook', + type: 'address', + indexed: false, + internalType: 'address', + }, + { + name: 'tokensTransferred', + type: 'uint256', + indexed: false, + internalType: 'uint256', + }, + { + name: 'liquidityTransferred', + type: 'uint256', + indexed: false, + internalType: 'uint256', + }, + ], + anonymous: false, + }, + { + type: 'event', + name: 'TokenRegistered', + inputs: [ + { + name: 'token', + type: 'address', + indexed: false, + internalType: 'address', + }, + { + name: 'curveId', + type: 'uint256', + indexed: false, + internalType: 'uint256', + }, + { + name: 'totalSupply', + type: 'uint256', + indexed: false, + internalType: 'uint256', + }, + ], + anonymous: false, + }, + { + type: 'event', + name: 'Trade', + inputs: [ + { + name: 'trader', + type: 'address', + indexed: false, + internalType: 'address', + }, + { + name: 'namespace', + type: 'address', + indexed: false, + internalType: 'address', + }, + { + name: 'isBuy', + type: 'bool', + indexed: false, + internalType: 'bool', + }, + { + name: 'communityTokenAmount', + type: 'uint256', + indexed: false, + internalType: 'uint256', + }, + { + name: 'ethAmount', + type: 'uint256', + indexed: false, + internalType: 'uint256', + }, + { + name: 'protocolEthAmount', + type: 'uint256', + indexed: false, + internalType: 'uint256', + }, + { + name: 'floatingSupply', + type: 'uint256', + indexed: false, + internalType: 'uint256', + }, + ], + anonymous: false, + }, +]; diff --git a/libs/model/src/services/commonProtocol/contestHelper.ts b/libs/model/src/services/commonProtocol/contestHelper.ts index f874536dfdb..2b4ef56fbda 100644 --- a/libs/model/src/services/commonProtocol/contestHelper.ts +++ b/libs/model/src/services/commonProtocol/contestHelper.ts @@ -3,9 +3,9 @@ import { commonProtocol } from '@hicommonwealth/shared'; import { Mutex } from 'async-mutex'; import Web3, { PayableCallOptions } from 'web3'; import { AbiItem } from 'web3-utils'; -import { config } from '../../config'; import { contestABI } from './abi/contestAbi'; import { feeManagerABI } from './abi/feeManagerAbi'; +import { createWeb3Provider } from './utils'; const nonceMutex = new Mutex(); @@ -31,23 +31,6 @@ export type ContestScores = { contestBalance: string; }; -/** - * A helper for creating the web3 provider via an RPC, including private key import - * @param rpc the rpc of the network to use helper with - * @returns - */ -// eslint-disable-next-line @typescript-eslint/require-await -const createWeb3Provider = async (rpc: string): Promise => { - if (!config.WEB3.PRIVATE_KEY) throw new AppError('WEB3 private key not set!'); - const web3 = new Web3(rpc); - const account = web3.eth.accounts.privateKeyToAccount( - config.WEB3.PRIVATE_KEY, - ); - web3.eth.accounts.wallet.add(account); - web3.eth.defaultAccount = account.address; - return web3; -}; - /** * Adds content to an active contest. Includes validation of contest state * @param rpcNodeUrl the rpc node url diff --git a/libs/model/src/services/commonProtocol/contractHelpers.ts b/libs/model/src/services/commonProtocol/contractHelpers.ts index 9dc14182e75..b0623255578 100644 --- a/libs/model/src/services/commonProtocol/contractHelpers.ts +++ b/libs/model/src/services/commonProtocol/contractHelpers.ts @@ -6,6 +6,7 @@ import { } from '@hicommonwealth/shared'; import Web3 from 'web3'; import { AbiItem } from 'web3-utils'; + import { Balances, TokenAttributes, getBalances } from '../tokenBalanceCache'; import { contestABI } from './abi/contestAbi'; diff --git a/libs/model/src/services/commonProtocol/index.ts b/libs/model/src/services/commonProtocol/index.ts index 2006ede5509..a5a776de4c6 100644 --- a/libs/model/src/services/commonProtocol/index.ts +++ b/libs/model/src/services/commonProtocol/index.ts @@ -6,7 +6,6 @@ import * as contest from './contestHelper'; import * as contract from './contractHelpers'; import * as launchpad from './launchpadHelpers'; -// export modules as objects so they can be stubbed in tests export const communityStakeConfigValidator = { ...stake }; export const contractHelpers = { ...contract }; export const contestHelper = { ...contest }; diff --git a/libs/model/src/services/commonProtocol/launchpadHelpers.ts b/libs/model/src/services/commonProtocol/launchpadHelpers.ts index 44d5078f8c4..211537810b1 100644 --- a/libs/model/src/services/commonProtocol/launchpadHelpers.ts +++ b/libs/model/src/services/commonProtocol/launchpadHelpers.ts @@ -4,8 +4,11 @@ import { launchpadTokenRegisteredEventSignature, launchpadTradeEventSignature, } from '@hicommonwealth/model'; +import { commonProtocol } from '@hicommonwealth/shared'; import { Web3 } from 'web3'; +import { LPBondingCurveAbi } from './abi/LPBondingCurve'; import { erc20Abi } from './abi/erc20'; +import { createWeb3Provider } from './utils'; const log = logger(import.meta); @@ -158,3 +161,49 @@ export async function getErc20TokenInfo({ totalSupply: totalSupply as bigint, }; } + +export async function transferLiquidityToUniswap({ + rpc, + lpBondingCurveAddress, + tokenAddress, +}: { + rpc: string; + lpBondingCurveAddress: string; + tokenAddress: string; +}) { + const web3 = await createWeb3Provider(rpc); + const contract = new web3.eth.Contract( + LPBondingCurveAbi, + lpBondingCurveAddress, + ); + await commonProtocol.transferLiquidity( + contract, + tokenAddress, + web3.eth.defaultAccount!, + ); +} + +export async function getToken({ + rpc, + lpBondingCurveAddress, + tokenAddress, +}: { + rpc: string; + lpBondingCurveAddress: string; + tokenAddress: string; +}): Promise<{ + launchpadLiquidity: bigint; + poolLiquidity: bigint; + curveId: bigint; + scalar: bigint; + reserveRation: bigint; + LPhook: string; + funded: boolean; +}> { + const web3 = new Web3(rpc); + const contract = new web3.eth.Contract( + LPBondingCurveAbi, + lpBondingCurveAddress, + ); + return await contract.methods.tokens(tokenAddress).call(); +} diff --git a/libs/model/src/services/commonProtocol/utils.ts b/libs/model/src/services/commonProtocol/utils.ts new file mode 100644 index 00000000000..6db5f74029a --- /dev/null +++ b/libs/model/src/services/commonProtocol/utils.ts @@ -0,0 +1,21 @@ +import { ServerError } from '@hicommonwealth/core'; +import Web3 from 'web3'; +import { config } from '../../config'; + +/** + * A helper for creating the web3 provider via an RPC, including private key import + * @param rpc the rpc of the network to use helper with + * @returns + */ +// eslint-disable-next-line @typescript-eslint/require-await +export const createWeb3Provider = async (rpc: string): Promise => { + if (!config.WEB3.PRIVATE_KEY) + throw new ServerError('WEB3 private key not set!'); + const web3 = new Web3(rpc); + const account = web3.eth.accounts.privateKeyToAccount( + config.WEB3.PRIVATE_KEY, + ); + web3.eth.accounts.wallet.add(account); + web3.eth.defaultAccount = account.address; + return web3; +}; diff --git a/packages/commonwealth/server/workers/commonwealthConsumer/policies/chainEventCreated/handleLaunchpadTrade.ts b/packages/commonwealth/server/workers/commonwealthConsumer/policies/chainEventCreated/handleLaunchpadTrade.ts index 6e88163e63e..2a85ee792f0 100644 --- a/packages/commonwealth/server/workers/commonwealthConsumer/policies/chainEventCreated/handleLaunchpadTrade.ts +++ b/packages/commonwealth/server/workers/commonwealthConsumer/policies/chainEventCreated/handleLaunchpadTrade.ts @@ -1,5 +1,6 @@ import { events, LaunchpadTrade } from '@hicommonwealth/core'; -import { models } from '@hicommonwealth/model'; +import { commonProtocol, models } from '@hicommonwealth/model'; +import { commonProtocol as sharedCommonProtocol } from '@hicommonwealth/shared'; import { BigNumber } from 'ethers'; import Web3 from 'web3'; import { z } from 'zod'; @@ -64,5 +65,31 @@ export async function handleLaunchpadTrade( }); } - // TODO: check that liquidity has been transferred if above threshold + const lpBondingCurveAddress = + sharedCommonProtocol.factoryContracts[ + chainNode!.eth_chain_id as sharedCommonProtocol.ValidChains + ].lpBondingCurve!; + + if ( + !token.liquidity_transferred && + BigNumber.from(floatingSupply).toBigInt() === + BigInt(token.launchpad_liquidity) + ) { + const onChainTokenData = await commonProtocol.launchpadHelpers.getToken({ + rpc: chainNode.private_url!, + tokenAddress, + lpBondingCurveAddress, + }); + + if (!onChainTokenData.funded) { + await commonProtocol.launchpadHelpers.transferLiquidityToUniswap({ + rpc: chainNode.private_url!, + tokenAddress, + lpBondingCurveAddress, + }); + } + + token.liquidity_transferred = true; + await token.save(); + } }