Skip to content

Commit

Permalink
fix: missing gas padding on native gas token deposits (#849)
Browse files Browse the repository at this point in the history
* feat: allow setting gas buffer per chain

* fix: take gas costs into account for max balance native

* chore: requested changes from review

* fixup

* fixup
  • Loading branch information
dohaki authored Oct 13, 2023
1 parent afe7436 commit 7cfa89b
Show file tree
Hide file tree
Showing 6 changed files with 152 additions and 30 deletions.
5 changes: 5 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -128,3 +128,8 @@ REACT_APP_DISABLED_CHAINS_FOR_AVAILABLE_ROUTES=

# Comma-separated list of wallet addresses to block from UI
REACT_APP_WALLET_BLACKLIST=

# Gas estimation padding multiplier.
# JSON with format: {[chainId]: }
# e.g: { "1": 1.1, "10": 1.05 }
REACT_APP_GAS_ESTIMATION_MULTIPLIER_PER_CHAIN={"1": 1.1}
13 changes: 7 additions & 6 deletions src/utils/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -376,9 +376,6 @@ export const tokenList = [
];

// process.env variables
export const gasEstimationMultiplier = Number(
process.env.REACT_APP_GAS_ESTIMATION_MULTIPLIER || 2
);
export const rewardsApiUrl =
process.env.REACT_APP_REWARDS_API_URL || "https://api.across.to";
export const airdropWindowIndex = Number(
Expand Down Expand Up @@ -643,9 +640,10 @@ export const rewardTiers = [
export const secondsPerYear = 31557600;
export const secondsPerDay = 86400; // 60 sec/min * 60 min/hr * 24 hr/day

export const gasMultiplier = process.env.REACT_APP_GAS_ESTIMATION_MULTIPLIER
? Number(process.env.REACT_APP_GAS_ESTIMATION_MULTIPLIER)
: undefined;
export const gasMultiplierPerChain: Record<string, number> = process.env
.REACT_APP_GAS_ESTIMATION_MULTIPLIER_PER_CHAIN
? JSON.parse(process.env.REACT_APP_GAS_ESTIMATION_MULTIPLIER_PER_CHAIN)
: {};

export const suggestedFeesDeviationBufferMultiplier = !Number.isNaN(
Number(
Expand Down Expand Up @@ -698,3 +696,6 @@ export const disabledChainIdsForAvailableRoutes = (
export const walletBlacklist = (process.env.REACT_APP_WALLET_BLACKLIST || "")
.split(",")
.map((address) => address.toLowerCase());

// Fallback gas costs for when the gas estimation fails
export const fallbackEstimatedGasCosts = ethers.utils.parseEther("0.01");
57 changes: 40 additions & 17 deletions src/utils/transactions.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { Contract, ContractTransaction, ethers } from "ethers";
import { parseEther } from "ethers/lib/utils";
import { fixedPointAdjustment, gasMultiplier } from "./constants";
import {
fixedPointAdjustment,
gasMultiplierPerChain,
hubPoolChainId,
} from "./constants";

/**
* This function takes a raw transaction and a signer and returns the result of signing the transaction.
Expand All @@ -26,34 +30,53 @@ export async function sendSignedTransaction(

type Transaction = Promise<ContractTransaction>;

export async function getPaddedGasEstimation(
chainId: number,
contract: Contract,
method: string,
...args: any[]
) {
const gasMultiplier = gasMultiplierPerChain[chainId];
/* If the gas multiplier hasn't been set, run this function as a normal tx */
if (!gasMultiplier) {
return contract.estimateGas[method](...args);
} else {
// Estimate the gas with the provided estimateGas logic
const gasEstimation = await contract.estimateGas[method](...args);
// Factor in the padding
const gasToRecommend = gasEstimation
.mul(parseEther(String(gasMultiplier)))
.div(fixedPointAdjustment);
return gasToRecommend;
}
}

/**
* Pads the gas estimation by a fixed amount dictated in the `REACT_SEND_TXN_GAS_ESTIMATION_MULTIPLIER` env var
* @param contract The contract that this transaction will originate from
* @param method The specific call method
* @returns A completed or failed transaction
*/
export function sendWithPaddedGas(contract: Contract, method: string) {
export function sendWithPaddedGas(
contract: Contract,
method: string,
chainId: number = hubPoolChainId
) {
/**
* Executes a given smart contract method with padded gas.
* @param args The arguments to supply this smart contract call
* @returns A contract transaction result.
*/
const fn = async (...args: any[]): Transaction => {
/* If the gas multiplier hasn't been set, run this function as a normal tx */
if (!gasMultiplier) {
return contract[method](...args) as Promise<ContractTransaction>;
} else {
// Estimate the gas with the provided estimateGas logic
const gasEstimation = await contract.estimateGas[method](...args);
// Factor in the padding
const gasToRecommend = gasEstimation
.mul(parseEther(String(gasMultiplier)))
.div(fixedPointAdjustment);
// Call the tx with the padded gas
return contract[method](...args, {
gasLimit: gasToRecommend,
});
}
const gasToRecommend = await getPaddedGasEstimation(
chainId,
contract,
method,
args
);
return contract[method](...args, {
gasLimit: gasToRecommend,
});
};
return fn;
}
16 changes: 10 additions & 6 deletions src/views/Bridge/hooks/useAmountInput.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
validateBridgeAmount,
areTokensInterchangeable,
} from "../utils";
import { useMaxBalance } from "./useMaxBalance";

export function useAmountInput(selectedRoute: Route) {
const [userAmountInput, setUserAmountInput] = useState("");
Expand All @@ -25,19 +26,21 @@ export function useAmountInput(selectedRoute: Route) {
selectedRoute.fromChain
);

const { data: maxBalance } = useMaxBalance(selectedRoute);

const token = getConfig().getTokenInfoBySymbol(
selectedRoute.fromChain,
selectedRoute.fromTokenSymbol
);

const handleClickMaxBalance = useCallback(() => {
if (balance) {
setUserAmountInput(utils.formatUnits(balance, token.decimals));
if (maxBalance) {
setUserAmountInput(utils.formatUnits(maxBalance, token.decimals));
addToAmpliQueue(() => {
trackMaxButtonClicked("bridgeForm");
});
}
}, [balance, token.decimals, addToAmpliQueue]);
}, [maxBalance, token.decimals, addToAmpliQueue]);

const handleChangeAmountInput = useCallback((changedInput: string) => {
setUserAmountInput(changedInput);
Expand Down Expand Up @@ -78,13 +81,14 @@ export function useAmountInput(selectedRoute: Route) {
userAmountInput,
parsedAmount,
balance,
maxBalance,
};
}

export function useValidAmount(
parsedAmount?: BigNumber,
isAmountTooLow?: boolean,
currentBalance?: BigNumber,
maxBalance?: BigNumber,
maxDeposit?: BigNumber
) {
const [validationError, setValidationError] = useState<
Expand All @@ -95,11 +99,11 @@ export function useValidAmount(
const { error } = validateBridgeAmount(
parsedAmount,
isAmountTooLow,
currentBalance,
maxBalance,
maxDeposit
);
setValidationError(error);
}, [parsedAmount, isAmountTooLow, currentBalance, maxDeposit]);
}, [parsedAmount, isAmountTooLow, maxBalance, maxDeposit]);

return {
amountValidationError: validationError,
Expand Down
3 changes: 2 additions & 1 deletion src/views/Bridge/hooks/useBridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export function useBridge() {
userAmountInput,
parsedAmount,
balance,
maxBalance,
} = useAmountInput(selectedRoute);

const { toAccount, setCustomToAddress } = useToAccount(selectedRoute.toChain);
Expand All @@ -70,7 +71,7 @@ export function useBridge() {
const { amountValidationError, isAmountValid } = useValidAmount(
parsedAmount,
quotedFees?.isAmountTooLow,
balance,
maxBalance,
quotedLimits?.maxDeposit
);

Expand Down
88 changes: 88 additions & 0 deletions src/views/Bridge/hooks/useMaxBalance.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { useQuery } from "react-query";
import { BigNumber, providers, constants, utils } from "ethers";

import { useBalanceBySymbol, useConnection } from "hooks";
import {
getConfig,
Route,
max,
getProvider,
fallbackEstimatedGasCosts,
} from "utils";
import { getPaddedGasEstimation } from "utils/transactions";

const config = getConfig();

export function useMaxBalance(selectedRoute: Route) {
const { balance } = useBalanceBySymbol(
selectedRoute.fromTokenSymbol,
selectedRoute.fromChain
);
const { account, signer } = useConnection();

return useQuery(
[
"max-balance",
selectedRoute.fromTokenSymbol,
selectedRoute.fromChain,
account,
],
async () => {
let maxBridgeAmount: BigNumber;

if (account && balance && signer) {
maxBridgeAmount =
selectedRoute.fromTokenSymbol !== "ETH"
? balance
: // For ETH, we need to take the gas costs into account before setting the max. bridgable amount
await estimateGasCostsForDeposit(selectedRoute, signer)
.then((estimatedGasCosts) =>
max(balance.sub(estimatedGasCosts), 0)
)
.catch((err) => {
console.error(err);
return max(balance.sub(fallbackEstimatedGasCosts), 0);
});
} else {
maxBridgeAmount = constants.Zero;
}

return maxBridgeAmount;
},
{
enabled: Boolean(account && balance && signer),
}
);
}

async function estimateGasCostsForDeposit(
selectedRoute: Route,
signer: providers.JsonRpcSigner
) {
const provider = getProvider(selectedRoute.fromChain);
const spokePool = config.getSpokePool(selectedRoute.fromChain, signer);
const tokenInfo = config.getTokenInfoByAddress(
selectedRoute.fromChain,
selectedRoute.fromTokenAddress
);
const amount = utils.parseUnits("0.000001", tokenInfo.decimals);
const argsForEstimation = {
recipient: await signer.getAddress(),
originToken: tokenInfo.address,
amount,
destinationChain: selectedRoute.toChain,
relayerFeePct: 0,
quoteTimestamp: BigNumber.from(Math.floor(Date.now() / 1000)).sub(60 * 60),
message: "0x",
maxCount: constants.MaxUint256,
};
const paddedGasEstimation = await getPaddedGasEstimation(
selectedRoute.fromChain,
spokePool,
"deposit",
...Object.values(argsForEstimation),
{ value: selectedRoute.isNative ? amount : 0 }
);
const gasPrice = await provider.getGasPrice();
return gasPrice.mul(paddedGasEstimation);
}

2 comments on commit 7cfa89b

@vercel
Copy link

@vercel vercel bot commented on 7cfa89b Oct 13, 2023

Choose a reason for hiding this comment

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

Successfully deployed to the following URLs:

goerli-frontend-v2 – ./

goerli-frontend-v2-git-master-uma.vercel.app
goerli-frontend-v2-uma.vercel.app
goerli-frontend-v2.vercel.app

@vercel
Copy link

@vercel vercel bot commented on 7cfa89b Oct 13, 2023

Choose a reason for hiding this comment

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

Please sign in to comment.