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 0000000..dfc7565 Binary files /dev/null and b/bun.lockb differ diff --git a/foundry.toml b/foundry.toml new file mode 100644 index 0000000..edafcd5 --- /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://arbitrum-mainnet.infura.io/v3/${API_KEY_INFURA}" + 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://eth-mainnet.g.alchemy.com/v2/${API_KEY_ALCHEMY}" + 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 + }); + } +}