From 139f66690c0a618c7f56ac19f9d115586f6dab38 Mon Sep 17 00:00:00 2001 From: kyriediculous Date: Fri, 12 Apr 2024 15:40:09 +0200 Subject: [PATCH] feat: add more fuzz tests, add deployment scripts, improve factory --- README.md | 117 ++++++++---------------------------- foundry.toml | 2 +- package.json | 2 +- script/Factory_Deploy.s.sol | 27 +++++++++ script/Swap_Deploy.s.sol | 15 +++-- src/Factory.sol | 33 +++++++--- src/Swap.sol | 10 +-- test/Swap.t.sol | 74 +++++++++++++++++++---- 8 files changed, 156 insertions(+), 124 deletions(-) create mode 100644 script/Factory_Deploy.s.sol 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 73c404e..eef7765 100644 --- a/foundry.toml +++ b/foundry.toml @@ -2,7 +2,7 @@ [profile.default] bytecode_hash = "none" -fuzz = { runs = 1_000 } +fuzz = { runs = 10_000 } gas_reports = ["*"] libs = ["lib"] # optimizer = true (default) 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/script/Factory_Deploy.s.sol b/script/Factory_Deploy.s.sol new file mode 100644 index 0000000..2bfedcb --- /dev/null +++ b/script/Factory_Deploy.s.sol @@ -0,0 +1,27 @@ +// 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); + +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 private constant salt = 0x0; + + // 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()); + address proxy = address(new ERC1967Proxy(fac, abi.encodeWithSelector(SwapFactory.initialize.selector))); + console2.log("SwapFactory deployed at: ", proxy); + console2.log("Implementation deployed at: ", fac); + vm.stopBroadcast(); + } +} diff --git a/script/Swap_Deploy.s.sol b/script/Swap_Deploy.s.sol index 7c08761..10171f1 100644 --- a/script/Swap_Deploy.s.sol +++ b/script/Swap_Deploy.s.sol @@ -6,6 +6,7 @@ import { ERC20 } from "solmate/tokens/ERC20.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); @@ -18,15 +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"); - ConstructorConfig cfg = - ConstructorConfig({ UNDERLYING: ERC20(underlying), BASE_FEE: SD59x18.wrap(0.0005e18), K: SD59x18.wrap(3e18) }); + 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 index ed45079..e9ff489 100644 --- a/src/Factory.sol +++ b/src/Factory.sol @@ -15,32 +15,47 @@ 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 Owned { +contract SwapFactory is Initializable, UUPSUpgradeable, OwnableUpgradeable { event SwapDeployed(address underlying, address swap, address implementation); event SwapUpgraded(address underlying, address swap, address implementation); - constructor(address _owner) Owned(_owner) { } + function initialize() public initializer { + __Ownable_init(); + __UUPSUpgradeable_init(); + } - function deploy(ConstructorConfig memory cfg) external onlyOwner { + constructor() { + _disableInitializers(); + } + + function deploy(ConstructorConfig memory cfg) external onlyOwner returns (address proxy, address implementation) { // Deploy the implementation - address implementation = address(new TenderSwap(cfg)); + implementation = address(new TenderSwap(cfg)); // deploy the contract - address instance = address(new ERC1967Proxy(implementation, "")); + proxy = address(new ERC1967Proxy(implementation, abi.encodeWithSelector(TenderSwap.initialize.selector))); - TenderSwap(instance).transferOwnership(owner); + TenderSwap(proxy).transferOwnership(owner()); - emit SwapDeployed(address(cfg.UNDERLYING), instance, implementation); + emit SwapDeployed(address(cfg.UNDERLYING), proxy, implementation); } - function upgrade(ConstructorConfig memory cfg, address swapProxy) external onlyOwner { + function upgrade(ConstructorConfig memory cfg, address swapProxy) external onlyOwner returns (address implementation) { if (TenderSwap(swapProxy).UNDERLYING() != cfg.UNDERLYING) { revert("SwapFactory: UNDERLYING_MISMATCH"); } - address implementation = address(new TenderSwap(cfg)); + implementation = address(new TenderSwap(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/Swap.sol b/src/Swap.sol index 89ab7af..6266d73 100644 --- a/src/Swap.sol +++ b/src/Swap.sol @@ -33,6 +33,10 @@ import { UnlockQueue } from "@tenderize/swap/UnlockQueue.sol"; 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); error ErrorInvalidAsset(address asset); @@ -112,11 +116,7 @@ contract TenderSwap is Initializable, UUPSUpgradeable, OwnableUpgradeable, SwapS // Cut of the fee for the relayer when an unlock is redeemed UD60x18 public constant RELAYER_CUT = UD60x18.wrap(0.01e18); - Registry private constant REGISTRY = Registry(0xa7cA8732Be369CaEaE8C230537Fc8EF82a3387EE); - ERC721 private constant UNLOCKS = ERC721(0xb98c7e67f63d198BD96574073AD5B3427a835796); - address private constant TREASURY = 0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419; - - function intialize() public initializer { + function initialize() public initializer { Data storage $ = _loadStorageSlot(); $.lpToken = new LPToken(UNDERLYING.name(), UNDERLYING.symbol()); __Ownable_init(); diff --git a/test/Swap.t.sol b/test/Swap.t.sol index d0a8bff..96dea8e 100644 --- a/test/Swap.t.sol +++ b/test/Swap.t.sol @@ -78,7 +78,7 @@ contract TenderSwapTest is Test { swap = new SwapHarness(cfg); address proxy = address(new ERC1967Proxy(address(swap), "")); swap = SwapHarness(proxy); - swap.intialize(); + swap.initialize(); } function testFuzz_deposits(uint256 x, uint256 y, uint256 l) public { @@ -124,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); @@ -257,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)); @@ -272,4 +262,66 @@ contract TenderSwapTest is Test { assertEq(out, amount - expFee, "swap out"); assertEq(swap.liquidity(), 90 ether, "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); + } }