Skip to content
This repository has been archived by the owner on Oct 15, 2024. It is now read-only.

Commit

Permalink
feat: add liquidity unbalanced permit2 (#1117)
Browse files Browse the repository at this point in the history
* feat: add permit2 signature step

* chore: rename includesNativeAsset

* feat: avoid buildcall when permit2 is not signed

* fix: add nonces query

* chore: add token symbols to permit2 transfer label

* refactor: extract base abstract class

* refactor: extract signature helpers

* fix: sign permit2 loading condition

* chore: extract token helpers

* feat: add 24h permit2 expiration

* fix: unit tests after rename

* chore: refactor hasValidPermit2

---------

Co-authored-by: uiuxxx <[email protected]>
  • Loading branch information
agualis and uiuxxx authored Oct 7, 2024
1 parent 78365e6 commit c2b5bba
Show file tree
Hide file tree
Showing 49 changed files with 953 additions and 248 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ jobs:
uses: ./.github/actions/setup
- name: Set up foundry (includes anvil)
uses: foundry-rs/foundry-toolchain@v1
# with:
# cache: false # Disable cache after foundry-toolchain upgrade
with:
cache: false # Disable cache after foundry-toolchain upgrade
- name: Run integration tests
run: pnpm test:integration
3 changes: 3 additions & 0 deletions app/(app)/debug/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ export default function Debug() {
<Link as={NextLink} href="/debug/revoke-relayer-approval">
Revoke relayer approval
</Link>
<Link as={NextLink} href="/debug/permit2-allowance">
Permit2 allowance
</Link>
</VStack>
</FadeInOnView>
)
Expand Down
64 changes: 64 additions & 0 deletions app/(app)/debug/permit2-allowance/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
'use client'

import { getGqlChain, getNetworkConfig } from '@/lib/config/app.config'
import { BPT_DECIMALS } from '@/lib/modules/pool/pool.constants'
import { useUserAccount } from '@/lib/modules/web3/UserAccountProvider'
import { permit2Abi } from '@balancer/sdk'
import { Center, Input, Text, VStack } from '@chakra-ui/react'
import { useState } from 'react'
import { Address, formatUnits } from 'viem'
import { sepolia } from 'viem/chains'
import { useReadContract } from 'wagmi'

export default function Page() {
const [tokenAddress, setTokenAddress] = useState<Address>('' as Address)

const { chain, userAddress } = useUserAccount()

const chainId = chain?.id || sepolia.id

const { data } = usePermit2Allowance({ chainId, tokenAddress, owner: userAddress })

return (
<Center>
<VStack w="50%">
<Text>
Enter address of token to check permit2 allowance in the current chain:{' '}
{chain ? chain.name : 'None'}
</Text>
<Input type="text" onChange={e => setTokenAddress(e.target.value as Address)} />

{data && (
<div>
<div>Amount: {formatUnits(data[0], BPT_DECIMALS).toString()}</div>
<div>Expires: {data[1]}</div>
<div>Nonce: {data[2]}</div>
</div>
)}
</VStack>
</Center>
)
}

type Params = {
chainId: number
tokenAddress: Address
owner: Address
}
function usePermit2Allowance({ chainId, tokenAddress, owner }: Params) {
const permit2Address = '0x000000000022D473030F116dDEE9F6B43aC78BA3'
const balancerRouter = getNetworkConfig(getGqlChain(chainId)).contracts.balancer.router!
const spender = balancerRouter

return useReadContract({
chainId,
address: permit2Address,
abi: permit2Abi,
functionName: 'allowance',
args: [owner, tokenAddress, spender],
query: {
enabled: !!tokenAddress && !!owner,
},
})
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { isHash } from 'viem'
import { usePoolRedirect } from '@/lib/modules/pool/pool.hooks'
import { DefaultPageContainer } from '@/lib/shared/components/containers/DefaultPageContainer'
import { AddLiquidityProvider } from '@/lib/modules/pool/actions/add-liquidity/AddLiquidityProvider'
import { Permit2SignatureProvider } from '@/lib/modules/tokens/approvals/permit2/Permit2SignatureProvider'

type Props = PropsWithChildren<{
params: { txHash?: string[] }
Expand Down Expand Up @@ -40,11 +41,13 @@ export default function AddLiquidityLayout({ params: { txHash }, children }: Pro
<DefaultPageContainer>
<TransactionStateProvider>
<RelayerSignatureProvider>
<TokenInputsValidationProvider>
<AddLiquidityProvider urlTxHash={urlTxHash}>
<PriceImpactProvider>{children}</PriceImpactProvider>
</AddLiquidityProvider>
</TokenInputsValidationProvider>
<Permit2SignatureProvider>
<TokenInputsValidationProvider>
<AddLiquidityProvider urlTxHash={urlTxHash}>
<PriceImpactProvider>{children}</PriceImpactProvider>
</AddLiquidityProvider>
</TokenInputsValidationProvider>
</Permit2SignatureProvider>
</RelayerSignatureProvider>
</TransactionStateProvider>
</DefaultPageContainer>
Expand Down
2 changes: 2 additions & 0 deletions lib/config/config.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ export interface ContractsConfig {
vaultV2: Address
// TODO: make it required when v3 is deployed in all networks
vaultV3?: Address
// TODO: make it required when v3 is deployed in all networks
router?: Address
relayerV6: Address
minter: Address
}
Expand Down
1 change: 1 addition & 0 deletions lib/config/networks/sepolia.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ const networkConfig: NetworkConfig = {
balancer: {
vaultV2: '0xBA12222222228d8Ba445958a75a0704d566BF2C8',
vaultV3: '0x0EF1c156a7986F394d90eD1bEeA6483Cc435F542',
router: '0xB12FcB422aAe6720f882E22C340964a7723f2387',
relayerV6: '0x7852fB9d0895e6e8b3EedA553c03F6e2F9124dF9',
minter: '0x1783Cd84b3d01854A96B4eD5843753C2CcbD574A',
},
Expand Down
49 changes: 33 additions & 16 deletions lib/modules/pool/actions/LiquidityActionHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,29 +11,31 @@ import {
MinimalToken,
NestedPoolState,
PoolState,
PoolStateWithBalances,
Token,
TokenAmount,
mapPoolToNestedPoolState,
mapPoolType,
PoolStateWithBalances,
Token,
} from '@balancer/sdk'
import { Hex, formatUnits, parseUnits, Address } from 'viem'
import {
isAffectedByCspIssue,
isComposableStableV1,
isCowAmmPool,
isGyro,
isV3Pool,
} from '../pool.helpers'
import { Pool } from '../PoolProvider'
import BigNumber from 'bignumber.js'
import { Address, Hex, formatUnits, parseUnits } from 'viem'
import { GetTokenFn } from '../../tokens/TokensProvider'
import {
isNativeAsset,
isNativeOrWrappedNative,
isWrappedNativeAsset,
swapNativeWithWrapped,
} from '../../tokens/token.helpers'
import { HumanTokenAmountWithAddress } from '../../tokens/token.types'
import BigNumber from 'bignumber.js'
import { Pool } from '../PoolProvider'
import {
isAffectedByCspIssue,
isComposableStableV1,
isCowAmmPool,
isGyro,
isV3Pool,
} from '../pool.helpers'
import { SdkQueryAddLiquidityOutput } from './add-liquidity/add-liquidity.types'

// Null object used to avoid conditional checks during hook loading state
const NullPool: Pool = {
Expand Down Expand Up @@ -313,10 +315,25 @@ export function hasNoLiquidity(pool: Pool): boolean {
}

// When the pool has version < v3, it adds extra buildCall params (sender and recipient) that must be present only in V1/V2
export function formatBuildCallParams<T>(buildCallParams: T, isV3Pool: boolean, account: Address) {
// sender must be undefined for v3 pools
if (isV3Pool) return buildCallParams

export function formatBuildCallParams<T>(buildCallParams: T, account: Address) {
// sender and recipient must be defined only for v1 and v2 pools
return { ...buildCallParams, sender: account, recipient: account }
}

export function getTokenSymbols(
getToken: GetTokenFn,
chain: GqlChain,
queryOutput?: SdkQueryAddLiquidityOutput
) {
if (!queryOutput?.sdkQueryOutput) return []
const amountsIn = queryOutput.sdkQueryOutput.amountsIn
const tokenSymbols = amountsIn
?.filter(a => a.amount > 0n)
.map(a => getToken(a.token.address, chain)?.symbol ?? 'Unknown')
return tokenSymbols
}

export function getTokenAddresses(queryOutput?: SdkQueryAddLiquidityOutput): Address[] | undefined {
if (!queryOutput?.sdkQueryOutput) return undefined
return queryOutput.sdkQueryOutput.amountsIn.map(t => t.token.address)
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { AddLiquidityNestedQueryOutput, AddLiquidityQueryOutput, TokenAmount } from '@balancer/sdk'
import {
AddLiquidityNestedQueryOutput,
AddLiquidityQueryOutput,
Permit2,
TokenAmount,
} from '@balancer/sdk'
import { Address } from 'viem'
import { HumanTokenAmountWithAddress } from '@/lib/modules/tokens/token.types'

Expand All @@ -17,6 +22,7 @@ export interface BuildAddLiquidityInput {
slippagePercent: string
queryOutput: QueryAddLiquidityOutput
relayerApprovalSignature?: Address //only used by Nested Add Liquidity in signRelayer mode
permit2?: Permit2 //only used by v3 add liquidity
}

/*
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { HumanTokenAmountWithAddress } from '@/lib/modules/tokens/token.types'
import { TransactionConfig } from '@/lib/modules/web3/contracts/contract.types'
import { BuildAddLiquidityInput, QueryAddLiquidityOutput } from '../add-liquidity.types'
import { HumanTokenAmountWithAddress } from '@/lib/modules/tokens/token.types'

/**
* AddLiquidityHandler is an interface that defines the methods that must be implemented by a handler.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,26 +8,17 @@ import {
AddLiquidityUnbalancedInput,
PriceImpact,
PriceImpactAmount,
Slippage,
} from '@balancer/sdk'
import { Pool } from '../../../PoolProvider'
import {
LiquidityActionHelpers,
formatBuildCallParams,
areEmptyAmounts,
} from '../../LiquidityActionHelpers'
import { AddLiquidityHandler } from './AddLiquidity.handler'
import { LiquidityActionHelpers, areEmptyAmounts } from '../../LiquidityActionHelpers'
import { SdkBuildAddLiquidityInput, SdkQueryAddLiquidityOutput } from '../add-liquidity.types'
import { AddLiquidityHandler } from './AddLiquidity.handler'

/**
* UnbalancedAddLiquidityHandler is a handler that implements the
* AddLiquidityHandler interface for unbalanced adds, e.g. where the user
* specifies the token amounts in. It uses the Balancer SDK to implement it's
* methods. It also handles the case where one of the input tokens is the native
* asset instead of the wrapped native asset.
* Base abstract class that shares common logic shared by v3 and v2/v1 pool unbalanced handlers
*/
export class UnbalancedAddLiquidityHandler implements AddLiquidityHandler {
helpers: LiquidityActionHelpers
export abstract class BaseUnbalancedAddLiquidityHandler implements AddLiquidityHandler {
protected helpers: LiquidityActionHelpers

constructor(pool: Pool) {
this.helpers = new LiquidityActionHelpers(pool)
Expand Down Expand Up @@ -60,41 +51,12 @@ export class UnbalancedAddLiquidityHandler implements AddLiquidityHandler {
return priceImpactABA.decimal
}

public async buildCallData({
humanAmountsIn,
account,
slippagePercent,
queryOutput,
}: SdkBuildAddLiquidityInput): Promise<TransactionConfig> {
const addLiquidity = new AddLiquidity()

const baseBuildCallParams = {
...queryOutput.sdkQueryOutput,
slippage: Slippage.fromPercentage(`${Number(slippagePercent)}`),
wethIsEth: this.helpers.isNativeAssetIn(humanAmountsIn),
}

const buildCallParams = formatBuildCallParams(
baseBuildCallParams,
this.helpers.isV3Pool(),
account
)

const { callData, to, value } = addLiquidity.buildCall(buildCallParams)

return {
account,
chainId: this.helpers.chainId,
data: callData,
to,
value,
}
}
public abstract buildCallData(input: SdkBuildAddLiquidityInput): Promise<TransactionConfig>

/**
* PRIVATE METHODS
*/
private constructSdkInput(
protected constructSdkInput(
humanAmountsIn: HumanTokenAmountWithAddress[]
): AddLiquidityUnbalancedInput {
const amountsIn = this.helpers.toSdkInputAmounts(humanAmountsIn)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,7 @@ import { getRpcUrl } from '@/lib/modules/web3/transports'

/**
* ProportionalAddLiquidityHandler is a handler that implements the
* AddLiquidityHandler interface for strictly proportional adds, e.g. where the user
* specifies the token amounts in. It uses the Balancer SDK to calculate the BPT
* out with the current pools state, then uses that bptOut for the query.
* AddLiquidityHandler interface for strictly proportional adds for V2 and CowAmm pools(v1).
*/
export class ProportionalAddLiquidityHandler implements AddLiquidityHandler {
helpers: LiquidityActionHelpers
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
/* eslint-disable max-len */
import networkConfig from '@/lib/config/networks/mainnet'
import { balAddress, wETHAddress, wjAuraAddress } from '@/lib/debug-helpers'
import { HumanTokenAmountWithAddress } from '@/lib/modules/tokens/token.types'
import { defaultTestUserAccount } from '@/test/anvil/anvil-setup'
import { aWjAuraWethPoolElementMock } from '@/test/msw/builders/gqlPoolElement.builders'
import { UnbalancedAddLiquidityHandler } from './UnbalancedAddLiquidity.handler'
import { UnbalancedAddLiquidityV2Handler } from './UnbalancedAddLiquidityV2.handler'
import { selectAddLiquidityHandler } from './selectAddLiquidityHandler'
import { HumanTokenAmountWithAddress } from '@/lib/modules/tokens/token.types'
import { Pool } from '../../../PoolProvider'

function selectUnbalancedHandler() {
return selectAddLiquidityHandler(aWjAuraWethPoolElementMock()) as UnbalancedAddLiquidityHandler
return selectAddLiquidityHandler(aWjAuraWethPoolElementMock()) as UnbalancedAddLiquidityV2Handler
}

describe('When adding unbalanced liquidity for a weighted V2 pool', () => {
Expand Down Expand Up @@ -73,45 +72,3 @@ describe('When adding unbalanced liquidity for a weighted V2 pool', () => {
expect(result.data).toBeDefined()
})
})

// TODO: unskip this test when sepolia V3 pools are available in production api
describe.skip('When adding unbalanced liquidity for a V3 pool', async () => {
// Sepolia
const balAddress = '0xb19382073c7a0addbb56ac6af1808fa49e377b75'
// const poolId = '0xec1b5ca86c83c7a85392063399e7d2170d502e00' // Sepolia B-50BAL-50WETH
// const v3Pool = await getPoolMock(poolId, GqlChain.Sepolia)
const v3Pool = {} as unknown as Pool

const handler = selectAddLiquidityHandler(v3Pool) as UnbalancedAddLiquidityHandler

const humanAmountsIn: HumanTokenAmountWithAddress[] = [
{ humanAmount: '0.1', tokenAddress: balAddress },
]

it('calculates price impact', async () => {
const priceImpact = await handler.getPriceImpact(humanAmountsIn)
expect(priceImpact).toBeGreaterThan(0.002)
})

it('queries bptOut', async () => {
const result = await handler.simulate(humanAmountsIn)

expect(result.bptOut.amount).toBeGreaterThan(100000000000000n)
})

it('builds Tx Config', async () => {
// Store query response in handler instance
const queryOutput = await handler.simulate(humanAmountsIn)

const result = await handler.buildCallData({
humanAmountsIn,
account: defaultTestUserAccount,
slippagePercent: '0.2',
queryOutput,
})

const sepoliaRouter = '0xB12FcB422aAe6720f882E22C340964a7723f2387'
expect(result.to).toBe(sepoliaRouter)
expect(result.data).toBeDefined()
})
})
Loading

0 comments on commit c2b5bba

Please sign in to comment.