Skip to content

Commit

Permalink
test: tests for audit fixes and withdrawal queue
Browse files Browse the repository at this point in the history
  • Loading branch information
kyriediculous committed Mar 29, 2024
1 parent 8802715 commit ea56228
Show file tree
Hide file tree
Showing 4 changed files with 109 additions and 39 deletions.
2 changes: 1 addition & 1 deletion script/Add_Liquidity.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
4 changes: 2 additions & 2 deletions script/Stats.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand All @@ -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);
Expand Down
55 changes: 32 additions & 23 deletions src/Swap.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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;
Expand Down Expand Up @@ -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);
Expand All @@ -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;
Expand All @@ -235,27 +240,30 @@ 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
// and convert it into LP shares to subtract from the available LP shares for the user
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;
}
}

// 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;
Expand Down Expand Up @@ -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();

Expand All @@ -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
Expand Down Expand Up @@ -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;
Expand All @@ -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
Expand Down Expand Up @@ -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();
}
}

Expand Down
87 changes: 74 additions & 13 deletions test/Swap.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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;
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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));

Expand Down Expand Up @@ -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);
Expand Down

0 comments on commit ea56228

Please sign in to comment.