0

I wrote the following index fund contract and deployed it on Arbitrum. When trying to call the mint function, I have insanely high gas fees ($100,000 for a call).

I know that my function is a bit complex, but not to the point to require 100K in gas fees. The problem remains the same no matter if I'm on mainnet, or on Sepolia using a mock swap router. Additionally, Foundry gas simulations give much more reasonable estimations. What is it possibly due to?

My index fund contract:

pragma solidity ^0.8.20; pragma abicoder v2; import "@uniswap-v3-periphery-1.4.4/libraries/TransferHelper.sol"; import "@uniswap-v3-periphery-1.4.4/interfaces/ISwapRouter.sol"; import "@openzeppelin-contracts-5.2.0-rc.1/utils/ReentrancyGuard.sol"; import "@openzeppelin-contracts-5.2.0-rc.1/access/Ownable.sol"; import "@chainlink-contracts-1.3.0/src/v0.8/shared/interfaces/AggregatorV3Interface.sol"; import "../interfaces/IPSVToken.sol"; import "../interfaces/IIndexFund.sol"; import "../lib/TokenDataFetcher.sol"; contract IndexFund is IIndexFund, ReentrancyGuard, Ownable { ISwapRouter public immutable swapRouter; struct UserData { uint256 mintedShares; uint256 tokenAAmount; uint256 tokenBAmount; } mapping(bytes32 => address) public tokenTickerToToken; mapping(address => UserData) public userToUserData; uint256 public mintPrice = 1; uint256 public mintFeeDivisor = 1000; uint24 public uniswapPoolFee = 3000; IPSVToken public psvToken; bytes32 public tokenATicker; bytes32 public tokenBTicker; bytes32 public stablecoinTicker; event FeeCollected(address indexed user, uint256 indexed feeAmount); event SharesMinted( address indexed user, uint256 indexed amount, uint256 indexed stablecoinIn ); event SharesBurned(address indexed user, uint256 indexed amount); modifier allowanceChecker( address token, address allower, address allowed, uint256 amount ) { if (IERC20(token).allowance(allower, allowed) < amount) { revert("Allowance too small"); } _; } constructor( address _swapRouter, address _tokenA, address _tokenB, address _stablecoin, bytes32 _tokenATicker, bytes32 _tokenBTicker, bytes32 _stablecoinTicker, address _psv ) Ownable(msg.sender) { swapRouter = ISwapRouter(_swapRouter); tokenATicker = _tokenATicker; tokenBTicker = _tokenBTicker; stablecoinTicker = _stablecoinTicker; tokenTickerToToken[tokenATicker] = _tokenA; tokenTickerToToken[tokenBTicker] = _tokenB; tokenTickerToToken[stablecoinTicker] = _stablecoin; psvToken = IPSVToken(_psv); TransferHelper.safeApprove( _tokenA, address(swapRouter), type(uint256).max ); TransferHelper.safeApprove( _tokenB, address(swapRouter), type(uint256).max ); TransferHelper.safeApprove( _stablecoin, address(swapRouter), type(uint256).max ); } function mintShare( uint256 stablecoinAmount, uint256 tokenAPrice, uint256 tokenBPrice ) public nonReentrant { require(stablecoinAmount > 0, "You need to provide some stablecoin"); IERC20 stablecoin = IERC20(tokenTickerToToken[stablecoinTicker]); require( stablecoin.balanceOf(msg.sender) >= stablecoinAmount, "Not enough stablecoin in user wallet" ); require( stablecoin.allowance(msg.sender, address(this)) >= stablecoinAmount, "Allowance too small" ); bool transferSuccess = stablecoin.transferFrom( msg.sender, address(this), stablecoinAmount ); require( transferSuccess, "Failed to transfer stablecoin from user wallet" ); ( uint256 stablecoinToInvest, uint256 tokenASwapped, uint256 tokenBSwapped ) = _investUserStablecoin( stablecoin, stablecoinAmount, tokenAPrice, tokenBPrice ); uint256 sharesToMint = stablecoinToInvest / mintPrice; userToUserData[msg.sender].mintedShares += sharesToMint; userToUserData[msg.sender].tokenAAmount += tokenASwapped; userToUserData[msg.sender].tokenBAmount += tokenBSwapped; psvToken.mint(msg.sender, sharesToMint); emit SharesMinted(msg.sender, sharesToMint, stablecoinAmount); } function burnShare( uint256 sharesToBurn, bool getBackIndexFundTokens, uint256 tokenAPrice, uint256 tokenBPrice ) public allowanceChecker( address(psvToken), msg.sender, address(this), sharesToBurn ) { UserData memory userData = userToUserData[msg.sender]; uint256 userMintedShares = userData.mintedShares; require(sharesToBurn <= userMintedShares, "Amount too big"); if (getBackIndexFundTokens) { IERC20 tokenA = IERC20(tokenTickerToToken[tokenATicker]); IERC20 tokenB = IERC20(tokenTickerToToken[tokenBTicker]); bool tokenATransferSuccess = tokenA.transfer( msg.sender, (userData.tokenAAmount * sharesToBurn) / userMintedShares ); require( tokenATransferSuccess, "Failed to transfer token A to user wallet" ); bool tokenBTransferSuccess = tokenB.transfer( msg.sender, (userData.tokenBAmount * sharesToBurn) / userMintedShares ); require( tokenBTransferSuccess, "Failed to transfer token B to user wallet" ); } else { IERC20 stablecoin = IERC20(tokenTickerToToken[stablecoinTicker]); ( uint256 stablecoinToSend, uint256 tokenASwapped, uint256 tokenBSwapped ) = _redeemUserStablecoin( stablecoin, userMintedShares, sharesToBurn, userData, tokenAPrice, tokenBPrice ); userToUserData[msg.sender].tokenAAmount -= tokenASwapped; userToUserData[msg.sender].tokenBAmount -= tokenBSwapped; bool transferSuccess = stablecoin.transfer( msg.sender, stablecoinToSend ); require( transferSuccess, "Failed to transfer stablecoin to user wallet" ); } userToUserData[msg.sender].mintedShares -= sharesToBurn; psvToken.burn(msg.sender, sharesToBurn); emit SharesBurned(msg.sender, sharesToBurn); } function _investUserStablecoin( IERC20 stablecoin, uint256 stablecoinAmount, uint256 tokenAPrice, uint256 tokenBPrice ) internal returns ( uint256 stablecoinInvested, uint256 tokenAAmount, uint256 tokenBAmount ) { uint256 mintFee = stablecoinAmount / mintFeeDivisor; emit FeeCollected(msg.sender, mintFee); stablecoinInvested = stablecoinAmount - mintFee; ( uint256 amountToInvestInTokenA, uint256 amountToInvestInTokenB ) = _computeTokenSwapInfoWhenMint( stablecoinInvested, tokenAPrice, tokenBPrice ); address tokenAAddress = address(tokenTickerToToken[tokenATicker]); address tokenBAddress = address(tokenTickerToToken[tokenBTicker]); tokenAAmount = _swap( address(stablecoin), tokenAAddress, amountToInvestInTokenA, amountToInvestInTokenA / tokenAPrice / 2, uniswapPoolFee ); tokenBAmount = _swap( address(stablecoin), tokenBAddress, amountToInvestInTokenB, amountToInvestInTokenB / tokenBPrice / 2, uniswapPoolFee ); } function _redeemUserStablecoin( IERC20 stablecoin, uint256 userMintedShares, uint256 sharesToBurn, UserData memory userData, uint256 tokenAPrice, uint256 tokenBPrice ) internal returns ( uint256 stablecoinRedeemed, uint256 tokenASold, uint256 tokenBSold ) { uint256 tokenAToSell = (userData.tokenAAmount * sharesToBurn) / userMintedShares; uint256 tokenBToSell = (userData.tokenBAmount * sharesToBurn) / userMintedShares; uint256 minimumStablecoinOutputA = (tokenAToSell * tokenAPrice) / 2; uint256 minimumStablecoinOutputB = (tokenBToSell * tokenBPrice) / 2; IERC20 tokenA = IERC20(tokenTickerToToken[tokenATicker]); IERC20 tokenB = IERC20(tokenTickerToToken[tokenBTicker]); uint256 redemmedStablecoin = _swap( address(tokenA), address(stablecoin), tokenAToSell, minimumStablecoinOutputA, uniswapPoolFee ) + _swap( address(tokenB), address(stablecoin), tokenBToSell, minimumStablecoinOutputB, uniswapPoolFee ); return (redemmedStablecoin, tokenAToSell, tokenBToSell); } function _computeTokenSwapInfoWhenMint( uint256 stablecoinToInvest, uint256 tokenAPrice, uint256 tokenBPrice ) internal view returns (uint256 amountToInvestInTokenA, uint256 amountToInvestInTokenB) { IERC20 tokenA = IERC20(tokenTickerToToken[tokenATicker]); IERC20 tokenB = IERC20(tokenTickerToToken[tokenBTicker]); uint256 tokenAMarketCap = TokenDataFetcher._getTokenMarketCap( uint256(tokenAPrice), tokenA, tokenATicker ); uint256 tokenBMarketCap = TokenDataFetcher._getTokenMarketCap( uint256(tokenBPrice), tokenB, tokenBTicker ); amountToInvestInTokenA = (stablecoinToInvest * tokenAMarketCap) / (tokenAMarketCap + tokenBMarketCap); amountToInvestInTokenB = stablecoinToInvest - amountToInvestInTokenA; } function _swap( address tokenA, address tokenB, uint256 amountIn, uint256 amountOutMinimum, uint24 poolFee ) private returns (uint256 amountOut) { ISwapRouter.ExactInputSingleParams memory params = ISwapRouter .ExactInputSingleParams({ tokenIn: tokenA, tokenOut: tokenB, fee: poolFee, recipient: msg.sender, deadline: block.timestamp + 300, amountIn: amountIn, amountOutMinimum: amountOutMinimum, sqrtPriceLimitX96: 0 }); amountOut = swapRouter.exactInputSingle(params); } function getUserData( address userAddress ) public view returns ( uint256 userMintedShares, uint256 userTokenAAmount, uint256 userTokenBAmount ) { UserData memory userData = userToUserData[userAddress]; return ( userData.mintedShares, userData.tokenAAmount, userData.tokenBAmount ); } function getSharesMintedNumber() public view returns (uint256) { return psvToken.totalSupply(); } function getMintFeeBalance() public view returns (uint256) { return IERC20(tokenTickerToToken[stablecoinTicker]).balanceOf( address(this) ); } function getTokenBought(bytes32 ticker) public view returns (uint256) { return IERC20(tokenTickerToToken[ticker]).balanceOf(address(this)); } function withdrawFees() public onlyOwner { IERC20 stablecoin = IERC20(tokenTickerToToken[stablecoinTicker]); bool transferSuccess = stablecoin.transfer( msg.sender, stablecoin.balanceOf(address(this)) ); require(transferSuccess, "Failed to transfer mint fees to owner"); } } 

My TokenDataFetcher library:

// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import "@openzeppelin/contracts/interfaces/IERC20.sol"; library TokenDataFetcher { function _getTokenMarketCap( uint256 tokenPrice, IERC20 token, bytes32 tokenTicker ) internal view returns (uint256) { return tokenPrice * _getTokenTotalSupply(token, tokenTicker); } function _getTokenTotalSupply( IERC20 token, bytes32 tokenTicker ) internal view returns (uint256) { if (tokenTicker == bytes32(abi.encodePacked("WBTC"))) { return 21_000_000; } else if (tokenTicker == bytes32(abi.encodePacked("WETH"))) { return 120_450_000; } return token.totalSupply(); } } 

2 Answers 2

1

The gas fee you see is not what you will pay, it's just the wallet not knowing what to do because it expects the transaction to fail, therefore setting the "gas limit" setting to its maximum, which ends up showing an insane amount to pay for transaction. Basically, the TX simulation fails which breaks the quote for the TX.

There's probably an error in your smart contract somewhere when you try to execute it on Sepolia or Mainnet that causes this behaviour.

If you really can't find what the error is, try to execute the transaction by setting the gas limit to a reasonable amount manually in your wallet when signing a TX on Sepolia, and use tools like Tenderly to check the Stack Trace.

0
1

I believe this happens when you try to mint something that is not "Mintable".

https://docs.openzeppelin.com/contracts/2.x/api/token/erc20#ERC20Mintable https://docs.openzeppelin.com/contracts/2.x/api/token/erc20#ERC20Pausable

Users need to know if the token has a supply cap, if its Pauseable, Burnable, and Mintable.

I'd reccomend using OpenZepplin Contract Wizard: https://wizard.openzeppelin.com as much as possible...then subtract and delete your custom code as needed.

// SPDX-License-Identifier: MIT // Compatible with OpenZeppelin Contracts ^5.0.0 pragma solidity ^0.8.22; import {ERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; import {ERC20BurnableUpgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC20BurnableUpgradeable.sol"; import {ERC20PausableUpgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC20PausableUpgradeable.sol"; import {ERC20PermitUpgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC20PermitUpgradeable.sol"; import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; contract IndexFund is Initializable, ERC20Upgradeable, ERC20BurnableUpgradeable, ERC20PausableUpgradeable, OwnableUpgradeable, ERC20PermitUpgradeable { /// @custom:oz-upgrades-unsafe-allow constructor constructor() { _disableInitializers(); } function initialize(address initialOwner) initializer public { __ERC20_init("IndexFund", "IDXF"); __ERC20Burnable_init(); __ERC20Pausable_init(); __Ownable_init(initialOwner); __ERC20Permit_init("IndexFund"); } function pause() public onlyOwner { _pause(); } function unpause() public onlyOwner { _unpause(); } function mint(address to, uint256 amount) public onlyOwner { _mint(to, amount); } // The following functions are overrides required by Solidity. function _update(address from, address to, uint256 value) internal override(ERC20Upgradeable, ERC20PausableUpgradeable) { super._update(from, to, value); } } 

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.