Skip to content

Commit

Permalink
scrvUSD mainnet integration + CI fixes (#1215)
Browse files Browse the repository at this point in the history
* add scrvUSD instance test

* linting fixes and remove console import

* remove console logs

* log to terminal in benchmark worflow

* set unbuffered so python logs to terminal

* add print

* update gas_benchmark

* add more threads

* wait for the process to finish

* skip tests again

* fix regex

* update solc requirement for scrvusd test

---------

Co-authored-by: Matthew Brown <[email protected]>
  • Loading branch information
MazyGio and sentilesdal authored Dec 2, 2024
1 parent 21e51ec commit 77a52c9
Show file tree
Hide file tree
Showing 3 changed files with 261 additions and 44 deletions.
4 changes: 3 additions & 1 deletion .github/workflows/benchmark.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,15 @@ jobs:
uses: foundry-rs/foundry-toolchain@v1
with:
version: nightly
- name: Install Dependencies
run: forge install
- uses: actions/setup-python@v4
with:
python-version: "3.10"

# Run the gas benchmark and stores the output to a json file.
- name: Run benchmark
run: python python/gas_benchmarks.py benchmarks.json
run: python -u python/gas_benchmarks.py benchmarks.json

# Load the benchmarks cache. We use a different cache key for every run
# because Github Actions caches are currently immutable. By specifying the
Expand Down
147 changes: 104 additions & 43 deletions python/gas_benchmarks.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import json
import subprocess
import sys
import json

OUTPUT_PATH = sys.argv[1]

Expand All @@ -17,50 +17,111 @@
"checkpoint",
]

# HACK: We have to ignore the Chainlink instance test and the combinatorial test
# since these tests fail during gas benchmarking.
print("Starting gas benchmarks...")
# Run the Solidity tests and write the test name and the gas used to a json file. The gas-report
# option outputs a gas summary for every contract that looks like this:
#
# | contracts/test/MockHyperdrive.sol:MockHyperdrive contract | | | | | |
# |-----------------------------------------------------------|-----------------|--------|--------|--------|---------|
# | Deployment Cost | Deployment Size | | | | |
# | 19563789 | 97621 | | | | |
# | Function Name | min | avg | median | max | # calls |
# | accrue | 43765 | 43792 | 43789 | 43801 | 108 |
# | balanceOf | 2640 | 2973 | 2640 | 4640 | 6 |
# | getCheckpointExposure | 7121 | 7121 | 7121 | 7121 | 216 |
# | getPoolConfig | 9814 | 16819 | 20314 | 22814 | 1548 |
# | getPoolInfo | 15073 | 26402 | 31392 | 31412 | 671 |
# | initialize | 355979 | 356037 | 356035 | 356107 | 228 |
# | openLong | 33370 | 170709 | 126607 | 286702 | 444 |
# | pause | 42450 | 42456 | 42456 | 42462 | 2 |
# | setLongExposure | 43914 | 43914 | 43914 | 43914 | 1 |
# | setPauser | 25306 | 25306 | 25306 | 25306 | 12 |
#
# Run the Solidity tests and write the test name and the gas used to a markdown table.
# We are only interested in the MockHyperdrive contract and functions listed in FUNCTION_NAMES.
# We parse the output with the following steps:
# 1. Check for the contract name.
# 2. Check for the beginning of the gas report summary by looking for "Function Name".
# 3. Go line by line and capture the gas report for functions that we are interested in.
# 4. When we reach the end of the report, break out of the for-loop.
try:
test_output = subprocess.check_output('FOUNDRY_FUZZ_RUNS=100 forge test --no-match-path \'test/instances/*\' --no-match-contract \'MultiToken__transferFrom\' --gas-report', shell=True).decode()
# HACK: We have to ignore certains tests that fail during gas benchmarking.
SKIP_TESTS = [
# ZombieInterestTest tests with high fuzz amounts
"test_zombie_interest_short_lp",
"test_zombie_interest_long_lp",
"test_zombie_long",
"test_zombie_short",
"test_zombie_long_short",
# IntraCheckpointNettingTest tests with high fuzz amounts
"test_netting_fuzz",
# ExtremeInputs tests with high fuzz amounts
"test__updateLiquidity__extremeValues__fuzz",
"test_short_below_minimum_share_reserves",
]
SKIP_CONTRACTS = ["MultiToken__transferFrom", "ExtremeInputs", "ZombieInterestTest", "IntraCheckpointNettingTest"]
process = subprocess.Popen(
f"FOUNDRY_FUZZ_RUNS=100 forge test --no-match-path 'test/instances/*' --no-match-test '{'|'.join(SKIP_TESTS)}' --no-match-contract '{'|'.join(SKIP_CONTRACTS)}' --gas-report",
shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
universal_newlines=True,
)
capture = []
found_contract: bool = False
found_report: bool = False
if process.stdout:
# Read the output line by line
for line in iter(process.stdout.readline, ""):
print(line)
# Split the line into columns which we can use to check if we are in the gas report summary.
cols = [col.strip() for col in line.split("|") if col.strip() != ""]

# Once we have found the contract look for the gas report summary.
if not found_contract and CONTRACT_NAME in line and len(cols) > 0:
found_contract = True

# Once we have found the gas report summary, start capturing the gas report.
if found_contract and not found_report and "Function Name" in line:
found_report = True

# Now, go line by line and capture the gas report for functions that we are interested in.
elif found_report and len(line) > 0 and len(cols) == 6:
function_name = cols[0]
if function_name in FUNCTION_NAMES:
capture += [
{
"name": f"{cols[0]}: min",
"value": cols[1],
"unit": "gas",
}
]
capture += [
{
"name": f"{cols[0]}: avg",
"value": cols[2],
"unit": "gas",
}
]
capture += [
{
"name": f"{cols[0]}: max",
"value": cols[3],
"unit": "gas",
}
]
# When we reach the end of the report, break out of the for-loop.
elif found_report:
break

# Wait for the process to finish.
process.wait()
if process.returncode != 0 and process.stderr:
print(process.stderr.read())
exit(1)

with open(OUTPUT_PATH, "w", encoding="utf-8") as f:
json.dump(capture, f)

except subprocess.CalledProcessError as e:
print(e.output)
exit(1)
capture = []
found_contract = ""
found_report = False
for line in test_output.split("\n"):
if not found_contract and CONTRACT_NAME in line:
found_contract = True
if found_contract and not found_report and "Function Name" in line:
found_report = True
elif found_report and len(line) > 0:
cols = line.split("|")
function_name = cols[1].strip()
if function_name in FUNCTION_NAMES:
capture += [
{
"name": f"{cols[1].strip()}: min",
"value": cols[2].strip(),
"unit": "gas",
}
]
capture += [
{
"name": f"{cols[1].strip()}: avg",
"value": cols[3].strip(),
"unit": "gas",
}
]
capture += [
{
"name": f"{cols[1].strip()}: max",
"value": cols[5].strip(),
"unit": "gas",
}
]
elif found_report:
break

with open(OUTPUT_PATH, "w") as f:
json.dump(capture, f)
154 changes: 154 additions & 0 deletions test/instances/erc4626/ScrvUSD.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
// SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.8.20;

import { stdStorage, StdStorage } from "forge-std/Test.sol";
import { IERC20 } from "../../../contracts/src/interfaces/IERC20.sol";
import { IERC4626 } from "../../../contracts/src/interfaces/IERC4626.sol";
import { IHyperdrive } from "../../../contracts/src/interfaces/IHyperdrive.sol";
import { HyperdriveUtils } from "../../utils/HyperdriveUtils.sol";
import { InstanceTest } from "../../utils/InstanceTest.sol";
import { Lib } from "../../utils/Lib.sol";
import { ERC4626HyperdriveInstanceTest } from "./ERC4626HyperdriveInstanceTest.t.sol";

interface ISCRVUSD {
function lastProfitUpdate() external view returns (uint256);
function fullProfitUnlockDate() external view returns (uint256);
}

contract scrvUSDHyperdriveTest is ERC4626HyperdriveInstanceTest {
using HyperdriveUtils for uint256;
using HyperdriveUtils for IHyperdrive;
using Lib for *;
using stdStorage for StdStorage;

/// @dev The crvUSD contract.
IERC20 internal constant CRVUSD =
IERC20(0xf939E0A03FB07F59A73314E73794Be0E57ac1b4E);

/// @dev The scrvUSD contract.
IERC4626 internal constant SCRVUSD =
IERC4626(0x0655977FEb2f289A4aB78af67BAB0d17aAb84367);

/// @dev Whale accounts.
address internal CRVUSD_TOKEN_WHALE =
address(0x0a7b9483030994016567b3B1B4bbB865578901Cb);
address[] internal baseTokenWhaleAccounts = [CRVUSD_TOKEN_WHALE];
address internal SCRVUSD_TOKEN_WHALE =
address(0x3Da232a0c0A5C59918D7B5fF77bf1c8Fc93aeE1B);
address[] internal vaultSharesTokenWhaleAccounts = [SCRVUSD_TOKEN_WHALE];

/// @notice Instantiates the instance testing suite with the configuration.
constructor()
InstanceTest(
InstanceTestConfig({
name: "Hyperdrive",
kind: "ERC4626Hyperdrive",
decimals: 18,
baseTokenWhaleAccounts: baseTokenWhaleAccounts,
vaultSharesTokenWhaleAccounts: vaultSharesTokenWhaleAccounts,
baseToken: CRVUSD,
vaultSharesToken: SCRVUSD,
shareTolerance: 1e3,
minimumShareReserves: 1e15,
minimumTransactionAmount: 1e15,
positionDuration: POSITION_DURATION,
fees: IHyperdrive.Fees({
curve: 0,
flat: 0,
governanceLP: 0,
governanceZombie: 0
}),
enableBaseDeposits: true,
enableShareDeposits: true,
enableBaseWithdraws: true,
enableShareWithdraws: true,
baseWithdrawError: new bytes(0),
isRebasing: false,
shouldAccrueInterest: true,
// The base test tolerances.
closeLongWithBaseTolerance: 20,
roundTripLpInstantaneousWithBaseTolerance: 1e5,
roundTripLpWithdrawalSharesWithBaseTolerance: 1e6,
roundTripLongInstantaneousWithBaseUpperBoundTolerance: 1e3,
roundTripLongInstantaneousWithBaseTolerance: 1e5,
roundTripLongMaturityWithBaseUpperBoundTolerance: 1e3,
roundTripLongMaturityWithBaseTolerance: 1e5,
roundTripShortInstantaneousWithBaseUpperBoundTolerance: 1e3,
roundTripShortInstantaneousWithBaseTolerance: 1e5,
roundTripShortMaturityWithBaseTolerance: 1e5,
// The share test tolerances.
closeLongWithSharesTolerance: 20,
closeShortWithSharesTolerance: 100,
roundTripLpInstantaneousWithSharesTolerance: 1e7,
roundTripLpWithdrawalSharesWithSharesTolerance: 1e7,
roundTripLongInstantaneousWithSharesUpperBoundTolerance: 1e3,
roundTripLongInstantaneousWithSharesTolerance: 1e5,
roundTripLongMaturityWithSharesUpperBoundTolerance: 1e3,
roundTripLongMaturityWithSharesTolerance: 1e5,
roundTripShortInstantaneousWithSharesUpperBoundTolerance: 1e3,
roundTripShortInstantaneousWithSharesTolerance: 1e5,
roundTripShortMaturityWithSharesTolerance: 1e5,
// The verification tolerances.
verifyDepositTolerance: 2,
verifyWithdrawalTolerance: 2
})
)
{}

/// @notice Forge function that is invoked to setup the testing environment.
function setUp() public override __mainnet_fork(21_188_049) {
// Invoke the Instance testing suite setup.
super.setUp();
}

/// Helpers ///

/// @dev Advance time and accrue interest.
/// @param timeDelta The time to advance.
/// @param variableRate The variable rate.
function advanceTime(
uint256 timeDelta,
int256 variableRate
) internal override {
// Get the total assets before advancing time.
uint256 totalAssets = SCRVUSD.totalAssets();

// Advance the time.
vm.warp(block.timestamp + timeDelta);

// Accrue interest in the scrvUSD market. This amounts to manually
// updating the total supply assets by updating the crvUSD balance of
// scrvUSD.
(totalAssets, ) = totalAssets.calculateInterest(
variableRate,
timeDelta
);

// scrvUSD profits can be unlocked over a period of time, which affects
// the totalSupply and pricePerShare according to the unlocking rate.
// We exclude this factor by updating the unlock date and lastProfitUpdate
// according to the timeDelta.

uint256 fullProfitUnlockDate = ISCRVUSD(address(SCRVUSD))
.fullProfitUnlockDate();
uint256 lastProfitUpdate = ISCRVUSD(address(SCRVUSD))
.lastProfitUpdate();

bytes32 fullProfitLocation = bytes32(uint256(38));
bytes32 lastProfitLocation = bytes32(uint256(40));

vm.store(
address(SCRVUSD),
fullProfitLocation,
bytes32(fullProfitUnlockDate + timeDelta)
);
vm.store(
address(SCRVUSD),
lastProfitLocation,
bytes32(lastProfitUpdate + timeDelta)
);

bytes32 idleLocation = bytes32(uint256(22));
vm.store(address(SCRVUSD), idleLocation, bytes32(totalAssets));
}
}

0 comments on commit 77a52c9

Please sign in to comment.