Skip to content

Commit

Permalink
Improved the debugging tools (#1057)
Browse files Browse the repository at this point in the history
* Improved the debugging tools

* Improved the debugging tools output

* Added a convenience script for the debugging tools

* Fixed nits

* Addressed nit
  • Loading branch information
jalextowle committed Jun 14, 2024
1 parent 56d7015 commit e5525e9
Show file tree
Hide file tree
Showing 7 changed files with 259 additions and 106 deletions.
9 changes: 9 additions & 0 deletions .env_template
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,18 @@
MAINNET_RPC_URL=
SEPOLIA_RPC_URL=
BASE_SEPOLIA_RPC_URL=
DEBUG_RPC_URL=

# private key used for running migration scripts
PRIVATE_KEY=

# API key used to verify contracts
ETHERSCAN_API_KEY=

# debugging settings
TX_HASH=
BLOCK=
TO=
FROM=
VALUE=
DATA=
7 changes: 2 additions & 5 deletions foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,8 @@ remappings = [
]
# gas limit - max u64
gas_limit = "18446744073709551615"
# Allows the scripts to write the addresses to a file.
fs_permissions = [
{ access = "write", path = "./artifacts/script_addresses.json" },
{ access = "write", path = "./test/Generated.t.sol" },
]
# allows the ffi to be used
ffi = true
# See more config options https://github.com/foundry-rs/foundry/tree/master/config

[profile.production]
Expand Down
3 changes: 3 additions & 0 deletions scripts/debug.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/bin/bash

DEBUG=true forge test -vv --match-test "test_debug"
203 changes: 203 additions & 0 deletions test/debug/Debug.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
// SPDX-License-Identifier: Apache-2.0
pragma solidity 0.8.20;

import { console2 as console } from "forge-std/console2.sol";
import { IHyperdriveCore } from "contracts/src/interfaces/IHyperdriveCore.sol";
import { IMultiTokenCore } from "contracts/src/interfaces/IMultiTokenCore.sol";
import { ETH } from "contracts/src/libraries/Constants.sol";
import { BaseTest } from "test/utils/BaseTest.sol";
import { EtchingUtils } from "test/utils/EtchingUtils.sol";
import { Lib } from "test/utils/Lib.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 Debug is BaseTest, EtchingUtils {
using Lib for *;

/// @dev A minimal Ethereum transaction.
struct Transaction {
address to;
address from;
uint256 value;
bytes data;
}

function test_debug() external {
// Skip this test unless the debugging environment variable is set.
vm.skip(!vm.envOr("DEBUG", false));

// Set up the fork environment and get the appropriate transaction
// details given the mode that we are using. If a tx hash was provided,
// we default to using the transaction debugging mode. Otherwise, we
// will see if the data was provided manually.
Transaction memory tx_;
string memory rpcURL = vm.envString("DEBUG_RPC_URL");
bytes32 txHash = vm.envOr("TX_HASH", bytes32(0));
if (txHash != bytes32(0)) {
// Fork the chain specified by the debug RPC URL at the specified
// transaction hash.
uint256 forkId = vm.createFork(rpcURL);
vm.selectFork(forkId);
vm.rollFork(txHash);

// Get the transaction details of the transaction. We assume that this
// is a transaction to a Hyperdrive instance.
tx_ = getTransaction(rpcURL, txHash);
} else {
// Fork the chain specified by the debug RPC URL at the specified
// block height.
uint256 forkId = vm.createFork(rpcURL);
vm.selectFork(forkId);
vm.rollFork(vm.envUint("BLOCK"));

// Manually construct the transaction.
tx_.to = vm.envAddress("TO");
tx_.from = vm.envAddress("FROM");
tx_.value = vm.envUint("VALUE");
tx_.data = vm.envBytes("DATA");
}

// Debug the transaction.
debug(tx_);
}

/// @dev Debugs the provided Hyperdrive transaction.
/// @param _tx The transaction to debug.
function debug(Transaction memory _tx) internal {
// Etch the hyperdrive instance to add console logs.
(string memory name, string memory version) = etchHyperdrive(_tx.to);

// Log a preamble with the Hyperdrive name, version, and the function
// that will be called.
console.log(
"[test_debug] Found instance named %s at version %s",
name,
version
);
(string memory targetName, bool wasRecognized) = getTargetName(
_tx.data
);
if (wasRecognized) {
console.log('[test_debug] Simulating a call to "%s"', targetName);
} else {
console.log(
"[test_debug] Simulating a call to an unrecognized function"
);
}

// Debugging the transaction.
vm.startPrank(_tx.from);
(bool success, bytes memory returndata) = _tx.to.call{
value: _tx.value
}(_tx.data);

// 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)
);
}
}

/// @dev Gets the name of the function that was called with the provided
/// calldata.
/// @param _data The transaction calldata.
/// @return The name of the function that was called.
/// @return A flag indicating whether or not the target name was
/// successfully found.
function getTargetName(
bytes memory _data
) internal pure returns (string memory, bool) {
// Attempt to match the selector to one of the functions in the
// Hyperdrive interface. If the selector doesn't match any of the
// functions, we return a failure flag indicating that.
bytes4 selector = bytes4(_data);
if (selector == IHyperdriveCore.openLong.selector) {
return ("openLong", true);
} else if (selector == IHyperdriveCore.openShort.selector) {
return ("openShort", true);
} else if (selector == IHyperdriveCore.closeLong.selector) {
return ("closeLong", true);
} else if (selector == IHyperdriveCore.closeShort.selector) {
return ("closeShort", true);
} else if (selector == IHyperdriveCore.initialize.selector) {
return ("initialize", true);
} else if (selector == IHyperdriveCore.addLiquidity.selector) {
return ("addLiquidity", true);
} else if (selector == IHyperdriveCore.removeLiquidity.selector) {
return ("removeLiquidity", true);
} else if (
selector == IHyperdriveCore.redeemWithdrawalShares.selector
) {
return ("redeemWithdrawalShares", true);
} else if (selector == IHyperdriveCore.checkpoint.selector) {
return ("checkpoint", true);
} else if (selector == IHyperdriveCore.collectGovernanceFee.selector) {
return ("collectGovernanceFee", true);
} else if (selector == IHyperdriveCore.pause.selector) {
return ("pause", true);
} else if (selector == IHyperdriveCore.setFeeCollector.selector) {
return ("setFeeCollector", true);
} else if (selector == IHyperdriveCore.setSweepCollector.selector) {
return ("setSweepCollector", true);
} else if (selector == IHyperdriveCore.setGovernance.selector) {
return ("setGovernance", true);
} else if (selector == IHyperdriveCore.setPauser.selector) {
return ("setPauser", true);
} else if (selector == IHyperdriveCore.sweep.selector) {
return ("sweep", true);
} else if (selector == IMultiTokenCore.transferFrom.selector) {
return ("transferFrom", true);
} else if (selector == IMultiTokenCore.transferFromBridge.selector) {
return ("transferFromBridge", true);
} else if (selector == IMultiTokenCore.setApproval.selector) {
return ("setApproval", true);
} else if (selector == IMultiTokenCore.setApprovalBridge.selector) {
return ("setApprovalBridge", true);
} else if (selector == IMultiTokenCore.setApprovalForAll.selector) {
return ("setApprovalForAll", true);
} else if (selector == IMultiTokenCore.batchTransferFrom.selector) {
return ("batchTransferFrom", true);
} else if (selector == IMultiTokenCore.permitForAll.selector) {
return ("permitForAll", true);
}
return ("", false);
}

/// @dev Gets a transaction on a specified chain.
/// @param _rpcURL The RPC URL to query.
/// @param _txHash The transaction hash.
/// @return tx_ The transaction.
function getTransaction(
string memory _rpcURL,
bytes32 _txHash
) internal returns (Transaction memory tx_) {
// Shell out to cast to make the RPC call.
string[] memory command = new string[](6);
command[0] = "cast";
command[1] = "tx";
command[2] = vm.toString(_txHash);
command[3] = "--json";
command[4] = "--rpc-url";
command[5] = _rpcURL;
string memory json = string(vm.ffi(command));

// Parse the result json into the transaction.
tx_.from = vm.parseJsonAddress(json, ".from");
tx_.to = vm.parseJsonAddress(json, ".to");
tx_.value = vm.parseJsonUint(json, ".value");
tx_.data = vm.parseJsonBytes(json, ".input");

return tx_;
}
}
87 changes: 0 additions & 87 deletions test/debug/Debugging.t.sol

This file was deleted.

26 changes: 20 additions & 6 deletions test/debug/README.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,28 @@
# 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:
transactions on the sepolia testnet. The debugging tools rely on an RPC URL
that is provided by the `DEBUG_RPC_URL` environment variable. This tool supports
two different modes:

- Transaction Mode: In this mode, you provide a transaction hash with the
`TX_HASH` environment variable. The transaction specified by this environment
variable will be simulated exactly as it was executed. For this to work, the
`TX_HASH` value must specify a transaction that was mined on the chain
specified by `DEBUG_RPC_URL`. This mode is preferred for mined transactions.
- Simulation Mode: In this mode, you provide a block height with the `BLOCK`
environment variable and transaction details including the `FROM` address, the
`TO` address, the amount of `VALUE` to send, and the `DATA` to send. All of
these environment variables must be set for this mode to work properly. This
mode will execute the specified transaction at the specified block height on
the forked chain. This mode can be used when a transaction hasn't been mined
or to simulate internal calls.

The environment variables can be put in the `.env` file for convenience or passed
manually. Here is an example of running the script in transaction mode:

```
forge test -vv --match-test "test_debug"
DEBUG_RPC_URL=http://localhost:8545 TX_HASH=0x278d43d20d134fcfcd92c05d02b33cee1855de8ecae20a3c4ca47e1647e171a7 ./scripts/debug.sh
```

To get more insight into what went wrong (or right) during the transaction
Expand Down
Loading

0 comments on commit e5525e9

Please sign in to comment.