diff --git a/foundry.toml b/foundry.toml index 3ecd04c..a49267c 100644 --- a/foundry.toml +++ b/foundry.toml @@ -3,6 +3,8 @@ src = "src" out = "out" libs = ["lib"] evm_version = "cancun" +optimizer = true +optimizer_runs = 10_000 [fuzz] max_test_rejects = 2_147_483_648 diff --git a/src/ValidlyFactory.sol b/src/ValidlyFactory.sol index 234513b..fcb743e 100644 --- a/src/ValidlyFactory.sol +++ b/src/ValidlyFactory.sol @@ -159,9 +159,7 @@ contract ValidlyFactory is IValidlyFactory { } /** - * @notice Claims rebase token fees accumulated in this contract. - * @dev By design of Sovereign Pools, manager fees for rebase tokens - * get transferred on every swap to its manager (this contract). + * @notice Claims accummulated fees accumulated in this contract. * @param _token The address of the token to claim. * @param _recipient The address of the recipient. */ @@ -189,8 +187,7 @@ contract ValidlyFactory is IValidlyFactory { * @param _pool The address of the pool to claim the pool manager fees for. */ function claimFees(address _pool) external { - // It marks all fees as protocol fees to be used by gauge - ISovereignPool(_pool).claimPoolManagerFees(10_000, 10_000); + ISovereignPool(_pool).claimPoolManagerFees(0, 0); emit FeesClaimed(_pool); } diff --git a/src/ValidlyLens.sol b/src/ValidlyLens.sol new file mode 100644 index 0000000..918577e --- /dev/null +++ b/src/ValidlyLens.sol @@ -0,0 +1,187 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.24; + +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; +import {ISovereignPool} from "@valantis-core/pools/interfaces/ISovereignPool.sol"; + +import {IValidly} from "./interfaces/IValidly.sol"; + +/** + * @title Validly Lens. + * @notice Helper contract with read-only functions for Validly. + */ +contract ValidlyLens { + /** + * + * CONSTANTS + * + */ + uint256 private constant MIN_LIQUIDITY = 1000; + uint256 private constant BIPS = 10_000; + + /** + * + * VIEW FUNCTIONS + * + */ + + /** + * @notice Simulate deposit liquidity into Validly and mint LP tokens. + * @param _validly Address of Validly deployment. + * @param _amount0Max Maximum amount of token0 to deposit. + * @param _amount1Max Maximum amount of token1 to deposit. + * @return shares Amount of shares minted. + * @return amount0 Correct amount of token0 to deposit. + * @return amount1 Correct amount of token1 to deposit. + */ + function simulateDeposit(address _validly, uint256 _amount0Max, uint256 _amount1Max) + external + view + returns (uint256 shares, uint256 amount0, uint256 amount1) + { + uint256 totalSupplyCache = ERC20(_validly).totalSupply(); + if (totalSupplyCache == 0) { + amount0 = _amount0Max; + amount1 = _amount1Max; + + shares = Math.sqrt(amount0 * amount1) - MIN_LIQUIDITY; + } else { + ISovereignPool pool = IValidly(_validly).pool(); + (uint256 reserve0, uint256 reserve1) = pool.getReserves(); + + uint256 shares0 = Math.mulDiv(_amount0Max, totalSupplyCache, reserve0); + uint256 shares1 = Math.mulDiv(_amount1Max, totalSupplyCache, reserve1); + + if (shares0 < shares1) { + shares = shares0; + amount1 = Math.mulDiv(reserve1, shares, totalSupplyCache, Math.Rounding.Ceil); + amount0 = _amount0Max; + } else { + shares = shares1; + amount0 = Math.mulDiv(reserve0, shares, totalSupplyCache, Math.Rounding.Ceil); + amount1 = _amount1Max; + } + } + } + + /** + * @notice Simulate withdraw liquidity from Validly and burn LP tokens. + * @param _validly Address of Validly deployment. + * @param _shares Amount of LP tokens to burn. + * @return amount0 Amount of token0 withdrawn. WARNING: Potentially innacurate in case token0 is rebase. + * @return amount1 Amount of token1 withdrawn. WARNING: Potentially innacurate in case token1 is rebase. + */ + function simulateWithdraw(address _validly, uint256 _shares) + external + view + returns (uint256 amount0, uint256 amount1) + { + if (_shares == 0) return (0, 0); + + ISovereignPool pool = IValidly(_validly).pool(); + (uint256 reserve0, uint256 reserve1) = pool.getReserves(); + + uint256 totalSupplyCache = ERC20(_validly).totalSupply(); + amount0 = Math.mulDiv(reserve0, _shares, totalSupplyCache); + amount1 = Math.mulDiv(reserve1, _shares, totalSupplyCache); + + if (amount0 == 0 || amount1 == 0) revert("zero_amount_withdrawn"); + } + + /** + * @notice Simulate swap quote from Validly. + * @param _validly Address of Validly deployment. + * @param _isZeroToOne Direction of the swap. + * @param _amountIn Amount of input token to swap. + * @return amountOut Amount of output token received after swap. + */ + function simulateSwap(address _validly, bool _isZeroToOne, uint256 _amountIn) + external + view + returns (uint256 amountOut) + { + if (_amountIn == 0) return 0; + + IValidly validly = IValidly(_validly); + + ISovereignPool pool = validly.pool(); + bool isStable = validly.isStable(); + + (uint256 reserve0, uint256 reserve1) = pool.getReserves(); + + uint256 amountInWithoutFee = Math.mulDiv(_amountIn, BIPS, BIPS + pool.defaultSwapFeeBips()); + + (uint256 reserveIn, uint256 reserveOut) = _isZeroToOne ? (reserve0, reserve1) : (reserve1, reserve0); + + uint256 invariant; + if (isStable) { + uint256 decimals0 = validly.decimals0(); + uint256 decimals1 = validly.decimals1(); + + invariant = _stableInvariant(reserve0, reserve1, decimals0, decimals1); + // Scale reserves and amounts to 18 decimals + reserveIn = _isZeroToOne ? (reserveIn * 1e18) / decimals0 : (reserveIn * 1e18) / decimals1; + reserveOut = _isZeroToOne ? (reserveOut * 1e18) / decimals1 : (reserveOut * 1e18) / decimals0; + uint256 amountIn = + _isZeroToOne ? (amountInWithoutFee * 1e18) / decimals0 : (amountInWithoutFee * 1e18) / decimals1; + amountOut = reserveOut - _get_y_stableInvariant(amountIn + reserveIn, invariant, reserveOut); + + amountOut = (amountOut * (_isZeroToOne ? decimals1 : decimals0)) / 1e18; + } else { + invariant = reserve0 * reserve1; + + amountOut = (reserveOut * amountInWithoutFee) / (reserveIn + amountInWithoutFee); + } + } + + /** + * + * PRIVATE FUNCTIONS + * + */ + function _stableInvariant(uint256 x, uint256 y, uint256 decimals0, uint256 decimals1) + private + pure + returns (uint256) + { + uint256 _x = (x * 1e18) / decimals0; + uint256 _y = (y * 1e18) / decimals1; + uint256 _a = (_x * _y) / 1e18; + uint256 _b = ((_x * _x) / 1e18 + (_y * _y) / 1e18); + return (_a * _b) / 1e18; // x3y+y3x >= k + } + + function _f(uint256 x0, uint256 y) private pure returns (uint256) { + return (x0 * ((((y * y) / 1e18) * y) / 1e18)) / 1e18 + (((((x0 * x0) / 1e18) * x0) / 1e18) * y) / 1e18; + } + + function _d(uint256 x0, uint256 y) private pure returns (uint256) { + return (3 * x0 * ((y * y) / 1e18)) / 1e18 + ((((x0 * x0) / 1e18) * x0) / 1e18); + } + + function _get_y_stableInvariant(uint256 x0, uint256 invariant, uint256 y) private pure returns (uint256) { + for (uint256 i = 0; i < 255; i++) { + uint256 y_prev = y; + uint256 k = _f(x0, y); + if (k < invariant) { + uint256 dy = ((invariant - k) * 1e18) / _d(x0, y); + y = y + dy; + } else { + uint256 dy = ((k - invariant) * 1e18) / _d(x0, y); + y = y - dy; + } + if (y > y_prev) { + if (y - y_prev <= 1) { + return y; + } + } else { + if (y_prev - y <= 1) { + return y; + } + } + } + // Did not converge in 255 fixed point iterations + revert("stable_invariant_not_converged"); + } +} diff --git a/test/Validly.t.sol b/test/Validly.t.sol index dc8f80e..3cc4cfc 100644 --- a/test/Validly.t.sol +++ b/test/Validly.t.sol @@ -11,10 +11,13 @@ import {SovereignPoolFactory} from "@valantis-core/pools/factories/SovereignPool import {ALMLiquidityQuoteInput} from "@valantis-core/ALM/structs/SovereignALMStructs.sol"; import {Validly} from "../src/Validly.sol"; +import {ValidlyLens} from "../src/ValidlyLens.sol"; import {ValidlyFactory} from "../src/ValidlyFactory.sol"; contract ValidlyTest is Test { ValidlyFactory public factory; + ValidlyLens public lens; + ERC20Mock public token0; ERC20Mock public token1; @@ -28,6 +31,8 @@ contract ValidlyTest is Test { token0 = new ERC20Mock(); token1 = new ERC20Mock(); + lens = new ValidlyLens(); + ProtocolFactory protocolFactory = new ProtocolFactory(address(this)); SovereignPoolFactory poolFactory = new SovereignPoolFactory(); @@ -79,9 +84,29 @@ contract ValidlyTest is Test { token0.approve(address(volatilePair), 1000 ether); token1.approve(address(volatilePair), 1000 ether); - volatilePair.deposit(1 ether, 10 ether, 0, block.timestamp + 1, address(this), ""); - - volatilePair.deposit(1 ether, 20 ether, 0, block.timestamp + 1, address(this), ""); + (uint256 sharesSimulation, uint256 amount0Simulation, uint256 amount1Simulation) = + lens.simulateDeposit(address(volatilePair), 1 ether, 10 ether); + (uint256 shares, uint256 amount0, uint256 amount1) = + volatilePair.deposit(1 ether, 10 ether, 0, block.timestamp + 1, address(this), ""); + assertEq(sharesSimulation, shares); + assertEq(amount0Simulation, amount0); + assertEq(amount1Simulation, amount1); + + (sharesSimulation, amount0Simulation, amount1Simulation) = + lens.simulateDeposit(address(volatilePair), 40 ether, 0.1 ether); + (shares, amount0, amount1) = + volatilePair.deposit(40 ether, 0.1 ether, 0, block.timestamp + 1, address(this), ""); + assertEq(sharesSimulation, shares); + assertEq(amount0Simulation, amount0); + assertEq(amount1Simulation, amount1); + + (sharesSimulation, amount0Simulation, amount1Simulation) = + lens.simulateDeposit(address(volatilePair), 0.1 ether, 60 ether); + (shares, amount0, amount1) = + volatilePair.deposit(0.1 ether, 60 ether, 0, block.timestamp + 1, address(this), ""); + assertEq(sharesSimulation, shares); + assertEq(amount0Simulation, amount0); + assertEq(amount1Simulation, amount1); vm.expectRevert(Validly.Validly__deposit_zeroShares.selector); volatilePair.deposit(1 ether, 0, 0, block.timestamp + 1, address(this), ""); @@ -116,8 +141,12 @@ contract ValidlyTest is Test { vm.expectRevert(Validly.Validly__withdraw_insufficientToken1Withdrawn.selector); volatilePair.withdraw(sharesToWithdraw, 0, 100 ether, block.timestamp + 1, address(this), ""); + (uint256 amount0Simulation, uint256 amount1Simulation) = + lens.simulateWithdraw(address(volatilePair), sharesToWithdraw); (uint256 amount0, uint256 amount1) = volatilePair.withdraw(sharesToWithdraw, 0, 0, block.timestamp + 1, address(this), ""); + assertEq(amount0Simulation, amount0); + assertEq(amount1Simulation, amount1); assertEq(amount0, expectedAmount0); assertEq(amount1, expectedAmount1); @@ -144,7 +173,9 @@ contract ValidlyTest is Test { token1.approve(address(stablePool), 1 ether); + uint256 amountOutSimulation = lens.simulateSwap(address(stablePair), params.isZeroToOne, params.amountIn); (uint256 amountInUsed, uint256 amountOut) = stablePool.swap(params); + assertEq(amountOutSimulation, amountOut); assertApproxEqAbs(amountOut, amountInUsed, Math.mulDiv(amountInUsed, 1, 1000)); } @@ -178,7 +209,9 @@ contract ValidlyTest is Test { token0.approve(address(volatilePool), 1 ether); token1.approve(address(volatilePool), 10 ether); + uint256 amountOutSimulation = lens.simulateSwap(address(volatilePair), params.isZeroToOne, params.amountIn); (uint256 amountInUsed, uint256 amountOut) = volatilePool.swap(params); + assertEq(amountOutSimulation, amountOut); uint256 expectedAmountOut = Math.mulDiv( reserve1, Math.mulDiv(amountInUsed, 10000, 10001), reserve0 + Math.mulDiv(amountInUsed, 10000, 10001) diff --git a/test/ValidlyFactory.t.sol b/test/ValidlyFactory.t.sol index 18194aa..7930f78 100644 --- a/test/ValidlyFactory.t.sol +++ b/test/ValidlyFactory.t.sol @@ -100,18 +100,19 @@ contract ValidlyFactoryTest is Test { address pool = factory.pools(key); + token0.mint(address(pool), 1e18); + token1.mint(address(pool), 10e18); + vm.store(address(pool), bytes32(uint256(5)), bytes32(uint256(1e18))); vm.store(address(pool), bytes32(uint256(6)), bytes32(uint256(10e18))); factory.claimFees(pool); - assertEq(SovereignPool(pool).feeProtocol0(), 1e18); - assertEq(SovereignPool(pool).feeProtocol1(), 10e18); + assertEq(token0.balanceOf(address(factory)), 1e18); + assertEq(token1.balanceOf(address(factory)), 10e18); } function test_claimTokens() public { - token0.mint(address(factory), 1e18); - address ALICE = makeAddr("ALICE"); vm.expectRevert(ValidlyFactory.ValidlyFactory__onlyProtocolManager.selector); @@ -124,8 +125,12 @@ contract ValidlyFactoryTest is Test { vm.expectRevert(ValidlyFactory.ValidlyFactory__claimTokens_invalidRecipient.selector); factory.claimTokens(address(token0), address(0)); - factory.claimTokens(address(token0), ALICE); + test_claimFees(); + factory.claimTokens(address(token0), ALICE); assertEq(token0.balanceOf(ALICE), 1e18); + + factory.claimTokens(address(token1), ALICE); + assertEq(token1.balanceOf(ALICE), 10e18); } } diff --git a/test/ValidlyFuzz.t.sol b/test/ValidlyFuzz.t.sol index 6fcdc4c..37cd2e8 100644 --- a/test/ValidlyFuzz.t.sol +++ b/test/ValidlyFuzz.t.sol @@ -14,12 +14,14 @@ import {SovereignPoolFactory} from "@valantis-core/pools/factories/SovereignPool import {ALMLiquidityQuoteInput, ALMLiquidityQuote} from "@valantis-core/ALM/structs/SovereignALMStructs.sol"; import {Validly} from "../src/Validly.sol"; +import {ValidlyLens} from "../src/ValidlyLens.sol"; import {ValidlyFactory} from "../src/ValidlyFactory.sol"; contract ValidlyFuzzTest is Test { error SovereignPool__swap_zeroAmountInOrOut(); ValidlyFactory factory; + ValidlyLens lens; ERC20Mock token0; ERC20Mock token1; @@ -34,6 +36,8 @@ contract ValidlyFuzzTest is Test { token0 = new ERC20Mock(); token1 = new ERC20Mock(); + lens = new ValidlyLens(); + (token0, token1) = address(token0) < address(token1) ? (token0, token1) : (token1, token0); ProtocolFactory protocolFactory = new ProtocolFactory(address(this)); @@ -88,9 +92,15 @@ contract ValidlyFuzzTest is Test { return; } + (uint256 sharesSimulation, uint256 amount0DepositedSimulation, uint256 amount1DepositedSimulation) = + lens.simulateDeposit(address(volatilePair), amount0, amount1); (uint256 shares, uint256 amount0Deposited, uint256 amount1Deposited) = volatilePair.deposit(amount0, amount1, 0, block.timestamp + 1, address(1), ""); + assertEq(sharesSimulation, shares); + assertEq(amount0DepositedSimulation, amount0Deposited); + assertEq(amount1DepositedSimulation, amount1Deposited); + assertEq(amount0Deposited, amount0); assertEq(amount1Deposited, amount1); assertEq(shares, expectedShares - 1000); @@ -103,9 +113,15 @@ contract ValidlyFuzzTest is Test { return; } + (uint256 sharesSimulation, uint256 amount0DepositedSimulation, uint256 amount1DepositedSimulation) = + lens.simulateDeposit(address(volatilePair), amount0, amount1); (uint256 shares, uint256 amount0Deposited, uint256 amount1Deposited) = volatilePair.deposit(amount0, amount1, 0, block.timestamp + 1, address(1), ""); + assertEq(sharesSimulation, shares); + assertEq(amount0DepositedSimulation, amount0Deposited); + assertEq(amount1DepositedSimulation, amount1Deposited); + assertLe(amount0Deposited, amount0); assertLe(amount1Deposited, amount1); assertEq(shares, expectedShares); @@ -149,7 +165,10 @@ contract ValidlyFuzzTest is Test { return; } + (uint256 amount0Simulation, uint256 amount1Simulation) = lens.simulateWithdraw(address(volatilePair), shares); (uint256 amount0, uint256 amount1) = volatilePair.withdraw(shares, 0, 0, block.timestamp + 1, address(this), ""); + assertEq(amount0, amount0Simulation); + assertEq(amount1, amount1Simulation); assertEq(amount0, expectedAmount0); assertEq(amount1, expectedAmount1); @@ -197,7 +216,9 @@ contract ValidlyFuzzTest is Test { return; } + uint256 amountOutSimulation = lens.simulateSwap(address(volatilePair), params.isZeroToOne, params.amountIn); (uint256 amountInUsed, uint256 amountOut) = ISovereignPool(volatilePool).swap(params); + assertEq(amountOut, amountOutSimulation); uint256 k_post = isZeroToOne ? (reserve0 + amountInUsed) * (reserve1 - amountOut) @@ -237,7 +258,9 @@ contract ValidlyFuzzTest is Test { uint256 k_pre = _stableInvariant(reserve0, reserve1); + uint256 amountOutSimulation = lens.simulateSwap(address(stablePair), params.isZeroToOne, params.amountIn); (uint256 amountInUsed, uint256 amountOut) = ISovereignPool(stablePool).swap(params); + assertEq(amountOutSimulation, amountOut); uint256 k_post = isZeroToOne ? _stableInvariant(reserve0 + amountInUsed, reserve1 - amountOut) @@ -301,7 +324,9 @@ contract ValidlyFuzzTest is Test { return; } + uint256 amountOutSimulation = lens.simulateSwap(address(stablePair), params.isZeroToOne, params.amountIn); (uint256 amountInUsed, uint256 amountOut) = ISovereignPool(stablePool).swap(params); + assertEq(amountOut, amountOutSimulation); k_post = isZeroToOne ? _stableInvariant(reserve0 + amountInUsed - 10, reserve1 - amountOut)