From e22bb345a6717e68609dd25e8ac8cbc4e0a61055 Mon Sep 17 00:00:00 2001 From: UncleGrandpa925 Date: Mon, 11 Mar 2024 23:21:30 +0800 Subject: [PATCH] first commit --- .env.example | 11 ++ .github/FUNDING.yml | 2 + .github/scripts/rename.sh | 38 ++++++ .github/workflows/ci.yml | 92 +++++++++++++ .github/workflows/create.yml | 52 +++++++ .gitignore | 36 +++++ .gitpod.yml | 14 ++ .prettierignore | 17 +++ .prettierrc.yml | 7 + .solhint.json | 14 ++ LICENSE.md | 16 +++ README.md | 3 + bun.lockb | Bin 0 -> 51363 bytes foundry.toml | 53 +++++++ package.json | 40 ++++++ remappings.txt | 4 + script/Base.s.sol | 41 ++++++ script/Deploy.s.sol | 13 ++ src/Foo.sol | 8 ++ test/RouterSample.sol | 258 +++++++++++++++++++++++++++++++++++ 20 files changed, 719 insertions(+) create mode 100644 .env.example create mode 100644 .github/FUNDING.yml create mode 100755 .github/scripts/rename.sh create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/create.yml create mode 100644 .gitignore create mode 100644 .gitpod.yml create mode 100644 .prettierignore create mode 100644 .prettierrc.yml create mode 100644 .solhint.json create mode 100644 LICENSE.md create mode 100644 README.md create mode 100755 bun.lockb create mode 100644 foundry.toml create mode 100644 package.json create mode 100644 remappings.txt create mode 100644 script/Base.s.sol create mode 100644 script/Deploy.s.sol create mode 100644 src/Foo.sol create mode 100644 test/RouterSample.sol diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..98c1028 --- /dev/null +++ b/.env.example @@ -0,0 +1,11 @@ +export API_KEY_ALCHEMY="YOUR_API_KEY_ALCHEMY" +export API_KEY_ARBISCAN="YOUR_API_KEY_ARBISCAN" +export API_KEY_BSCSCAN="YOUR_API_KEY_BSCSCAN" +export API_KEY_ETHERSCAN="YOUR_API_KEY_ETHERSCAN" +export API_KEY_GNOSISSCAN="YOUR_API_KEY_GNOSISSCAN" +export API_KEY_INFURA="YOUR_API_KEY_INFURA" +export API_KEY_OPTIMISTIC_ETHERSCAN="YOUR_API_KEY_OPTIMISTIC_ETHERSCAN" +export API_KEY_POLYGONSCAN="YOUR_API_KEY_POLYGONSCAN" +export API_KEY_SNOWTRACE="YOUR_API_KEY_SNOWTRACE" +export MNEMONIC="YOUR_MNEMONIC" +export FOUNDRY_PROFILE="default" diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..b763d0f --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,2 @@ +custom: "https://omo.so/prberg" +github: "PaulRBerg" diff --git a/.github/scripts/rename.sh b/.github/scripts/rename.sh new file mode 100755 index 0000000..62e37dd --- /dev/null +++ b/.github/scripts/rename.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash + +# https://gist.github.com/vncsna/64825d5609c146e80de8b1fd623011ca +set -euo pipefail + +# Define the input vars +GITHUB_REPOSITORY=${1?Error: Please pass username/repo, e.g. prb/foundry-template} +GITHUB_REPOSITORY_OWNER=${2?Error: Please pass username, e.g. prb} +GITHUB_REPOSITORY_DESCRIPTION=${3:-""} # If null then replace with empty string + +echo "GITHUB_REPOSITORY: $GITHUB_REPOSITORY" +echo "GITHUB_REPOSITORY_OWNER: $GITHUB_REPOSITORY_OWNER" +echo "GITHUB_REPOSITORY_DESCRIPTION: $GITHUB_REPOSITORY_DESCRIPTION" + +# jq is like sed for JSON data +JQ_OUTPUT=`jq \ + --arg NAME "@$GITHUB_REPOSITORY" \ + --arg AUTHOR_NAME "$GITHUB_REPOSITORY_OWNER" \ + --arg URL "https://github.com/$GITHUB_REPOSITORY_OWNER" \ + --arg DESCRIPTION "$GITHUB_REPOSITORY_DESCRIPTION" \ + '.name = $NAME | .description = $DESCRIPTION | .author |= ( .name = $AUTHOR_NAME | .url = $URL )' \ + package.json +` + +# Overwrite package.json +echo "$JQ_OUTPUT" > package.json + +# Make sed command compatible in both Mac and Linux environments +# Reference: https://stackoverflow.com/a/38595160/8696958 +sedi () { + sed --version >/dev/null 2>&1 && sed -i -- "$@" || sed -i "" "$@" +} + +# Rename instances of "PaulRBerg/foundry-template" to the new repo name in README.md for badges only +sedi "/gitpod/ s|PaulRBerg/foundry-template|"${GITHUB_REPOSITORY}"|;" "README.md" +sedi "/gitpod-badge/ s|PaulRBerg/foundry-template|"${GITHUB_REPOSITORY}"|;" "README.md" +sedi "/gha/ s|PaulRBerg/foundry-template|"${GITHUB_REPOSITORY}"|;" "README.md" +sedi "/gha-badge/ s|PaulRBerg/foundry-template|"${GITHUB_REPOSITORY}"|;" "README.md" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..b0e556d --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,92 @@ +name: "CI" + +env: + API_KEY_ALCHEMY: ${{ secrets.API_KEY_ALCHEMY }} + FOUNDRY_PROFILE: "ci" + +on: + workflow_dispatch: + pull_request: + push: + branches: + - "main" + +jobs: + lint: + runs-on: "ubuntu-latest" + steps: + - name: "Check out the repo" + uses: "actions/checkout@v4" + + - name: "Install Foundry" + uses: "foundry-rs/foundry-toolchain@v1" + + - name: "Install Bun" + uses: "oven-sh/setup-bun@v1" + + - name: "Install the Node.js dependencies" + run: "bun install" + + # - name: "Lint the code" + # run: "bun run lint" + + # - name: "Add lint summary" + # run: | + # echo "## Lint result" >> $GITHUB_STEP_SUMMARY + # echo "✅ Passed" >> $GITHUB_STEP_SUMMARY + + build: + runs-on: "ubuntu-latest" + steps: + - name: "Check out the repo" + uses: "actions/checkout@v4" + + - name: "Install Foundry" + uses: "foundry-rs/foundry-toolchain@v1" + + - name: "Install Bun" + uses: "oven-sh/setup-bun@v1" + + - name: "Install the Node.js dependencies" + run: "bun install" + + - name: "Build the contracts and print their size" + run: "forge build --sizes" + + - name: "Add build summary" + run: | + echo "## Build result" >> $GITHUB_STEP_SUMMARY + echo "✅ Passed" >> $GITHUB_STEP_SUMMARY + + test: + needs: ["lint", "build"] + runs-on: "ubuntu-latest" + steps: + - name: "Check out the repo" + uses: "actions/checkout@v4" + + - name: "Install Foundry" + uses: "foundry-rs/foundry-toolchain@v1" + + - name: "Install Bun" + uses: "oven-sh/setup-bun@v1" + + - name: "Install the Node.js dependencies" + run: "bun install" + + - name: "Show the Foundry config" + run: "forge config" + + - name: "Generate a fuzz seed that changes weekly to avoid burning through RPC allowance" + run: > + echo "FOUNDRY_FUZZ_SEED=$( + echo $(($EPOCHSECONDS - $EPOCHSECONDS % 604800)) + )" >> $GITHUB_ENV + + - name: "Run the tests" + run: "forge test" + + - name: "Add test summary" + run: | + echo "## Tests result" >> $GITHUB_STEP_SUMMARY + echo "✅ Passed" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/create.yml b/.github/workflows/create.yml new file mode 100644 index 0000000..e0e9369 --- /dev/null +++ b/.github/workflows/create.yml @@ -0,0 +1,52 @@ +name: "Create" + +# The workflow will run only when the "Use this template" button is used +on: + create: + +jobs: + create: + # We only run this action when the repository isn't the template repository. References: + # - https://docs.github.com/en/actions/learn-github-actions/contexts + # - https://docs.github.com/en/actions/learn-github-actions/expressions + if: ${{ !github.event.repository.is_template }} + permissions: "write-all" + runs-on: "ubuntu-latest" + steps: + - name: "Check out the repo" + uses: "actions/checkout@v4" + + - name: "Update package.json" + env: + GITHUB_REPOSITORY_DESCRIPTION: ${{ github.event.repository.description }} + run: + ./.github/scripts/rename.sh "$GITHUB_REPOSITORY" "$GITHUB_REPOSITORY_OWNER" "$GITHUB_REPOSITORY_DESCRIPTION" + + - name: "Add rename summary" + run: | + echo "## Commit result" >> $GITHUB_STEP_SUMMARY + echo "✅ Passed" >> $GITHUB_STEP_SUMMARY + + - name: "Remove files not needed in the user's copy of the template" + run: | + rm -f "./.github/FUNDING.yml" + rm -f "./.github/scripts/rename.sh" + rm -f "./.github/workflows/create.yml" + + - name: "Add remove summary" + run: | + echo "## Remove result" >> $GITHUB_STEP_SUMMARY + echo "✅ Passed" >> $GITHUB_STEP_SUMMARY + + - name: "Update commit" + uses: "stefanzweifel/git-auto-commit-action@v4" + with: + commit_message: "feat: initial commit" + commit_options: "--amend" + push_options: "--force" + skip_fetch: true + + - name: "Add commit summary" + run: | + echo "## Commit result" >> $GITHUB_STEP_SUMMARY + echo "✅ Passed" >> $GITHUB_STEP_SUMMARY diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..dcb571c --- /dev/null +++ b/.gitignore @@ -0,0 +1,36 @@ +.env +.vscode/ +.idea/ +out/ + +typechain-types + +# OS file +.DS_Store + +# Dependency directories +node_modules +jspm_packages +.yarn/* +!.yarn/patches +!.yarn/releases +!.yarn/plugins +!.yarn/sdks +!.yarn/versions +.pnp.* + +# Compiled binary addons (http://nodejs.org/api/addons.html) +bin +build +artifacts +cache +dist + +# Coverage directory used by tools like istanbul +.coverageArtifacts +coverage +coverage.json +__pycache__/ + +.npmrc +.editorconfig \ No newline at end of file diff --git a/.gitpod.yml b/.gitpod.yml new file mode 100644 index 0000000..b9646d8 --- /dev/null +++ b/.gitpod.yml @@ -0,0 +1,14 @@ +image: "gitpod/workspace-bun" + +tasks: + - name: "Install dependencies" + before: | + curl -L https://foundry.paradigm.xyz | bash + source ~/.bashrc + foundryup + init: "bun install" + +vscode: + extensions: + - "esbenp.prettier-vscode" + - "NomicFoundation.hardhat-solidity" diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..3996d20 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,17 @@ +# directories +broadcast +cache +coverage +node_modules +out + +# files +*.env +*.log +.DS_Store +.pnp.* +bun.lockb +lcov.info +package-lock.json +pnpm-lock.yaml +yarn.lock diff --git a/.prettierrc.yml b/.prettierrc.yml new file mode 100644 index 0000000..a1ecdbb --- /dev/null +++ b/.prettierrc.yml @@ -0,0 +1,7 @@ +bracketSpacing: true +printWidth: 120 +proseWrap: "always" +singleQuote: false +tabWidth: 2 +trailingComma: "all" +useTabs: false diff --git a/.solhint.json b/.solhint.json new file mode 100644 index 0000000..7a15ca0 --- /dev/null +++ b/.solhint.json @@ -0,0 +1,14 @@ +{ + "extends": "solhint:recommended", + "rules": { + "code-complexity": ["error", 8], + "compiler-version": ["error", ">=0.8.23"], + "func-name-mixedcase": "off", + "func-visibility": ["error", { "ignoreConstructors": true }], + "max-line-length": ["error", 120], + "named-parameters-mapping": "warn", + "no-console": "off", + "not-rely-on-time": "off", + "one-contract-per-file": "off" + } +} diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..88a2b87 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,16 @@ +MIT License + +Copyright (c) 2023 Paul Razvan Berg + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit +persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the +Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..3fa819d --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# Pendle examples + +Code examples of how to interact with various Pendle contracts \ No newline at end of file diff --git a/bun.lockb b/bun.lockb new file mode 100755 index 0000000000000000000000000000000000000000..dfc7565b5d9d6b92adee90cbd0a5ad1373958d33 GIT binary patch literal 51363 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 literal 0 HcmV?d00001 diff --git a/foundry.toml b/foundry.toml new file mode 100644 index 0000000..c618993 --- /dev/null +++ b/foundry.toml @@ -0,0 +1,53 @@ +# Full reference https://github.com/foundry-rs/foundry/tree/master/crates/config + +[profile.default] +auto_detect_solc = false +block_timestamp = 1_680_220_800 # March 31, 2023 at 00:00 GMT +bytecode_hash = "none" +evm_version = "paris" # See https://www.evmdiff.com/features?name=PUSH0&kind=opcode +fuzz = { runs = 1_000 } +gas_reports = ["*"] +optimizer = true +optimizer_runs = 10_000 +out = "out" +script = "script" +solc = "0.8.23" +src = "src" +test = "test" + +[profile.ci] +fuzz = { runs = 10_000 } +verbosity = 4 + +[etherscan] +arbitrum = { key = "${API_KEY_ARBISCAN}" } +avalanche = { key = "${API_KEY_SNOWTRACE}" } +bnb_smart_chain = { key = "${API_KEY_BSCSCAN}" } +gnosis_chain = { key = "${API_KEY_GNOSISSCAN}" } +goerli = { key = "${API_KEY_ETHERSCAN}" } +mainnet = { key = "${API_KEY_ETHERSCAN}" } +optimism = { key = "${API_KEY_OPTIMISTIC_ETHERSCAN}" } +polygon = { key = "${API_KEY_POLYGONSCAN}" } +sepolia = { key = "${API_KEY_ETHERSCAN}" } + +[fmt] +bracket_spacing = true +int_types = "long" +line_length = 120 +multiline_func_header = "all" +number_underscore = "thousands" +quote_style = "double" +tab_width = 4 +wrap_comments = true + +[rpc_endpoints] +arbitrum = "https://rpc.ankr.com/arbitrum" +avalanche = "https://avalanche-mainnet.infura.io/v3/${API_KEY_INFURA}" +bnb_smart_chain = "https://bsc-dataseed.binance.org" +gnosis_chain = "https://rpc.gnosischain.com" +goerli = "https://goerli.infura.io/v3/${API_KEY_INFURA}" +localhost = "http://localhost:8545" +mainnet = "https://rpc.ankr.com/eth" +optimism = "https://optimism-mainnet.infura.io/v3/${API_KEY_INFURA}" +polygon = "https://polygon-mainnet.infura.io/v3/${API_KEY_INFURA}" +sepolia = "https://sepolia.infura.io/v3/${API_KEY_INFURA}" diff --git a/package.json b/package.json new file mode 100644 index 0000000..9a1ce1c --- /dev/null +++ b/package.json @@ -0,0 +1,40 @@ +{ + "name": "@prb/foundry-template", + "description": "Foundry-based template for developing Solidity smart contracts", + "version": "1.0.0", + "author": { + "name": "Paul Razvan Berg", + "url": "https://github.com/PaulRBerg" + }, + "dependencies": { + "@openzeppelin/contracts": "^5.0.1" + }, + "devDependencies": { + "@prb/test": "^0.6.4", + "forge-std": "github:foundry-rs/forge-std#v1.7.5", + "prettier": "^3.0.0", + "solhint": "^3.6.2", + "@pendle/core-v2": "^4.1.0" + }, + "keywords": [ + "blockchain", + "ethereum", + "forge", + "foundry", + "smart-contracts", + "solidity", + "template" + ], + "private": true, + "scripts": { + "clean": "rm -rf cache out", + "build": "forge build", + "lint": "bun run lint:sol && bun run prettier:check", + "lint:sol": "forge fmt --check && bun solhint {script,src,test}/**/*.sol", + "prettier:check": "prettier --check **/*.{json,md,yml} --ignore-path=.prettierignore", + "prettier:write": "prettier --write **/*.{json,md,yml} --ignore-path=.prettierignore", + "test": "forge test", + "test:coverage": "forge coverage", + "test:coverage:report": "forge coverage --report lcov && genhtml lcov.info --branch-coverage --output-dir coverage" + } +} \ No newline at end of file diff --git a/remappings.txt b/remappings.txt new file mode 100644 index 0000000..31c4484 --- /dev/null +++ b/remappings.txt @@ -0,0 +1,4 @@ +@openzeppelin/contracts/=node_modules/@openzeppelin/contracts/ +@prb/test/=node_modules/@prb/test/ +forge-std/=node_modules/forge-std/ +@pendle/core-v2/=node_modules/@pendle/core-v2/ \ No newline at end of file diff --git a/script/Base.s.sol b/script/Base.s.sol new file mode 100644 index 0000000..794d2de --- /dev/null +++ b/script/Base.s.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.23 <0.9.0; + +import { Script } from "forge-std/src/Script.sol"; + +abstract contract BaseScript is Script { + /// @dev Included to enable compilation of the script without a $MNEMONIC environment variable. + string internal constant TEST_MNEMONIC = "test test test test test test test test test test test junk"; + + /// @dev Needed for the deterministic deployments. + bytes32 internal constant ZERO_SALT = bytes32(0); + + /// @dev The address of the transaction broadcaster. + address internal broadcaster; + + /// @dev Used to derive the broadcaster's address if $ETH_FROM is not defined. + string internal mnemonic; + + /// @dev Initializes the transaction broadcaster like this: + /// + /// - If $ETH_FROM is defined, use it. + /// - Otherwise, derive the broadcaster address from $MNEMONIC. + /// - If $MNEMONIC is not defined, default to a test mnemonic. + /// + /// The use case for $ETH_FROM is to specify the broadcaster key and its address via the command line. + constructor() { + address from = vm.envOr({ name: "ETH_FROM", defaultValue: address(0) }); + if (from != address(0)) { + broadcaster = from; + } else { + mnemonic = vm.envOr({ name: "MNEMONIC", defaultValue: TEST_MNEMONIC }); + (broadcaster,) = deriveRememberKey({ mnemonic: mnemonic, index: 0 }); + } + } + + modifier broadcast() { + vm.startBroadcast(broadcaster); + _; + vm.stopBroadcast(); + } +} diff --git a/script/Deploy.s.sol b/script/Deploy.s.sol new file mode 100644 index 0000000..4d977c4 --- /dev/null +++ b/script/Deploy.s.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.23 <0.9.0; + +import { Foo } from "../src/Foo.sol"; + +import { BaseScript } from "./Base.s.sol"; + +/// @dev See the Solidity Scripting tutorial: https://book.getfoundry.sh/tutorials/solidity-scripting +contract Deploy is BaseScript { + function run() public broadcast returns (Foo foo) { + foo = new Foo(); + } +} diff --git a/src/Foo.sol b/src/Foo.sol new file mode 100644 index 0000000..2c0986d --- /dev/null +++ b/src/Foo.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.23; + +contract Foo { + function id(uint256 value) external pure returns (uint256) { + return value; + } +} diff --git a/test/RouterSample.sol b/test/RouterSample.sol new file mode 100644 index 0000000..cc57092 --- /dev/null +++ b/test/RouterSample.sol @@ -0,0 +1,258 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.23 <0.9.0; + +import { PRBTest } from "@prb/test/src/PRBTest.sol"; +import { console2 as console } from "forge-std/src/console2.sol"; +import { StdCheats } from "forge-std/src/StdCheats.sol"; +import "@pendle/core-v2/contracts/interfaces/IPAllActionV3.sol"; +import "@pendle/core-v2/contracts/interfaces/IPMarket.sol"; + +contract RouterSample is PRBTest, StdCheats { + IPAllActionV3 public constant router = IPAllActionV3(0x00000000005BBB0EF59571E58418F9a4357b68A0); + IPMarket public constant market = IPMarket(0xD0354D4e7bCf345fB117cabe41aCaDb724eccCa2); + address public constant wstETH = 0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0; + + IStandardizedYield public SY; + IPPrincipalToken public PT; + IPYieldToken public YT; + + // Default params + ApproxParams public defaultApprox = ApproxParams(0, type(uint256).max, 0, 256, 1e15); + LimitOrderData public emptyLimitStruct; + SwapData public emptySwapData; + + function setUp() public virtual { + vm.createSelectFork({ urlOrAlias: "mainnet", blockNumber: 19_405_040 }); + + (SY, PT, YT) = IPMarket(market).readTokens(); + + deal(wstETH, address(this), 1e19); + IERC20(wstETH).approve(address(router), type(uint256).max); + IERC20(SY).approve(address(router), type(uint256).max); + IERC20(PT).approve(address(router), type(uint256).max); + IERC20(YT).approve(address(router), type(uint256).max); + IERC20(market).approve(address(router), type(uint256).max); + } + + function test_buy_and_sell_PT() external { + (uint256 netPtOut,,) = router.swapExactTokenForPt( + address(this), address(market), 0, defaultApprox, createTokenInputStruct(wstETH, 1e18), emptyLimitStruct + ); + console.log("netPtOut: %s", netPtOut); + + uint256 exactPtIn = netPtOut; + (uint256 netTokenOut,,) = router.swapExactPtForToken( + address(this), address(market), exactPtIn, createTokenOutputStruct(wstETH, 0), emptyLimitStruct + ); + + console.log("netTokenOut: %s", netTokenOut); + } + + function test_buy_and_sell_YT() external { + (uint256 netYtOut,,) = router.swapExactTokenForYt( + address(this), address(market), 0, defaultApprox, createTokenInputStruct(wstETH, 1e18), emptyLimitStruct + ); + console.log("netYtOut: %s", netYtOut); + + uint256 exactYtIn = netYtOut; + (uint256 netTokenOut,,) = router.swapExactYtForToken( + address(this), address(market), exactYtIn, createTokenOutputStruct(wstETH, 0), emptyLimitStruct + ); + + console.log("netTokenOut: %s", netTokenOut); + } + + function test_ZapIn_and_ZapOut() external { + (uint256 netLpOut,,) = router.addLiquiditySingleToken( + address(this), address(market), 0, defaultApprox, createTokenInputStruct(wstETH, 1e18), emptyLimitStruct + ); + + console.log("netLpOut: %s", netLpOut); + + uint256 exactLpIn = netLpOut; + (uint256 netTokenOut,,) = router.removeLiquiditySingleToken( + address(this), address(market), exactLpIn, createTokenOutputStruct(wstETH, 0), emptyLimitStruct + ); + + console.log("netTokenOut: %s", netTokenOut); + } + + function test_ZeroPriceImpact_Zap() external { + (uint256 netLpOut, uint256 netYtOut,,) = router.addLiquiditySingleTokenKeepYt( + address(this), address(market), 0, 0, createTokenInputStruct(wstETH, 1e18) + ); + + console.log("netLpOut: %s, netYtOut: %s", netLpOut, netYtOut); + } + + function test_swap_between_PT_and_YT() external { + uint256 exactPtIn = 1e18; + deal(address(PT), address(this), 1e18); + + (uint256 netYtOut,) = router.swapExactPtForYt(address(this), address(market), exactPtIn, 0, defaultApprox); + + console.log("netYtOut: %s", netYtOut); + + uint256 exactYtIn = netYtOut; + + (uint256 netPtOut,) = router.swapExactYtForPt(address(this), address(market), exactYtIn, 0, defaultApprox); + + console.log("netPtOut: %s", netPtOut); + } + + function test_mint_redeem_SY() external { + uint256 netSyOut = router.mintSyFromToken(address(this), address(SY), 0, createTokenInputStruct(wstETH, 1e18)); + + console.log("netSyOut: %s", netSyOut); + + uint256 exactSyIn = netSyOut; + + uint256 netTokenOut = + router.redeemSyToToken(address(this), address(SY), exactSyIn, createTokenOutputStruct(wstETH, 0)); + + console.log("netTokenOut: %s", netTokenOut); + } + + function test_mint_redeem_PT_and_YT() external { + (uint256 netPyOut,) = + router.mintPyFromToken(address(this), address(YT), 0, createTokenInputStruct(wstETH, 1e18)); + + console.log("netPyOut: %s", netPyOut); + + uint256 exactPyIn = netPyOut; + + (uint256 netTokenOut,) = + router.redeemPyToToken(address(this), address(YT), exactPyIn, createTokenOutputStruct(wstETH, 0)); + + console.log("netTokenOut: %s", netTokenOut); + } + + /// @dev use crvUSD market instead since wstETH doesn't have rewards + function test_redeem_SY_YT_LP_interest_and_rewards() external { + IStandardizedYield SYcrvUSD = IStandardizedYield(0x60D1AfD87c5Ab1a13E27638f1e75277fEbF4908C); + IPYieldToken YTcrvUSD = IPYieldToken(0xAf86Da90A5C1e1a41Be3b944b16944081087b7Aa); + IPMarket LPcrvUSD = IPMarket(0xBBd395D4820da5C89A3bCA4FA28Af97254a0FCBe); + address marketCrvUSD = address(LPcrvUSD); + + deal(address(SYcrvUSD), address(this), 3e18); + SYcrvUSD.approve(address(router), type(uint256).max); + router.swapExactSyForYt(address(this), marketCrvUSD, 1e18, 0, defaultApprox, emptyLimitStruct); + router.addLiquiditySingleSy(address(this), marketCrvUSD, 1e18, 0, defaultApprox, emptyLimitStruct); + + vm.warp(block.timestamp + 1 weeks); + vm.roll(block.number + 1000); + + { + // Redeem SY's rewards. No interest + address[] memory rewardTokens = SYcrvUSD.getRewardTokens(); + uint256[] memory rewardAmounts = SYcrvUSD.claimRewards(address(this)); + + assert(rewardTokens.length == 1); + + console.log("[SY] reward amount: %s %s", rewardAmounts[0], IERC20Metadata(rewardTokens[0]).symbol()); + } + + { + // Redeem YT's rewards and interest + address[] memory rewardTokens = YTcrvUSD.getRewardTokens(); + (uint256 interestAmount, uint256[] memory rewardAmounts) = + YTcrvUSD.redeemDueInterestAndRewards(address(this), true, true); + + assert(rewardTokens.length == 1); + + console.log("[YT] interest amount: %s", interestAmount); + console.log("[YT] reward amount: %s %s", rewardAmounts[0], IERC20Metadata(rewardTokens[0]).symbol()); + } + + { + // Redeem LP's rewards and interest + address[] memory rewardTokens = LPcrvUSD.getRewardTokens(); + uint256[] memory rewardAmounts = LPcrvUSD.redeemRewards(address(this)); + + assert(rewardTokens.length == 2); + console.log("[LP] reward amount: %s %s", rewardAmounts[0], IERC20Metadata(rewardTokens[0]).symbol()); + console.log("[LP] reward amount: %s %s", rewardAmounts[1], IERC20Metadata(rewardTokens[1]).symbol()); + } + + { + // batch redeem + + address[] memory SYs = new address[](1); + SYs[0] = address(SYcrvUSD); + + address[] memory YTs = new address[](1); + YTs[0] = address(YTcrvUSD); + + address[] memory LPs = new address[](1); + LPs[0] = address(LPcrvUSD); + + router.redeemDueInterestAndRewards(address(this), SYs, YTs, LPs); + } + } + + function test_multicall() external { + bytes memory ptBuy = abi.encodeWithSelector( + IPActionSwapPTV3.swapExactTokenForPt.selector, + address(this), + address(market), + 0, + defaultApprox, + createTokenInputStruct(wstETH, 1e18), + emptyLimitStruct + ); + + bytes memory ytBuy = abi.encodeWithSelector( + IPActionSwapYTV3.swapExactTokenForYt.selector, + address(this), + address(market), + 0, + defaultApprox, + createTokenInputStruct(wstETH, 1e18), + emptyLimitStruct + ); + + IPActionMiscV3.Call3[] memory calls = new IPActionMiscV3.Call3[](2); + calls[0] = IPActionMiscV3.Call3({ allowFailure: false, callData: ptBuy }); + calls[1] = IPActionMiscV3.Call3({ allowFailure: false, callData: ytBuy }); + + IPActionMiscV3.Result[] memory res = router.multicall(calls); + + (uint256 netPtOut,,) = abi.decode(res[0].returnData, (uint256, uint256, uint256)); + (uint256 netYtOut,,) = abi.decode(res[1].returnData, (uint256, uint256, uint256)); + + console.log("netPtOut: %s, netYtOut: %s", netPtOut, netYtOut); + } + + // ----------------- Helper functions ----------------- + + /// @notice create a simple TokenInput struct without using any aggregators. For more info please refer to + /// IPAllActionTypeV3.sol + function createTokenInputStruct(address tokenIn, uint256 netTokenIn) internal view returns (TokenInput memory) { + return TokenInput({ + tokenIn: tokenIn, + netTokenIn: netTokenIn, + tokenMintSy: tokenIn, + pendleSwap: address(0), + swapData: emptySwapData + }); + } + + /// @notice create a simple TokenOutput struct without using any aggregators. For more info please refer to + /// IPAllActionTypeV3.sol + function createTokenOutputStruct( + address tokenOut, + uint256 minTokenOut + ) + internal + view + returns (TokenOutput memory) + { + return TokenOutput({ + tokenOut: tokenOut, + minTokenOut: minTokenOut, + tokenRedeemSy: tokenOut, + pendleSwap: address(0), + swapData: emptySwapData + }); + } +}