From ea56228eae8faf7aecdd9813c76f16a0d8897b8e Mon Sep 17 00:00:00 2001 From: kyriediculous Date: Fri, 29 Mar 2024 16:41:34 +0100 Subject: [PATCH] test: tests for audit fixes and withdrawal queue --- script/Add_Liquidity.s.sol | 2 +- script/Stats.s.sol | 4 +- src/Swap.sol | 55 ++++++++++++++---------- test/Swap.t.sol | 87 ++++++++++++++++++++++++++++++++------ 4 files changed, 109 insertions(+), 39 deletions(-) diff --git a/script/Add_Liquidity.s.sol b/script/Add_Liquidity.s.sol index b72d247..9030e19 100644 --- a/script/Add_Liquidity.s.sol +++ b/script/Add_Liquidity.s.sol @@ -19,7 +19,7 @@ contract Add_Liquidity is Script { vm.startBroadcast(deployerPrivateKey); TenderSwap swap = TenderSwap(0x2C7b29B0d07276bA2DF4abE02E9A38b5693af9c6); ERC20(underlying).approve(address(swap), 500_000 ether); - swap.deposit(500_000 ether); + swap.deposit(500_000 ether, 0); console2.log("liabilities", swap.liabilities()); console2.log("liquidity", swap.liquidity()); // ERC20(0x2eaC4210B90D13666f7E88635096BdC17C51FB70).approve(address(swap), 10 ether); diff --git a/script/Stats.s.sol b/script/Stats.s.sol index 036ef5e..fe6f0e4 100644 --- a/script/Stats.s.sol +++ b/script/Stats.s.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.17; import { Script, console2 } from "forge-std/Script.sol"; import { ERC20 } from "solmate/tokens/ERC20.sol"; import { TenderSwap, Config } from "@tenderize/swap/Swap.sol"; -import { SD59x18 } from "@prb/math/SD59x18.sol"; +import { UD60x18 } from "@prb/math/UD60x18.sol"; import { Tenderizer } from "@tenderize/stake/tenderizer/Tenderizer.sol"; import { StakingXYZ } from "lib/staking/test/helpers/StakingXYZ.sol"; @@ -29,7 +29,7 @@ contract Stats is Script { swap.swap(address(0xE3350e66D3850B4f4C97b6737E9e8Ff78CFC1b00), 1 ether, 0); uint256 liabilities = swap.liabilities(); uint256 liquidity = swap.liquidity(); - SD59x18 utilisation = swap.utilisation(); + UD60x18 utilisation = swap.utilisation(); console2.log("liabilities %s", liabilities); console2.log("liquidity %s", liquidity); diff --git a/src/Swap.sol b/src/Swap.sol index 2e0a881..07062d1 100644 --- a/src/Swap.sol +++ b/src/Swap.sol @@ -14,6 +14,7 @@ import { UD60x18, ZERO as ZERO_UD60, UNIT as UNIT_60x18, ud } from "@prb/math/UD import { ERC20 } from "solmate/tokens/ERC20.sol"; import { ERC721 } from "solmate/tokens/ERC721.sol"; import { SafeTransferLib } from "solmate/utils/SafeTransferLib.sol"; +import { FixedPointMathLib } from "solmate/utils/FixedPointMathLib.sol"; import { Adapter } from "@tenderize/stake/adapters/Adapter.sol"; import { Registry } from "@tenderize/stake/registry/Registry.sol"; import { Tenderizer, TenderizerImmutableArgs } from "@tenderize/stake/tenderizer/Tenderizer.sol"; @@ -30,21 +31,24 @@ import { ERC721Receiver } from "@tenderize/swap/util/ERC721Receiver.sol"; import { LPToken } from "@tenderize/swap/LPToken.sol"; import { UnlockQueue } from "@tenderize/swap/UnlockQueue.sol"; +import { console } from "forge-std/Test.sol"; + pragma solidity 0.8.19; -error UnlockNotMature(uint256 maturity, uint256 timestamp); -error UnlockAlreadyMature(uint256 maturity, uint256 timestamp); -error InvalidAsset(address asset); -error SlippageThresholdExceeded(uint256 out, uint256 minOut); -error InsufficientAssets(uint256 requested, uint256 available); -error RecoveryMode(); -error WithdrawalCooldown(uint256 lpSharesRequested, uint256 lpSharesAvailable); +error ErrorNotMature(uint256 maturity, uint256 timestamp); +error ErrorAlreadyMature(uint256 maturity, uint256 timestamp); +error ErrorInvalidAsset(address asset); +error ErrorSlippage(uint256 out, uint256 minOut); +error ErrorInsufficientAssets(uint256 requested, uint256 available); +error ErrorRecoveryMode(); +error ErrorCalculateLPShares(); +error ErrorWithdrawCooldown(uint256 lpSharesRequested, uint256 lpSharesAvailable); SD59x18 constant BASE_FEE = SD59x18.wrap(0.0005e18); UD60x18 constant RELAYER_CUT = UD60x18.wrap(0.1e18); UD60x18 constant MIN_LP_CUT = UD60x18.wrap(0.1e18); SD59x18 constant K = SD59x18.wrap(3e18); -uint64 constant COOLDOWN = 1 days; +uint64 constant COOLDOWN = 12 hours; struct Config { ERC20 underlying; @@ -198,8 +202,9 @@ contract TenderSwap is Initializable, UUPSUpgradeable, OwnableUpgradeable, SwapS uint256 timePassed = block.timestamp - ld.timestamp; if (timePassed < COOLDOWN) { uint256 remaining = COOLDOWN - timePassed; - uint256 newAmount = ld.amount * remaining / COOLDOWN; - amount += newAmount; + uint256 newAmount = FixedPointMathLib.mulDivUp(ld.amount, remaining, COOLDOWN); + ld.amount += SafeCastLib.safeCastTo192(newAmount); + ld.timestamp = uint64(block.timestamp); } } else { ld.timestamp = uint64(block.timestamp); @@ -211,7 +216,7 @@ contract TenderSwap is Initializable, UUPSUpgradeable, OwnableUpgradeable, SwapS // Calculate LP tokens to mint lpShares = _calculateLpShares(amount); - if (lpShares < minLpShares) revert SlippageThresholdExceeded(lpShares, minLpShares); + if (lpShares < minLpShares) revert ErrorSlippage(lpShares, minLpShares); // Update liabilities $.liabilities += amount; @@ -235,7 +240,7 @@ contract TenderSwap is Initializable, UUPSUpgradeable, OwnableUpgradeable, SwapS uint256 available = liquidity(); - if (amount > available) revert InsufficientAssets(amount, available); + if (amount > available) revert ErrorInsufficientAssets(amount, available); // If there is an existing cooldown since deposit want to check if the cooldown has passed // If not we want to calculate the linear regrassion of the remaining amount and time @@ -243,10 +248,12 @@ contract TenderSwap is Initializable, UUPSUpgradeable, OwnableUpgradeable, SwapS uint256 availableLpShares = lpToken.balanceOf(msg.sender); LastDeposit storage ld = $.lastDeposit[msg.sender]; if (ld.timestamp > 0) { + console.log("time passed", block.timestamp - ld.timestamp); uint256 timePassed = block.timestamp - ld.timestamp; if (timePassed < COOLDOWN) { uint256 remaining = COOLDOWN - timePassed; - uint256 cdAmount = ld.amount * remaining / COOLDOWN; + uint256 cdAmount = FixedPointMathLib.mulDivUp(ld.amount, remaining, COOLDOWN); + console.log("cdAmount", cdAmount); uint256 cdLpShares = _calculateLpShares(cdAmount); availableLpShares -= cdLpShares; } @@ -254,8 +261,9 @@ contract TenderSwap is Initializable, UUPSUpgradeable, OwnableUpgradeable, SwapS // Calculate LP tokens to burn uint256 lpShares = _calculateLpShares(amount); - if (lpShares > availableLpShares) revert WithdrawalCooldown(lpShares, availableLpShares); - if (lpShares > maxLpSharesBurnt) revert SlippageThresholdExceeded(lpShares, maxLpSharesBurnt); + console.log("wanted-avail", lpShares, availableLpShares); + if (lpShares > availableLpShares) revert ErrorWithdrawCooldown(lpShares, availableLpShares); + if (lpShares > maxLpSharesBurnt) revert ErrorSlippage(lpShares, maxLpSharesBurnt); // Update liabilities $.liabilities -= amount; @@ -325,7 +333,7 @@ contract TenderSwap is Initializable, UUPSUpgradeable, OwnableUpgradeable, SwapS * @return fee Amount of fees paid */ function swap(address asset, uint256 amount, uint256 minOut) external returns (uint256 out, uint256 fee) { - if (!_isValidAsset(asset)) revert InvalidAsset(asset); + if (!_isValidAsset(asset)) revert ErrorInvalidAsset(asset); Data storage $ = _loadStorageSlot(); @@ -339,7 +347,7 @@ contract TenderSwap is Initializable, UUPSUpgradeable, OwnableUpgradeable, SwapS (out, fee) = _quote(amount, p); // Revert if slippage threshold is exceeded, i.e. if `out` is less than `minOut` - if (out < minOut) revert SlippageThresholdExceeded(out, minOut); + if (out < minOut) revert ErrorSlippage(out, minOut); // update pool state // - Update total amount unlocking @@ -377,7 +385,7 @@ contract TenderSwap is Initializable, UUPSUpgradeable, OwnableUpgradeable, SwapS // Can not purchase unlocks in recovery mode // The fees need to flow back to paying off debt and relayers are cheaper - if ($.recovery > 0) revert RecoveryMode(); + if ($.recovery > 0) revert ErrorRecoveryMode(); // get newest item from unlock queue UnlockQueue.Item memory unlock = $.unlockQ.popTail().data; @@ -387,7 +395,7 @@ contract TenderSwap is Initializable, UUPSUpgradeable, OwnableUpgradeable, SwapS (address tenderizer,) = _decodeTokenId(tokenId); Adapter adapter = Tenderizer(tenderizer).adapter(); uint256 time = adapter.currentTime(); - if (unlock.maturity <= time) revert UnlockAlreadyMature(unlock.maturity, block.timestamp); + if (unlock.maturity <= time) revert ErrorAlreadyMature(unlock.maturity, block.timestamp); // Calculate the reward for purchasing the unlock // The base reward is the fee minus the MIN_LP_CUT going to liquidity providers @@ -590,14 +598,15 @@ contract TenderSwap is Initializable, UUPSUpgradeable, OwnableUpgradeable, SwapS Data storage $ = _loadStorageSlot(); uint256 supply = lpToken.totalSupply(); + uint256 liabilities = $.liabilities; - if (supply == 0) { - return amount; + if (liabilities == 0) { + return amount * 1e18; } - shares = amount * 1e18 * supply / $.liabilities; + shares = amount * (supply / liabilities); // calculate factor first since it's scaled up if (shares == 0) { - revert InsufficientAssets(amount, $.liabilities); + revert ErrorCalculateLPShares(); } } diff --git a/test/Swap.t.sol b/test/Swap.t.sol index 6d4bf25..cb6bc1d 100644 --- a/test/Swap.t.sol +++ b/test/Swap.t.sol @@ -19,7 +19,17 @@ import { Adapter } from "@tenderize/stake/adapters/Adapter.sol"; import { Registry } from "@tenderize/stake/registry/Registry.sol"; import { Tenderizer, TenderizerImmutableArgs } from "@tenderize/stake/tenderizer/Tenderizer.sol"; -import { TenderSwap, Config, BASE_FEE, RELAYER_CUT, MIN_LP_CUT, _encodeTokenId, _decodeTokenId } from "@tenderize/swap/Swap.sol"; +import { + TenderSwap, + Config, + BASE_FEE, + RELAYER_CUT, + MIN_LP_CUT, + _encodeTokenId, + _decodeTokenId, + COOLDOWN, + ErrorWithdrawCooldown +} from "@tenderize/swap/Swap.sol"; import { LPToken } from "@tenderize/swap/LPToken.sol"; import { SD59x18, ZERO, UNIT, unwrap, sd } from "@prb/math/SD59x18.sol"; @@ -30,6 +40,8 @@ import { UnlockQueue } from "@tenderize/swap/UnlockQueue.sol"; import { acceptableDelta } from "./helpers/Utils.sol"; +import { console } from "forge-std/console.sol"; + contract TenderSwapTest is Test { MockERC20 underlying; MockERC20 tToken0; @@ -72,31 +84,80 @@ contract TenderSwapTest is Test { } function testFuzz_deposits(uint256 x, uint256 y, uint256 l) public { - uint256 deposit1 = bound(x, 1, type(uint128).max); - uint256 deposit2 = bound(y, 1, type(uint128).max); - l = bound(l, 1, type(uint128).max); + console.log(block.timestamp); + uint256 deposit1 = bound(x, 100, type(uint128).max); + l = bound(l, deposit1, deposit1 * 1e18); + uint256 deposit2 = bound(y, 100, type(uint128).max); underlying.mint(addr1, deposit1); underlying.mint(addr2, deposit2); vm.startPrank(addr1); underlying.approve(address(swap), deposit1); - swap.deposit(deposit1); + swap.deposit(deposit1, 0); vm.stopPrank(); // Change liabilities ! swap.exposed_setLiabilities(l); + underlying.mint(address(swap), l - deposit1); vm.startPrank(addr2); underlying.approve(address(swap), deposit2); - swap.deposit(deposit2); + swap.deposit(deposit2, 0); vm.stopPrank(); - uint256 expBalY = deposit2 * deposit1 / l; + uint256 expBal2 = deposit2 * (deposit1 * 1e18 / l); + + console.log("u b of", underlying.balanceOf(address(swap))); + + assertEq(swap.lpToken().totalSupply(), (deposit1 * 1e18 + expBal2), "lpToken totalSupply"); + assertEq(swap.lpToken().balanceOf(addr1), deposit1 * 1e18, "addr1 lpToken balance"); + assertEq(swap.lpToken().balanceOf(addr2), expBal2, "addr2 lpToken balance"); + assertEq(underlying.balanceOf(address(swap)), l + deposit2, "TenderSwap underlying balance"); + } + + function test_withdrawCooldown(uint256 deposit) public { + uint256 start = 1; + vm.warp(1); + deposit = bound(deposit, 100, type(uint64).max); + underlying.mint(address(this), deposit); + + underlying.approve(address(swap), deposit); + swap.deposit(deposit, 0); - assertEq(swap.lpToken().totalSupply(), deposit1 + expBalY, "lpToken totalSupply"); - assertEq(swap.lpToken().balanceOf(addr1), deposit1, "addr1 lpToken balance"); - assertEq(swap.lpToken().balanceOf(addr2), expBalY, "addr2 lpToken balance"); - assertEq(underlying.balanceOf(address(swap)), deposit1 + deposit2, "TenderSwap underlying balance"); + vm.expectRevert(); + // even withdrawing '1' will revert, since no time has elapsed + swap.withdraw(1, type(uint256).max); + + vm.warp(block.timestamp + COOLDOWN / 2); + + // withdrawing half + 1 fails as it exceeds the available amount + // after only half the time has elapsed + vm.expectRevert(); + swap.withdraw(deposit / 2 + 1, type(uint256).max); + + uint256 balBefore = underlying.balanceOf(address(this)); + swap.withdraw(deposit / 2, type(uint256).max); + uint256 balAfter = underlying.balanceOf(address(this)); + assertEq(balAfter - balBefore, deposit / 2, "withdraw half"); + + // deposit again, the new cooldown amount will be half of the previous plus our new deposit + uint256 deposit2 = bound(deposit, 100, deposit); + console.log("deposit2", deposit2); + underlying.mint(address(this), deposit2); + underlying.approve(address(swap), deposit2); + swap.deposit(deposit2, 0); + // withdrawing half the original amount should still fail + vm.expectRevert(); + swap.withdraw(deposit / 2 + 1, type(uint256).max); + + vm.warp(start + COOLDOWN); + // withdrawing half should work now, withdrawing deposit2 should fail + vm.expectRevert(); + swap.withdraw(deposit2, type(uint256).max); + + swap.withdraw(deposit - deposit / 2, type(uint256).max); + balAfter = underlying.balanceOf(address(this)); + assertEq(balAfter, deposit, "withdraw half"); } function test_claimRelayerRewards(uint256 amount) public { @@ -131,7 +192,7 @@ contract TenderSwapTest is Test { uint256 liquidity = 100 ether; underlying.mint(address(this), liquidity); underlying.approve(address(swap), liquidity); - swap.deposit(liquidity); + swap.deposit(liquidity, 0); vm.mockCall(address(tToken0), abi.encodeWithSelector(TenderizerImmutableArgs.adapter.selector), abi.encode(adapter)); @@ -241,7 +302,7 @@ contract TenderSwapTest is Test { uint256 liquidity = 100 ether; underlying.mint(address(this), liquidity); underlying.approve(address(swap), liquidity); - swap.deposit(liquidity); + swap.deposit(liquidity, 0); uint256 amount = 10 ether; uint256 tokenId = _encodeTokenId(address(tToken0), 0);