diff --git a/.gitmodules b/.gitmodules index d178bc2..21402db 100644 --- a/.gitmodules +++ b/.gitmodules @@ -21,3 +21,6 @@ [submodule "lib/openzeppelin-contracts-upgradeable"] path = lib/openzeppelin-contracts-upgradeable url = https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable +[submodule "lib/openzeppelin-contracts"] + path = lib/openzeppelin-contracts + url = https://github.com/OpenZeppelin/openzeppelin-contracts diff --git a/README.md b/README.md index c68a431..6cafa8c 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Foundry Template [![Open in Gitpod][gitpod-badge]][gitpod] [![Github Actions][gha-badge]][gha] [![Foundry][foundry-badge]][foundry] [![License: MIT][license-badge]][license] +# TenderSwap - Unified Liquidity for Staked Assets 🌊 [![Open in Gitpod][gitpod-badge]][gitpod] [![Github Actions][gha-badge]][gha] [![Foundry][foundry-badge]][foundry] [![License: MIT][license-badge]][license] [gitpod]: https://gitpod.io/#https://github.com/Tenderize/tenderswap [gitpod-badge]: https://img.shields.io/badge/Gitpod-Open%20in%20Gitpod-FFB45B?logo=gitpod @@ -9,81 +9,12 @@ [license]: https://opensource.org/licenses/MIT [license-badge]: https://img.shields.io/badge/License-MIT-blue.svg -A Foundry-based template for developing Solidity smart contracts, with sensible defaults. +## Overview -## What's Inside +For an overview of the mechanics and implemented mathematics in TenderSwap, check following sources: -- [Forge](https://github.com/foundry-rs/foundry/blob/master/forge): compile, test, fuzz, format, and deploy smart - contracts -- [Forge Std](https://github.com/foundry-rs/forge-std): collection of helpful contracts and cheatcodes for testing -- [PRBTest](https://github.com/PaulRBerg/prb-test): modern collection of testing assertions and logging utilities -- [Prettier](https://github.com/prettier/prettier): code formatter for non-Solidity files -- [Solhint Community](https://github.com/solhint-community/solhint-community): linter for Solidity code - -## Getting Started - -Click the [`Use this template`](https://github.com/PaulRBerg/foundry-template/generate) button at the top of the page to -create a new repository with this repo as the initial state. - -Or, if you prefer to install the template manually: - -```sh -$ mkdir my-project -$ cd my-project -$ forge init --template PaulRBerg/foundry-template -$ pnpm install # install Solhint, Prettier, and other Node.js deps -``` - -If this is your first time with Foundry, check out the -[installation](https://github.com/foundry-rs/foundry#installation) instructions. - -## Features - -This template builds upon the frameworks and libraries mentioned above, so for details about their specific features, -please consult their respective documentation. - -For example, if you're interested in exploring Foundry in more detail, you should look at the -[Foundry Book](https://book.getfoundry.sh/). In particular, you may be interested in reading the -[Writing Tests](https://book.getfoundry.sh/forge/writing-tests.html) tutorial. - -### Sensible Defaults - -This template comes with a set of sensible default configurations for you to use. These defaults can be found in the -following files: - -```text -├── .editorconfig -├── .gitignore -├── .prettierignore -├── .prettierrc.yml -├── .solhint.json -├── foundry.toml -└── remappings.txt -``` - -### VSCode Integration - -This template is IDE agnostic, but for the best user experience, you may want to use it in VSCode alongside Nomic -Foundation's [Solidity extension](https://marketplace.visualstudio.com/items?itemName=NomicFoundation.hardhat-solidity). - -For guidance on how to integrate a Foundry project in VSCode, please refer to this -[guide](https://book.getfoundry.sh/config/vscode). - -### GitHub Actions - -This template comes with GitHub Actions pre-configured. Your contracts will be linted and tested on every push and pull -request made to the `main` branch. - -You can edit the CI script in [.github/workflows/ci.yml](./.github/workflows/ci.yml). - -## Writing Tests - -To write a new test contract, you start by importing [PRBTest](https://github.com/PaulRBerg/prb-test) and inherit from -it in your test contract. PRBTest comes with a pre-instantiated [cheatcodes](https://book.getfoundry.sh/cheatcodes/) -environment accessible via the `vm` property. If you would like to view the logs in the terminal output you can add the -`-vvv` flag and use [console.log](https://book.getfoundry.sh/faq?highlight=console.log#how-do-i-use-consolelog). - -This template comes with an example test contract [Foo.t.sol](./test/Foo.t.sol) +- [White Paper](https://whitepaper.tenderize.me/core-architecture/tenderswap) +- [Yellow Paper](https://whitepaper.tenderize.me/tenderswap/yellow-paper) ## Usage @@ -94,7 +25,7 @@ This is a list of the most frequently needed commands. Build the contracts: ```sh -$ forge build +forge build ``` ### Clean @@ -102,7 +33,7 @@ $ forge build Delete the build artifacts and cache directories: ```sh -$ forge clean +forge clean ``` ### Compile @@ -110,7 +41,7 @@ $ forge clean Compile the contracts: ```sh -$ forge build +forge build ``` ### Coverage @@ -118,29 +49,33 @@ $ forge build Get a test coverage report: ```sh -$ forge coverage +forge coverage ``` ### Deploy -Deploy to Anvil: +Deployments and upgrades can through the `SwapFactory` contract for indexing purposes on the Tenderize Subgraph. + +TenderSwap can also be deployed standlone following the same pattern as the `SwapFactory` contract. + +### Format + +Format the contracts: ```sh -$ forge script script/Deploy.s.sol --broadcast --fork-url http://localhost:8545 +forge fmt ``` -For this script to work, you need to have a `MNEMONIC` environment variable set to a valid -[BIP39 mnemonic](https://iancoleman.io/bip39/). - -For instructions on how to deploy to a testnet or mainnet, check out the -[Solidity Scripting](https://book.getfoundry.sh/tutorials/solidity-scripting.html) tutorial. +or -### Format +```sh +yarn lint:sol +``` -Format the contracts: +Format other files: ```sh -$ forge fmt +yarn prettier:write ``` ### Gas Usage @@ -148,7 +83,7 @@ $ forge fmt Get a gas report: ```sh -$ forge test --gas-report +forge test --gas-report ``` ### Lint @@ -156,7 +91,7 @@ $ forge test --gas-report Lint the contracts: ```sh -$ pnpm lint +pnpm lint ``` ### Test @@ -164,7 +99,7 @@ $ pnpm lint Run the tests: ```sh -$ forge test +forge test ``` ## Notes diff --git a/foundry.toml b/foundry.toml index 224002f..eef7765 100644 --- a/foundry.toml +++ b/foundry.toml @@ -2,13 +2,13 @@ [profile.default] bytecode_hash = "none" -fuzz = { runs = 1_000 } +fuzz = { runs = 10_000 } gas_reports = ["*"] libs = ["lib"] # optimizer = true (default) optimizer_runs = 200 fs_permissions = [{ access = "read-write", path = "./" }] -solc = "0.8.19" +solc = "0.8.20" [profile.ci] verbosity = 4 diff --git a/lib/openzeppelin-contracts b/lib/openzeppelin-contracts new file mode 160000 index 0000000..dbb6104 --- /dev/null +++ b/lib/openzeppelin-contracts @@ -0,0 +1 @@ +Subproject commit dbb6104ce834628e473d2173bbc9d47f81a9eec3 diff --git a/package.json b/package.json index 611460c..3e6898a 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "scripts": { "clean": "rm -rf cache out", "lint": "yarn lint:sol && yarn prettier:write", - "lint:sol": "yarn solhint {src,test}/**/*.sol", + "lint:sol": "forge fmt && yarn solhint src/**/*.sol", "prettier:check": "prettier --check **/*.{json,md,yml} --ignore-path=.prettierignore", "prettier:write": "prettier --write **/*.{json,md,yml} --ignore-path=.prettierignore" } diff --git a/remappings.txt b/remappings.txt index 08677db..54900da 100644 --- a/remappings.txt +++ b/remappings.txt @@ -7,4 +7,4 @@ solmate/=lib/solmate/src/ @tenderize/stake/=lib/staking/src/ test/=test/ openzeppelin-contracts-upgradeable/=lib/staking/lib/openzeppelin-contracts-upgradeable/contracts/ -openzeppelin-contracts/=lib/staking/lib/openzeppelin-contracts/contracts/ \ No newline at end of file +openzeppelin-contracts/=lib/openzeppelin-contracts/contracts/ \ No newline at end of file diff --git a/script/Add_Liquidity.s.sol b/script/Add_Liquidity.s.sol index 9030e19..adebae7 100644 --- a/script/Add_Liquidity.s.sol +++ b/script/Add_Liquidity.s.sol @@ -3,7 +3,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 { TenderSwap, ConstructorConfig } from "@tenderize/swap/Swap.sol"; contract Add_Liquidity is Script { // Contracts are deployed deterministically. diff --git a/script/Factory_Deploy.s.sol b/script/Factory_Deploy.s.sol new file mode 100644 index 0000000..ff3ed15 --- /dev/null +++ b/script/Factory_Deploy.s.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +import { Script, console2 } from "forge-std/Script.sol"; +import { SwapFactory } from "@tenderize/swap/Factory.sol"; +import { ERC1967Proxy } from "openzeppelin-contracts/proxy/ERC1967/ERC1967Proxy.sol"; + +address constant FACTORY = address(0); + +uint256 constant VERSION = 1; + +contract Swap_Deploy is Script { + // Contracts are deployed deterministically. + // e.g. `foo = new Foo{salt: salt}(constructorArgs)` + // The presence of the salt argument tells forge to use https://github.com/Arachnid/deterministic-deployment-proxy + bytes32 constant SALT = bytes32(VERSION); + + // Start broadcasting with private key from `.env` file + uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + + function run() public { + vm.startBroadcast(deployerPrivateKey); + address fac = address(new SwapFactory{ salt: SALT }()); + address proxy = address(new ERC1967Proxy{ salt: SALT }(fac, abi.encodeWithSelector(SwapFactory.initialize.selector))); + console2.log("SwapFactory deployed at: ", proxy); + console2.log("Implementation deployed at: ", fac); + vm.stopBroadcast(); + } +} diff --git a/script/Stats.s.sol b/script/Stats.s.sol index fe6f0e4..9362763 100644 --- a/script/Stats.s.sol +++ b/script/Stats.s.sol @@ -3,7 +3,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 { TenderSwap, ConstructorConfig } from "@tenderize/swap/Swap.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"; diff --git a/script/Swap_Deploy.s.sol b/script/Swap_Deploy.s.sol index fc6f81f..10171f1 100644 --- a/script/Swap_Deploy.s.sol +++ b/script/Swap_Deploy.s.sol @@ -3,7 +3,12 @@ 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 { TenderSwap, ConstructorConfig } from "@tenderize/swap/Swap.sol"; +import { SwapFactory } from "@tenderize/swap/Factory.sol"; +import { SD59x18 } from "@prb/math/SD59x18.sol"; +import { ERC1967Proxy } from "openzeppelin-contracts/proxy/ERC1967/ERC1967Proxy.sol"; + +address constant FACTORY = address(0); contract Swap_Deploy is Script { // Contracts are deployed deterministically. @@ -14,14 +19,17 @@ contract Swap_Deploy is Script { // Start broadcasting with private key from `.env` file uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); address underlying = vm.envAddress("UNDERLYING"); - address registry = vm.envAddress("REGISTRY"); - address unlocks = vm.envAddress("UNLOCKS"); - Config cfg = Config({ underlying: ERC20(underlying), registry: registry, unlocks: unlocks }); + SD59x18 BASE_FEE = SD59x18.wrap(vm.envInt("BASE_FEE")); + SD59x18 K = SD59x18.wrap(vm.envInt("K")); + + ConstructorConfig cfg = ConstructorConfig({ UNDERLYING: ERC20(underlying), BASE_FEE: BASE_FEE, K: K }); function run() public { vm.startBroadcast(deployerPrivateKey); - TenderSwap swap = new TenderSwap{salt: salt}(cfg); - console2.log("TenderSwap deployed at: ", address(swap)); + (address proxy, address implementation) = SwapFactory(FACTORY).deploy(cfg); + console2.log("Deployment for ", underlying); + console2.log("TenderSwap deployed at: ", proxy); + console2.log("Implementation deployed at: ", implementation); vm.stopBroadcast(); } } diff --git a/src/Factory.sol b/src/Factory.sol new file mode 100644 index 0000000..be50192 --- /dev/null +++ b/src/Factory.sol @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: MIT +// +// _____ _ _ +// |_ _| | | (_) +// | | ___ _ __ __| | ___ _ __ _ _______ +// | |/ _ \ '_ \ / _` |/ _ \ '__| |_ / _ \ +// | | __/ | | | (_| | __/ | | |/ / __/ +// \_/\___|_| |_|\__,_|\___|_| |_/___\___| +// +// Copyright (c) Tenderize Labs Ltd + +pragma solidity ^0.8.20; + +import { Owned } from "solmate/auth/Owned.sol"; +import { ERC1967Proxy } from "openzeppelin-contracts/proxy/ERC1967/ERC1967Proxy.sol"; +import { TenderSwap, ConstructorConfig } from "@tenderize/swap/Swap.sol"; + +import { OwnableUpgradeable } from "openzeppelin-contracts-upgradeable/access/OwnableUpgradeable.sol"; +import { Initializable } from "openzeppelin-contracts-upgradeable/proxy/utils/Initializable.sol"; +import { UUPSUpgradeable } from "openzeppelin-contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; + +// Used for subgraph indexing and atomic deployments + +contract SwapFactory is Initializable, UUPSUpgradeable, OwnableUpgradeable { + event SwapDeployed(address underlying, address swap, address implementation); + event SwapUpgraded(address underlying, address swap, address implementation); + + mapping(address pool => uint256 v) public version; + + function initialize() public initializer { + __Ownable_init(); + __UUPSUpgradeable_init(); + } + + constructor() { + _disableInitializers(); + } + + function deploy(ConstructorConfig memory cfg) external onlyOwner returns (address proxy, address implementation) { + uint256 v = 1; + // Deploy the implementation + implementation = address(new TenderSwap{ salt: bytes32(v) }(cfg)); + // deploy the contract + proxy = address( + new ERC1967Proxy{ salt: bytes32("tenderswap") }(implementation, abi.encodeWithSelector(TenderSwap.initialize.selector)) + ); + + TenderSwap(proxy).transferOwnership(owner()); + version[proxy] = v; + emit SwapDeployed(address(cfg.UNDERLYING), proxy, implementation); + } + + function upgrade(ConstructorConfig memory cfg, address swapProxy) external onlyOwner returns (address implementation) { + if (TenderSwap(swapProxy).UNDERLYING() != cfg.UNDERLYING) { + revert("SwapFactory: UNDERLYING_MISMATCH"); + } + + uint256 v = ++version[swapProxy]; + + implementation = address(new TenderSwap{ salt: bytes32(v) }(cfg)); + + TenderSwap(swapProxy).upgradeTo(implementation); + } + + ///@dev required by the OZ UUPS module + // solhint-disable-next-line no-empty-blocks + function _authorizeUpgrade(address) internal override onlyOwner { } +} diff --git a/src/LPToken.sol b/src/LPToken.sol index c494ab7..235f17a 100644 --- a/src/LPToken.sol +++ b/src/LPToken.sol @@ -41,6 +41,6 @@ contract LPToken is ERC20 { } function _encodeSymbol(string memory _symbol) internal pure returns (string memory) { - return string.concat("tSWAP", " ", _symbol); + return string.concat("ts", _symbol); } } diff --git a/src/Swap.sol b/src/Swap.sol index 0e64266..6266d73 100644 --- a/src/Swap.sol +++ b/src/Swap.sol @@ -31,7 +31,11 @@ import { ERC721Receiver } from "@tenderize/swap/util/ERC721Receiver.sol"; import { LPToken } from "@tenderize/swap/LPToken.sol"; import { UnlockQueue } from "@tenderize/swap/UnlockQueue.sol"; -pragma solidity 0.8.19; +pragma solidity 0.8.20; + +Registry constant REGISTRY = Registry(0xa7cA8732Be369CaEaE8C230537Fc8EF82a3387EE); +ERC721 constant UNLOCKS = ERC721(0xb98c7e67f63d198BD96574073AD5B3427a835796); +address constant TREASURY = 0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419; error ErrorNotMature(uint256 maturity, uint256 timestamp); error ErrorAlreadyMature(uint256 maturity, uint256 timestamp); @@ -40,18 +44,11 @@ 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 = 12 hours; - -struct Config { - ERC20 underlying; - address registry; - address unlocks; + +struct ConstructorConfig { + ERC20 UNDERLYING; + SD59x18 BASE_FEE; + SD59x18 K; } struct SwapParams { @@ -61,33 +58,29 @@ struct SwapParams { SD59x18 S; } -struct LastDeposit { - uint192 amount; - uint64 timestamp; -} - abstract contract SwapStorage { uint256 private constant SSLOT = uint256(keccak256("xyz.tenderize.swap.storage.location")) - 1; struct Data { + LPToken lpToken; // total amount unlocking uint256 unlocking; // total amount of liabilities owed to LPs uint256 liabilities; // sum of token supplies that have outstanding unlocks SD59x18 S; - // Unlock queue to hold unlocks - UnlockQueue.Data unlockQ; // Recovery amount, if `recovery` > 0 enable recovery mode uint256 recovery; + // treasury share of rewards pending withdrawal + uint256 treasuryRewards; + // Unlock queue to hold unlocks + UnlockQueue.Data unlockQ; // amount unlocking per asset mapping(address asset => uint256 unlocking) unlockingForAsset; // last supply of a tenderizer when seen, tracked because they are rebasing tokens mapping(address asset => SD59x18 lastSupply) lastSupplyForAsset; // relayer fees mapping(address relayer => uint256 reward) relayerRewards; - // last deposits (used to check cooldown) - mapping(address => LastDeposit) lastDeposit; } function _loadStorageSlot() internal pure returns (Data storage $) { @@ -107,34 +100,40 @@ contract TenderSwap is Initializable, UUPSUpgradeable, OwnableUpgradeable, SwapS event Deposit(address indexed from, uint256 amount, uint256 lpSharesMinted); event Withdraw(address indexed to, uint256 amount, uint256 lpSharesBurnt); - event Swap(address indexed caller, address indexed asset, uint256 amountIn, uint256 amountOut); + event Swap(address indexed caller, address indexed asset, uint256 amountIn, uint256 fee, uint256 unlockId); event UnlockBought(address indexed caller, uint256 tokenId, uint256 amount, uint256 reward, uint256 lpFees); event UnlockRedeemed(address indexed relayer, uint256 tokenId, uint256 amount, uint256 reward, uint256 lpFees); event RelayerRewardsClaimed(address indexed relayer, uint256 rewards); - LPToken public immutable lpToken; - ERC20 private immutable underlying; - address private immutable registry; - address private immutable unlocks; + ERC20 public immutable UNDERLYING; + SD59x18 public immutable BASE_FEE; + SD59x18 public immutable K; + + // Minimum cut of the fee for LPs when an unlock is bought + UD60x18 public constant MIN_LP_CUT = UD60x18.wrap(0.05e18); + // Cut of the fee for the treasury when an unlock is bought or redeemed + UD60x18 public constant TREASURY_CUT = UD60x18.wrap(0.01e18); + // Cut of the fee for the relayer when an unlock is redeemed + UD60x18 public constant RELAYER_CUT = UD60x18.wrap(0.01e18); - function intialize() public initializer { + function initialize() public initializer { + Data storage $ = _loadStorageSlot(); + $.lpToken = new LPToken(UNDERLYING.name(), UNDERLYING.symbol()); __Ownable_init(); __UUPSUpgradeable_init(); } /// @custom:oz-upgrades-unsafe-allow constructor - constructor(Config memory config) { - lpToken = new LPToken(config.underlying.name(), config.underlying.symbol()); - underlying = config.underlying; - registry = config.registry; - unlocks = config.unlocks; + constructor(ConstructorConfig memory config) { + UNDERLYING = config.UNDERLYING; + BASE_FEE = config.BASE_FEE; + K = config.K; _disableInitializers(); } - modifier supplyUpdateHook(address asset) { + function lpToken() public view returns (ERC20) { Data storage $ = _loadStorageSlot(); - // _supplyUpdateHook(asset); - _; + return ERC20($.lpToken); } /** @@ -194,23 +193,8 @@ contract TenderSwap is Initializable, UUPSUpgradeable, OwnableUpgradeable, SwapS function deposit(uint256 amount, uint256 minLpShares) external returns (uint256 lpShares) { Data storage $ = _loadStorageSlot(); - // if there is an existing deposit cooldown we want to do a linear regression of the current amount and remaining time - LastDeposit storage ld = $.lastDeposit[msg.sender]; - if (ld.timestamp > 0) { - uint256 timePassed = block.timestamp - ld.timestamp; - if (timePassed < COOLDOWN) { - uint256 remaining = COOLDOWN - timePassed; - uint256 newAmount = FixedPointMathLib.mulDivUp(ld.amount, remaining, COOLDOWN); - ld.amount += SafeCastLib.safeCastTo192(newAmount); - ld.timestamp = uint64(block.timestamp); - } - } else { - ld.timestamp = uint64(block.timestamp); - ld.amount = SafeCastLib.safeCastTo192(amount); - } - // Transfer tokens to the pool - underlying.safeTransferFrom(msg.sender, address(this), amount); + UNDERLYING.safeTransferFrom(msg.sender, address(this), amount); // Calculate LP tokens to mint lpShares = _calculateLpShares(amount); @@ -220,7 +204,7 @@ contract TenderSwap is Initializable, UUPSUpgradeable, OwnableUpgradeable, SwapS $.liabilities += amount; // Mint LP tokens to the caller - lpToken.mint(msg.sender, lpShares); + $.lpToken.mint(msg.sender, lpShares); emit Deposit(msg.sender, amount, lpShares); } @@ -240,34 +224,18 @@ contract TenderSwap is Initializable, UUPSUpgradeable, OwnableUpgradeable, SwapS 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) { - uint256 timePassed = block.timestamp - ld.timestamp; - if (timePassed < COOLDOWN) { - uint256 remaining = COOLDOWN - timePassed; - uint256 cdAmount = FixedPointMathLib.mulDivUp(ld.amount, remaining, COOLDOWN); - uint256 cdLpShares = _calculateLpShares(cdAmount); - availableLpShares -= cdLpShares; - } - } - // Calculate LP tokens to burn uint256 lpShares = _calculateLpShares(amount); - if (lpShares > availableLpShares) revert ErrorWithdrawCooldown(lpShares, availableLpShares); if (lpShares > maxLpSharesBurnt) revert ErrorSlippage(lpShares, maxLpSharesBurnt); // Update liabilities $.liabilities -= amount; // Burn LP tokens from the caller - lpToken.burn(msg.sender, lpShares); + $.lpToken.burn(msg.sender, lpShares); // Transfer tokens to caller - underlying.safeTransfer(msg.sender, amount); + UNDERLYING.safeTransfer(msg.sender, amount); emit Withdraw(msg.sender, amount, lpShares); } @@ -283,11 +251,21 @@ contract TenderSwap is Initializable, UUPSUpgradeable, OwnableUpgradeable, SwapS delete $.relayerRewards[msg.sender]; - underlying.safeTransfer(msg.sender, relayerReward); + UNDERLYING.safeTransfer(msg.sender, relayerReward); emit RelayerRewardsClaimed(msg.sender, relayerReward); } + function claimTreasuryRewards() public onlyOwner returns (uint256 treasuryReward) { + Data storage $ = _loadStorageSlot(); + + treasuryReward = $.treasuryRewards; + + $.treasuryRewards = 0; + + UNDERLYING.safeTransfer(TREASURY, treasuryReward); + } + /** * @notice Check outstanding rewards for a relayer. * @param relayer Address of the relayer @@ -358,12 +336,12 @@ contract TenderSwap is Initializable, UUPSUpgradeable, OwnableUpgradeable, SwapS ERC20(asset).safeTransferFrom(msg.sender, address(this), amount); // Handle Unlocking of assets - _unlock(asset, amount, fee); + uint256 id = _unlock(asset, amount, fee); // Transfer `out` of `to` to msg.sender - underlying.safeTransfer(msg.sender, out); + UNDERLYING.safeTransfer(msg.sender, out); - emit Swap(msg.sender, asset, amount, out); + emit Swap(msg.sender, asset, amount, fee, id); } /** @@ -393,20 +371,32 @@ contract TenderSwap is Initializable, UUPSUpgradeable, OwnableUpgradeable, SwapS 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 + // The base reward is the fee minus the MIN_LP_CUT going to liquidity providers and minus the TREASURY_CUT going to the + // treasury // The base reward then further decays as time to maturity decreases uint256 reward; + uint256 lpCut; + uint256 treasuryCut; { - UD60x18 progress = ud(unlock.maturity - time).div(ud(adapter.unlockTime())); UD60x18 fee60x18 = ud(unlock.fee); - reward = fee60x18.sub(fee60x18.mul(MIN_LP_CUT)).mul(UNIT_60x18.sub(progress)).unwrap(); + lpCut = fee60x18.mul(MIN_LP_CUT).unwrap(); + treasuryCut = fee60x18.mul(TREASURY_CUT).unwrap(); + uint256 baseReward = unlock.fee - lpCut - treasuryCut; + UD60x18 progress = ud(unlock.maturity - time).div(ud(adapter.unlockTime())); + reward = ud(baseReward).mul(UNIT_60x18.sub(progress)).unwrap(); + // Adjust lpCut by the remaining amount after subtracting the reward + // This step seems to adjust lpCut to balance out the distribution + // Assuming the final lpCut should encompass any unallocated fee portions + lpCut += baseReward - reward; } // Update pool state // - update unlocking $.unlocking -= unlock.amount; // - Update liabilities to distribute LP rewards - $.liabilities += unlock.fee - reward; + $.liabilities += lpCut; + // - Update treasury rewards + $.treasuryRewards += treasuryCut; uint256 ufa = $.unlockingForAsset[tenderizer] - unlock.amount; // - Update S if unlockingForAsset is now zero @@ -418,12 +408,13 @@ contract TenderSwap is Initializable, UUPSUpgradeable, OwnableUpgradeable, SwapS $.unlockingForAsset[tenderizer] = ufa; // transfer unlock amount minus reward from caller to pool - underlying.safeTransferFrom(msg.sender, address(this), unlock.amount - reward); + // the reward is the discount paid. 'reward < unlock.fee' always. + UNDERLYING.safeTransferFrom(msg.sender, address(this), unlock.amount - reward); // transfer unlock to caller - ERC721(unlocks).safeTransferFrom(address(this), msg.sender, tokenId); + UNLOCKS.safeTransferFrom(address(this), msg.sender, tokenId); - emit UnlockBought(msg.sender, tokenId, unlock.amount, reward, unlock.fee - reward); + emit UnlockBought(msg.sender, tokenId, unlock.amount, reward, lpCut); } /** @@ -443,12 +434,7 @@ contract TenderSwap is Initializable, UUPSUpgradeable, OwnableUpgradeable, SwapS // this will revert if unlock is not at maturity uint256 amountReceived = Tenderizer(tenderizer).withdraw(address(this), id); - //calculate the relayer reward - uint256 relayerReward = ud(unlock.fee).mul(RELAYER_CUT).unwrap(); - // update relayer rewards - $.relayerRewards[msg.sender] += relayerReward; - - uint256 fee = unlock.fee - relayerReward; + uint256 fee = unlock.fee; { uint256 recovery = $.recovery; @@ -500,10 +486,21 @@ contract TenderSwap is Initializable, UUPSUpgradeable, OwnableUpgradeable, SwapS // - Update unlockingForAsset $.unlockingForAsset[tenderizer] = ufa; + //calculate the relayer reward + uint256 relayerReward = ud(unlock.fee).mul(RELAYER_CUT).unwrap(); + // update relayer rewards + $.relayerRewards[msg.sender] += relayerReward; + // - Update liabilities to distribute LP rewards - $.liabilities += fee; + uint256 lpReward; + if (fee > 0) { + uint256 treasuryCut = ud(fee).mul(TREASURY_CUT).unwrap(); + $.treasuryRewards += treasuryCut; + lpReward = fee - treasuryCut - relayerReward; + $.liabilities += lpReward; + } - emit UnlockRedeemed(msg.sender, unlock.id, amountReceived, relayerReward, fee); + emit UnlockRedeemed(msg.sender, unlock.id, amountReceived, relayerReward, lpReward); } function _quote(uint256 amount, SwapParams memory p) internal view returns (uint256 out, uint256 fee) { @@ -536,17 +533,17 @@ contract TenderSwap is Initializable, UUPSUpgradeable, OwnableUpgradeable, SwapS // (((u + x)*k - U + u)*((U + x)/L)**k + (-k*u + U - u)*(U/L)**k)*(S + U)/(k*(1 + k)*(s + u)) /** - * @notice checks if an asset is a valid tenderizer for `underlying` + * @notice checks if an asset is a valid tenderizer for `UNDERLYING` */ function _isValidAsset(address asset) internal view returns (bool) { - return Registry(registry).isTenderizer(asset) && Tenderizer(asset).asset() == address(underlying); + return REGISTRY.isTenderizer(asset) && Tenderizer(asset).asset() == address(UNDERLYING); } - function _utilisation(uint256 unlocking, uint256 liabilities) internal pure returns (UD60x18 r) { - r = ud(unlocking).div(ud(liabilities)); + function _utilisation(uint256 U, uint256 L) internal pure returns (UD60x18 r) { + r = ud(U).div(ud(L)); } - function _unlock(address asset, uint256 amount, uint256 fee) internal { + function _unlock(address asset, uint256 amount, uint256 fee) internal returns (uint256) { Data storage $ = _loadStorageSlot(); Tenderizer t = Tenderizer(asset); @@ -565,6 +562,8 @@ contract TenderSwap is Initializable, UUPSUpgradeable, OwnableUpgradeable, SwapS maturity: maturity }) ); + + return key; } /** @@ -592,14 +591,14 @@ contract TenderSwap is Initializable, UUPSUpgradeable, OwnableUpgradeable, SwapS function _calculateLpShares(uint256 amount) internal view returns (uint256 shares) { Data storage $ = _loadStorageSlot(); - uint256 supply = lpToken.totalSupply(); - uint256 liabilities = $.liabilities; + uint256 supply = $.lpToken.totalSupply(); + uint256 L = $.liabilities; - if (liabilities == 0) { + if (L == 0) { return amount * 1e18; } - shares = amount * (supply / liabilities); // calculate factor first since it's scaled up + shares = amount * (supply / L); // calculate factor first since it's scaled up if (shares == 0) { revert ErrorCalculateLPShares(); } diff --git a/test/Swap.harness.sol b/test/Swap.harness.sol index e769301..263657a 100644 --- a/test/Swap.harness.sol +++ b/test/Swap.harness.sol @@ -11,13 +11,13 @@ pragma solidity >=0.8.19; -import { TenderSwap, Config } from "@tenderize/swap/Swap.sol"; +import { TenderSwap, ConstructorConfig } from "@tenderize/swap/Swap.sol"; import { UnlockQueue } from "@tenderize/swap/UnlockQueue.sol"; // solhint-disable func-name-mixedcase contract SwapHarness is TenderSwap { - constructor(Config memory config) TenderSwap(config) { } + constructor(ConstructorConfig memory config) TenderSwap(config) { } function exposed_setLiabilities(uint256 _liabilities) public { Data storage $ = _loadStorageSlot(); diff --git a/test/Swap.t.sol b/test/Swap.t.sol index 42078f1..96dea8e 100644 --- a/test/Swap.t.sol +++ b/test/Swap.t.sol @@ -14,22 +14,13 @@ pragma solidity >=0.8.19; import { Test, console } from "forge-std/Test.sol"; import { ERC721 } from "solmate/tokens/ERC721.sol"; import { MockERC20 } from "test/helpers/MockERC20.sol"; +import { ERC1967Proxy } from "openzeppelin-contracts/proxy/ERC1967/ERC1967Proxy.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"; -import { - TenderSwap, - Config, - BASE_FEE, - RELAYER_CUT, - MIN_LP_CUT, - _encodeTokenId, - _decodeTokenId, - COOLDOWN, - ErrorWithdrawCooldown -} from "@tenderize/swap/Swap.sol"; +import { TenderSwap, ConstructorConfig, _encodeTokenId, _decodeTokenId } from "@tenderize/swap/Swap.sol"; import { LPToken } from "@tenderize/swap/LPToken.sol"; import { SD59x18, ZERO, UNIT, unwrap, sd } from "@prb/math/SD59x18.sol"; @@ -49,6 +40,7 @@ contract TenderSwapTest is Test { address registry; address unlocks; + address treasury; address adapter; address addr1; @@ -56,13 +48,18 @@ contract TenderSwapTest is Test { event RelayerRewardsClaimed(address indexed relayer, uint256 rewards); + Registry private constant REGISTRY = Registry(0xa7cA8732Be369CaEaE8C230537Fc8EF82a3387EE); + ERC721 private constant UNLOCKS = ERC721(0xb98c7e67f63d198BD96574073AD5B3427a835796); + address private constant TREASURY = 0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419; + function setUp() public { underlying = new MockERC20("network.xyz", "XYZ", 18); tToken0 = new MockERC20("tXYZ_0x00", "tXYZ_0x00", 18); tToken1 = new MockERC20("tXYZ_0x01", "tXYZ_0x00", 18); - registry = vm.addr(123); - unlocks = vm.addr(567); + registry = address(REGISTRY); + unlocks = address(UNLOCKS); + treasury = address(TREASURY); adapter = vm.addr(789); addr1 = vm.addr(111); @@ -77,8 +74,11 @@ contract TenderSwapTest is Test { address(tToken1), abi.encodeWithSelector(TenderizerImmutableArgs.asset.selector), abi.encode(address(underlying)) ); - Config memory cfg = Config({ underlying: underlying, registry: registry, unlocks: unlocks }); + ConstructorConfig memory cfg = ConstructorConfig({ UNDERLYING: underlying, BASE_FEE: sd(0.0005e18), K: sd(3e18) }); swap = new SwapHarness(cfg); + address proxy = address(new ERC1967Proxy(address(swap), "")); + swap = SwapHarness(proxy); + swap.initialize(); } function testFuzz_deposits(uint256 x, uint256 y, uint256 l) public { @@ -110,50 +110,6 @@ contract TenderSwapTest is Test { 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); - - 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); - 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 { amount = 10 ether; swap.exposed_setRelayerRewards(amount, addr1); @@ -168,15 +124,6 @@ contract TenderSwapTest is Test { assertEq(underlying.balanceOf(addr1), amount, "addr1 balance"); } - // write end to end swap test with checking the queue - // make three swaps, check the queue state (check head and tail) - // buy up the last unlock and check all code paths - // * mock unlocks as ERC721 mock transfer - // process blocks and redeem the first unlock and check all code paths - // * mock Tenderizer.withdraw() - // check that queue is now only containing the second unlock - // * Mock Tenderizer.unlock() and Tenderizer.unlockMaturity() - function test_scenario_full() public { uint256 unlockTime = 100; tToken0.mint(address(this), 10_000 ether); @@ -256,18 +203,17 @@ contract TenderSwapTest is Test { // console.log("block num %s", block.number); uint256 liabilitiesBefore = swap.liabilities(); + // buy unlock 3 + assertEq(swap.buyUnlock(), _encodeTokenId(address(tToken0), 3), "bought id"); + UD60x18 unlockTimeUD = ud(unlockTime); { - // buy unlock 3 - assertEq(swap.buyUnlock(), _encodeTokenId(address(tToken0), 3), "bought id"); UD60x18 tailFee = ud(tail.fee); - UD60x18 baseReward = tailFee.sub(tailFee.mul(MIN_LP_CUT)); - UD60x18 timeLeft = ud(tail.maturity - currentTime); - UD60x18 unlockTimex18 = ud(unlockTime); - UD60x18 progress = timeLeft.div(unlockTimex18); - assertEq(swap.liabilities(), liabilitiesBefore + tailFee.sub(baseReward.mul(progress)).unwrap(), "liabilities"); - // sanity check that the LP cut is half of the baseReward plus the LP cut + UD60x18 treasuryCut = tailFee.mul(swap.TREASURY_CUT()); + UD60x18 reward = tailFee.sub(treasuryCut).sub(tailFee.mul(swap.MIN_LP_CUT())).mul( + UNIT_60x18.sub(ud(tail.maturity - currentTime).div(unlockTimeUD)) + ); assertEq( - swap.liabilities(), liabilitiesBefore + tailFee.sub(baseReward.div(ud(2e18))).unwrap(), "liabilities sanity check" + liabilitiesBefore + tailFee.sub(reward).sub(treasuryCut).unwrap(), swap.liabilities(), "liabilities after buyUnlock" ); } assertEq(swap.exposed_unlocking(), 20 ether + 10 ether, "unlocking"); @@ -282,8 +228,11 @@ contract TenderSwapTest is Test { vm.mockCall(address(tToken0), abi.encodeWithSelector(Tenderizer.withdraw.selector, address(swap), 1), abi.encode(10 ether)); liabilitiesBefore = swap.liabilities(); swap.redeemUnlock(); - assertEq(swap.liabilities(), liabilitiesBefore + ud(head.fee).sub(ud(head.fee).mul(RELAYER_CUT)).unwrap(), "liabilities"); - assertEq(swap.pendingRelayerRewards(address(this)), ud(head.fee).mul(RELAYER_CUT).unwrap(), "relayer rewards"); + UD60x18 headFee = ud(head.fee); + uint256 expLiabilities = + liabilitiesBefore + ud(head.fee).sub(headFee.mul(swap.TREASURY_CUT())).sub(headFee.mul(swap.RELAYER_CUT())).unwrap(); + assertEq(swap.liabilities(), expLiabilities, "liabilities after redeemUnlock"); + assertEq(swap.pendingRelayerRewards(address(this)), ud(head.fee).mul(swap.RELAYER_CUT()).unwrap(), "relayer rewards"); assertEq(swap.exposed_unlocking(), 20 ether, "unlocking"); // unlock 2 remains assertEq(swap.exposed_unlockingForAsset(address(tToken0)), 20 ether, "unlocking for asset"); // unlock 2 remains head = swap.oldestUnlock(); @@ -299,7 +248,6 @@ contract TenderSwapTest is Test { swap.deposit(liquidity, 0); uint256 amount = 10 ether; - uint256 tokenId = _encodeTokenId(address(tToken0), 0); vm.mockCall(address(tToken0), abi.encodeWithSelector(Tenderizer.unlock.selector, amount), abi.encode(0)); vm.mockCall(address(tToken0), abi.encodeWithSelector(Tenderizer.unlockMaturity.selector, 0), abi.encode(block.number + 100)); @@ -315,109 +263,65 @@ contract TenderSwapTest is Test { assertEq(swap.liquidity(), 90 ether, "TenderSwap available liquidity"); } - // function testFuzz_swap_other( - // uint256 liquidity, - // uint256 t0Supply, - // uint256 t1Supply, - // uint256 t0Amount, - // uint256 t1Amount - // ) - // public - // { - // vm.assume(liquidity >= 10 ether && liquidity <= type(uint128).max); - // t0Supply = bound(t0Supply, 1 ether, liquidity); - // t1Supply = bound(t1Supply, 1 ether, liquidity); - // t0Amount = bound(t0Amount, 1 ether / 5, t0Supply / 5); - // t1Amount = bound(t1Amount, 1 ether / 5, t1Supply / 5); - - // underlying.mint(address(this), liquidity); - // underlying.approve(address(swap), liquidity); - // swap.deposit(liquidity); - - // uint256 tokenId = _encodeTokenId(address(tToken0), 0); - - // vm.mockCall(address(tToken0), abi.encodeWithSelector(Tenderizer.unlock.selector, t0Amount), abi.encode(0)); - // vm.mockCall(address(tToken0), abi.encodeWithSelector(Tenderizer.unlockMaturity.selector, 0), abi.encode(block.number + - // 100)); - - // tToken0.mint(address(this), t0Amount); - // tToken1.mint(address(this), t1Amount); - // tToken0.approve(address(swap), t0Amount); - // (uint256 out, uint256 fee) = swap.swap(address(tToken0), t0Amount, 0 ether); - - // (out, fee) = swap.quote(address(tToken1), t1Amount); - // console.log("swap quote 1", out, fee); - // // Fee should be 0.15% or 0.0015 - // // As utilisation after is 0.1 and 0.1^3 = 0.001 - // // Base fee is 0.005 so that makes 0.0015 - // // Since there is only 1 token drawing liquidity, its weight is 1 - - // // uint256 expFee = amount * 15 / 10_000; - - // // assertEq(fee, expFee, "swap fee"); - // // assertEq(out, amount - expFee, "swap out"); - // // assertEq(swap.liquidity(), 90 ether, "TenderSwap available liquidity"); - // } - - // function test_swap_other() public { - // uint256 liquidity = 2_000_000 ether; - // underlying.mint(address(this), liquidity); - // underlying.approve(address(swap), liquidity); - // swap.deposit(liquidity); - - // uint256 amount = 1 ether; - // uint256 tokenId = _encodeTokenId(address(tToken0), 0); - - // vm.mockCall(address(tToken0), abi.encodeWithSelector(Tenderizer.unlock.selector, amount), abi.encode(0)); - // vm.mockCall(address(tToken0), abi.encodeWithSelector(Tenderizer.unlockMaturity.selector, 0), abi.encode(block.number + - // 100)); - - // tToken0.mint(address(this), 34_000 ether); - // tToken1.mint(address(this), 14_000 ether); - // tToken0.approve(address(swap), 1500 ether); - // (uint256 out, uint256 fee) = swap.swap(address(tToken0), amount, 0 ether); - - // (out, fee) = swap.quote(address(tToken1), 50 ether); - // console.log("swap quote 1", out, fee); - // // Fee should be 0.15% or 0.0015 - // // As utilisation after is 0.1 and 0.1^3 = 0.001 - // // Base fee is 0.005 so that makes 0.0015 - // // Since there is only 1 token drawing liquidity, its weight is 1 - // uint256 expFee = amount * 15 / 10_000; - - // // assertEq(fee, expFee, "swap fee"); - // // assertEq(out, amount - expFee, "swap out"); - // // assertEq(swap.liquidity(), 90 ether, "TenderSwap available liquidity"); - // } - - // // function testFuzz_swap_basic(uint256 liquidity, uint256 amount) public { - // // liquidity = bound(liquidity, 1e18, type(uint128).max); - // // amount = bound(amount, 1e3, liquidity); - - // // underlying.mint(address(this), liquidity); - // // underlying.approve(address(swap), liquidity); - // // swap.deposit(liquidity); - - // // vm.mockCall(address(tToken0), abi.encodeWithSelector(Tenderizer.unlock.selector, amount), abi.encode(0)); - // // vm.mockCall(address(tToken0), abi.encodeWithSelector(Tenderizer.unlockMaturity.selector, 0), abi.encode(block.number + - // // 100)); - - // // tToken0.mint(address(this), liquidity); - // // tToken0.approve(address(swap), amount); - // // (uint256 out, uint256 fee) = swap.swap(address(tToken0), amount, 0); - - // // uint256 expFee = uint256( - // // sd(int256(amount)).mul(BASE_FEE).add( - // // sd(int256(amount)).mul((sd(int256(amount)).div(sd(int256(liquidity))).pow(sd(3e18)))) - // // ).unwrap() - // // ); - // // expFee = expFee >= amount ? amount : expFee; - - // // console.log("expFee", expFee); - // // console.log("fee", fee); - - // // assertTrue(acceptableDelta(fee, expFee, 2), "fee amount"); - // // assertTrue(acceptableDelta(out, amount - expFee, 2), "swap out"); - // // assertEq(swap.liquidity(), liquidity - amount, "TenderSwap available liquidity"); - // // } + function testFuzz_swap(uint256 liquidity) public { + liquidity = bound(liquidity, 1e18, type(uint128).max); + underlying.mint(address(this), liquidity); + underlying.approve(address(swap), liquidity); + swap.deposit(liquidity, 0); + + uint256 amount = bound(liquidity, 1e9, liquidity); + + vm.mockCall(address(tToken0), abi.encodeWithSelector(Tenderizer.unlock.selector, amount), abi.encode(0)); + vm.mockCall(address(tToken0), abi.encodeWithSelector(Tenderizer.unlockMaturity.selector, 0), abi.encode(block.number + 100)); + + tToken0.mint(address(this), amount); + tToken0.approve(address(swap), amount); + (uint256 out, uint256 fee) = swap.swap(address(tToken0), amount, 0); + console.log("out %s", out); + console.log("fee %s", fee); + // just assert the call doesnt fail for now + } + + function testFuzz_swap_multiple(uint256 liquidity) public { + liquidity = bound(liquidity, 10e18, type(uint128).max); + underlying.mint(address(this), liquidity); + underlying.approve(address(swap), liquidity); + swap.deposit(liquidity, 0); + + uint256 amount_1 = bound(liquidity, 10e9, liquidity / 2); + vm.mockCall(address(tToken0), abi.encodeWithSelector(Tenderizer.unlock.selector, amount_1), abi.encode(0)); + vm.mockCall(address(tToken0), abi.encodeWithSelector(Tenderizer.unlockMaturity.selector, 0), abi.encode(block.number + 100)); + tToken0.mint(address(this), amount_1); + tToken0.approve(address(swap), amount_1); + (uint256 out, uint256 fee) = swap.swap(address(tToken0), amount_1, 0); + console.log("amount_1"); + console.log("out %s", out); + console.log("fee %s", fee); + console.log("============="); + assertTrue(fee <= out); + + uint256 amount_2 = bound(liquidity, 10e9, (liquidity - amount_1) / 2); + vm.mockCall(address(tToken0), abi.encodeWithSelector(Tenderizer.unlock.selector, amount_2), abi.encode(1)); + vm.mockCall(address(tToken0), abi.encodeWithSelector(Tenderizer.unlockMaturity.selector, 1), abi.encode(block.number + 101)); + tToken0.mint(address(this), amount_2); + tToken0.approve(address(swap), amount_2); + (out, fee) = swap.swap(address(tToken0), amount_2, 0); + console.log("amount_2"); + console.log("out %s", out); + console.log("fee %s", fee); + console.log("============="); + assertTrue(fee <= out); + + uint256 amount_3 = bound(liquidity, 1e9, liquidity - amount_1 - amount_2); + vm.mockCall(address(tToken0), abi.encodeWithSelector(Tenderizer.unlock.selector, amount_3), abi.encode(2)); + vm.mockCall(address(tToken0), abi.encodeWithSelector(Tenderizer.unlockMaturity.selector, 2), abi.encode(block.number + 102)); + tToken0.mint(address(this), amount_3); + tToken0.approve(address(swap), amount_3); + (out, fee) = swap.swap(address(tToken0), amount_3, 0); + console.log("amount_3"); + console.log("out %s", out); + console.log("fee %s", fee); + console.log("============="); + assertTrue(fee <= out); + } }