Skip to content

Commit

Permalink
Merge branch 'main' into feat/echidna-erc4626-tests
Browse files Browse the repository at this point in the history
  • Loading branch information
sandybradley committed Aug 4, 2023
2 parents bbad294 + c9e4f4c commit 9cf716a
Show file tree
Hide file tree
Showing 16 changed files with 250 additions and 157 deletions.
5 changes: 1 addition & 4 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,6 @@
[submodule "lib/openzeppelin-contracts"]
path = lib/openzeppelin-contracts
url = https://github.com/OpenZeppelin/openzeppelin-contracts
[submodule "lib/solady"]
path = lib/solady
url = https://github.com/Vectorized/solady
[submodule "lib/properties"]
path = lib/properties
url = https://github.com/crytic/properties
url = https://github.com/crytic/properties
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,11 +85,11 @@ mev-eth is has a couple core modules and functionality.

### Ownership

First, because mev-eth is a centralized LSR, and dependent on Manifold Finance to actuall stake the Ether, ownership is given, with a couple key roles. An address can be designed as an operator, operators are intended to be automated, keeper style addresses which can redirect Ether to beacon chain validators. Operators are also expected to post oracle updates from the Beacon chain to update the contract on any rewards accrued. Additionally, there is the ManifoldOwner role, which gives the rights to control key management functions such as withdrawing fees, and setting various configuration variables such as cache balance threshold, where Ether will be held to buffer withdrawls.
First, because mev-eth is a centralized LSR, and dependent on Manifold Finance to actually stake the Ether, ownership is given, with a couple key roles. An address can be designed as an operator, operators are intended to be automated, keeper style addresses which can redirect Ether to beacon chain validators. Operators are also expected to post oracle updates from the Beacon chain to update the contract on any rewards accrued. Additionally, there is the ManifoldOwner role, which gives the rights to control key management functions such as withdrawing fees, and setting various configuration variables such as cache balance threshold, where Ether will be held to buffer withdrawals.

### Token Design

mev-eth supports the ERC4626 interface to handle itself as an LSR. This allows many key integrations, such as Yearn Vault integrations, or any other protocols which require a yield source. This also means that mev-eth supports ERC20 as a base transferable token. Breaking from ERC4626, mev-eth also supports deposits via its fallback (recieve technically) function for call-data free deposits.
mev-eth supports the ERC4626 interface to handle itself as an LSR. This allows many key integrations, such as Yearn Vault integrations, or any other protocols which require a yield source. This also means that mev-eth supports ERC20 as a base transferable token. Breaking from ERC4626, mev-eth also supports deposits via its fallback (receive technically) function for call-data free deposits.

The token keeps track of this by accounting with simple fractional math, which while a bit confusing, is the most simplistic approach, where each mev-eth token is a share which accumulates interest, and as interest grows will eventually be worth greater than it 1 eth per.

Expand Down
1 change: 0 additions & 1 deletion lib/solady
Submodule solady deleted from a549da
2 changes: 1 addition & 1 deletion nix/packages.nix
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
configurePhase = ''
cp -r ${inputs.forge-std} lib/forge-std
cp -r ${inputs.openzeppelin-contracts} lib/openzeppelin-contracts
cp -r ${inputs.solady} lib/solady
cp -r ${inputs.pigeon} lib/pigeon
cp -r ${inputs.solmate} lib/solmate
'';

Expand Down
207 changes: 99 additions & 108 deletions src/MevEth.sol

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion src/MevEthShareVault.sol
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ contract MevEthShareVault is Auth, IMevEthShareVault {
/// Validators associated with the MevETH protocol set the block builder's address as the feeRecepient for the block.
/// The block builder attaches a transaction to the end of the block sending the MEV rewards to the MevEthShareVault.
/// This then emits the RewardPayment event, allowing the offchain operators to track the protocolFeesOwed.
/// This approach trusts that the operators are acting honestly and the protocolFeesOwed is accurately caculated.
/// This approach trusts that the operators are acting honestly and the protocolFeesOwed is accurately calculated.

function logRewards(uint128 protocolFeesOwed) external onlyOperator {
// Cahce the protocol balance
Expand Down
22 changes: 5 additions & 17 deletions src/interfaces/Errors.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,10 @@ pragma solidity 0.8.19;

interface MevEthErrors {
/// Errors
error BelowMinimum();
error DepositFailed();
error InsufficientBufferedEth();
error TooManyValidatorRegistrations();
error ExceedsStakingAllowance();
error StakingPaused();
error NotEnoughEth();
error DepositTooLow();
error ZeroShares();
error ZeroValue();
error ReportedBeaconValidatorsGreaterThanTotalValidators();
error ReportedBeaconValidatorsDecreased();
error BeaconDepositFailed();
error InvalidWithdrawalCredentials();
error OperatorsNotCommitted();
error OperatorMaxValidatorsReached();
error OperatorNotCommitted();
error MaxValidatorError();
error InvalidOperator();
error ValidatorPreviouslyRegistered();
error NotAuthorized();
error DepositTooSmall();
error InvalidSender();
error PrematureStakingModuleUpdateFinalization();
Expand All @@ -39,4 +22,9 @@ interface MevEthErrors {
error FeesTooHigh();
error WrongDepositAmount();
error UnAuthorizedCaller();
error WithdrawTooSmall();
error NotFinalised();
error AlreadyClaimed();
error AlreadyFinalised();
error IndexExceedsQueueLength();
}
1 change: 1 addition & 0 deletions src/interfaces/IStakingModule.sol
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ interface IStakingModule {
function VALIDATOR_DEPOSIT_SIZE() external view returns (uint256);

// onlyAdmin Functions
function payRewards() external;
function payValidatorWithdraw(uint256 amount) external;
function recoverToken(address token, address recipient, uint256 amount) external;
}
16 changes: 10 additions & 6 deletions src/libraries/Auth.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,16 @@ pragma solidity 0.8.19;

contract Auth {
error Unauthorized();
error WrongRole();

enum Roles {
OPERATOR,
ADMIN
}
error NoAdmin();

event AdminAdded(address indexed newAdmin);
event AdminDeleted(address indexed oldAdmin);
event OperatorAdded(address indexed newOperator);
event OperatorDeleted(address indexed oldOperator);

// admin counter (assuming 255 admins to be max)
uint8 adminsCounter;

// Keeps track of all operators
mapping(address => bool) public operators;

Expand All @@ -23,6 +21,9 @@ contract Auth {

constructor(address initialAdmin) {
admins[initialAdmin] = true;
unchecked {
++adminsCounter;
}
operators[initialAdmin] = true;
}

Expand All @@ -48,11 +49,14 @@ contract Auth {
Maintenance Functions
//////////////////////////////////////////////////////////////*/
function addAdmin(address newAdmin) external onlyAdmin {
++adminsCounter;
admins[newAdmin] = true;
emit AdminAdded(newAdmin);
}

function deleteAdmin(address oldAdmin) external onlyAdmin {
--adminsCounter;
if (adminsCounter == 0) revert NoAdmin();
admins[oldAdmin] = false;
emit AdminDeleted(oldAdmin);
}
Expand Down
6 changes: 4 additions & 2 deletions test/MevEthTest.sol
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ contract MevEthTest is Test {
uint16 constant POLYGON_ID = 109;
uint16 constant ARBITRUM_ID = 110;

uint64 MODULE_UPDATE_TIME_DELAY = 7 days;

// Admin account

uint256 constant SAM_BACHA_PRIVATE_KEY = 0x01;
Expand Down Expand Up @@ -141,7 +143,7 @@ contract MevEthTest is Test {
// Helper function to update the staking module for testing
function _updateStakingModule(IStakingModule newStakingModule) internal {
// Commit update to the staking module
uint64 finalizationTimestamp = uint64(block.timestamp + mevEth.MODULE_UPDATE_TIME_DELAY());
uint64 finalizationTimestamp = uint64(block.timestamp + MODULE_UPDATE_TIME_DELAY);

vm.prank(SamBacha);
mevEth.commitUpdateStakingModule(newStakingModule);
Expand All @@ -159,7 +161,7 @@ contract MevEthTest is Test {
// Helper function to update the share vault for testing
function _updateShareVault(address newShareVault) internal {
// Commit update to the staking module
uint64 finalizationTimestamp = uint64(block.timestamp + mevEth.MODULE_UPDATE_TIME_DELAY());
uint64 finalizationTimestamp = uint64(block.timestamp + MODULE_UPDATE_TIME_DELAY);

vm.prank(SamBacha);
mevEth.commitUpdateMevEthShareVault(newShareVault);
Expand Down
4 changes: 3 additions & 1 deletion test/attacks/ReentrancyAttack.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ pragma solidity 0.8.19;
import "../MevEthTest.sol";

contract ReentrancyAttackTest is MevEthTest {
uint128 MAX_DEPOSIT = type(uint128).max;

function setUp() public override {
super.setUp();
vm.deal(User02, 0.1 ether);
Expand All @@ -20,7 +22,7 @@ contract ReentrancyAttackTest is MevEthTest {

function testAttack(uint128 amount) external payable {
vm.assume(amount > mevEth.MIN_DEPOSIT());
vm.assume(amount < mevEth.MAX_DEPOSIT() - 0.1 ether);
vm.assume(amount < MAX_DEPOSIT - 0.1 ether);
vm.deal(address(this), amount);
uint256 bal = address(this).balance;
mevEth.deposit{ value: amount }(amount, address(this));
Expand Down
56 changes: 56 additions & 0 deletions test/attacks/WithdrawalQueueDOS.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/// SPDX: License-Identifier: GPL-3.0-only
pragma solidity 0.8.19;

import "../MevEthTest.sol";

contract WithdrawalQueueAttackTest is MevEthTest {
function testDosAttackQueueLength() public {
vm.deal(User01, 63 ether);
vm.startPrank(User01);

IStakingModule.ValidatorData memory validatorData = mockValidatorData(User01, 32 ether / 1 gwei);

// 1. Attacker deposits 63 Eth to MevEth
weth.deposit{ value: 63 ether }();
weth.approve(address(mevEth), 63 ether);
mevEth.deposit(63 ether, User01);
assertEq(address(mevEth).balance, 63 ether);

// 2. `createValidator()` is called -> MevEth balance 31 Ether
vm.stopPrank();
vm.startPrank(Operator01);
mevEth.createValidator(validatorData);
assertEq(address(mevEth).balance, 31 ether);

vm.stopPrank();
vm.startPrank(User01);
// 3. Attackers withdraws 31 ETH -> MevEth balance 0 Ether
mevEth.withdraw(31 ether, User01, User01);
assertEq(address(mevEth).balance, 0 ether);

// 4. Attackers shares are still worth 32 ether
assertEq(mevEth.convertToAssets(mevEth.balanceOf(User01)), 32 ether);

// ~10 blocks worth of txs
for (uint256 i = 0; i < 1000; i++) {
// Attacker withdraws 0.01 ETH worth of shares, but because contract has no balance left the queueLength increases by 1.
// Attacker repeats step above many times.
mevEth.withdraw(0.011 ether, User01, User01);
}

vm.stopPrank();
vm.startPrank(Operator01);
// Give mevEth enough ether to process all withdrawals
vm.deal(address(mevEth), 100 ether);

// Block gas limit
mevEth.processWithdrawalQueue(mevEth.queueLength());
for (uint256 i = 1; i < 1001; i++) {
mevEth.claim(i);
}
assertEq(weth.balanceOf(User01), 42 ether);
assertEq(mevEth.queueLength(), 1000);
assertEq(mevEth.requestsFinalisedUntil(), 1000);
assertEq(mevEth.withdrawalAmountQueued(), 0);
}
}
13 changes: 7 additions & 6 deletions test/unit/Admin.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ contract MevAdminTest is MevEthTest {
function testNegativeAddOperator(address newOperator) public {
vm.assume(newOperator != 0x7109709ECfa91a80626fF3989D68f67F5b1DD12D);
vm.assume(newOperator != 0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf);
vm.assume(newOperator != Operator01);
vm.assume(newOperator != address(0));
vm.assume(newOperator != Operator01);
vm.expectRevert(Auth.Unauthorized.selector);
Expand Down Expand Up @@ -252,7 +253,7 @@ contract MevAdminTest is MevEthTest {

// Commit an update to the staking module and check the effects
vm.expectEmit(true, true, true, false, address(mevEth));
uint64 finalizationTimestamp = uint64(block.timestamp + mevEth.MODULE_UPDATE_TIME_DELAY());
uint64 finalizationTimestamp = uint64(block.timestamp + MODULE_UPDATE_TIME_DELAY);
emit StakingModuleUpdateCommitted(existingStakingModule, address(newModule), finalizationTimestamp);

vm.prank(SamBacha);
Expand Down Expand Up @@ -293,7 +294,7 @@ contract MevAdminTest is MevEthTest {
address existingStakingModule = address(mevEth.stakingModule());

// Commit an update to the staking module
uint64 finalizationTimestamp = uint64(block.timestamp + mevEth.MODULE_UPDATE_TIME_DELAY());
uint64 finalizationTimestamp = uint64(block.timestamp + MODULE_UPDATE_TIME_DELAY);

vm.prank(SamBacha);
mevEth.commitUpdateStakingModule(IStakingModule(address(newModule)));
Expand Down Expand Up @@ -329,7 +330,7 @@ contract MevAdminTest is MevEthTest {
// Commit a new staking module
DepositContract newModule = new DepositContract();
address existingStakingModule = address(mevEth.stakingModule());
uint64 finalizationTimestamp = uint64(block.timestamp + mevEth.MODULE_UPDATE_TIME_DELAY());
uint64 finalizationTimestamp = uint64(block.timestamp + MODULE_UPDATE_TIME_DELAY);
vm.prank(SamBacha);
mevEth.commitUpdateStakingModule(IStakingModule(address(newModule)));

Expand Down Expand Up @@ -417,7 +418,7 @@ contract MevAdminTest is MevEthTest {

// Commit an update to the staking module and check the effects
vm.expectEmit(true, true, true, false, address(mevEth));
uint64 finalizationTimestamp = uint64(block.timestamp + mevEth.MODULE_UPDATE_TIME_DELAY());
uint64 finalizationTimestamp = uint64(block.timestamp + MODULE_UPDATE_TIME_DELAY);
emit MevEthShareVaultUpdateCommitted(existingVault, newVault, finalizationTimestamp);

vm.prank(SamBacha);
Expand Down Expand Up @@ -458,7 +459,7 @@ contract MevAdminTest is MevEthTest {
address existingVault = address(mevEth.mevEthShareVault());

// Commit an update to the mev share vault
uint64 finalizationTimestamp = uint64(block.timestamp + mevEth.MODULE_UPDATE_TIME_DELAY());
uint64 finalizationTimestamp = uint64(block.timestamp + MODULE_UPDATE_TIME_DELAY);

vm.prank(SamBacha);
mevEth.commitUpdateMevEthShareVault(newVault);
Expand Down Expand Up @@ -495,7 +496,7 @@ contract MevAdminTest is MevEthTest {
address existingVault = address(mevEth.mevEthShareVault());

// Commit an update to the mev share vault
uint64 finalizationTimestamp = uint64(block.timestamp + mevEth.MODULE_UPDATE_TIME_DELAY());
uint64 finalizationTimestamp = uint64(block.timestamp + MODULE_UPDATE_TIME_DELAY);
vm.prank(SamBacha);
mevEth.commitUpdateMevEthShareVault(newVault);

Expand Down
10 changes: 10 additions & 0 deletions test/unit/ERC4626.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,15 @@ contract ERC4626Test is MevEthTest {
assertEq(base, 1 ether);
}

function testExcessDeposit() public {
vm.deal(User01, 1.1 ether);
vm.startPrank(User01);

vm.expectRevert(MevEthErrors.WrongDepositAmount.selector);
// Deposit 1 ETH into the mevETH contract with an excess payment
mevEth.deposit{ value: 1.1 ether }(1 ether, User01);
}

function testFuzzSimpleDeposit(uint256 amount) public {
vm.assume(amount > mevEth.MIN_DEPOSIT());
vm.assume(amount < 2 ** 128 - 1);
Expand Down Expand Up @@ -245,6 +254,7 @@ contract ERC4626Test is MevEthTest {

// Withdraw 1 mevETH
mevEth.withdraw(amount, User01, User01);

assertEq(mevEth.balanceOf(User01), 0 ether);
assertEq(weth.balanceOf(User01), amount);
}
Expand Down
31 changes: 31 additions & 0 deletions test/unit/MevEthRateProvider.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;

import "forge-std/console.sol";
import "../MevEthTest.sol";
import "src/interfaces/Errors.sol";
import "src/MevEthRateProvider.sol";
import "src/interfaces/IMevEth.sol";

contract MevEthRateProviderTest is MevEthTest {
MevETHRateProvider provider;

function setUp() public override {
super.setUp();
provider = new MevETHRateProvider(IMevEth(address(mevEth)));
}

function testRate() public {
assertEq(provider.getRate(), 1 ether);
}

function testRateUpdate() public {
vm.deal(address(this), 10 ether);
mevEth.deposit{ value: 10 ether }(10 ether, address(this));
address staker = address(mevEth.stakingModule());
vm.deal(staker, 2 ether);
vm.prank(SamBacha);
IStakingModule(staker).payRewards();
assertGt(provider.getRate(), 1 ether);
}
}
Loading

0 comments on commit 9cf716a

Please sign in to comment.