Skip to content

Commit

Permalink
test: support native eth in integration tests (#340)
Browse files Browse the repository at this point in the history
see PR for changelog
  • Loading branch information
wadealexc authored Nov 29, 2023
1 parent d804820 commit 5a6bca7
Show file tree
Hide file tree
Showing 9 changed files with 938 additions and 54 deletions.
4 changes: 2 additions & 2 deletions src/contracts/libraries/BeaconChainProofs.sol
Original file line number Diff line number Diff line change
Expand Up @@ -453,7 +453,7 @@ library BeaconChainProofs {
/**
* @dev Retrieves a validator's pubkey hash
*/
function getPubkeyHash(bytes32[] memory validatorFields) internal pure returns (bytes32) {
function getPubkeyHash(bytes32[] memory validatorFields) internal pure returns (bytes32) {
return
validatorFields[VALIDATOR_PUBKEY_INDEX];
}
Expand All @@ -466,7 +466,7 @@ library BeaconChainProofs {
/**
* @dev Retrieves a validator's effective balance (in gwei)
*/
function getEffectiveBalanceGwei(bytes32[] memory validatorFields) internal pure returns (uint64) {
function getEffectiveBalanceGwei(bytes32[] memory validatorFields) internal pure returns (uint64) {
return
Endian.fromLittleEndianUint64(validatorFields[VALIDATOR_BALANCE_INDEX]);
}
Expand Down
45 changes: 29 additions & 16 deletions src/test/integration/IntegrationBase.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ abstract contract IntegrationBase is IntegrationDeployer {
function _newRandomStaker() internal returns (User, IStrategy[] memory, uint[] memory) {
(User staker, IStrategy[] memory strategies, uint[] memory tokenBalances) = _randUser();

assert_HasUnderlyingTokenBalances(staker, strategies, tokenBalances, "_newStaker: failed to award token balances");
assert_HasUnderlyingTokenBalances(staker, strategies, tokenBalances, "_newRandomStaker: failed to award token balances");

return (staker, strategies, tokenBalances);
}
Expand All @@ -33,9 +33,9 @@ abstract contract IntegrationBase is IntegrationDeployer {
operator.registerAsOperator();
operator.depositIntoEigenlayer(strategies, tokenBalances);

assert_Snap_AddedStakerShares(operator, strategies, tokenBalances, "_newOperator: failed to add delegatable shares");
assert_Snap_AddedOperatorShares(operator, strategies, tokenBalances, "_newOperator: failed to award shares to operator");
assertTrue(delegationManager.isOperator(address(operator)), "_newOperator: operator should be registered");
assert_Snap_AddedStakerShares(operator, strategies, tokenBalances, "_newRandomOperator: failed to add delegatable shares");
assert_Snap_AddedOperatorShares(operator, strategies, tokenBalances, "_newRandomOperator: failed to award shares to operator");
assertTrue(delegationManager.isOperator(address(operator)), "_newRandomOperator: operator should be registered");

return (operator, strategies, tokenBalances);
}
Expand Down Expand Up @@ -88,9 +88,15 @@ abstract contract IntegrationBase is IntegrationDeployer {

uint actualShares;
if (strat == BEACONCHAIN_ETH_STRAT) {
// TODO
// actualShares = eigenPodManager.podOwnerShares(address(user));
revert("unimplemented");
// This method should only be used for tests that handle positive
// balances. Negative balances are an edge case that require
// the own tests and helper methods.
int shares = eigenPodManager.podOwnerShares(address(user));
if (shares < 0) {
revert("assert_HasExpectedShares: negative shares");
}

actualShares = uint(shares);
} else {
actualShares = strategyManager.stakerStrategyShares(address(user), strat);
}
Expand Down Expand Up @@ -251,9 +257,7 @@ abstract contract IntegrationBase is IntegrationDeployer {

uint tokenBalance = tokenBalances[i];
if (strat == BEACONCHAIN_ETH_STRAT) {
// TODO - need to calculate this
// expectedShares[i] = eigenPodManager.underlyingToShares(tokenBalance);
revert("_calculateExpectedShares: unimplemented for native eth");
expectedShares[i] = tokenBalances[i];
} else {
expectedShares[i] = strat.underlyingToShares(tokenBalance);
}
Expand All @@ -271,9 +275,7 @@ abstract contract IntegrationBase is IntegrationDeployer {
IStrategy strat = strategies[i];

if (strat == BEACONCHAIN_ETH_STRAT) {
// TODO - need to calculate this
// expectedTokens[i] = eigenPodManager.underlyingToShares(tokenBalance);
revert("_calculateExpectedShares: unimplemented for native eth");
expectedTokens[i] = shares[i];
} else {
expectedTokens[i] = strat.sharesToUnderlying(shares[i]);
}
Expand Down Expand Up @@ -323,8 +325,15 @@ abstract contract IntegrationBase is IntegrationDeployer {
IStrategy strat = strategies[i];

if (strat == BEACONCHAIN_ETH_STRAT) {
// curShares[i] = eigenPodManager.podOwnerShares(address(staker));
revert("TODO: unimplemented");
// This method should only be used for tests that handle positive
// balances. Negative balances are an edge case that require
// the own tests and helper methods.
int shares = eigenPodManager.podOwnerShares(address(staker));
if (shares < 0) {
revert("_getStakerShares: negative shares");
}

curShares[i] = uint(shares);
} else {
curShares[i] = strategyManager.stakerStrategyShares(address(staker), strat);
}
Expand All @@ -349,7 +358,11 @@ abstract contract IntegrationBase is IntegrationDeployer {
uint[] memory balances = new uint[](tokens.length);

for (uint i = 0; i < tokens.length; i++) {
balances[i] = tokens[i].balanceOf(address(staker));
if (tokens[i] == NATIVE_ETH) {
balances[i] = address(staker).balance;
} else {
balances[i] = tokens[i].balanceOf(address(staker));
}
}

return balances;
Expand Down
88 changes: 66 additions & 22 deletions src/test/integration/IntegrationDeployer.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ import "src/contracts/permissions/PauserRegistry.sol";

import "src/test/mocks/EmptyContract.sol";
import "src/test/mocks/ETHDepositMock.sol";
import "src/test/mocks/BeaconChainOracleMock.sol";
import "src/test/integration/mocks/BeaconChainOracleMock.t.sol";
import "src/test/integration/mocks/BeaconChainMock.t.sol";

import "src/test/integration/User.t.sol";

Expand Down Expand Up @@ -54,6 +55,7 @@ abstract contract IntegrationDeployer is Test, IUserDeployer {
// Mock Contracts to deploy
ETHPOSDepositMock ethPOSDeposit;
BeaconChainOracleMock beaconChainOracle;
BeaconChainMock public beaconChain;

// ProxyAdmin
ProxyAdmin eigenLayerProxyAdmin;
Expand All @@ -72,7 +74,10 @@ abstract contract IntegrationDeployer is Test, IUserDeployer {
// Constants
uint64 constant MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR = 32e9;
uint64 constant GOERLI_GENESIS_TIME = 1616508000;

IStrategy constant BEACONCHAIN_ETH_STRAT = IStrategy(0xbeaC0eeEeeeeEEeEeEEEEeeEEeEeeeEeeEEBEaC0);
IERC20 constant NATIVE_ETH = IERC20(0xbeaC0eeEeeeeEEeEeEEEEeeEEeEeeeEeeEEBEaC0);

uint constant MIN_BALANCE = 1e6;
uint constant MAX_BALANCE = 5e6;

Expand All @@ -85,12 +90,12 @@ abstract contract IntegrationDeployer is Test, IUserDeployer {
uint constant NO_ASSETS = (FLAG << 0); // will have no assets
uint constant HOLDS_LST = (FLAG << 1); // will hold some random amount of LSTs
uint constant HOLDS_ETH = (FLAG << 2); // will hold some random amount of ETH
uint constant HOLDS_MIX = (FLAG << 3); // will hold a mix of LSTs and ETH
uint constant HOLDS_ALL = (FLAG << 3); // will hold every LST and ETH

/// @dev User contract flags
/// These are used with _configRand to determine what User contracts can be deployed
uint constant DEFAULT = (FLAG << 0);
uint constant SIGNED_METHODS = (FLAG << 1);
uint constant ALT_METHODS = (FLAG << 1);

// /// @dev Withdrawal flags
// /// These are used with _configRand to determine how a user conducts a withdrawal
Expand All @@ -117,10 +122,10 @@ abstract contract IntegrationDeployer is Test, IUserDeployer {
assetTypeToStr[NO_ASSETS] = "NO_ASSETS";
assetTypeToStr[HOLDS_LST] = "HOLDS_LST";
assetTypeToStr[HOLDS_ETH] = "HOLDS_ETH";
assetTypeToStr[HOLDS_MIX] = "HOLDS_MIX";
assetTypeToStr[HOLDS_ALL] = "HOLDS_ALL";

userTypeToStr[DEFAULT] = "DEFAULT";
userTypeToStr[SIGNED_METHODS] = "SIGNED_METHODS";
userTypeToStr[ALT_METHODS] = "ALT_METHODS";
}

function setUp() public virtual {
Expand Down Expand Up @@ -253,7 +258,12 @@ abstract contract IntegrationDeployer is Test, IUserDeployer {
ethStrats.push(BEACONCHAIN_ETH_STRAT);
mixedStrats.push(BEACONCHAIN_ETH_STRAT);

// Create time machine and set block timestamp forward so we can create EigenPod proofs in the past
timeMachine = new TimeMachine();
timeMachine.setProofGenStartTime(2 hours);

// Create mock beacon chain / proof gen interface
beaconChain = new BeaconChainMock(timeMachine, beaconChainOracle);
}

/// @dev Deploy a strategy and its underlying token, push to global lists of tokens/strategies, and whitelist
Expand Down Expand Up @@ -291,8 +301,6 @@ abstract contract IntegrationDeployer is Test, IUserDeployer {
emit log_named_uint("_configRand: set random seed to: ", _randomSeed);
random = keccak256(abi.encodePacked(_randomSeed));

emit log_named_uint("_configRand: allowed asset types: ", _assetTypes);

// Convert flag bitmaps to bytes of set bits for easy use with _randUint
assetTypes = _bitmapToBytes(_assetTypes);
userTypes = _bitmapToBytes(_userTypes);
Expand Down Expand Up @@ -328,11 +336,12 @@ abstract contract IntegrationDeployer is Test, IUserDeployer {
User user;
if (userType == DEFAULT) {
user = new User();
} else if (userType == SIGNED_METHODS) {
// User will use `delegateToBySignature` and `depositIntoStrategyWithSignature`
user = User(new User_SignedMethods());
} else if (userType == ALT_METHODS) {
// User will use nonstandard methods like:
// `delegateToBySignature` and `depositIntoStrategyWithSignature`
user = User(new User_AltMethods());
} else {
revert("_newUser: unimplemented userType");
revert("_randUser: unimplemented userType");
}

// For the specific asset selection we made, get a random assortment of
Expand All @@ -348,9 +357,10 @@ abstract contract IntegrationDeployer is Test, IUserDeployer {
/// NO_ASSETS - return will be empty
/// HOLDS_LST - `strategies` will be a random subset of initialized strategies
/// `tokenBalances` will be the user's balances in each token
/// HOLDS_ETH - `strategies` will only contain BEACON_CHAIN_ETH_STRAT, and
/// HOLDS_ETH - `strategies` will only contain BEACONCHAIN_ETH_STRAT, and
/// `tokenBalances` will contain the user's eth balance
/// HOLDS_MIX - random combination of `HOLDS_LST` and `HOLDS_ETH`
/// HOLDS_ALL - `strategies` will contain ALL initialized strategies AND BEACONCHAIN_ETH_STRAT, and
/// `tokenBalances` will contain random token/eth balances accordingly
function _dealRandAssets(User user, uint assetType) internal returns (IStrategy[] memory, uint[] memory) {

IStrategy[] memory strategies;
Expand Down Expand Up @@ -378,14 +388,42 @@ abstract contract IntegrationDeployer is Test, IUserDeployer {
tokenBalances[i] = balance;
strategies[i] = strat;
}

return (strategies, tokenBalances);
} else if (assetType == HOLDS_ETH) {
revert("_getRandAssets: HOLDS_ETH unimplemented");
} else if (assetType == HOLDS_MIX) {
revert("_getRandAssets: HOLDS_MIX unimplemented");
strategies = new IStrategy[](1);
tokenBalances = new uint[](1);

// Award the user with a random multiple of 32 ETH
uint amount = 32 ether * _randUint({ min: 1, max: 3 });
cheats.deal(address(user), amount);

strategies[0] = BEACONCHAIN_ETH_STRAT;
tokenBalances[0] = amount;
} else if (assetType == HOLDS_ALL) {
uint numLSTs = lstStrats.length;
strategies = new IStrategy[](numLSTs + 1);
tokenBalances = new uint[](numLSTs + 1);

// For each LST, award the user a random balance of the underlying token
for (uint i = 0; i < numLSTs; i++) {
IStrategy strat = lstStrats[i];
IERC20 underlyingToken = strat.underlyingToken();

uint balance = _randUint({ min: MIN_BALANCE, max: MAX_BALANCE });
StdCheats.deal(address(underlyingToken), address(user), balance);

tokenBalances[i] = balance;
strategies[i] = strat;
}

// Award the user with a random multiple of 32 ETH
uint amount = 32 ether * _randUint({ min: 1, max: 3 });
cheats.deal(address(user), amount);

// Add BEACONCHAIN_ETH_STRAT and eth balance
strategies[numLSTs] = BEACONCHAIN_ETH_STRAT;
tokenBalances[numLSTs] = amount;
} else {
revert("_getRandAssets: assetType unimplemented");
revert("_dealRandAssets: assetType unimplemented");
}

return (strategies, tokenBalances);
Expand Down Expand Up @@ -467,10 +505,16 @@ abstract contract IntegrationDeployer is Test, IUserDeployer {

for (uint i = 0; i < strategies.length; i++) {
IStrategy strat = strategies[i];
IERC20 underlyingToken = strat.underlyingToken();

emit log_named_string("token name: ", IERC20Metadata(address(underlyingToken)).name());
emit log_named_uint("token balance: ", tokenBalances[i]);
if (strat == BEACONCHAIN_ETH_STRAT) {
emit log_named_string("token name: ", "Native ETH");
emit log_named_uint("token balance: ", tokenBalances[i]);
} else {
IERC20 underlyingToken = strat.underlyingToken();

emit log_named_string("token name: ", IERC20Metadata(address(underlyingToken)).name());
emit log_named_uint("token balance: ", tokenBalances[i]);
}
}
}
}
5 changes: 2 additions & 3 deletions src/test/integration/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ Looking at the current tests is a good place to start.

During the test, the config passed into `_configRand` will randomly generate only the values you configure:
* `assetTypes` affect the assets granted to Users when they are first created. You can use this to ensure your flows and assertions work when users are holding only LSTs, native ETH, or some combination.
* `userTypes` affect the actual `User` contract being deployed. The `DEFAULT` flag deploys the base `User` contract, while `SIGNED_METHODS` deploys a version that derives from the same contract, but overrides some methods to use "functionWithSignature" variants.
* `userTypes` affect the actual `User` contract being deployed. The `DEFAULT` flag deploys the base `User` contract, while `ALT_METHODS` deploys a version that derives from the same contract, but overrides some methods to use "functionWithSignature" and other variants.

Here's an example:

Expand All @@ -27,7 +27,7 @@ function testFuzz_deposit_delegate_EXAMPLE(uint24 _random) public {
_configRand({
_randomSeed: _random,
_assetTypes: HOLDS_LST,
_userTypes: DEFAULT | SIGNED_METHODS
_userTypes: DEFAULT | ALT_METHODS
});
// Because of the `assetTypes` flags above, this will create two Users for our test,
Expand Down Expand Up @@ -93,5 +93,4 @@ function testFuzz_deposit_delegate_EXAMPLE(uint24 _random) public {
### What needs to be done?

* Suggest or PR cleanup if you have ideas. Currently, the `IntegrationDeployer` contract is pretty messy.
* Currently, the only supported assetTypes are `NO_ASSETS` and `HOLDS_LST`. There are flags for `HOLDS_ETH` and `HOLDS_MIXED`, but we need to implement `EigenPod` proof generation/usage before they can be used.
* Coordinate in Slack to pick out some user flows to write tests for!
12 changes: 12 additions & 0 deletions src/test/integration/TimeMachine.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ contract TimeMachine is Test {
bool pastExists = false;
uint lastSnapshot;

uint64 public proofGenStartTime;

function createSnapshot() public returns (uint) {
uint snapshot = cheats.snapshot();
lastSnapshot = snapshot;
Expand All @@ -30,4 +32,14 @@ contract TimeMachine is Test {
function warpToPresent(uint curState) public {
cheats.revertTo(curState);
}

/// @dev Sets the timestamp we use for proof gen to now,
/// then sets block timestamp to now + secondsAgo.
///
/// This means we can create mock proofs using an oracle time
/// of `proofGenStartTime`.
function setProofGenStartTime(uint secondsAgo) public {
proofGenStartTime = uint64(block.timestamp);
cheats.warp(block.timestamp + secondsAgo);
}
}
Loading

0 comments on commit 5a6bca7

Please sign in to comment.