diff --git a/contracts/utils/BuyGDClone.sol b/contracts/utils/BuyGDClone.sol index 08f8659c..6b021b12 100644 --- a/contracts/utils/BuyGDClone.sol +++ b/contracts/utils/BuyGDClone.sol @@ -3,43 +3,61 @@ pragma solidity >=0.8; import "@openzeppelin/contracts-upgradeable/proxy/ClonesUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import "@openzeppelin/contracts/utils/math/Math.sol"; import "@mean-finance/uniswap-v3-oracle/solidity/interfaces/IStaticOracle.sol"; import "../Interfaces.sol"; +import "../MentoInterfaces.sol"; /* * @title BuyGDClone - * @notice This contract allows users to swap Celo or cUSD for GoodDollar (GD) tokens. + * @notice This contract allows users to swap Celo or stable for GoodDollar (GD) tokens. * @dev This contract is a clone of the BuyGD contract, which is used to buy GD tokens on the GoodDollar platform. * @dev This contract uses the SwapRouter contract to perform the swaps. */ -contract BuyGDClone is Initializable { +contract BuyGDCloneV2 is Initializable { error REFUND_FAILED(uint256); error NO_BALANCE(); + error MENTO_NOT_CONFIGURED(); event Bought(address inToken, uint256 inAmount, uint256 outAmount); + event BoughtFromMento(address inToken, uint256 inAmount, uint256 outAmount); + event BoughtFromUniswap(address inToken, uint256 inAmount, uint256 outAmount); ISwapRouter public immutable router; address public constant celo = 0x471EcE3750Da237f93B8E339c536989b8978a438; + address public constant CUSD = 0x765DE816845861e75A25fCA122bb6898B8B1282a; + uint24 public constant GD_FEE_TIER = 500; uint32 public immutable twapPeriod; - address public immutable cusd; + address public immutable stable; address public immutable gd; IStaticOracle public immutable oracle; + // Mento reserve configuration (optional) + IBroker public immutable mentoBroker; + address public immutable mentoExchangeProvider; + bytes32 public immutable mentoExchangeId; + address public owner; receive() external payable {} constructor( ISwapRouter _router, - address _cusd, + address _stable, address _gd, - IStaticOracle _oracle + IStaticOracle _oracle, + IBroker _mentoBroker, + address _mentoExchangeProvider, + bytes32 _mentoExchangeId ) { router = _router; - cusd = _cusd; + stable = _stable; gd = _gd; oracle = _oracle; twapPeriod = 300; //5 minutes + mentoBroker = _mentoBroker; + mentoExchangeProvider = _mentoExchangeProvider; + mentoExchangeId = _mentoExchangeId; } /** @@ -51,9 +69,9 @@ contract BuyGDClone is Initializable { } /** - * @notice Swaps either Celo or cUSD for GD tokens. + * @notice Swaps either Celo or stable for GD tokens. * @dev If the contract has a balance of Celo, it will swap Celo for GD tokens. - * @dev If the contract has a balance of cUSD, it will swap cUSD for GD tokens. + * @dev If the contract has a balance of stable, it will swap stable for GD tokens. * @param _minAmount The minimum amount of GD tokens to receive from the swap. */ function swap( @@ -67,7 +85,7 @@ contract BuyGDClone is Initializable { emit Bought(celo, balance, bought); return bought; } - balance = ERC20(cusd).balanceOf(address(this)); + balance = ERC20(CUSD).balanceOf(address(this)); if (balance > 0) { bought = swapCusd(_minAmount, refundGas); emit Bought(celo, balance, bought); @@ -86,11 +104,14 @@ contract BuyGDClone is Initializable { address payable refundGas ) public payable returns (uint256 bought) { uint256 gasCosts; + uint24[] memory fees = new uint24[](1); + fees[0] = 500; if (refundGas != owner) { - (gasCosts, ) = oracle.quoteAllAvailablePoolsWithTimePeriod( + (gasCosts, ) = oracle.quoteSpecificFeeTiersWithTimePeriod( 1e17, //0.1$ - cusd, + stable, celo, + fees, 60 ); } @@ -102,7 +123,7 @@ contract BuyGDClone is Initializable { ERC20(celo).approve(address(router), amountIn); ISwapRouter.ExactInputParams memory params = ISwapRouter.ExactInputParams({ - path: abi.encodePacked(celo, uint24(3000), cusd, uint24(10000), gd), + path: abi.encodePacked(celo, uint24(500), stable, GD_FEE_TIER, gd), recipient: owner, amountIn: amountIn, amountOutMinimum: _minAmount @@ -115,30 +136,151 @@ contract BuyGDClone is Initializable { } /** - * @notice Swaps cUSD for GD tokens. + * @notice Swaps cUSD for GD tokens, choosing the best route between Uniswap and Mento. + * @dev Compares expected returns from both Uniswap and Mento (if available) and uses the better option. * @param _minAmount The minimum amount of GD tokens to receive from the swap. + * @param refundGas The address to refund gas costs to (if not owner). + * @return bought The amount of GD tokens received. */ function swapCusd( uint256 _minAmount, address refundGas ) public returns (uint256 bought) { uint256 gasCosts = refundGas != owner ? 1e17 : 0; //fixed 0.1$ - uint256 amountIn = ERC20(cusd).balanceOf(address(this)) - gasCosts; + uint256 amountIn = ERC20(CUSD).balanceOf(address(this)) - gasCosts; + require(amountIn > 0, "No cUSD balance"); + + // Get expected return from Uniswap + uint256 uniswapExpected = getExpectedReturnFromUniswap(amountIn); + + // Get expected return from Mento (if configured) + uint256 mentoExpected = 0; + bool mentoAvailable = address(mentoBroker) != address(0) && + mentoExchangeProvider != address(0) && + mentoExchangeId != bytes32(0); + + if (mentoAvailable) { + mentoExpected = getExpectedReturnFromMento(amountIn); + } + uint256 maxExpected = Math.max(_minAmount, Math.max(uniswapExpected, mentoExpected)); + // Choose the better option + if (mentoExpected > uniswapExpected) { + // Use Mento if it provides better return + bought = _swapCusdFromMento(maxExpected, refundGas); + } else { + // Use Uniswap (default or if Mento not available/not better) + bought = _swapCUSDfromUniswap(maxExpected, refundGas); + } + } - (uint256 minByTwap, ) = minAmountByTWAP(amountIn, cusd, twapPeriod); - _minAmount = _minAmount > minByTwap ? _minAmount : minByTwap; + /** + * @notice Swaps cUSD for GD tokens using Uniswap pools. + * @param _minAmount The minimum amount of GD tokens to receive from the swap. + * @param refundGas The address to refund gas costs to (if not owner). + * @return bought The amount of GD tokens received. + */ + function _swapCUSDfromUniswap( + uint256 _minAmount, + address refundGas + ) internal returns (uint256 bought) { + uint256 gasCosts = refundGas != owner ? 1e17 : 0; //fixed 0.1$ + uint256 amountIn = ERC20(CUSD).balanceOf(address(this)) - gasCosts; - ERC20(cusd).approve(address(router), amountIn); + ERC20(CUSD).approve(address(router), amountIn); + bytes memory path; + if (stable == CUSD) { + path = abi.encodePacked(CUSD, GD_FEE_TIER, gd); + } else { + path = abi.encodePacked(CUSD, uint24(100), stable, GD_FEE_TIER, gd); + } ISwapRouter.ExactInputParams memory params = ISwapRouter.ExactInputParams({ - path: abi.encodePacked(cusd, uint24(10000), gd), + path: path, recipient: owner, amountIn: amountIn, amountOutMinimum: _minAmount }); bought = router.exactInput(params); if (refundGas != owner) { - ERC20(cusd).transfer(refundGas, gasCosts); + ERC20(CUSD).transfer(refundGas, gasCosts); + } + emit BoughtFromUniswap(CUSD, amountIn, bought); + } + + /** + * @notice Swaps cUSD for G$ tokens using Mento reserve. + * @dev Requires Mento broker, exchange provider, and exchange ID to be configured. + * @param _minAmount The minimum amount of G$ tokens to receive from the swap. + * @param refundGas The address to refund gas costs to (if not owner). + * @return bought The amount of G$ tokens received. + */ + function _swapCusdFromMento( + uint256 _minAmount, + address refundGas + ) internal returns (uint256 bought) { + if (address(mentoBroker) == address(0) || mentoExchangeProvider == address(0) || mentoExchangeId == bytes32(0)) { + revert MENTO_NOT_CONFIGURED(); + } + + uint256 gasCosts = refundGas != owner ? 1e17 : 0; //fixed 0.1$ + uint256 amountIn = ERC20(CUSD).balanceOf(address(this)) - gasCosts; + require(amountIn > 0, "No cUSD balance"); + + // Approve broker to spend cUSD + ERC20(CUSD).approve(address(mentoBroker), amountIn); + + // Execute swap through Mento broker + bought = mentoBroker.swapIn( + mentoExchangeProvider, + mentoExchangeId, + CUSD, + gd, + amountIn, + _minAmount + ); + + // Transfer G$ to owner + ERC20(gd).transfer(owner, bought); + + // Refund gas costs if needed + if (refundGas != owner && gasCosts > 0) { + ERC20(CUSD).transfer(refundGas, gasCosts); } + + emit BoughtFromMento(CUSD, amountIn, bought); + } + + /** + * @notice Gets expected return from Uniswap for a given amount of cUSD. + * @param cusdAmount The amount of cUSD to swap. + * @return expectedReturn The expected amount of G$ tokens to receive from Uniswap. + */ + function getExpectedReturnFromUniswap( + uint256 cusdAmount + ) public view returns (uint256 expectedReturn) { + (uint256 minByTwap,) = minAmountByTWAP(cusdAmount, CUSD, twapPeriod); + return minByTwap; + } + + /** + * @notice Calculates the expected return of G$ tokens for a given amount of cUSD using Mento reserve. + * @dev This is a view function that queries the Mento broker for the expected output. + * @param cusdAmount The amount of cUSD to swap. + * @return expectedReturn The expected amount of G$ tokens to receive. + */ + function getExpectedReturnFromMento( + uint256 cusdAmount + ) public view returns (uint256 expectedReturn) { + if (address(mentoBroker) == address(0) || mentoExchangeProvider == address(0) || mentoExchangeId == bytes32(0)) { + revert MENTO_NOT_CONFIGURED(); + } + + expectedReturn = mentoBroker.getAmountOut( + mentoExchangeProvider, + mentoExchangeId, + CUSD, + gd, + cusdAmount + ); } /** @@ -154,21 +296,34 @@ contract BuyGDClone is Initializable { uint32 period ) public view returns (uint256 minTwap, uint256 quote) { uint24[] memory fees = new uint24[](1); - fees[0] = 10000; uint128 toConvert = uint128(baseAmount); if (baseToken == celo) { - (quote, ) = oracle.quoteAllAvailablePoolsWithTimePeriod( + /// Set the fee to 500 since there is no pool with a 100 fee tier + fees[0] = 500; + (quote, ) = oracle.quoteSpecificFeeTiersWithTimePeriod( toConvert, baseToken, - cusd, + stable, + fees, + period + ); + toConvert = uint128(quote); + } else if (baseToken == CUSD && stable != CUSD) { + fees[0] = 100; + (quote, ) = oracle.quoteSpecificFeeTiersWithTimePeriod( + toConvert, + baseToken, + stable, + fees, period ); toConvert = uint128(quote); } + fees[0] = GD_FEE_TIER; (quote, ) = oracle.quoteSpecificFeeTiersWithTimePeriod( toConvert, - cusd, + stable, gd, fees, period @@ -191,7 +346,7 @@ contract BuyGDClone is Initializable { } } -contract DonateGDClone is BuyGDClone { +contract DonateGDClone is BuyGDCloneV2 { error EXEC_FAILED(bytes error); event Donated( @@ -206,10 +361,13 @@ contract DonateGDClone is BuyGDClone { constructor( ISwapRouter _router, - address _cusd, + address _stable, address _gd, - IStaticOracle _oracle - ) BuyGDClone(_router, _cusd, _gd, _oracle) {} + IStaticOracle _oracle, + IBroker _mentoBroker, + address _mentoExchangeProvider, + bytes32 _mentoExchangeId + ) BuyGDCloneV2(_router, _stable, _gd, _oracle, _mentoBroker, _mentoExchangeProvider, _mentoExchangeId) {} /** * @notice Initializes the contract with the owner's address. @@ -248,13 +406,13 @@ contract DonateGDClone is BuyGDClone { //now exec if (callData.length > 0) { // approve spend of the different possible tokens, before calling the target contract - uint256 cusdBalance = ERC20(cusd).balanceOf(address(this)); + uint256 cusdBalance = ERC20(CUSD).balanceOf(address(this)); uint256 gdBalance = ERC20(gd).balanceOf(address(this)); uint256 celoBalance = address(this).balance; if (cusdBalance > 0) { - ERC20(cusd).approve(address(owner), cusdBalance); - token = cusd; + ERC20(CUSD).approve(address(owner), cusdBalance); + token = CUSD; donated = cusdBalance; } if (gdBalance > 0) { @@ -302,11 +460,14 @@ contract BuyGDCloneFactory { IQuoterV2 public constant quoter = IQuoterV2(0x82825d0554fA07f7FC52Ab63c961F330fdEFa8E8); // celo quoter + address public constant CUSD = 0x765DE816845861e75A25fCA122bb6898B8B1282a; + address public constant celo = 0x471EcE3750Da237f93B8E339c536989b8978a438; + uint24 public constant PERIOD = 600; address public immutable impl; address public immutable donateImpl; address public immutable gd; - address public immutable cusd; + address public immutable stable; IStaticOracle public immutable oracle; ISwapRouter public immutable router; @@ -318,26 +479,48 @@ contract BuyGDCloneFactory { bytes note ); + // Mento configuration (optional) + IBroker public immutable mentoBroker; + address public immutable mentoExchangeProvider; + bytes32 public immutable mentoExchangeId; + /** * @notice Initializes the BuyGDCloneFactory contract with the provided parameters. * @param _router The address of the SwapRouter contract. - * @param _cusd The address of the cUSD token contract. + * @param _stable The address of the stable token contract. * @param _gd The address of the GD token contract. * @param _oracle The address of the StaticOracle contract. + * @param _mentoBroker The address of the Mento broker contract (optional, can be address(0)). + * @param _mentoExchangeProvider The address of the Mento exchange provider (optional, can be address(0)). + * @param _mentoExchangeId The exchange ID for the Mento G$/cUSD exchange (optional, can be bytes32(0)). */ constructor( ISwapRouter _router, - address _cusd, + address _stable, address _gd, - IStaticOracle _oracle + IStaticOracle _oracle, + IBroker _mentoBroker, + address _mentoExchangeProvider, + bytes32 _mentoExchangeId ) { - impl = address(new BuyGDClone(_router, _cusd, _gd, _oracle)); - donateImpl = address(new DonateGDClone(_router, _cusd, _gd, _oracle)); + impl = address(new BuyGDCloneV2(_router, _stable, _gd, _oracle, _mentoBroker, _mentoExchangeProvider, _mentoExchangeId)); + donateImpl = address(new DonateGDClone(_router, _stable, _gd, _oracle, _mentoBroker, _mentoExchangeProvider, _mentoExchangeId)); gd = _gd; - cusd = _cusd; + stable = _stable; oracle = _oracle; router = _router; - _oracle.prepareAllAvailablePoolsWithTimePeriod(_gd, _cusd, 600); + + mentoBroker = _mentoBroker; + mentoExchangeProvider = _mentoExchangeProvider; + mentoExchangeId = _mentoExchangeId; + + _oracle.prepareAllAvailablePoolsWithTimePeriod(_gd, _stable, PERIOD); //stable/gd pools + _oracle.prepareAllAvailablePoolsWithTimePeriod( + celo, + _stable, + PERIOD + ); //celo/stable pools + _oracle.prepareAllAvailablePoolsWithTimePeriod(CUSD, _stable, PERIOD); //cusd/stable pools } /** @@ -348,7 +531,7 @@ contract BuyGDCloneFactory { function create(address owner) public returns (address) { bytes32 salt = keccak256(abi.encode(owner)); address clone = ClonesUpgradeable.cloneDeterministic(impl, salt); - BuyGDClone(payable(clone)).initialize(owner); + BuyGDCloneV2(payable(clone)).initialize(owner); return clone; } @@ -376,7 +559,7 @@ contract BuyGDCloneFactory { uint256 minAmount ) external returns (address) { address clone = create(owner); - BuyGDClone(payable(clone)).swap(minAmount, payable(msg.sender)); + BuyGDCloneV2(payable(clone)).swap(minAmount, payable(msg.sender)); return clone; } @@ -432,134 +615,134 @@ contract BuyGDCloneFactory { return block.basefee; } - function onTokenTransfer( - address from, - uint256 amount, - bytes calldata data - ) external returns (bool) { - if (msg.sender != gd) revert NOT_GD_TOKEN(); - (address to, uint256 minAmount, bytes memory note) = abi.decode( - data, - (address, uint256, bytes) - ); - if (to == address(0)) revert RECIPIENT_ZERO(); - - uint256 amountIn = ERC20(gd).balanceOf(address(this)); - - uint256 amountReceived = swapToCusd(amountIn, minAmount, to); - emit GDSwapToCusd(from, to, amount, amountReceived, note); - return true; - } - - /** - * @notice Swaps cUSD for GD tokens. - * @param _minAmount The minimum amount of GD tokens to receive from the swap. - */ - function swapToCusd( - uint256 amountIn, - uint256 _minAmount, - address recipient - ) public returns (uint256) { - if (msg.sender != gd) { - ERC20(gd).transferFrom(msg.sender, address(this), amountIn); - } - - if (_minAmount == 0) - (_minAmount, ) = minAmountByTWAP(amountIn, gd, cusd, 60); - - ERC20(gd).approve(address(router), amountIn); - ISwapRouter.ExactInputParams memory params = ISwapRouter.ExactInputParams({ - path: abi.encodePacked(gd, uint24(10000), cusd), - recipient: recipient, - amountIn: amountIn, - amountOutMinimum: _minAmount - }); - return router.exactInput(params); - } - - /** - * @notice Swaps cUSD for GD tokens. - * @param _minAmount The minimum amount of GD tokens to receive from the swap. - */ - function swapFromCusd( - uint256 amountIn, - uint256 _minAmount, - address recipient - ) public returns (uint256) { - ERC20(cusd).transferFrom(msg.sender, address(this), amountIn); - - if (_minAmount == 0) - (_minAmount, ) = minAmountByTWAP(amountIn, cusd, gd, 60); - - ERC20(cusd).approve(address(router), amountIn); - ISwapRouter.ExactInputParams memory params = ISwapRouter.ExactInputParams({ - path: abi.encodePacked(cusd, uint24(10000), gd), - recipient: recipient, - amountIn: amountIn, - amountOutMinimum: _minAmount - }); - return router.exactInput(params); - } - - /** - * @notice Calculates the minimum amount of tokens that can be received for a given amount of base tokens, - * based on the time-weighted average price (TWAP) of the token pair over a specified period of time. - * @param baseAmount The amount of base tokens to swap. - * @param baseToken The address of the base token. - * @param qtToken The address of the quote token. - - * @return minTwap The minimum amount of G$ expected to receive by twap - */ - function minAmountByTWAP( - uint256 baseAmount, - address baseToken, - address qtToken, - uint32 period - ) public view returns (uint256 minTwap, uint256 quote) { - uint24[] memory fees = new uint24[](1); - fees[0] = 10000; - uint128 toConvert = uint128(baseAmount); - (quote, ) = oracle.quoteSpecificFeeTiersWithTimePeriod( - toConvert, - baseToken, - qtToken, - fees, - period - ); - - (uint256 curPrice, ) = oracle.quoteSpecificFeeTiersWithTimePeriod( - toConvert, - baseToken, - qtToken, - fees, - 0 - ); - - // (ie we dont expect price movement > 2% in timePeriod) - if ((quote * 98) / 100 > curPrice) { - revert INVALID_TWAP(); - } - //minAmount should not be 2% under curPrice (including slippage and price impact) - //this is just a guesstimate, for accurate results use uniswap sdk to get price quote - //v3 price quote is not available on chain - return ((curPrice * 980) / 1000, quote); - } - - function quoteCusd(uint256 amountIn) external returns (uint256 amountOut) { - return quoteToken(amountIn, 10000, cusd); - } - - function quoteToken( - uint256 amountIn, - uint24 fee, - address targetToken - ) public returns (uint256 amountOut) { - IQuoterV2.QuoteExactInputSingleParams memory params; - params.amountIn = amountIn; - params.tokenIn = gd; - params.tokenOut = targetToken; - params.fee = fee; - - (amountOut, , , ) = quoter.quoteExactInputSingle(params); - } + // function onTokenTransfer( + // address from, + // uint256 amount, + // bytes calldata data + // ) external returns (bool) { + // if (msg.sender != gd) revert NOT_GD_TOKEN(); + // (address to, uint256 minAmount, bytes memory note) = abi.decode( + // data, + // (address, uint256, bytes) + // ); + // if (to == address(0)) revert RECIPIENT_ZERO(); + + // uint256 amountIn = ERC20(gd).balanceOf(address(this)); + + // uint256 amountReceived = swapToCusd(amountIn, minAmount, to); + // emit GDSwapToCusd(from, to, amount, amountReceived, note); + // return true; + // } + + // /** + // * @notice Swaps cUSD for GD tokens. + // * @param _minAmount The minimum amount of GD tokens to receive from the swap. + // */ + // function swapToCusd( + // uint256 amountIn, + // uint256 _minAmount, + // address recipient + // ) public returns (uint256) { + // if (msg.sender != gd) { + // ERC20(gd).transferFrom(msg.sender, address(this), amountIn); + // } + + // if (_minAmount == 0) + // (_minAmount, ) = minAmountByTWAP(amountIn, gd, cusd, 60); + + // ERC20(gd).approve(address(router), amountIn); + // ISwapRouter.ExactInputParams memory params = ISwapRouter.ExactInputParams({ + // path: abi.encodePacked(gd, uint24(10000), cusd), + // recipient: recipient, + // amountIn: amountIn, + // amountOutMinimum: _minAmount + // }); + // return router.exactInput(params); + // } + + // /** + // * @notice Swaps cUSD for GD tokens. + // * @param _minAmount The minimum amount of GD tokens to receive from the swap. + // */ + // function swapFromCusd( + // uint256 amountIn, + // uint256 _minAmount, + // address recipient + // ) public returns (uint256) { + // ERC20(cusd).transferFrom(msg.sender, address(this), amountIn); + + // if (_minAmount == 0) + // (_minAmount, ) = minAmountByTWAP(amountIn, cusd, gd, 60); + + // ERC20(cusd).approve(address(router), amountIn); + // ISwapRouter.ExactInputParams memory params = ISwapRouter.ExactInputParams({ + // path: abi.encodePacked(cusd, uint24(10000), gd), + // recipient: recipient, + // amountIn: amountIn, + // amountOutMinimum: _minAmount + // }); + // return router.exactInput(params); + // } + + // /** + // * @notice Calculates the minimum amount of tokens that can be received for a given amount of base tokens, + // * based on the time-weighted average price (TWAP) of the token pair over a specified period of time. + // * @param baseAmount The amount of base tokens to swap. + // * @param baseToken The address of the base token. + // * @param qtToken The address of the quote token. + + // * @return minTwap The minimum amount of G$ expected to receive by twap + // */ + // function minAmountByTWAP( + // uint256 baseAmount, + // address baseToken, + // address qtToken, + // uint32 period + // ) public view returns (uint256 minTwap, uint256 quote) { + // uint24[] memory fees = new uint24[](1); + // fees[0] = 10000; + // uint128 toConvert = uint128(baseAmount); + // (quote, ) = oracle.quoteSpecificFeeTiersWithTimePeriod( + // toConvert, + // baseToken, + // qtToken, + // fees, + // period + // ); + + // (uint256 curPrice, ) = oracle.quoteSpecificFeeTiersWithTimePeriod( + // toConvert, + // baseToken, + // qtToken, + // fees, + // 0 + // ); + + // // (ie we dont expect price movement > 2% in timePeriod) + // if ((quote * 98) / 100 > curPrice) { + // revert INVALID_TWAP(); + // } + // //minAmount should not be 2% under curPrice (including slippage and price impact) + // //this is just a guesstimate, for accurate results use uniswap sdk to get price quote + // //v3 price quote is not available on chain + // return ((curPrice * 980) / 1000, quote); + // } + + // function quoteCusd(uint256 amountIn) external returns (uint256 amountOut) { + // return quoteToken(amountIn, 10000, cusd); + // } + + // function quoteToken( + // uint256 amountIn, + // uint24 fee, + // address targetToken + // ) public returns (uint256 amountOut) { + // IQuoterV2.QuoteExactInputSingleParams memory params; + // params.amountIn = amountIn; + // params.tokenIn = gd; + // params.tokenOut = targetToken; + // params.fee = fee; + + // (amountOut, , , ) = quoter.quoteExactInputSingle(params); + // } } diff --git a/package.json b/package.json index 68a7c8ea..3b124dfc 100644 --- a/package.json +++ b/package.json @@ -76,7 +76,7 @@ "@jsier/retrier": "^1.2.4", "@mean-finance/uniswap-v3-oracle": "^1.0.3", "@nomicfoundation/hardhat-chai-matchers": "1", - "@nomicfoundation/hardhat-network-helpers": "^1.0.8", + "@nomicfoundation/hardhat-network-helpers": "^1.1.2", "@nomicfoundation/hardhat-verify": "^2.1.0", "@nomiclabs/hardhat-ethers": "^2.2.1", "@nomiclabs/hardhat-waffle": "^2.0.6", @@ -110,7 +110,7 @@ "fs-extra": "9.0.0", "graphql": "^15.5.0", "graphql-request": "^3.4.0", - "hardhat": "2.27.2", + "hardhat": "2.26", "hardhat-contract-sizer": "^2.6.1", "hardhat-deploy": "^1.0.4", "hardhat-gas-reporter": "^1.0.8", diff --git a/test/ubi/UBIScheme.e2e.test.ts b/test/ubi/UBIScheme.e2e.test.ts index 78e632dd..964f8787 100644 --- a/test/ubi/UBIScheme.e2e.test.ts +++ b/test/ubi/UBIScheme.e2e.test.ts @@ -207,7 +207,7 @@ describe("UBIScheme - network e2e tests", () => { founder.address, ethers.utils.parseEther("1000") ); - dai.approve(simpleStaking.address, ethers.utils.parseEther("1000")); + await dai.approve(simpleStaking.address, ethers.utils.parseEther("1000")); await simpleStaking.stake(ethers.utils.parseEther("1000"), 0, false); await cDAI["mint(address,uint256)"]( founder.address, diff --git a/test/utils/BuyGDClone.test.ts b/test/utils/BuyGDClone.test.ts new file mode 100644 index 00000000..efe9099b --- /dev/null +++ b/test/utils/BuyGDClone.test.ts @@ -0,0 +1,493 @@ +/** + * @file E2E test for BuyGDClone contract on Celo fork + * + * This test suite verifies the BuyGDClone contract functionality on a Celo mainnet fork. + * It tests the cUSD -> GLOUSD -> G$ swap path as specified in the GitHub issue. + * + * To run this test: + * 1. Make sure you have a Celo RPC endpoint available (or use public forno.celo.org) + * 2. Run: npx hardhat test test/utils/BuyGDClone.celo-fork.test.ts + * + * Note: This test forks Celo mainnet, so it requires network access and may take longer to run. + */ + +import { ethers, network } from "hardhat"; +import { expect } from "chai"; +import { loadFixture } from "@nomicfoundation/hardhat-network-helpers"; +import { BuyGDCloneV2, BuyGDCloneFactory } from "../../types"; +import deployments from "../../releases/deployment.json"; +import * as networkHelpers from "@nomicfoundation/hardhat-network-helpers"; + +// Celo mainnet addresses +const CELO_MAINNET_RPC = "https://forno.celo.org"; +const CELO_CHAIN_ID = 42220; + +// Production Celo addresses from deployment.json (used for existing contracts on fork) +const PRODUCTION_CELO = deployments["production-celo"]; +const GOODDOLLAR = PRODUCTION_CELO.GoodDollar; +const CUSD = PRODUCTION_CELO.CUSD; +const UNISWAP_V3_ROUTER = PRODUCTION_CELO.UniswapV3Router; +const STATIC_ORACLE = PRODUCTION_CELO.StaticOracle; +const MENTO_BROKER = PRODUCTION_CELO.MentoBroker; +const MENTO_EXCHANGE_PROVIDER = PRODUCTION_CELO.MentoExchangeProvider; +const MENTO_EXCHANGE_ID = PRODUCTION_CELO.CUSDExchangeId; +const CELO = "0x471EcE3750Da237f93B8E339c536989b8978a438"; +const QUOTE = "0x82825d0554fA07f7FC52Ab63c961F330fdEFa8E8"; + +// GLOUSD address on Celo mainnet +const GLOUSD_REFERENCE = "0x4F604735c1cF31399C6E711D5962b2B3E0225AD3"; // Common GLOUSD address + +// Account with cUSD balance on Celo (for impersonation) +const CUSD_WHALE = "0xAC19B8Ab514623144CBc92C9C4ACb3583E594bE3"; // Example whale address + +describe("BuyGDClone - Celo Fork E2E", function () { + // Increase timeout for fork tests + this.timeout(600000); + + this.afterAll(async function () { + await networkHelpers.reset(); + }); + before(async function () { + await networkHelpers.reset(CELO_MAINNET_RPC); + }); + + async function forkCelo() { + const [deployer, user] = await ethers.getSigners(); + + // Get existing contracts from Celo (for router, oracle, tokens) + const router = await ethers.getContractAt("contracts/Interfaces.sol:ISwapRouter", UNISWAP_V3_ROUTER); + const oracleAddress = STATIC_ORACLE; + const gdToken = await ethers.getContractAt("contracts/Interfaces.sol:ERC20", GOODDOLLAR); + const cusdToken = await ethers.getContractAt("contracts/Interfaces.sol:ERC20", CUSD); + const celoToken = await ethers.getContractAt("contracts/Interfaces.sol:ERC20", CELO); + + const stableAddress = process.env.GLOUSD_ADDRESS || GLOUSD_REFERENCE; + console.log("Using stable token (GLOUSD):", stableAddress); + + // Deploy BuyGDCloneFactory + const BuyGDCloneFactoryFactory = await ethers.getContractFactory("BuyGDCloneFactory"); + const factory = (await BuyGDCloneFactoryFactory.deploy( + router.address, + stableAddress, + GOODDOLLAR, + oracleAddress, + MENTO_BROKER, + MENTO_EXCHANGE_PROVIDER, + MENTO_EXCHANGE_ID, + {gasLimit: 15000000} + )) as BuyGDCloneFactory; + + await factory.deployed(); + console.log("✓ BuyGDCloneFactory deployed at:", factory.address); + + // Verify the stable token in the factory + const factoryStable = await factory.stable(); + expect(factoryStable.toLowerCase()).to.equal(stableAddress.toLowerCase()); + console.log("✓ Factory stable token verified:", factoryStable); + + // Impersonate a whale account to get cUSD + const whale = await ethers.getImpersonatedSigner(CUSD_WHALE); + await ethers.provider.send("hardhat_setBalance", [ + CUSD_WHALE, + "0x1000000000000000000", + ]); + + return { + deployer, + user, + factory, + gdToken, + cusdToken, + celoToken, + stableAddress, + whale, + router, + oracleAddress, + MENTO_BROKER, + MENTO_EXCHANGE_PROVIDER, + MENTO_EXCHANGE_ID + }; + } + + describe("cUSD Swap Tests", function () { + it("Should use Uniswap when Mento is not configured", async function () { + const { deployer, user, gdToken, cusdToken, whale, router, oracleAddress } = await loadFixture(forkCelo); + + // Create factory without Mento configuration + const BuyGDCloneFactoryFactory = await ethers.getContractFactory("BuyGDCloneFactory"); + const factoryWithoutMento = (await BuyGDCloneFactoryFactory.deploy( + router.address, + process.env.GLOUSD_ADDRESS || GLOUSD_REFERENCE, + GOODDOLLAR, + oracleAddress, + ethers.constants.AddressZero, + ethers.constants.AddressZero, + ethers.constants.HashZero + )) as BuyGDCloneFactory; + + await factoryWithoutMento.create(user.address); + const cloneAddress = await factoryWithoutMento.predict(user.address); + const clone = (await ethers.getContractAt( + "BuyGDCloneV2", + cloneAddress + )) as BuyGDCloneV2; + + const swapAmount = ethers.utils.parseEther("5"); + const whaleBalance = await cusdToken.balanceOf(whale.address); + + if (whaleBalance.lt(swapAmount)) { + throw new Error(`Whale doesn't have enough cUSD. Balance: ${ethers.utils.formatEther(whaleBalance)}, Required: ${ethers.utils.formatEther(swapAmount)}`); + } + + await cusdToken.connect(whale).transfer(cloneAddress, swapAmount); + + // Should be able to get Uniswap expected return + const uniswapExpected = await clone.getExpectedReturnFromUniswap(swapAmount); + expect(uniswapExpected).to.be.gt(0); + + // Should revert when trying to get Mento expected return + await expect(clone.getExpectedReturnFromMento(swapAmount)).to.be.revertedWithCustomError( + clone, + "MENTO_NOT_CONFIGURED" + ); + + const initialGdBalance = await gdToken.balanceOf(user.address); + const [minByTwap] = await clone.minAmountByTWAP(swapAmount, CUSD, 300); + const minAmount = minByTwap; + + // swapCusd should use Uniswap (only option) + const swapTx = await clone.swapCusd(minAmount, user.address); + const swapReceipt = await swapTx.wait(); + + // Should emit BoughtFromUniswap event + const uniswapEvent = swapReceipt.events?.find( + (e: any) => e.event === "BoughtFromUniswap" + ); + expect(uniswapEvent).to.not.be.undefined; + console.log("✓ BoughtFromUniswap event emitted:", { + inToken: uniswapEvent?.args?.inToken, + inAmount: ethers.utils.formatEther(uniswapEvent?.args?.inAmount), + outAmount: ethers.utils.formatEther(uniswapEvent?.args?.outAmount), + }); + + // Should not emit BoughtFromMento event + const mentoEvent = swapReceipt.events?.find( + (e: any) => e.event === "BoughtFromMento" + ); + expect(mentoEvent).to.be.undefined; + + const finalGdBalance = await gdToken.balanceOf(user.address); + const gdReceived = finalGdBalance.sub(initialGdBalance); + expect(gdReceived).to.be.gt(0); + expect(gdReceived).to.be.gte(minAmount); + console.log("✓ Used Uniswap when Mento not configured, received:", ethers.utils.formatEther(gdReceived), "G$"); + }); + it("Should compare Uniswap vs Mento and choose better route", async function () { + const { factory, user, gdToken, cusdToken, whale } = await loadFixture(forkCelo); + + await factory.create(user.address); + const cloneAddress = await factory.predict(user.address); + const clone = (await ethers.getContractAt( + "BuyGDCloneV2", + cloneAddress + )) as BuyGDCloneV2; + + const swapAmount = ethers.utils.parseEther("5"); + const whaleBalance = await cusdToken.balanceOf(whale.address); + + if (whaleBalance.lt(swapAmount)) { + throw new Error(`Whale doesn't have enough cUSD. Balance: ${ethers.utils.formatEther(whaleBalance)}, Required: ${ethers.utils.formatEther(swapAmount)}`); + } + + await cusdToken.connect(whale).transfer(cloneAddress, swapAmount); + + // Get expected returns + const uniswapExpected = await clone.getExpectedReturnFromUniswap(swapAmount); + const mentoExpected = await clone.getExpectedReturnFromMento(swapAmount); + + console.log("Route comparison:"); + console.log(" Uniswap expected:", ethers.utils.formatEther(uniswapExpected), "G$"); + console.log(" Mento expected:", ethers.utils.formatEther(mentoExpected), "G$"); + + const initialGdBalance = await gdToken.balanceOf(user.address); + const [minByTwap] = await clone.minAmountByTWAP(swapAmount, CUSD, 300); + const minAmount = minByTwap; + + // Call swapCusd which should choose the better route + const swapTx = await clone.swapCusd(minAmount, user.address); + const swapReceipt = await swapTx.wait(); + + const finalGdBalance = await gdToken.balanceOf(user.address); + const gdReceived = finalGdBalance.sub(initialGdBalance); + + expect(gdReceived).to.be.gt(0); + expect(gdReceived).to.be.gte(minAmount); + + // Check which route was used based on event + const mentoEvent = swapReceipt.events?.find( + (e: any) => e.event === "BoughtFromMento" + ); + const uniswapEvent = swapReceipt.events?.find( + (e: any) => e.event === "BoughtFromUniswap" + ); + const usedMento = mentoEvent !== undefined; + const usedUniswap = uniswapEvent !== undefined; + + if (mentoExpected.gt(uniswapExpected)) { + expect(usedMento).to.be.true; + expect(usedUniswap).to.be.false; + console.log("✓ Correctly chose Mento (better return)"); + console.log("✓ BoughtFromMento event emitted"); + } else { + expect(usedMento).to.be.false; + expect(usedUniswap).to.be.true; + console.log("✓ Correctly chose Uniswap (better return)"); + console.log("✓ BoughtFromUniswap event emitted"); + } + + console.log("✓ Received:", ethers.utils.formatEther(gdReceived), "G$"); + }); + }); + + describe("CELO Swap Tests", function () { + it("Should swap Celo -> GLOUSD -> G$ via clone", async function () { + /// Skip test because forking does not fork the precompiled contracts from celo mainnet + if(network.name === 'hardhat') { + this.skip(); + return; + } + const { factory, user, gdToken, celoToken, whale } = await loadFixture(forkCelo); + + // Create clone + await factory.create(user.address); + const cloneAddress = await factory.predict(user.address); + const clone = (await ethers.getContractAt( + "BuyGDCloneV2", + cloneAddress + )) as BuyGDCloneV2; + + // Transfer CELO to clone (simulating onramp service) + const swapAmount = ethers.utils.parseEther("1000"); + const whaleCeloBalance = await celoToken.balanceOf(whale.address); + + if (whaleCeloBalance.lt(swapAmount)) { + throw new Error(`Whale doesn't have enough CELO. Balance: ${ethers.utils.formatEther(whaleCeloBalance)}, Required: ${ethers.utils.formatEther(swapAmount)}`); + } + + // Transfer CELO from whale to clone + // await celoToken.connect(whale).transfer(cloneAddress, swapAmount); + await whale.sendTransaction({ + to: cloneAddress, + value: swapAmount, + }); + + const cloneCeloBalance = await celoToken.balanceOf(cloneAddress); + expect(cloneCeloBalance).to.equal(swapAmount); + console.log("✓ CELO transferred to clone:", ethers.utils.formatEther(swapAmount)); + + // Get initial G$ balance + const initialGdBalance = await gdToken.balanceOf(user.address); + console.log("Initial G$ balance:", ethers.utils.formatEther(initialGdBalance)); + + // Calculate min amount using TWAP + const [minByTwap] = await clone.minAmountByTWAP( + swapAmount, + CELO, + 300 // 5 minutes + ); + console.log("Min amount by TWAP:", ethers.utils.formatEther(minByTwap)); + + // Perform swap - minTwap is already 98% of quote + const minAmount = minByTwap; + console.log("Using minAmount:", ethers.utils.formatEther(minAmount)); + + const swapTx = await clone.swap(minAmount, user.address); + const swapReceipt = await swapTx.wait(); + + // Check for Bought event + const boughtEvent = swapReceipt.events?.find( + (e: any) => e.event === "Bought" + ); + expect(boughtEvent).to.not.be.undefined; + console.log("✓ Bought event emitted:", { + inToken: boughtEvent?.args?.inToken, + inAmount: ethers.utils.formatEther(boughtEvent?.args?.inAmount), + outAmount: ethers.utils.formatEther(boughtEvent?.args?.outAmount), + }); + + // Check final G$ balance + const finalGdBalance = await gdToken.balanceOf(user.address); + const gdReceived = finalGdBalance.sub(initialGdBalance); + expect(gdReceived).to.be.gt(0); + console.log("✓ G$ received:", ethers.utils.formatEther(gdReceived)); + console.log("Final G$ balance:", ethers.utils.formatEther(finalGdBalance)); + + // Verify minimum amount + expect(gdReceived).to.be.gte(minAmount); + console.log("✓ Received amount >= minAmount"); + }); + }); + + describe("TWAP and Price Comparison Tests", function () { + it("Should compare TWAP quote vs actual pool price", async function () { + const { factory, user, router } = await loadFixture(forkCelo); + + // Create clone + await factory.create(user.address); + const cloneAddress = await factory.predict(user.address); + const clone = (await ethers.getContractAt( + "BuyGDCloneV2", + cloneAddress + )) as BuyGDCloneV2; + + const testAmount = ethers.utils.parseEther("10"); // 10 cUSD + const stableAddress = await clone.stable(); + const gdAddress = await clone.gd(); + + // Get TWAP quote from oracle + const [minTwap, twapQuote] = await clone.minAmountByTWAP( + testAmount, + CUSD, + 300 // 5 minutes + ); + + console.log("TWAP Oracle Quote:"); + console.log(" Input:", ethers.utils.formatEther(testAmount), "cUSD"); + console.log(" Min TWAP (98%):", ethers.utils.formatEther(minTwap), "G$"); + console.log(" TWAP Quote:", ethers.utils.formatEther(twapQuote), "G$"); + + // Get actual pool price using QuoterV2 + const quoter = await ethers.getContractAt("contracts/Interfaces.sol:IQuoterV2", QUOTE); + + // Build path: CUSD -> stable -> G$ (using same encoding as contract) + let path: string; + if (stableAddress.toLowerCase() === CUSD.toLowerCase()) { + path = ethers.utils.solidityPack( + ["address", "uint24", "address"], + [CUSD, 500, gdAddress] // GD_FEE_TIER = 500 + ); + } else { + path = ethers.utils.solidityPack( + ["address", "uint24", "address", "uint24", "address"], + [CUSD, 100, stableAddress, 500, gdAddress] // 100 for CUSD->stable, 500 for stable->G$ + ); + } + + // Get quote from actual pool + const [actualAmountOut] = await quoter.callStatic.quoteExactInput(path, testAmount); + + console.log("Actual Pool Price:"); + console.log(" Input:", ethers.utils.formatEther(testAmount), "cUSD"); + console.log(" Actual Output:", ethers.utils.formatEther(actualAmountOut), "G$"); + + // Compare TWAP vs actual + const twapVsActual = twapQuote.mul(100).div(actualAmountOut); + const minTwapVsActual = minTwap.mul(100).div(actualAmountOut); + + console.log("Comparison:"); + console.log(" TWAP Quote vs Actual:", twapVsActual.toString(), "%"); + console.log(" Min TWAP vs Actual:", minTwapVsActual.toString(), "%"); + + // Min TWAP should be less than or equal to actual + // But allow some tolerance for price movement + expect(minTwap).to.be.lte(actualAmountOut); + expect(minTwap).to.be.gte(actualAmountOut.mul(98).div(100)); + + console.log("✓ TWAP quote comparison completed"); + }); + + it("Should revert when minAmount is more than quote", async function () { + const { factory, user, cusdToken, whale } = await loadFixture(forkCelo); + + // Create clone + await factory.create(user.address); + const cloneAddress = await factory.predict(user.address); + const clone = (await ethers.getContractAt( + "BuyGDCloneV2", + cloneAddress + )) as BuyGDCloneV2; + + // Transfer cUSD to clone + const swapAmount = ethers.utils.parseEther("5"); + const whaleBalance = await cusdToken.balanceOf(whale.address); + + if (whaleBalance.lt(swapAmount)) { + throw new Error(`Whale doesn't have enough cUSD. Balance: ${ethers.utils.formatEther(whaleBalance)}, Required: ${ethers.utils.formatEther(swapAmount)}`); + } + + await cusdToken.connect(whale).transfer(cloneAddress, swapAmount); + + // Get TWAP quote + const [, twapQuote] = await clone.minAmountByTWAP( + swapAmount, + CUSD, + 300 + ); + + console.log("TWAP values:"); + console.log(" TWAP Quote:", ethers.utils.formatEther(twapQuote), "G$"); + + // Use minAmount = 102% of quote + const excessiveMinAmount = twapQuote.mul(102).div(100); + console.log("Using excessive minAmount (102% of quote):", ethers.utils.formatEther(excessiveMinAmount)); + + // The swap should revert because excessiveMinAmount > actual pool output + // The contract enforces: amountOutMinimum = excessiveMinAmount + // But the pool likely can't provide that much due to slippage/price impact + await expect( + clone.swap(excessiveMinAmount, user.address) + ).to.be.reverted; // Should revert with Uniswap "STF" (insufficient output amount) or similar + + console.log("✓ Swap correctly reverts when minAmount = 102% of TWAP quote"); + }); + }); + + describe("Factory Helper Functions", function () { + it("Should handle createAndSwap in one transaction", async function () { + const { factory, user, deployer, gdToken, cusdToken, whale } = await loadFixture( + forkCelo + ); + + const swapAmount = ethers.utils.parseEther("5"); + const whaleBalance = await cusdToken.balanceOf(whale.address); + + if (whaleBalance.lt(swapAmount)) { + throw new Error(`Whale doesn't have enough cUSD. Balance: ${ethers.utils.formatEther(whaleBalance)}, Required: ${ethers.utils.formatEther(swapAmount)}`); + } + + // Get initial G$ balance + const initialGdBalance = await gdToken.balanceOf(user.address); + + // Create clone and get address + await factory.create(deployer.address); + const cloneAddress = await factory.predict(deployer.address); + const clone = (await ethers.getContractAt( + "BuyGDCloneV2", + cloneAddress + )) as BuyGDCloneV2; + + // Calculate min amount + const [minByTwap] = await clone.minAmountByTWAP( + swapAmount, + CUSD, + 300 + ); + const minAmount = minByTwap; + + const predictedAddress = await factory.predict(user.address); + cusdToken.connect(whale).transfer(predictedAddress, swapAmount); + // Use createAndSwap + await cusdToken.connect(user).approve(factory.address, swapAmount); + const tx = await factory.connect(user).createAndSwap(user.address, minAmount); + const receipt = await tx.wait(); + + // Check final balance + const finalGdBalance = await gdToken.balanceOf(user.address); + const gdReceived = finalGdBalance.sub(initialGdBalance); + + expect(gdReceived).to.be.gte(minAmount); + console.log("✓ createAndSwap successful, G$ received:", ethers.utils.formatEther(gdReceived)); + }); + }); +}); + diff --git a/yarn.lock b/yarn.lock index a910a9a2..95ea455c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2792,7 +2792,7 @@ __metadata: "@jsier/retrier": ^1.2.4 "@mean-finance/uniswap-v3-oracle": ^1.0.3 "@nomicfoundation/hardhat-chai-matchers": 1 - "@nomicfoundation/hardhat-network-helpers": ^1.0.8 + "@nomicfoundation/hardhat-network-helpers": ^1.1.2 "@nomicfoundation/hardhat-verify": ^2.1.0 "@nomiclabs/hardhat-ethers": ^2.2.1 "@nomiclabs/hardhat-waffle": ^2.0.6 @@ -2826,7 +2826,7 @@ __metadata: fs-extra: 9.0.0 graphql: ^15.5.0 graphql-request: ^3.4.0 - hardhat: 2.27.2 + hardhat: 2.26 hardhat-contract-sizer: ^2.6.1 hardhat-deploy: ^1.0.4 hardhat-gas-reporter: ^1.0.8 @@ -3164,67 +3164,67 @@ __metadata: languageName: node linkType: hard -"@nomicfoundation/edr-darwin-arm64@npm:0.12.0-next.16": - version: 0.12.0-next.16 - resolution: "@nomicfoundation/edr-darwin-arm64@npm:0.12.0-next.16" - checksum: a72237ed7aad4cb2aa45e1236be023c584a4447869e11393bdf232bad32cc34c8182e8159bc2b77ac9e0befd450e727cffb946d72a32b6b66c937b25f1924953 +"@nomicfoundation/edr-darwin-arm64@npm:0.11.3": + version: 0.11.3 + resolution: "@nomicfoundation/edr-darwin-arm64@npm:0.11.3" + checksum: e14f01c373f2fb5e7ac9e293f1f2c66d745ad4c73024afd44be72d004236b05f56205b899fb96d1abae01c5523f42402806fec8e200aa7c7d918a171f08f114a languageName: node linkType: hard -"@nomicfoundation/edr-darwin-x64@npm:0.12.0-next.16": - version: 0.12.0-next.16 - resolution: "@nomicfoundation/edr-darwin-x64@npm:0.12.0-next.16" - checksum: db10ff877a3f994a6ccfe7f1b10268364f41983a226cd2f9274698e3a71dbffefe031ab9a4a19ce8a46e6ff4781e15cc808c1d3d27b4e4bf0b9b36556e8d3a9d +"@nomicfoundation/edr-darwin-x64@npm:0.11.3": + version: 0.11.3 + resolution: "@nomicfoundation/edr-darwin-x64@npm:0.11.3" + checksum: d31317ed11e2f96af5753d22259249dafeeec7b8eb7fb45721b724fed26eda1c9fedb37561ad2504d6df5276e5b8647d589d19dbb8564aaa9f3b501ef67ef9e5 languageName: node linkType: hard -"@nomicfoundation/edr-linux-arm64-gnu@npm:0.12.0-next.16": - version: 0.12.0-next.16 - resolution: "@nomicfoundation/edr-linux-arm64-gnu@npm:0.12.0-next.16" - checksum: 0ffd2bb6ee6f20605bacfa361d44d5e9d7331940258b56fa6d6ba917c3ae7fcb7c64b968d23f546a26adacfaed9755fb4d722aac80b76b5d36b3d54cda1a116f +"@nomicfoundation/edr-linux-arm64-gnu@npm:0.11.3": + version: 0.11.3 + resolution: "@nomicfoundation/edr-linux-arm64-gnu@npm:0.11.3" + checksum: ec6898f9a4558e1f23086b327de87f1f3d4fbb686280aa3e977e4577027aa61be826950d15f8729d996a67c79b30cfd25a3f9f2894ca022433c35b72fe3fe6d9 languageName: node linkType: hard -"@nomicfoundation/edr-linux-arm64-musl@npm:0.12.0-next.16": - version: 0.12.0-next.16 - resolution: "@nomicfoundation/edr-linux-arm64-musl@npm:0.12.0-next.16" - checksum: 2caaf25e27c60c1c21cd860f1937a0358dd40f01e51950276dc14a5026cbcf2d8fee02572f5b799147baed89306963ddcaff1745a04d2515040dd65dfaa394cd +"@nomicfoundation/edr-linux-arm64-musl@npm:0.11.3": + version: 0.11.3 + resolution: "@nomicfoundation/edr-linux-arm64-musl@npm:0.11.3" + checksum: 750ef8948030d7efbc87a17e0e0707f06b9976dcda2d4a1fb41c46b13b377a6ff8a6091662afa3bf20e4e9167a0006b5aa1f5bb3355f5b3cce08b527d6d85708 languageName: node linkType: hard -"@nomicfoundation/edr-linux-x64-gnu@npm:0.12.0-next.16": - version: 0.12.0-next.16 - resolution: "@nomicfoundation/edr-linux-x64-gnu@npm:0.12.0-next.16" - checksum: 8c7fc00626d60e61d5960a7dc21e1955b7f5c5f14df73517f702892cb96d1536a12b121894671d6380de026cc9517ad579f50c93435d020a8595bd9fc9401045 +"@nomicfoundation/edr-linux-x64-gnu@npm:0.11.3": + version: 0.11.3 + resolution: "@nomicfoundation/edr-linux-x64-gnu@npm:0.11.3" + checksum: bcdeac2b242b6b799c902589f9629573815e6773b7ce285012021a02aa51585543c470e1c2504d4cad99f28644c78f5801c34dc31eff58309cf15f1609d60c08 languageName: node linkType: hard -"@nomicfoundation/edr-linux-x64-musl@npm:0.12.0-next.16": - version: 0.12.0-next.16 - resolution: "@nomicfoundation/edr-linux-x64-musl@npm:0.12.0-next.16" - checksum: 13b503ae7625c1ec30d70462322bc36133b8a66ce5b27cf7a4ac7a1fe8ae4d77c652ff0aeaaeb6e4d1f942f99e1a7840a7b72f90ba95b3f3e8374c9d891d1164 +"@nomicfoundation/edr-linux-x64-musl@npm:0.11.3": + version: 0.11.3 + resolution: "@nomicfoundation/edr-linux-x64-musl@npm:0.11.3" + checksum: a8412a808cd80b5c74ad99fbb189daf1ab007e621b67ed3294647f64e11798784ae34b6f1f8e9d0084ba7067601b1a6e9c56f5cabb3b28b9bafec88c48a93d2e languageName: node linkType: hard -"@nomicfoundation/edr-win32-x64-msvc@npm:0.12.0-next.16": - version: 0.12.0-next.16 - resolution: "@nomicfoundation/edr-win32-x64-msvc@npm:0.12.0-next.16" - checksum: 53ef3ce82c7187f42f8d93099325259b5037487170cc559115d2ecf138583512b8f5f900e4445885c4e0ffa6f06dcdceaf635b67ac6d6008600f1010edd035a8 +"@nomicfoundation/edr-win32-x64-msvc@npm:0.11.3": + version: 0.11.3 + resolution: "@nomicfoundation/edr-win32-x64-msvc@npm:0.11.3" + checksum: dcf2e4e01ceca4bf00c008b672c0f59880e771849a20164edafd54db7805f73c145f4ef0fee49dc6612f7d2c912124072b1c98a03ad1a9b9692eefeab2bbadd2 languageName: node linkType: hard -"@nomicfoundation/edr@npm:0.12.0-next.16": - version: 0.12.0-next.16 - resolution: "@nomicfoundation/edr@npm:0.12.0-next.16" +"@nomicfoundation/edr@npm:^0.11.3": + version: 0.11.3 + resolution: "@nomicfoundation/edr@npm:0.11.3" dependencies: - "@nomicfoundation/edr-darwin-arm64": 0.12.0-next.16 - "@nomicfoundation/edr-darwin-x64": 0.12.0-next.16 - "@nomicfoundation/edr-linux-arm64-gnu": 0.12.0-next.16 - "@nomicfoundation/edr-linux-arm64-musl": 0.12.0-next.16 - "@nomicfoundation/edr-linux-x64-gnu": 0.12.0-next.16 - "@nomicfoundation/edr-linux-x64-musl": 0.12.0-next.16 - "@nomicfoundation/edr-win32-x64-msvc": 0.12.0-next.16 - checksum: a40fabbb58d609289bdb1aacd2ac94f46855ce20acf4016374df1ac7611c979ee1ac9285d16b55a13226be6934b47169ceb5de8c07aab9689c37645f9b865c36 + "@nomicfoundation/edr-darwin-arm64": 0.11.3 + "@nomicfoundation/edr-darwin-x64": 0.11.3 + "@nomicfoundation/edr-linux-arm64-gnu": 0.11.3 + "@nomicfoundation/edr-linux-arm64-musl": 0.11.3 + "@nomicfoundation/edr-linux-x64-gnu": 0.11.3 + "@nomicfoundation/edr-linux-x64-musl": 0.11.3 + "@nomicfoundation/edr-win32-x64-msvc": 0.11.3 + checksum: f3e92e1da7f30ae8a377f9f315b28d42296fd0551bc63c4b0f594f3a93e1ec767dc396c50643f086d73cb9cdd0b9466a197dd4e6470a142dbde2aa95d5e11ef9 languageName: node linkType: hard @@ -3246,14 +3246,14 @@ __metadata: languageName: node linkType: hard -"@nomicfoundation/hardhat-network-helpers@npm:^1.0.8": - version: 1.0.8 - resolution: "@nomicfoundation/hardhat-network-helpers@npm:1.0.8" +"@nomicfoundation/hardhat-network-helpers@npm:^1.1.2": + version: 1.1.2 + resolution: "@nomicfoundation/hardhat-network-helpers@npm:1.1.2" dependencies: ethereumjs-util: ^7.1.4 peerDependencies: - hardhat: ^2.9.5 - checksum: cf865301fa7a8cebf5c249bc872863d2e69f0f3d14cceadbc5d5761bd97745f38fdb17c9074d46ef0d3a75748f43c0e14d37a54a09ae3b7e0e981c7f437c8553 + hardhat: ^2.26.0 + checksum: 9b0f3152ebd7ec0b813372f3e837bc249f54e86b76a96bedb78f6142fb1ccc21e3f877a0003542d847a0a8a0aa4243f682bdc1dfbcd964b16e9d2c6f04baec3c languageName: node linkType: hard @@ -12983,13 +12983,13 @@ __metadata: languageName: node linkType: hard -"hardhat@npm:2.27.2": - version: 2.27.2 - resolution: "hardhat@npm:2.27.2" +"hardhat@npm:2.26": + version: 2.26.5 + resolution: "hardhat@npm:2.26.5" dependencies: "@ethereumjs/util": ^9.1.0 "@ethersproject/abi": ^5.1.2 - "@nomicfoundation/edr": 0.12.0-next.16 + "@nomicfoundation/edr": ^0.11.3 "@nomicfoundation/solidity-analyzer": ^0.1.0 "@sentry/node": ^5.18.1 adm-zip: ^0.4.16 @@ -13036,7 +13036,7 @@ __metadata: optional: true bin: hardhat: internal/cli/bootstrap.js - checksum: 004d6441bf8353d6b01b0a52c0a7897388e07be7d25605298040a7439c9bc25126033062e6dfc7fad05f5e077dd900f4bb7999ec3d9634f7425bc6f21ce5c27e + checksum: 06577ead0dd45516788ce0ebe9cd64aa121a93eec6cb10f7ecc6fa56c3af7533856298dc6563cd025f3300caff5692c732b675e966cfd4048744996bd9e0b6a4 languageName: node linkType: hard