From ae1cd7e983c2d647a78808c99696b9d6e124e1ba Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Thu, 11 Apr 2024 13:42:51 -0500 Subject: [PATCH] Etching Tool (#959) * Updated the foundry dev tools to support etching onto Sepolia * Fixed the etching tools * Added `DebugTest.sol` * Cleaned up the debugging tools * Updated the information to be a successful `ERC4626` call * Updated CI * Fixed the spell-check linter * Removed goerli from the repo * Added a README * Addressed review feedback from @jrhea --- .env_template | 2 +- .github/workflows/benchmark.yml | 2 +- .github/workflows/coverage.yml | 2 +- .github/workflows/solidity_lint.yml | 2 +- .github/workflows/solidity_test.yml | 8 +- cSpell.json | 3 + foundry.toml | 2 +- test/debug/Debugging.t.sol | 81 +++++++++++++ test/debug/README.md | 47 +++++++ test/utils/BaseTest.sol | 13 +- test/utils/EtchingUtils.sol | 182 ++++++++++++++++++++++++++++ 11 files changed, 327 insertions(+), 17 deletions(-) create mode 100644 test/debug/Debugging.t.sol create mode 100644 test/debug/README.md create mode 100644 test/utils/EtchingUtils.sol diff --git a/.env_template b/.env_template index 9892d7bf1..5da82d32c 100644 --- a/.env_template +++ b/.env_template @@ -1,6 +1,6 @@ # ethereum rpc endpoints used in fork tests and migration scripts -GOERLI_RPC_URL= MAINNET_RPC_URL= +SEPOLIA_RPC_URL= # private key used for running migration scripts PRIVATE_KEY= diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index 1e46d42a2..7e7777a92 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -17,8 +17,8 @@ jobs: needs: detect-changes if: needs.detect-changes.outputs.changed == 'true' env: - GOERLI_RPC_URL: ${{ secrets.GOERLI_RPC_URL }} MAINNET_RPC_URL: ${{ secrets.MAINNET_RPC_URL }} + SEPOLIA_RPC_URL: ${{ secrets.SEPOLIA_RPC_URL }} steps: # Install the dependencies. - uses: actions/checkout@v2 diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index bb86ae421..25321b75c 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -17,8 +17,8 @@ jobs: needs: detect-changes if: needs.detect-changes.outputs.changed == 'true' env: - GOERLI_RPC_URL: ${{ secrets.GOERLI_RPC_URL }} MAINNET_RPC_URL: ${{ secrets.MAINNET_RPC_URL }} + SEPOLIA_RPC_URL: ${{ secrets.SEPOLIA_RPC_URL }} steps: - uses: actions/checkout@v3 with: diff --git a/.github/workflows/solidity_lint.yml b/.github/workflows/solidity_lint.yml index ae67038f1..1242bc8be 100644 --- a/.github/workflows/solidity_lint.yml +++ b/.github/workflows/solidity_lint.yml @@ -17,8 +17,8 @@ jobs: needs: detect-changes if: needs.detect-changes.outputs.changed == 'true' env: - GOERLI_RPC_URL: ${{ secrets.GOERLI_RPC_URL }} MAINNET_RPC_URL: ${{ secrets.MAINNET_RPC_URL }} + SEPOLIA_RPC_URL: ${{ secrets.SEPOLIA_RPC_URL }} steps: - uses: actions/checkout@v3 with: diff --git a/.github/workflows/solidity_test.yml b/.github/workflows/solidity_test.yml index 47fc1f7b2..4dca99a16 100644 --- a/.github/workflows/solidity_test.yml +++ b/.github/workflows/solidity_test.yml @@ -17,8 +17,8 @@ jobs: needs: detect-changes if: needs.detect-changes.outputs.changed == 'true' env: - GOERLI_RPC_URL: ${{ secrets.GOERLI_RPC_URL }} MAINNET_RPC_URL: ${{ secrets.MAINNET_RPC_URL }} + SEPOLIA_RPC_URL: ${{ secrets.SEPOLIA_RPC_URL }} steps: - uses: actions/checkout@v3 with: @@ -39,8 +39,8 @@ jobs: needs: detect-changes if: needs.detect-changes.outputs.changed == 'true' env: - GOERLI_RPC_URL: ${{ secrets.GOERLI_RPC_URL }} MAINNET_RPC_URL: ${{ secrets.MAINNET_RPC_URL }} + SEPOLIA_RPC_URL: ${{ secrets.SEPOLIA_RPC_URL }} steps: - uses: actions/checkout@v3 with: @@ -61,8 +61,8 @@ jobs: needs: detect-changes if: needs.detect-changes.outputs.changed == 'true' env: - GOERLI_RPC_URL: ${{ secrets.GOERLI_RPC_URL }} MAINNET_RPC_URL: ${{ secrets.MAINNET_RPC_URL }} + SEPOLIA_RPC_URL: ${{ secrets.SEPOLIA_RPC_URL }} steps: - uses: actions/checkout@v3 with: @@ -83,8 +83,8 @@ jobs: needs: detect-changes if: needs.detect-changes.outputs.changed == 'true' env: - GOERLI_RPC_URL: ${{ secrets.GOERLI_RPC_URL }} MAINNET_RPC_URL: ${{ secrets.MAINNET_RPC_URL }} + SEPOLIA_RPC_URL: ${{ secrets.SEPOLIA_RPC_URL }} steps: - uses: actions/checkout@v3 with: diff --git a/cSpell.json b/cSpell.json index 36c455e54..639145617 100644 --- a/cSpell.json +++ b/cSpell.json @@ -97,6 +97,9 @@ "sdiv", "secp", "selfdestruct", + "sepolia", + "Sepolia", + "SEPOLIA", "Sighash", "sload", "sloads", diff --git a/foundry.toml b/foundry.toml index f23a86294..9ca15dedc 100644 --- a/foundry.toml +++ b/foundry.toml @@ -43,4 +43,4 @@ runs = 1000 [rpc_endpoints] mainnet = "${MAINNET_RPC_URL}" -goerli = "${GOERLI_RPC_URL}" +sepolia = "${SEPOLIA_RPC_URL}" diff --git a/test/debug/Debugging.t.sol b/test/debug/Debugging.t.sol new file mode 100644 index 000000000..941a8090f --- /dev/null +++ b/test/debug/Debugging.t.sol @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.20; + +import { console2 as console } from "forge-std/console2.sol"; +import { IHyperdrive } from "contracts/src/interfaces/IHyperdrive.sol"; +import { ETH } from "contracts/src/libraries/Constants.sol"; +import { BaseTest } from "test/utils/BaseTest.sol"; +import { EtchingUtils } from "test/utils/EtchingUtils.sol"; + +/// @author DELV +/// @title Debugging +/// @notice A test suite to help debugging on Sepolia or Mainnet. +/// @custom:disclaimer The language used in this code is for coding convenience +/// only, and is not intended to, and does not, have any +/// particular legal or regulatory significance. +contract Debugging is BaseTest, EtchingUtils { + /// @dev The block to fork. If you're debugging a failing transaction, this + /// should be the block at which the transaction was failing. If you're + /// debugging a successful transaction, you may need to subtract one + /// from the block. + uint256 internal constant FORK_BLOCK = 5676348; + + /// @dev The hyperdrive instance to connect to. If you're debugging a + /// Hyperdrive transaction, this should probably be the `to` address in + /// the failing transaction. + IHyperdrive internal constant HYPERDRIVE = + IHyperdrive(address(0x392839dA0dACAC790bd825C81ce2c5E264D793a8)); + + /// @dev The sender to use in the debugging call. If you're debugging a + /// Hyperdrive transaction, this should probably be the `from` + /// address in the failing transaction. + address internal constant SENDER = + address(0x2C76cc659ec83E36323f32E6a9789C29e7b56c4B); + + /// @dev The value to use in the debugging call. If you're debugging a + /// Hyperdrive transaction, this should probably be the `value` + /// sent in the failing transaction. + uint256 internal constant VALUE = 0; + + /// @dev The calldata to use in the debugging call. If you're debugging a + /// Hyperdrive transaction, this should probably be the calldata from + /// the failing transaction (remove the "0x" prefix from the calldata). + bytes internal constant CALLDATA = + hex"cba2e58d0000000000000000000000000000000000000000000007d9a5405edc26fa36b50000000000000000000000000000000000000000000007d7f3c7d8d354cd71990000000000000000000000000000000000000000000000000de326f195450cd200000000000000000000000000000000000000000000000000000000000000800000000000000000000000002c76cc659ec83e36323f32e6a9789c29e7b56c4b0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000000000000000000"; + + function test_debug() external __sepolia_fork(FORK_BLOCK) { + // Etch the hyperdrive instance to add console logs. + if (HYPERDRIVE.baseToken() == ETH) { + etchStETHHyperdrive(address(HYPERDRIVE)); + } else { + etchERC4626Hyperdrive(address(HYPERDRIVE)); + } + + // Log a preamble. + console.log("---------------"); + console.log("-- Debugging --"); + console.log("---------------"); + console.log(""); + console.log("[test_debug] Sending the debugging call..."); + console.log(""); + + // Debugging the transaction. + vm.startPrank(address(SENDER)); + (bool success, bytes memory returndata) = address(HYPERDRIVE).call{ + value: VALUE + }(CALLDATA); + + // Log a summary. + if (success) { + console.log( + "[test_debug] The call succeeded and returned the following returndata: %s", + vm.toString(returndata) + ); + } else { + console.log( + "[test_debug] The call failed and returned the following returndata: %s", + vm.toString(returndata) + ); + } + } +} diff --git a/test/debug/README.md b/test/debug/README.md new file mode 100644 index 000000000..a03686251 --- /dev/null +++ b/test/debug/README.md @@ -0,0 +1,47 @@ +# Debugging + +This folder contains a testing utility that can be used to debug Hyperdrive +transactions on the sepolia testnet. To use this tool, update the constants in +`test/debug/Debugging.t.sol` with the values from the transaction you'd like to +debug. In particular, you'll need to set the fork block, the hyperdrive address, +the transaction value, and the transaction calldata. Once you've done this, you +can run the following command to run the debugging tool: + +``` +forge test -vv --match-test "test_debug" +``` + +To get more insight into what went wrong (or right) during the transaction +execution, you can add some `console.log` statements in the Hyperdrive codebase +and re-run the tests. Assuming the logs are in the execution path of the debug +transaction, you'll see these logs when you re-run the test. + +## Tips + +1. To add `console.log` statements, you'll need to import `console2` from + `forge-std/console2.sol` in each file that you add logs to. +2. You can pretty-print fixed point numbers with the `Lib.toString` function + from `test/utils/Lib.sol`. You can add `using Lib for *;` on the line after + the contract declaration to use method syntax like `value.toString(18)`. The + `18` in this example indicates that the fixed point value has 18 decimals. +3. Start by adding some `console.log` statements to the start of the flow that + you're calling. You can find the start of the core flows in the following + files: + - `HyperdriveLong`: `_openLong`, `_closeLong` + - `HyperdriveShort`: `_openShort`, `_closeShort` + - `HyperdriveLP`: `_addLiquidity`, `_removeLiquidity`, `_redeemWithdrawalShares` + - `HyperdriveCheckpoint`: `_checkpoint` +4. If you want to inspect what happened during `_deposit` or `_withdraw`, you + may need to add logs to the appropriate `Base` contract for the integration + that you're using. If you're debugging a stETH yield source, add logs to + `_depositWithBase`, `_depositWithShares`, or one of the other functions in + `contracts/src/instances/steth/StETHBase.sol`. Similarly, if you're debugging + an ERC4626 yield source (like sDAI), you can add logs to the corresponding + functions in `contracts/src/instances/erc4626/ERC4626Base.sol`. +5. If you want to inspect what happened within the yield source, you can add logs + to `contracts/test/MockLido.sol` if you're debugging a stETH yield source or + to `contracts/test/MockERC4626.sol` if you're debugging an ERC4626 yield + source (like sDAI). +6. If you want to inspect what happened within the base token, you can add logs + to `contracts/test/ERC20Mintable.sol`. Note that the base token for stETH is + a placeholder since stETH uses ETH as a base token. diff --git a/test/utils/BaseTest.sol b/test/utils/BaseTest.sol index 501a890c7..9fb8f2884 100644 --- a/test/utils/BaseTest.sol +++ b/test/utils/BaseTest.sol @@ -25,13 +25,10 @@ contract BaseTest is Test { error WhaleBalanceExceeded(); error WhaleIsContract(); - uint256 mainnetForkId; - uint256 goerliForkId; - uint256 __init__; // time setup function was ran string MAINNET_RPC_URL = vm.envString("MAINNET_RPC_URL"); - string GOERLI_RPC_URL = vm.envString("GOERLI_RPC_URL"); + string SEPOLIA_RPC_URL = vm.envString("SEPOLIA_RPC_URL"); bool isForked; @@ -53,7 +50,7 @@ contract BaseTest is Test { } modifier __mainnet_fork(uint256 blockNumber) { - mainnetForkId = vm.createFork(MAINNET_RPC_URL); + uint256 mainnetForkId = vm.createFork(MAINNET_RPC_URL); vm.selectFork(mainnetForkId); vm.rollFork(blockNumber); isForked = true; @@ -61,9 +58,9 @@ contract BaseTest is Test { _; } - modifier __goerli_fork(uint256 blockNumber) { - goerliForkId = vm.createFork(GOERLI_RPC_URL); - vm.selectFork(goerliForkId); + modifier __sepolia_fork(uint256 blockNumber) { + uint256 sepoliaForkId = vm.createFork(SEPOLIA_RPC_URL); + vm.selectFork(sepoliaForkId); vm.rollFork(blockNumber); isForked = true; diff --git a/test/utils/EtchingUtils.sol b/test/utils/EtchingUtils.sol new file mode 100644 index 000000000..3b2b29a97 --- /dev/null +++ b/test/utils/EtchingUtils.sol @@ -0,0 +1,182 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.20; + +import { Test } from "forge-std/Test.sol"; +import { ERC4626Hyperdrive } from "contracts/src/instances/erc4626/ERC4626Hyperdrive.sol"; +import { ERC4626Target0 } from "contracts/src/instances/erc4626/ERC4626Target0.sol"; +import { ERC4626Target1 } from "contracts/src/instances/erc4626/ERC4626Target1.sol"; +import { ERC4626Target2 } from "contracts/src/instances/erc4626/ERC4626Target2.sol"; +import { ERC4626Target3 } from "contracts/src/instances/erc4626/ERC4626Target3.sol"; +import { ERC4626Target4 } from "contracts/src/instances/erc4626/ERC4626Target4.sol"; +import { StETHHyperdrive } from "contracts/src/instances/steth/StETHHyperdrive.sol"; +import { StETHTarget0 } from "contracts/src/instances/steth/StETHTarget0.sol"; +import { StETHTarget1 } from "contracts/src/instances/steth/StETHTarget1.sol"; +import { StETHTarget2 } from "contracts/src/instances/steth/StETHTarget2.sol"; +import { StETHTarget3 } from "contracts/src/instances/steth/StETHTarget3.sol"; +import { StETHTarget4 } from "contracts/src/instances/steth/StETHTarget4.sol"; +import { IHyperdrive } from "contracts/src/interfaces/IHyperdrive.sol"; +import { ERC20Mintable } from "contracts/test/ERC20Mintable.sol"; +import { EtchingVault } from "contracts/test/EtchingVault.sol"; +import { MockLido } from "contracts/test/MockLido.sol"; +import { MockERC4626 } from "contracts/test/MockERC4626.sol"; + +contract EtchingUtils is Test { + function etchERC4626Hyperdrive(address _hyperdrive) internal { + // Get an interface to the target Hyperdrive instance. This will be + // used to load immutables that will be used during the etching process. + IHyperdrive hyperdrive = IHyperdrive(_hyperdrive); + + // Etch the base contract. + { + ERC20Mintable target = ERC20Mintable(hyperdrive.baseToken()); + ERC20Mintable template = new ERC20Mintable( + target.name(), + target.symbol(), + target.decimals(), + address(0), + target.isCompetitionMode(), + target.maxMintAmount() + ); + vm.etch(address(target), address(template).code); + } + + // Etch the vault contract. + { + MockERC4626 target = MockERC4626(hyperdrive.vaultSharesToken()); + MockERC4626 template = new MockERC4626( + ERC20Mintable(address(target.asset())), + target.name(), + target.symbol(), + 0, + address(0), + target.isCompetitionMode(), + target.maxMintAmount() + ); + vm.etch(address(target), address(template).code); + } + + // Etch the target0 contract. + { + ERC4626Target0 template = new ERC4626Target0( + hyperdrive.getPoolConfig() + ); + vm.etch(hyperdrive.target0(), address(template).code); + } + + // Etch the target1 contract. + { + ERC4626Target1 template = new ERC4626Target1( + hyperdrive.getPoolConfig() + ); + vm.etch(hyperdrive.target1(), address(template).code); + } + + // Etch the target2 contract. + { + ERC4626Target2 template = new ERC4626Target2( + hyperdrive.getPoolConfig() + ); + vm.etch(hyperdrive.target2(), address(template).code); + } + + // Etch the target3 contract. + { + ERC4626Target3 template = new ERC4626Target3( + hyperdrive.getPoolConfig() + ); + vm.etch(hyperdrive.target3(), address(template).code); + } + + // Etch the target4 contract. + { + ERC4626Target4 template = new ERC4626Target4( + hyperdrive.getPoolConfig() + ); + vm.etch(hyperdrive.target4(), address(template).code); + } + + // Etch the hyperdrive contract. + { + ERC4626Hyperdrive template = new ERC4626Hyperdrive( + hyperdrive.getPoolConfig(), + hyperdrive.target0(), + hyperdrive.target1(), + hyperdrive.target2(), + hyperdrive.target3(), + hyperdrive.target4() + ); + vm.etch(address(hyperdrive), address(template).code); + } + } + + function etchStETHHyperdrive(address _hyperdrive) internal { + // Get an interface to the target Hyperdrive instance. This will be + // used to load immutables that will be used during the etching process. + IHyperdrive hyperdrive = IHyperdrive(_hyperdrive); + + // Etch the vault contract. + { + MockLido target = MockLido(hyperdrive.vaultSharesToken()); + MockLido template = new MockLido( + 0, + address(0), + target.isCompetitionMode(), + target.maxMintAmount() + ); + vm.etch(address(target), address(template).code); + } + + // Etch the target0 contract. + { + StETHTarget0 template = new StETHTarget0( + hyperdrive.getPoolConfig() + ); + vm.etch(hyperdrive.target0(), address(template).code); + } + + // Etch the target1 contract. + { + StETHTarget1 template = new StETHTarget1( + hyperdrive.getPoolConfig() + ); + vm.etch(hyperdrive.target1(), address(template).code); + } + + // Etch the target2 contract. + { + StETHTarget2 template = new StETHTarget2( + hyperdrive.getPoolConfig() + ); + vm.etch(hyperdrive.target2(), address(template).code); + } + + // Etch the target3 contract. + { + StETHTarget3 template = new StETHTarget3( + hyperdrive.getPoolConfig() + ); + vm.etch(hyperdrive.target3(), address(template).code); + } + + // Etch the target4 contract. + { + StETHTarget4 template = new StETHTarget4( + hyperdrive.getPoolConfig() + ); + vm.etch(hyperdrive.target4(), address(template).code); + } + + // Etch the hyperdrive contract. + { + StETHHyperdrive template = new StETHHyperdrive( + hyperdrive.getPoolConfig(), + hyperdrive.target0(), + hyperdrive.target1(), + hyperdrive.target2(), + hyperdrive.target3(), + hyperdrive.target4() + ); + vm.etch(address(hyperdrive), address(template).code); + } + } +}