Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(earn): prepare transactions for supply when gas fee is covered #5483

Merged
merged 6 commits into from
May 29, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 82 additions & 2 deletions src/earn/prepareTransactions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
prepareSupplyTransactions,
prepareWithdrawAndClaimTransactions,
} from 'src/earn/prepareTransactions'
import { getDynamicConfigParams } from 'src/statsig'
import { getDynamicConfigParams, getFeatureGate } from 'src/statsig'
import { StatsigDynamicConfigs } from 'src/statsig/types'
import { TokenBalance } from 'src/tokens/slice'
import { Network, NetworkId } from 'src/transactions/types'
Expand Down Expand Up @@ -63,10 +63,11 @@ describe('prepareTransactions', () => {
jest.mocked(encodeFunctionData).mockReturnValue('0xencodedData')
jest.mocked(getDynamicConfigParams).mockImplementation(({ configName, defaultValues }) => {
if (configName === StatsigDynamicConfigs.EARN_STABLECOIN_CONFIG) {
return { ...defaultValues, depositGasPadding: 100 }
return { ...defaultValues, depositGasPadding: 100, approveGasPadding: 200 }
}
return defaultValues
})
jest.mocked(getFeatureGate).mockReturnValue(false)
})

describe('prepareSupplyTransactions', () => {
Expand Down Expand Up @@ -146,6 +147,84 @@ describe('prepareTransactions', () => {
feeCurrencies: [mockFeeCurrency],
spendToken: mockToken,
spendTokenAmount: new BigNumber(5),
isGasSubsidized: false,
})
})
it('prepares fees from the cloud function for approve and supply when subsidizing gas fees', async () => {
mockFetch.mockResponseOnce(
JSON.stringify({
status: 'OK',
simulatedTransactions: [
{
status: 'success',
blockNumber: '1',
gasNeeded: 3000,
gasUsed: 2800,
gasPrice: '1',
},
{
status: 'success',
blockNumber: '1',
gasNeeded: 50000,
gasUsed: 49800,
gasPrice: '1',
},
],
})
)
jest.mocked(getFeatureGate).mockReturnValue(true)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

specify the feature gate here. E.g., jest.mocked(getFeatureGate).mockImplementation(gate => gate === <gate-name>), so this can be cleaned up when removing a gate. Also should this be reset in a beforeEach so it doesn't affect other tests?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

already had it reset in the beforeEach, but I'll do the mockImplementation in both


const result = await prepareSupplyTransactions({
amount: '5',
token: mockToken,
walletAddress: '0x1234',
feeCurrencies: [mockFeeCurrency],
poolContractAddress: '0x5678',
})

const expectedTransactions = [
{
from: '0x1234',
to: mockTokenAddress,
data: '0xencodedData',
gas: BigInt(3200),
_estimatedGasUse: BigInt(2800),
},
{
from: '0x1234',
to: '0x5678',
data: '0xencodedData',
gas: BigInt(50100),
_estimatedGasUse: BigInt(49800),
},
]
expect(result).toEqual({
type: 'possible',
feeCurrency: mockFeeCurrency,
transactions: expectedTransactions,
})
expect(publicClient[Network.Arbitrum].readContract).toHaveBeenCalledWith({
address: mockTokenAddress,
abi: erc20.abi,
functionName: 'allowance',
args: ['0x1234', '0x5678'],
})
expect(encodeFunctionData).toHaveBeenNthCalledWith(1, {
abi: erc20.abi,
functionName: 'approve',
args: ['0x5678', BigInt(5e6)],
})
expect(encodeFunctionData).toHaveBeenNthCalledWith(2, {
abi: aavePool,
functionName: 'supply',
args: [mockTokenAddress, BigInt(5e6), '0x1234', 0],
})
expect(prepareTransactions).toHaveBeenCalledWith({
baseTransactions: expectedTransactions,
feeCurrencies: [mockFeeCurrency],
spendToken: mockToken,
spendTokenAmount: new BigNumber(5),
isGasSubsidized: true,
})
})

Expand Down Expand Up @@ -204,6 +283,7 @@ describe('prepareTransactions', () => {
feeCurrencies: [mockFeeCurrency],
spendToken: mockToken,
spendTokenAmount: new BigNumber(5),
isGasSubsidized: false,
})
})

Expand Down
20 changes: 17 additions & 3 deletions src/earn/prepareTransactions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
import aavePool from 'src/abis/AavePoolV3'
import erc20 from 'src/abis/IERC20'
import { RewardsInfo } from 'src/earn/types'
import { getDynamicConfigParams } from 'src/statsig'
import { getDynamicConfigParams, getFeatureGate } from 'src/statsig'
import { DynamicConfigs } from 'src/statsig/constants'
import { StatsigDynamicConfigs } from 'src/statsig/types'
import { StatsigDynamicConfigs, StatsigFeatureGates } from 'src/statsig/types'
import { TokenBalance } from 'src/tokens/slice'
import Logger from 'src/utils/Logger'
import { ensureError } from 'src/utils/ensureError'
Expand Down Expand Up @@ -122,7 +122,7 @@
)
}

const { depositGasPadding } = getDynamicConfigParams(
const { depositGasPadding, approveGasPadding } = getDynamicConfigParams(
DynamicConfigs[StatsigDynamicConfigs.EARN_STABLECOIN_CONFIG]
)

Expand All @@ -131,11 +131,25 @@
)
baseTransactions[baseTransactions.length - 1]._estimatedGasUse = BigInt(supplySimulatedTx.gasUsed)

const isGasSubsidized = getFeatureGate(StatsigFeatureGates.SUBSIDIZE_STABLECOIN_EARN_GAS_FEES)
if (isGasSubsidized && baseTransactions.length > 1) {
// extract fee of the approve transaction and set gas fields
const approveSimulatedTx = simulatedTransactions[0]
if (approveSimulatedTx.status !== 'success') {
throw new Error(

Check warning on line 139 in src/earn/prepareTransactions.ts

View check run for this annotation

Codecov / codecov/patch

src/earn/prepareTransactions.ts#L139

Added line #L139 was not covered by tests
`Failed to simulate approve transaction. response: ${JSON.stringify(simulatedTransactions)}`
)
}
baseTransactions[0].gas = BigInt(approveSimulatedTx.gasNeeded) + BigInt(approveGasPadding)
baseTransactions[0]._estimatedGasUse = BigInt(approveSimulatedTx.gasUsed)
}

return prepareTransactions({
feeCurrencies,
baseTransactions,
spendToken: token,
spendTokenAmount: new BigNumber(amount),
isGasSubsidized,
})
}

Expand Down
1 change: 1 addition & 0 deletions src/statsig/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ export const DynamicConfigs = {
providerLogoUrl: '',
providerTermsAndConditionsUrl: '',
depositGasPadding: 0,
approveGasPadding: 0,
moreAavePoolsUrl: '',
},
},
Expand Down
81 changes: 81 additions & 0 deletions src/viem/prepareTransactions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,47 @@ describe('prepareTransactions module', () => {
feeCurrencies: mockFeeCurrencies,
})
})
it("returns a 'possible' result when the balances for feeCurrencies are too low to cover the fee but isGasSubsidized is true", async () => {
mocked(estimateFeesPerGas).mockResolvedValue({
maxFeePerGas: BigInt(100),
maxPriorityFeePerGas: BigInt(2),
baseFeePerGas: BigInt(50),
})
mocked(estimateGas).mockResolvedValue(BigInt(1_000))

// max gas fee is 10 * 10k = 100k units, too high for either fee currency
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

where do the 10 and 10k come from? should this be 100 (maxFeePerGas) * 1k (gas) ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was copy pasted from the previous test...I think you're right. I'll fix it in both!


const result = await prepareTransactions({
feeCurrencies: mockFeeCurrencies,
spendToken: mockSpendToken,
spendTokenAmount: new BigNumber(45_000),
decreasedAmountGasFeeMultiplier: 1,
baseTransactions: [
{
from: '0xfrom' as Address,
to: '0xto' as Address,
data: '0xdata',
},
],
isGasSubsidized: true,
})
expect(result).toStrictEqual({
type: 'possible',
feeCurrency: mockFeeCurrencies[0],
transactions: [
{
from: '0xfrom',
to: '0xto',
data: '0xdata',

gas: BigInt(1000),
maxFeePerGas: BigInt(100),
maxPriorityFeePerGas: BigInt(2),
_baseFeePerGas: BigInt(50),
},
],
})
})
it("returns a 'not-enough-balance-for-gas' result when gas estimation throws error due to insufficient funds", async () => {
mocked(estimateFeesPerGas).mockResolvedValue({
maxFeePerGas: BigInt(100),
Expand Down Expand Up @@ -304,6 +345,46 @@ describe('prepareTransactions module', () => {
decreasedSpendAmount: new BigNumber(4.35), // 70.0 balance minus maxGasFee
})
})
it("returns a 'possible' result when spending the exact max amount of a feeCurrency, and no other feeCurrency has enough balance to pay for the fee and isGasSubsidized is true", async () => {
mocked(estimateFeesPerGas).mockResolvedValue({
maxFeePerGas: BigInt(1),
maxPriorityFeePerGas: BigInt(2),
baseFeePerGas: BigInt(1),
})

const result = await prepareTransactions({
feeCurrencies: mockFeeCurrencies,
spendToken: mockFeeCurrencies[1],
spendTokenAmount: mockFeeCurrencies[1].balance.shiftedBy(mockFeeCurrencies[1].decimals),
decreasedAmountGasFeeMultiplier: 1.01,
isGasSubsidized: true,
baseTransactions: [
{
from: '0xfrom' as Address,
to: '0xto' as Address,
data: '0xdata',
_estimatedGasUse: BigInt(50),
gas: BigInt(15_000),
},
],
})
expect(result).toStrictEqual({
type: 'possible',
feeCurrency: mockFeeCurrencies[0],
transactions: [
{
_baseFeePerGas: BigInt(1),
_estimatedGasUse: BigInt(50),
from: '0xfrom',
gas: BigInt(15_000),
maxFeePerGas: BigInt(1),
maxPriorityFeePerGas: BigInt(2),
to: '0xto',
data: '0xdata',
},
],
})
})
it("returns a 'need-decrease-spend-amount-for-gas' result when spending close to the max amount of a feeCurrency, and no other feeCurrency has enough balance to pay for the fee", async () => {
mocked(estimateFeesPerGas).mockResolvedValue({
maxFeePerGas: BigInt(1),
Expand Down
10 changes: 7 additions & 3 deletions src/viem/prepareTransactions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,7 @@ export async function tryEstimateTransactions(
* @param decreasedAmountGasFeeMultiplier
* @param baseTransactions
* @param throwOnSpendTokenAmountExceedsBalance
* @param isGasSubsidized - This flag should only be set to true if all of the baseTransactions already have gas estimates, aka the 'gas' and '_estimatedGasUse' fields have been manually set
*/
export async function prepareTransactions({
feeCurrencies,
Expand All @@ -269,13 +270,15 @@ export async function prepareTransactions({
decreasedAmountGasFeeMultiplier = 1,
baseTransactions,
throwOnSpendTokenAmountExceedsBalance = true,
isGasSubsidized = false,
}: {
feeCurrencies: TokenBalance[]
spendToken?: TokenBalance
spendTokenAmount?: BigNumber
decreasedAmountGasFeeMultiplier?: number
baseTransactions: (TransactionRequest & { gas?: bigint })[]
throwOnSpendTokenAmountExceedsBalance?: boolean
isGasSubsidized?: boolean
}): Promise<PreparedTransactionsResult> {
if (!spendToken && spendTokenAmount.isGreaterThan(0)) {
throw new Error(
Expand All @@ -299,7 +302,7 @@ export async function prepareTransactions({
estimatedGasFeeInDecimal: BigNumber
}> = []
for (const feeCurrency of feeCurrencies) {
if (feeCurrency.balance.isLessThanOrEqualTo(0)) {
if (feeCurrency.balance.isLessThanOrEqualTo(0) && !isGasSubsidized) {
// No balance, try next fee currency
continue
}
Expand All @@ -314,15 +317,16 @@ export async function prepareTransactions({
const estimatedGasFee = getEstimatedGasFee(estimatedTransactions)
const estimatedGasFeeInDecimal = estimatedGasFee?.shiftedBy(-feeDecimals)
gasFees.push({ feeCurrency, maxGasFeeInDecimal, estimatedGasFeeInDecimal })
if (maxGasFeeInDecimal.isGreaterThan(feeCurrency.balance)) {
if (maxGasFeeInDecimal.isGreaterThan(feeCurrency.balance) && !isGasSubsidized) {
// Not enough balance to pay for gas, try next fee currency
continue
}
const spendAmountDecimal = spendTokenAmount.shiftedBy(-(spendToken?.decimals ?? 0))
if (
spendToken &&
spendToken.tokenId === feeCurrency.tokenId &&
spendAmountDecimal.plus(maxGasFeeInDecimal).isGreaterThan(spendToken.balance)
spendAmountDecimal.plus(maxGasFeeInDecimal).isGreaterThan(spendToken.balance) &&
!isGasSubsidized
) {
// Not enough balance to pay for gas, try next fee currency
continue
Expand Down
Loading