import { TokenWithAmount, TokenWithSupply, TxOverrides } from '@shared/types' import { erc20ABI, getTokenAllowances, getTokenBalances, getTokenInfo, getVaultId, twabControllerABI, validateAddress, validateClientNetwork, vaultABI } from '@shared/utilities' import { Address, parseUnits, PublicClient, WalletClient } from 'viem' /** * This class provides read and write functions to interact with a vault */ export class Vault { readonly id: string walletClient: WalletClient | undefined decimals: number | undefined tokenAddress: Address | undefined tokenData: TokenWithSupply | undefined shareData: TokenWithSupply | undefined exchangeRate: bigint | undefined liquidationPair: Address | undefined claimer: Address | undefined yieldSource: Address | undefined twabController: Address | undefined feePercent: number | undefined feeRecipient: Address | undefined feesAvailable: bigint | undefined owner: Address | undefined name: string | undefined logoURI: string | undefined tags: string[] | undefined tokenLogoURI: string | undefined yieldSourceName: string | undefined yieldSourceURI: string | undefined /** * Creates an instance of a Vault with a given public and optional wallet client * * NOTE: If initialized without a wallet Viem client, write functions will not be available * @param chainId the vault's chain ID * @param address the vault's address * @param publicClient a public Viem client for the network the vault is deployed on * @param options optional parameters (including wallet client) */ constructor( public chainId: number, public address: Address, public publicClient: PublicClient, options?: { walletClient?: WalletClient decimals?: number tokenAddress?: Address name?: string logoURI?: string tags?: string[] tokenLogoURI?: string yieldSourceName?: string yieldSourceURI?: string } ) { this.id = getVaultId({ address, chainId }) this.walletClient = options?.walletClient this.decimals = options?.decimals this.tokenAddress = options?.tokenAddress this.name = options?.name this.logoURI = options?.logoURI this.tags = options?.tags this.tokenLogoURI = options?.tokenLogoURI this.yieldSourceName = options?.yieldSourceName this.yieldSourceURI = options?.yieldSourceURI } /* ============================== Read Functions ============================== */ /** * Returns the address of the vault's underlying asset * @returns */ async getTokenAddress(): Promise
{ if (this.tokenAddress !== undefined) return this.tokenAddress const source = 'Vault [getTokenAddress]' await validateClientNetwork(this.chainId, this.publicClient, source) const tokenAddress = await this.publicClient.readContract({ address: this.address, abi: vaultABI, functionName: 'asset' }) this.tokenAddress = tokenAddress return this.tokenAddress } /** * Returns basic data about the vault's underlying asset * @returns */ async getTokenData(): Promise { if (this.tokenData !== undefined) return this.tokenData const source = 'Vault [getTokenData]' await validateClientNetwork(this.chainId, this.publicClient, source) const tokenAddress = await this.getTokenAddress() const tokenData = (await getTokenInfo(this.publicClient, [tokenAddress]))[tokenAddress] if (!!tokenData && !isNaN(tokenData.decimals)) { this.decimals = tokenData.decimals } this.tokenData = tokenData return this.tokenData } /** * Returns basic data about the vault's share token * @returns */ async getShareData(): Promise { if (this.shareData !== undefined) return this.shareData const source = 'Vault [getShareData]' await validateClientNetwork(this.chainId, this.publicClient, source) const shareData = (await getTokenInfo(this.publicClient, [this.address]))[this.address] if (!!shareData) { if (!isNaN(shareData.decimals)) { this.decimals = shareData.decimals } if (this.name === undefined) { this.name = shareData.name } } this.shareData = shareData return this.shareData } /** * Returns a user's balance for the vault's underlying asset * @param userAddress the user's address to get a balance for * @returns */ async getUserTokenBalance(userAddress: string): Promise { const source = 'Vault [getUserTokenBalance]' validateAddress(userAddress, source) await validateClientNetwork(this.chainId, this.publicClient, source) const tokenAddress = await this.getTokenAddress() const tokenBalance = await getTokenBalances(this.publicClient, userAddress as Address, [ tokenAddress ]) return tokenBalance[tokenAddress] } /** * Returns a user's balance for the vault's share token * @param userAddress the user's address to get a balance for * @returns */ async getUserShareBalance(userAddress: string): Promise { const source = 'Vault [getUserShareBalance]' validateAddress(userAddress, source) await validateClientNetwork(this.chainId, this.publicClient, source) const shareBalance = await getTokenBalances(this.publicClient, userAddress as Address, [ this.address ]) return shareBalance[this.address] } /** * Returns a user's delegate balance for the vault's share token * @param userAddress the user's address to get a balance for * @returns */ async getUserDelegateBalance(userAddress: string): Promise { const source = 'Vault [getUserDelegateBalance]' await validateClientNetwork(this.chainId, this.publicClient, source) const twabController = await this.getTWABController() const delegateBalance = await this.publicClient.readContract({ address: twabController, abi: twabControllerABI, functionName: 'delegateBalanceOf', args: [this.address, userAddress as Address] }) return delegateBalance } /** * Returns a user's delegate for the vault * @param userAddress the user's address to get delegate for * @returns */ async getUserDelegate(userAddress: string): Promise
{ const source = 'Vault [getUserDelegate]' await validateClientNetwork(this.chainId, this.publicClient, source) const twabController = await this.getTWABController() const delegate = await this.publicClient.readContract({ address: twabController, abi: twabControllerABI, functionName: 'delegateOf', args: [this.address, userAddress as Address] }) return delegate } /** * Returns a user's allowance for the vault's underlying asset * @param userAddress the user's address to get an allowance for * @returns */ async getUserTokenAllowance(userAddress: string): Promise { const source = 'Vault [getUserTokenAllowance]' validateAddress(userAddress, source) await validateClientNetwork(this.chainId, this.publicClient, source) const tokenAddress = await this.getTokenAddress() const tokenAllowance = await getTokenAllowances( this.publicClient, userAddress as Address, this.address, [tokenAddress] ) return tokenAllowance[tokenAddress] } /** * Returns the equivalent amount of underlying assets from a share amount * @param shares the amount of shares to convert * @returns */ async getAssetsFromShares(shares: bigint): Promise { const source = 'Vault [getAssetsFromShares]' await validateClientNetwork(this.chainId, this.publicClient, source) const assets = await this.publicClient.readContract({ address: this.address, abi: vaultABI, functionName: 'convertToAssets', args: [shares] }) return assets } /** * Returns the equivalent amount of shares from an underlying asset amount * @param assets the amount of assets to convert * @returns */ async getSharesFromAssets(assets: bigint): Promise { const source = 'Vault [getSharesFromAssets]' await validateClientNetwork(this.chainId, this.publicClient, source) const shares = await this.publicClient.readContract({ address: this.address, abi: vaultABI, functionName: 'convertToShares', args: [assets] }) return shares } /** * Returns the total amount of underlying assets deposited in the vault * @returns */ async getTotalTokenBalance(): Promise { const source = 'Vault [getTotalTokenBalance]' await validateClientNetwork(this.chainId, this.publicClient, source) const tokenData = await this.getTokenData() const getTotalAssets = () => this.publicClient .readContract({ address: this.address, abi: vaultABI, functionName: 'totalPreciseAssets' }) .catch(() => { console.warn( `${source} | Could not query "totalPreciseAssets" for ${this.address} on network ${this.chainId}, falling back to "totalAssets"` ) return this.publicClient.readContract({ address: this.address, abi: vaultABI, functionName: 'totalAssets' }) }) const totalAssets = await getTotalAssets() return { ...tokenData, amount: totalAssets } } /** * Returns the exchange rate from 1 share to the vault's underlying asset * @returns */ async getExchangeRate(): Promise { const source = 'Vault [getExchangeRate]' await validateClientNetwork(this.chainId, this.publicClient, source) const decimals = this.decimals ?? (await this.getTokenData()).decimals const exchangeRate = await this.getAssetsFromShares(parseUnits('1', decimals)) this.exchangeRate = exchangeRate return this.exchangeRate } /** * Returns the address of the vault's currently set liquidation pair contract * @returns */ async getLiquidationPair(): Promise
{ if (this.liquidationPair !== undefined) return this.liquidationPair const source = 'Vault [getLiquidationPair]' await validateClientNetwork(this.chainId, this.publicClient, source) const liquidationPair = await this.publicClient.readContract({ address: this.address, abi: vaultABI, functionName: 'liquidationPair' }) this.liquidationPair = liquidationPair return this.liquidationPair } /** * Returns the address of the vault's currently set claimer contract * @returns */ async getClaimer(): Promise
{ if (this.claimer !== undefined) return this.claimer const source = 'Vault [getClaimer]' await validateClientNetwork(this.chainId, this.publicClient, source) const claimer = await this.publicClient.readContract({ address: this.address, abi: vaultABI, functionName: 'claimer' }) this.claimer = claimer return this.claimer } /** * Returns the address of the vault's underlying yield source * @returns */ async getYieldSource(): Promise
{ if (this.yieldSource !== undefined) return this.yieldSource const source = 'Vault [getYieldSource]' await validateClientNetwork(this.chainId, this.publicClient, source) const yieldSource = await this.publicClient.readContract({ address: this.address, abi: vaultABI, functionName: 'yieldVault' }) this.yieldSource = yieldSource return this.yieldSource } /** * Returns the address of the vault's TWAB controller * @returns */ async getTWABController(): Promise
{ if (this.twabController !== undefined) return this.twabController const source = 'Vault [getTWABController]' await validateClientNetwork(this.chainId, this.publicClient, source) const twabController = await this.publicClient.readContract({ address: this.address, abi: vaultABI, functionName: 'twabController' }) this.twabController = twabController return this.twabController } /** * Returns the vault's current fee configuration * @returns */ async getFeeInfo(): Promise<{ percent: number; recipient: Address }> { if (this.feePercent !== undefined && this.feeRecipient !== undefined) { return { percent: this.feePercent, recipient: this.feeRecipient } } const source = 'Vault [getFeeInfo]' await validateClientNetwork(this.chainId, this.publicClient, source) if (this.feePercent === undefined) { const feeValue = await this.publicClient.readContract({ address: this.address, abi: vaultABI, functionName: 'yieldFeePercentage' }) this.feePercent = Number(feeValue) } if (this.feeRecipient === undefined) { const feeRecipient = await this.publicClient.readContract({ address: this.address, abi: vaultABI, functionName: 'yieldFeeRecipient' }) this.feeRecipient = feeRecipient } return { percent: this.feePercent, recipient: this.feeRecipient } } /** * Returns the vault's currently available fee balance to claim * @returns */ async getFeesAvailable(): Promise { const source = 'Vault [getFeesAvailable]' await validateClientNetwork(this.chainId, this.publicClient, source) const feesAvailable = await this.publicClient.readContract({ address: this.address, abi: vaultABI, functionName: 'yieldFeeBalance' }) this.feesAvailable = feesAvailable return this.feesAvailable } /** * Returns the address of the vault's owner * @returns */ async getOwner(): Promise
{ if (this.owner !== undefined) return this.owner const source = 'Vault [getOwner]' await validateClientNetwork(this.chainId, this.publicClient, source) const owner = await this.publicClient.readContract({ address: this.address, abi: vaultABI, functionName: 'owner' }) this.owner = owner return this.owner } /** * Returns the vault's total delegate supply (total supply - sponsored supply) * @returns */ async getTotalDelegateSupply(): Promise { const source = 'Vault [getTotalDelegateSupply]' await validateClientNetwork(this.chainId, this.publicClient, source) const twabControllerAddress = await this.getTWABController() const totalDelegateSupply = await this.publicClient.readContract({ address: twabControllerAddress, abi: twabControllerABI, functionName: 'totalSupplyDelegateBalance', args: [this.address] }) return totalDelegateSupply } /* ============================== Write Functions ============================== */ /** * Submits a transaction to deposit into the vault * @param amount an unformatted token amount (w/ decimals) * @param overrides optional overrides for this transaction * @returns */ async deposit(amount: bigint, overrides?: TxOverrides) { const source = 'Vault [deposit]' if (!this.walletClient?.account) { throw new Error(`${source} | Invalid/Unavailable Viem Wallet Client`) } const { request } = await this.publicClient.simulateContract({ account: this.walletClient.account, address: this.address, abi: vaultABI, functionName: 'deposit', args: [amount, this.walletClient.account.address], chain: this.walletClient.chain, ...overrides }) const txHash = await this.walletClient.writeContract(request) return txHash } /** * Submits a transaction to deposit into the vault and send shares to another address * @param amount an unformatted token amount (w/ decimals) * @param receiver the address to send shares to * @param overrides optional overrides for this transaction * @returns */ async depositTo(amount: bigint, receiver: Address, overrides?: TxOverrides) { const source = 'Vault [depositTo]' if (!this.walletClient?.account) { throw new Error(`${source} | Invalid/Unavailable Viem Wallet Client`) } validateAddress(receiver, source) const { request } = await this.publicClient.simulateContract({ account: this.walletClient.account, address: this.address, abi: vaultABI, functionName: 'deposit', args: [amount, receiver], chain: this.walletClient.chain, ...overrides }) const txHash = await this.walletClient.writeContract(request) return txHash } /** * Submits a transaction to withdraw from the vault * @param amount an unformatted token amount (w/ decimals) * @param options optional args or tx overrides * @returns */ async withdraw(amount: bigint, options?: { maxShares?: bigint; overrides?: TxOverrides }) { const source = 'Vault [withdraw]' if (!this.walletClient?.account) { throw new Error(`${source} | Invalid/Unavailable Viem Wallet Client`) } const { request } = await this.publicClient.simulateContract({ account: this.walletClient.account, address: this.address, abi: vaultABI, functionName: 'withdraw', args: !!options?.maxShares ? [ amount, this.walletClient.account.address, this.walletClient.account.address, options.maxShares ] : [amount, this.walletClient.account.address, this.walletClient.account.address], chain: this.walletClient.chain, ...options?.overrides }) const txHash = await this.walletClient.writeContract(request) return txHash } /** * Submits a transaction to withdraw from the vault and send underlying assets to another address * @param amount an unformatted token amount (w/ decimals) * @param receiver the address to send assets to * @param options optional args or tx overrides * @returns */ async withdrawTo( amount: bigint, receiver: Address, options?: { maxShares?: bigint; overrides?: TxOverrides } ) { const source = 'Vault [withdrawTo]' if (!this.walletClient?.account) { throw new Error(`${source} | Invalid/Unavailable Viem Wallet Client`) } validateAddress(receiver, source) const { request } = await this.publicClient.simulateContract({ account: this.walletClient.account, address: this.address, abi: vaultABI, functionName: 'withdraw', args: !!options?.maxShares ? [amount, receiver, this.walletClient.account.address, options.maxShares] : [amount, receiver, this.walletClient.account.address], chain: this.walletClient.chain, ...options?.overrides }) const txHash = await this.walletClient.writeContract(request) return txHash } /** * Submits a transaction to redeem shares from the vault * @param amount an unformatted share amount (w/ decimals) * @param options optional args or tx overrides * @returns */ async redeem(amount: bigint, options?: { minAssets?: bigint; overrides?: TxOverrides }) { const source = 'Vault [redeem]' if (!this.walletClient?.account) { throw new Error(`${source} | Invalid/Unavailable Viem Wallet Client`) } const { request } = await this.publicClient.simulateContract({ account: this.walletClient.account, address: this.address, abi: vaultABI, functionName: 'redeem', args: !!options?.minAssets ? [ amount, this.walletClient.account.address, this.walletClient.account.address, options.minAssets ] : [amount, this.walletClient.account.address, this.walletClient.account.address], chain: this.walletClient.chain, ...options?.overrides }) const txHash = await this.walletClient.writeContract(request) return txHash } /** * Submits a transaction to redeem shares from the vault and send underlying assets to another address * @param amount an unformatted share amount (w/ decimals) * @param receiver the address to send assets to * @param options optional args or tx overrides * @returns */ async redeemTo( amount: bigint, receiver: Address, options?: { minAssets?: bigint; overrides?: TxOverrides } ) { const source = 'Vault [redeemTo]' if (!this.walletClient?.account) { throw new Error(`${source} | Invalid/Unavailable Viem Wallet Client`) } validateAddress(receiver, source) const { request } = await this.publicClient.simulateContract({ account: this.walletClient.account, address: this.address, abi: vaultABI, functionName: 'redeem', args: !!options?.minAssets ? [amount, receiver, this.walletClient.account.address, options.minAssets] : [amount, receiver, this.walletClient.account.address], chain: this.walletClient.chain, ...options?.overrides }) const txHash = await this.walletClient.writeContract(request) return txHash } /** * Submits a transaction to set an allowance for vault deposits * @param amount an unformatted token amount (w/ decimals) * @param overrides optional overrides for this transaction * @returns */ async approveDeposit(amount: bigint, overrides?: TxOverrides) { const source = 'Vault [approveDeposit]' if (!this.walletClient?.account) { throw new Error(`${source} | Invalid/Unavailable Viem Wallet Client`) } const tokenAddress = await this.getTokenAddress() const { request } = await this.publicClient.simulateContract({ account: this.walletClient.account, address: tokenAddress, abi: erc20ABI, functionName: 'approve', args: [this.address, amount], chain: this.walletClient.chain, ...overrides }) const txHash = await this.walletClient.writeContract(request) return txHash } /** * Submits a transaction to set an allowance of 0 for vault deposits * @param overrides optional overrides for this transaction * @returns */ async revokeAllowance(overrides?: TxOverrides) { const source = 'Vault [revokeAllowance]' if (!this.walletClient?.account) { throw new Error(`${source} | Invalid/Unavailable Viem Wallet Client`) } const tokenAddress = await this.getTokenAddress() const { request } = await this.publicClient.simulateContract({ account: this.walletClient.account, address: tokenAddress, abi: erc20ABI, functionName: 'approve', args: [this.address, 0n], chain: this.walletClient.chain, ...overrides }) const txHash = await this.walletClient.writeContract(request) return txHash } /** * Submits a transaction to claim yield fees * @param amount an unformatted share amount (w/ decimals) * @param overrides optional overrides for this transaction * @returns */ async claimFees(amount: bigint, overrides?: TxOverrides) { const source = 'Vault [claimFees]' if (!this.walletClient?.account) { throw new Error(`${source} | Invalid/Unavailable Viem Wallet Client`) } const { request } = await this.publicClient.simulateContract({ account: this.walletClient.account, address: this.address, abi: vaultABI, functionName: 'claimYieldFeeShares', args: [amount], chain: this.walletClient.chain, ...overrides }) const txHash = await this.walletClient.writeContract(request) return txHash } }