From 29de65aaa8792278af4971e9cfc1b9b8c75b56ea Mon Sep 17 00:00:00 2001 From: Samuel <8148384+samparsky@users.noreply.github.com> Date: Wed, 27 Mar 2024 15:03:56 -0400 Subject: [PATCH] ObolCollector implementation (#116) * feat: add obol collector impl * feat: stakewise split - wip * feat: stakewise tests * chore: forge fmt * chore: add obol collector implementation and tests * chore(test): update fuzz test implementation * chore: passed vaultToken for stakewise implementation * feat: ethersfi split implementation * chore: file rename * chore: renamed EthersFi to EtherFi * chore: base split implementation * chore: ran formatter on Base contracts * feat: base split & split factory implementation + refactoring * chore: fixed tests --------- Co-authored-by: cosminobol --- foundry.toml | 1 + script/OWRFactoryScript.s.sol | 14 +- script/ObolLidoSetupScript.sol | 249 +++++++++--------- script/ObolLidoSplitFactoryScript.s.sol | 28 +- src/base/BaseSplit.sol | 104 ++++++++ src/base/BaseSplitFactory.sol | 18 ++ src/collector/ObolCollector.sol | 26 ++ src/collector/ObolCollectorFactory.sol | 40 +++ src/controllers/ImmutableSplitController.sol | 2 +- .../ImmutableSplitControllerFactory.sol | 2 +- src/etherfi/ObolEtherfiSplit.sol | 55 ++++ src/etherfi/ObolEtherfiSplitFactory.sol | 43 +++ src/interfaces/IEigenLayer.sol | 235 +++++++++-------- src/interfaces/IweETH.sol | 9 + src/interfaces/IwstETH.sol | 2 +- src/lido/ObolLidoSplit.sol | 87 +----- src/lido/ObolLidoSplitFactory.sol | 32 +-- src/owr/OptimisticWithdrawalRecipient.sol | 2 +- .../OptimisticWithdrawalRecipientFactory.sol | 2 +- src/test/collector/ObolCollector.t.sol | 197 ++++++++++++++ src/test/collector/ObolCollectorFactory.t.sol | 32 +++ src/test/controllers/IMSC.t.sol | 2 +- src/test/controllers/IMSCFactory.t.sol | 16 +- src/test/eigenlayer/OELPCFactory.t.sol | 13 +- .../ObolEigenLayerPodController.t.sol | 4 +- .../integration/OELPCIntegration.t.sol | 4 +- src/test/etherfi/ObolEtherfiSplit.t.sol | 189 +++++++++++++ .../etherfi/ObolEtherfiSplitFactory.t.sol | 50 ++++ .../etherfi/ObolEtherfiSplitTestHelper.sol | 8 + .../ObolEtherfiSplitIntegrationTest.sol | 64 +++++ src/test/lido/ObolLIdoSplitFactory.t.sol | 35 +-- src/test/lido/ObolLidoSplit.t.sol | 52 ++-- .../integration/LidoSplitIntegrationTest.sol | 10 +- .../owr/OptimisticWithdrawalRecipient.t.sol | 16 +- ...OptimisticWithdrawalRecipientFactory.t.sol | 18 +- 35 files changed, 1179 insertions(+), 482 deletions(-) create mode 100644 src/base/BaseSplit.sol create mode 100644 src/base/BaseSplitFactory.sol create mode 100644 src/collector/ObolCollector.sol create mode 100644 src/collector/ObolCollectorFactory.sol create mode 100644 src/etherfi/ObolEtherfiSplit.sol create mode 100644 src/etherfi/ObolEtherfiSplitFactory.sol create mode 100644 src/interfaces/IweETH.sol create mode 100644 src/test/collector/ObolCollector.t.sol create mode 100644 src/test/collector/ObolCollectorFactory.t.sol create mode 100644 src/test/etherfi/ObolEtherfiSplit.t.sol create mode 100644 src/test/etherfi/ObolEtherfiSplitFactory.t.sol create mode 100644 src/test/etherfi/ObolEtherfiSplitTestHelper.sol create mode 100644 src/test/etherfi/integration/ObolEtherfiSplitIntegrationTest.sol diff --git a/foundry.toml b/foundry.toml index 4a7a30b..df6ef32 100644 --- a/foundry.toml +++ b/foundry.toml @@ -16,6 +16,7 @@ fs_permissions = [{ access = "read-write", path = "./"}] [rpc_endpoints] goerli = "${GOERLI_RPC_URL}" mainnet = "${MAINNET_RPC_URL}" +sepolia = "${SEPOLIA_RPC_URL}" # See more config options https://github.com/gakonst/foundry/tree/master/config [fmt] diff --git a/script/OWRFactoryScript.s.sol b/script/OWRFactoryScript.s.sol index 564559c..6568e95 100644 --- a/script/OWRFactoryScript.s.sol +++ b/script/OWRFactoryScript.s.sol @@ -5,19 +5,13 @@ import "forge-std/Script.sol"; import {OptimisticWithdrawalRecipientFactory} from "src/owr/OptimisticWithdrawalRecipientFactory.sol"; contract OWRFactoryScript is Script { - function run( - string memory _name, - address _ensReverseRegistrar, - address _ensOwner - ) external { + function run(string memory _name, address _ensReverseRegistrar, address _ensOwner) external { uint256 privKey = vm.envUint("PRIVATE_KEY"); - + vm.startBroadcast(privKey); - + new OptimisticWithdrawalRecipientFactory{salt: keccak256("obol.owrFactory.v1")}( - _name, - _ensReverseRegistrar, - _ensOwner + _name, _ensReverseRegistrar, _ensOwner ); vm.stopBroadcast(); diff --git a/script/ObolLidoSetupScript.sol b/script/ObolLidoSetupScript.sol index 8b59b71..ed2f7c8 100644 --- a/script/ObolLidoSetupScript.sol +++ b/script/ObolLidoSetupScript.sol @@ -3,24 +3,23 @@ pragma solidity 0.8.19; import "forge-std/Script.sol"; import {ISplitMain, SplitConfiguration} from "src/interfaces/ISplitMain.sol"; -import { ObolLidoSplitFactory } from "src/lido/ObolLidoSplitFactory.sol"; - +import {ObolLidoSplitFactory} from "src/lido/ObolLidoSplitFactory.sol"; /// @title ObolLidoScript /// @author Obol /// @notice Creates Split and ObolLidoSplit Adddresses /// -/// @dev Takes a json file following the format defined at ./data/lido-data-sample.json +/// @dev Takes a json file following the format defined at ./data/lido-data-sample.json /// and deploys split and ObolLido split contracts. /// /// It outputs the result of the script to "./result.json" /// -/// NOTE: It's COMPULSORY the json file supplied follows the arrangement format defined -/// in the sample file else the json parse will fail. +/// NOTE: It's COMPULSORY the json file supplied follows the arrangement format defined +/// in the sample file else the json parse will fail. /// /// /// To Run -/// +/// /// Step 1 fill in the appropriate details for env vars /// > cp .env.deployment .env /// @@ -29,136 +28,128 @@ import { ObolLidoSplitFactory } from "src/lido/ObolLidoSplitFactory.sol"; /// /// Step 3 Run forge script to simulate the execution of the transaction /// -/// > forge script script/ObolLidoSetupScript.sol:ObolLidoSetupScript --fork-url $RPC_URL -vvvv --sig "run(string,address,address)" "" $SPLITMAIN $OBOL_LIDO_SPLIT_FACTORY +/// > forge script script/ObolLidoSetupScript.sol:ObolLidoSetupScript --fork-url $RPC_URL -vvvv --sig +/// "run(string,address,address)" "" $SPLITMAIN +/// $OBOL_LIDO_SPLIT_FACTORY /// /// add --broadcast flag to broadcast to the public blockchain - contract ObolLidoSetupScript is Script { - - /// @dev invalid split accounts configuration - error InvalidSplit__TooFewAccounts(uint256 accountsLength); - /// @notice Array lengths of accounts & percentAllocations don't match - /// (`accountsLength` != `allocationsLength`) - /// @param accountsLength Length of accounts array - /// @param allocationsLength Length of percentAllocations array - error InvalidSplit__AccountsAndAllocationsMismatch(uint256 accountsLength, uint256 allocationsLength); - /// @notice Invalid percentAllocations sum `allocationsSum` must equal - /// `PERCENTAGE_SCALE` - /// @param allocationsSum Sum of percentAllocations array - error InvalidSplit__InvalidAllocationsSum(uint32 allocationsSum); - /// @notice Invalid accounts ordering at `index` - /// @param index Index of out-of-order account - error InvalidSplit__AccountsOutOfOrder(uint256 index); - /// @notice Invalid percentAllocation of zero at `index` - /// @param index Index of zero percentAllocation - error InvalidSplit__AllocationMustBePositive(uint256 index); - /// @notice Invalid distributorFee `distributorFee` cannot be greater than - /// 10% (1e5) - /// @param distributorFee Invalid distributorFee amount - error InvalidSplit__InvalidDistributorFee(uint32 distributorFee); - /// @notice Array of accounts size - /// @param size acounts size - error InvalidSplit__TooManyAccounts(uint256 size); - - uint256 internal constant PERCENTAGE_SCALE = 1e6; - uint256 internal constant MAX_DISTRIBUTOR_FEE = 1e5; - - - struct JsonSplitConfiguration { - address[] accounts; - address controller; - uint32 distributorFee; - uint32[] percentAllocations; + /// @dev invalid split accounts configuration + error InvalidSplit__TooFewAccounts(uint256 accountsLength); + /// @notice Array lengths of accounts & percentAllocations don't match + /// (`accountsLength` != `allocationsLength`) + /// @param accountsLength Length of accounts array + /// @param allocationsLength Length of percentAllocations array + error InvalidSplit__AccountsAndAllocationsMismatch(uint256 accountsLength, uint256 allocationsLength); + /// @notice Invalid percentAllocations sum `allocationsSum` must equal + /// `PERCENTAGE_SCALE` + /// @param allocationsSum Sum of percentAllocations array + error InvalidSplit__InvalidAllocationsSum(uint32 allocationsSum); + /// @notice Invalid accounts ordering at `index` + /// @param index Index of out-of-order account + error InvalidSplit__AccountsOutOfOrder(uint256 index); + /// @notice Invalid percentAllocation of zero at `index` + /// @param index Index of zero percentAllocation + error InvalidSplit__AllocationMustBePositive(uint256 index); + /// @notice Invalid distributorFee `distributorFee` cannot be greater than + /// 10% (1e5) + /// @param distributorFee Invalid distributorFee amount + error InvalidSplit__InvalidDistributorFee(uint32 distributorFee); + /// @notice Array of accounts size + /// @param size acounts size + error InvalidSplit__TooManyAccounts(uint256 size); + + uint256 internal constant PERCENTAGE_SCALE = 1e6; + uint256 internal constant MAX_DISTRIBUTOR_FEE = 1e5; + + struct JsonSplitConfiguration { + address[] accounts; + address controller; + uint32 distributorFee; + uint32[] percentAllocations; + } + + function run(string memory jsonFilePath, address splitMain, address obolLidoSplitFactory) external { + uint256 privKey = vm.envUint("PRIVATE_KEY"); + + string memory file = vm.readFile(jsonFilePath); + bytes memory parsedJson = vm.parseJson(file); + JsonSplitConfiguration[] memory configuration = abi.decode(parsedJson, (JsonSplitConfiguration[])); + _validateInputJson(configuration); + + // deploy the split and obol script + string memory jsonKey = "lidoObolDeploy"; + string memory finalJSON; + + for (uint256 j = 0; j < configuration.length; j++) { + string memory objKey = vm.toString(j); + // deploy split + JsonSplitConfiguration memory currentConfiguration = configuration[j]; + + vm.startBroadcast(privKey); + + address split = ISplitMain(splitMain).createSplit( + currentConfiguration.accounts, + currentConfiguration.percentAllocations, + currentConfiguration.distributorFee, + currentConfiguration.controller + ); + + // create obol split + address obolLidoSplitAdress = ObolLidoSplitFactory(obolLidoSplitFactory).createCollector(address(0), split); + + vm.stopBroadcast(); + + vm.serializeAddress(objKey, "splitAddress", split); + string memory repsonse = vm.serializeAddress(objKey, "obolLidoSplitAddress", obolLidoSplitAdress); + + finalJSON = vm.serializeString(jsonKey, objKey, repsonse); } - function run( - string memory jsonFilePath, - address splitMain, - address obolLidoSplitFactory - ) external { - uint256 privKey = vm.envUint("PRIVATE_KEY"); - - string memory file = vm.readFile(jsonFilePath); - bytes memory parsedJson = vm.parseJson(file); - JsonSplitConfiguration[] memory configuration = abi.decode(parsedJson, (JsonSplitConfiguration[])); - _validateInputJson(configuration); - - // deploy the split and obol script - string memory jsonKey = "lidoObolDeploy"; - string memory finalJSON; - - for (uint256 j = 0; j < configuration.length; j++) { - string memory objKey = vm.toString(j); - // deploy split - JsonSplitConfiguration memory currentConfiguration = configuration[j]; - - vm.startBroadcast(privKey); - - address split = ISplitMain(splitMain).createSplit( - currentConfiguration.accounts, - currentConfiguration.percentAllocations, - currentConfiguration.distributorFee, - currentConfiguration.controller - ); - - // create obol split - address obolLidoSplitAdress = ObolLidoSplitFactory(obolLidoSplitFactory).createSplit( - split - ); - - vm.stopBroadcast(); - - vm.serializeAddress(objKey, "splitAddress", split); - string memory repsonse = vm.serializeAddress(objKey, "obolLidoSplitAddress", obolLidoSplitAdress); - - finalJSON = vm.serializeString( - jsonKey, - objKey, - repsonse - ); - } - - vm.writeJson(finalJSON, "./result.json"); - } + vm.writeJson(finalJSON, "./result.json"); + } - function _validateInputJson(JsonSplitConfiguration[] memory configuration) internal pure { - for (uint256 i = 0; i < configuration.length; i++) { - address[] memory splitAddresses = configuration[i].accounts; - uint32[] memory percents = configuration[i].percentAllocations; - uint32 distributorFee = configuration[i].distributorFee; - _validSplit(splitAddresses, percents, distributorFee); - } + function _validateInputJson(JsonSplitConfiguration[] memory configuration) internal pure { + for (uint256 i = 0; i < configuration.length; i++) { + address[] memory splitAddresses = configuration[i].accounts; + uint32[] memory percents = configuration[i].percentAllocations; + uint32 distributorFee = configuration[i].distributorFee; + _validSplit(splitAddresses, percents, distributorFee); + } + } + + function _validSplit(address[] memory accounts, uint32[] memory percentAllocations, uint32 distributorFee) + internal + pure + { + if (accounts.length < 2) revert InvalidSplit__TooFewAccounts(accounts.length); + if (accounts.length != percentAllocations.length) { + revert InvalidSplit__AccountsAndAllocationsMismatch(accounts.length, percentAllocations.length); + } + // _getSum should overflow if any percentAllocation[i] < 0 + if (_getSum(percentAllocations) != PERCENTAGE_SCALE) { + revert InvalidSplit__InvalidAllocationsSum(_getSum(percentAllocations)); + } + unchecked { + // overflow should be impossible in for-loop index + // cache accounts length to save gas + uint256 loopLength = accounts.length - 1; + for (uint256 i = 0; i < loopLength; ++i) { + // overflow should be impossible in array access math + if (accounts[i] >= accounts[i + 1]) revert InvalidSplit__AccountsOutOfOrder(i); + if (percentAllocations[i] == uint32(0)) revert InvalidSplit__AllocationMustBePositive(i); + } + // overflow should be impossible in array access math with validated + // equal array lengths + if (percentAllocations[loopLength] == uint32(0)) revert InvalidSplit__AllocationMustBePositive(loopLength); } + if (distributorFee > MAX_DISTRIBUTOR_FEE) revert InvalidSplit__InvalidDistributorFee(distributorFee); + } - function _validSplit(address[] memory accounts, uint32[] memory percentAllocations, uint32 distributorFee) internal pure { - if (accounts.length < 2) revert InvalidSplit__TooFewAccounts(accounts.length); - if (accounts.length != percentAllocations.length) { - revert InvalidSplit__AccountsAndAllocationsMismatch(accounts.length, percentAllocations.length); - } - // _getSum should overflow if any percentAllocation[i] < 0 - if (_getSum(percentAllocations) != PERCENTAGE_SCALE) { - revert InvalidSplit__InvalidAllocationsSum(_getSum(percentAllocations)); - } - unchecked { - // overflow should be impossible in for-loop index - // cache accounts length to save gas - uint256 loopLength = accounts.length - 1; - for (uint256 i = 0; i < loopLength; ++i) { - // overflow should be impossible in array access math - if (accounts[i] >= accounts[i + 1]) revert InvalidSplit__AccountsOutOfOrder(i); - if (percentAllocations[i] == uint32(0)) revert InvalidSplit__AllocationMustBePositive(i); - } - // overflow should be impossible in array access math with validated - // equal array lengths - if (percentAllocations[loopLength] == uint32(0)) revert InvalidSplit__AllocationMustBePositive(loopLength); - } - if (distributorFee > MAX_DISTRIBUTOR_FEE) revert InvalidSplit__InvalidDistributorFee(distributorFee); - } - - function _getSum(uint32[] memory percents) internal pure returns (uint32 sum) { - for (uint32 i = 0; i < percents.length; i++) { - sum += percents[i]; - } + function _getSum(uint32[] memory percents) internal pure returns (uint32 sum) { + for (uint32 i = 0; i < percents.length; i++) { + sum += percents[i]; } -} \ No newline at end of file + } +} diff --git a/script/ObolLidoSplitFactoryScript.s.sol b/script/ObolLidoSplitFactoryScript.s.sol index 9d043e4..ffa778d 100644 --- a/script/ObolLidoSplitFactoryScript.s.sol +++ b/script/ObolLidoSplitFactoryScript.s.sol @@ -6,25 +6,15 @@ import {ObolLidoSplitFactory} from "src/lido/ObolLidoSplitFactory.sol"; import {ERC20} from "solmate/tokens/ERC20.sol"; contract ObolLidoSplitFactoryScript is Script { - function run( - address _feeRecipient, - uint256 _feeShare, - address _stETH, - address _wstETH - ) external { - uint256 privKey = vm.envUint("PRIVATE_KEY"); - vm.startBroadcast(privKey); - - ERC20 stETH = ERC20(_stETH); - ERC20 wstETH = ERC20(_wstETH); + function run(address _feeRecipient, uint256 _feeShare, address _stETH, address _wstETH) external { + uint256 privKey = vm.envUint("PRIVATE_KEY"); + vm.startBroadcast(privKey); - new ObolLidoSplitFactory{salt: keccak256("obol.lidoSplitFactory.v1")}( - _feeRecipient, - _feeShare, - stETH, - wstETH - ); + ERC20 stETH = ERC20(_stETH); + ERC20 wstETH = ERC20(_wstETH); - vm.stopBroadcast(); - } + new ObolLidoSplitFactory{salt: keccak256("obol.lidoSplitFactory.v1")}(_feeRecipient, _feeShare, stETH, wstETH); + + vm.stopBroadcast(); + } } diff --git a/src/base/BaseSplit.sol b/src/base/BaseSplit.sol new file mode 100644 index 0000000..f2a8063 --- /dev/null +++ b/src/base/BaseSplit.sol @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.19; + +import {ERC20} from "solmate/tokens/ERC20.sol"; +import {SafeTransferLib} from "solmate/utils/SafeTransferLib.sol"; +import {Clone} from "solady/utils/Clone.sol"; + +abstract contract BaseSplit is Clone { + error Invalid_Address(); + error Invalid_FeeShare(uint256 val); + error Invalid_FeeRecipient(); + + /// ----------------------------------------------------------------------- + /// libraries + /// ----------------------------------------------------------------------- + using SafeTransferLib for ERC20; + using SafeTransferLib for address; + + address internal constant ETH_ADDRESS = address(0); + uint256 internal constant PERCENTAGE_SCALE = 1e5; + + /// @notice fee share + uint256 public immutable feeShare; + + /// @notice fee address + address public immutable feeRecipient; + + // withdrawal (adress, 20 bytes) + // 0; first item + uint256 internal constant WITHDRAWAL_ADDRESS_OFFSET = 0; + // 20 = withdrawalAddress_offset (0) + withdrawalAddress_size (address, 20 bytes) + uint256 internal constant TOKEN_ADDRESS_OFFSET = 20; + + constructor(address _feeRecipient, uint256 _feeShare) { + if (_feeShare >= PERCENTAGE_SCALE) revert Invalid_FeeShare(_feeShare); + if (_feeShare > 0 && _feeRecipient == address(0)) revert Invalid_FeeRecipient(); + + feeShare = _feeShare; + feeRecipient = _feeRecipient; + } + + /// ----------------------------------------------------------------------- + /// View + /// ----------------------------------------------------------------------- + + /// Address to send funds to to + /// @dev equivalent to address public immutable withdrawalAddress + function withdrawalAddress() public pure returns (address) { + return _getArgAddress(WITHDRAWAL_ADDRESS_OFFSET); + } + + /// Token addresss + /// @dev equivalent to address public immutable token + function token() public pure virtual returns (address) { + return _getArgAddress(TOKEN_ADDRESS_OFFSET); + } + + /// ----------------------------------------------------------------------- + /// Public + /// ----------------------------------------------------------------------- + + /// @notice Rescue stuck ETH and tokens + /// Uses token == address(0) to represent ETH + /// @return balance Amount of ETH or tokens rescued + function rescueFunds(address tokenAddress) external virtual returns (uint256 balance) { + _beforeRescueFunds(tokenAddress); + + if (tokenAddress == ETH_ADDRESS) { + balance = address(this).balance; + if (balance > 0) withdrawalAddress().safeTransferETH(balance); + } else { + balance = ERC20(tokenAddress).balanceOf(address(this)); + if (balance > 0) ERC20(tokenAddress).safeTransfer(withdrawalAddress(), balance); + } + } + + /// @notice distribute funds to withdrawal address + function distribute() external virtual returns (uint256) { + (address tokenAddress, uint256 amount) = _beforeDistribute(); + + if (feeShare > 0) { + uint256 fee = (amount * feeShare) / PERCENTAGE_SCALE; + _transfer(tokenAddress, feeRecipient, fee); + _transfer(tokenAddress, withdrawalAddress(), amount -= fee); + } else { + _transfer(tokenAddress, withdrawalAddress(), amount); + } + + return amount; + } + + /// ----------------------------------------------------------------------- + /// Internal + /// ----------------------------------------------------------------------- + + function _beforeRescueFunds(address tokenAddress) internal virtual; + + function _beforeDistribute() internal virtual returns (address tokenAddress, uint256 amount); + + function _transfer(address tokenAddress, address receiver, uint256 amount) internal { + if (tokenAddress == ETH_ADDRESS) receiver.safeTransferETH(amount); + else ERC20(tokenAddress).safeTransfer(receiver, amount); + } +} diff --git a/src/base/BaseSplitFactory.sol b/src/base/BaseSplitFactory.sol new file mode 100644 index 0000000..01944da --- /dev/null +++ b/src/base/BaseSplitFactory.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.19; + +abstract contract BaseSplitFactory { + /// ----------------------------------------------------------------------- + /// errors + /// ----------------------------------------------------------------------- + /// @dev Invalid address + error Invalid_Address(); + + /// ----------------------------------------------------------------------- + /// events + /// ----------------------------------------------------------------------- + /// Emitted on createCollector + event CreateSplit(address token, address withdrawalAddress); + + function createCollector(address token, address withdrawalAddress) external virtual returns (address collector); +} diff --git a/src/collector/ObolCollector.sol b/src/collector/ObolCollector.sol new file mode 100644 index 0000000..756455a --- /dev/null +++ b/src/collector/ObolCollector.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.19; + +import {ERC20} from "solmate/tokens/ERC20.sol"; +import {SafeTransferLib} from "solmate/utils/SafeTransferLib.sol"; +import {Clone} from "solady/utils/Clone.sol"; +import {BaseSplit} from "../base/BaseSplit.sol"; + +/// @title ObolCollector +/// @author Obol +/// @notice An contract used to receive and distribute rewards minus fees +contract ObolCollector is BaseSplit { + constructor(address _feeRecipient, uint256 _feeShare) BaseSplit(_feeRecipient, _feeShare) {} + + function _beforeRescueFunds(address tokenAddress) internal pure override { + // prevent bypass + if (tokenAddress == token()) revert Invalid_Address(); + } + + function _beforeDistribute() internal view override returns (address tokenAddress, uint256 amount) { + tokenAddress = token(); + + if (tokenAddress == ETH_ADDRESS) amount = address(this).balance; + else amount = ERC20(tokenAddress).balanceOf(address(this)); + } +} diff --git a/src/collector/ObolCollectorFactory.sol b/src/collector/ObolCollectorFactory.sol new file mode 100644 index 0000000..c76c6fd --- /dev/null +++ b/src/collector/ObolCollectorFactory.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.19; + +import {LibClone} from "solady/utils/LibClone.sol"; +import {ObolCollector} from "./ObolCollector.sol"; +import {BaseSplitFactory} from "../base/BaseSplitFactory.sol"; + +/// @title ObolCollector +/// @author Obol +/// @notice A factory contract for cheaply deploying ObolCollector. +/// @dev The address returned should be used to as reward address collecting rewards +contract ObolCollectorFactory is BaseSplitFactory { + /// ----------------------------------------------------------------------- + /// libraries + /// ----------------------------------------------------------------------- + using LibClone for address; + + /// ----------------------------------------------------------------------- + /// storage + /// ----------------------------------------------------------------------- + + /// @dev collector implementation + ObolCollector public immutable collectorImpl; + + constructor(address _feeRecipient, uint256 _feeShare) { + collectorImpl = new ObolCollector(_feeRecipient, _feeShare); + } + + /// @dev Create a new collector + /// @dev address(0) is used to represent ETH + /// @param token collector token address + /// @param withdrawalAddress withdrawalAddress to receive tokens + function createCollector(address token, address withdrawalAddress) external override returns (address collector) { + if (withdrawalAddress == address(0)) revert Invalid_Address(); + + collector = address(collectorImpl).clone(abi.encodePacked(withdrawalAddress, token)); + + emit CreateSplit(token, withdrawalAddress); + } +} diff --git a/src/controllers/ImmutableSplitController.sol b/src/controllers/ImmutableSplitController.sol index 0c7348a..c9eea93 100644 --- a/src/controllers/ImmutableSplitController.sol +++ b/src/controllers/ImmutableSplitController.sol @@ -132,4 +132,4 @@ contract ImmutableSplitController is Clone { return _getArgUint256(RECIPIENTS_OFFSET + (i * ONE_WORD)); } } -} +} \ No newline at end of file diff --git a/src/controllers/ImmutableSplitControllerFactory.sol b/src/controllers/ImmutableSplitControllerFactory.sol index 4999872..d5506e2 100644 --- a/src/controllers/ImmutableSplitControllerFactory.sol +++ b/src/controllers/ImmutableSplitControllerFactory.sol @@ -232,4 +232,4 @@ contract ImmutableSplitControllerFactory { } } } -} +} \ No newline at end of file diff --git a/src/etherfi/ObolEtherfiSplit.sol b/src/etherfi/ObolEtherfiSplit.sol new file mode 100644 index 0000000..b9eebe0 --- /dev/null +++ b/src/etherfi/ObolEtherfiSplit.sol @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.19; + +import {ERC20} from "solmate/tokens/ERC20.sol"; +import {SafeTransferLib} from "solmate/utils/SafeTransferLib.sol"; +import {Clone} from "solady/utils/Clone.sol"; +import {IweETH} from "src/interfaces/IweETH.sol"; + +import {BaseSplit} from "../base/BaseSplit.sol"; + +/// @title ObolEtherfiSplit +/// @author Obol +/// @notice A wrapper for 0xsplits/split-contracts SplitWallet that transforms +/// eEth token to weETH token because eEth is a rebasing token +/// @dev Wraps eETH to weETH and +contract ObolEtherfiSplit is BaseSplit { + /// @notice eETH token + ERC20 public immutable eETH; + + /// @notice weETH token + ERC20 public immutable weETH; + + /// @notice Constructor + /// @param _feeRecipient address to receive fee + /// @param _feeShare fee share scaled by PERCENTAGE_SCALE + /// @param _eETH eETH address + /// @param _weETH weETH address + constructor(address _feeRecipient, uint256 _feeShare, ERC20 _eETH, ERC20 _weETH) BaseSplit(_feeRecipient, _feeShare) { + eETH = _eETH; + weETH = _weETH; + } + + function _beforeRescueFunds(address tokenAddress) internal view override { + // we check weETH here so rescueFunds can't be used + // to bypass fee + if (tokenAddress == address(eETH) || tokenAddress == address(weETH)) revert Invalid_Address(); + } + + /// Wraps the current eETH token balance to weETH + /// transfers the weETH balance to withdrawalAddress for distribution + function _beforeDistribute() internal override returns (address tokenAddress, uint256 amount) { + tokenAddress = address(weETH); + + // get current balance + uint256 balance = eETH.balanceOf(address(this)); + // approve the weETH + eETH.approve(address(weETH), balance); + // wrap into wseth + // we ignore the return value + IweETH(address(weETH)).wrap(balance); + // we use balanceOf here in case some weETH is stuck in the + // contract we would be able to rescue it + amount = ERC20(weETH).balanceOf(address(this)); + } +} diff --git a/src/etherfi/ObolEtherfiSplitFactory.sol b/src/etherfi/ObolEtherfiSplitFactory.sol new file mode 100644 index 0000000..bca33cf --- /dev/null +++ b/src/etherfi/ObolEtherfiSplitFactory.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.19; + +import {LibClone} from "solady/utils/LibClone.sol"; +import {ERC20} from "solmate/tokens/ERC20.sol"; +import "./ObolEtherfiSplit.sol"; +import {BaseSplitFactory} from "../base/BaseSplitFactory.sol"; + +/// @title ObolEtherfiSplitFactory +/// @author Obol +/// @notice A factory contract for cheaply deploying ObolEtherfiSplit. +/// @dev The address returned should be used to as reward address for EtherFi +contract ObolEtherfiSplitFactory is BaseSplitFactory { + /// ----------------------------------------------------------------------- + /// libraries + /// ----------------------------------------------------------------------- + using LibClone for address; + + /// ----------------------------------------------------------------------- + /// storage + /// ----------------------------------------------------------------------- + + /// @dev Ethersfi split implementation + ObolEtherfiSplit public immutable etherfiSplitImpl; + + constructor(address _feeRecipient, uint256 _feeShare, ERC20 _eETH, ERC20 _weETH) { + etherfiSplitImpl = new ObolEtherfiSplit(_feeRecipient, _feeShare, _eETH, _weETH); + } + + /// Creates a wrapper for splitWallet that transforms eETH token into + /// weETH + /// @dev Create a new collector + /// @dev address(0) is used to represent ETH + /// @param withdrawalAddress Address of the splitWallet to transfer weETH to + /// @return collector Address of the wrappper split + function createCollector(address, address withdrawalAddress) external override returns (address collector) { + if (withdrawalAddress == address(0)) revert Invalid_Address(); + + collector = address(etherfiSplitImpl).clone(abi.encodePacked(withdrawalAddress)); + + emit CreateSplit(address(0), collector); + } +} diff --git a/src/interfaces/IEigenLayer.sol b/src/interfaces/IEigenLayer.sol index 48e562a..fdc4670 100644 --- a/src/interfaces/IEigenLayer.sol +++ b/src/interfaces/IEigenLayer.sol @@ -1,144 +1,147 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.19; + import {ERC20} from "solmate/tokens/ERC20.sol"; interface IEigenLayerUtils { - // @notice Struct that bundles together a signature and an expiration time for the signature. Used primarily for stack management. - struct SignatureWithExpiry { - // the signature itself, formatted as a single bytes object - bytes signature; - // the expiration timestamp (UTC) of the signature - uint256 expiry; - } - - // @notice Struct that bundles together a signature, a salt for uniqueness, and an expiration time for the signature. Used primarily for stack management. - struct SignatureWithSaltAndExpiry { - // the signature itself, formatted as a single bytes object - bytes signature; - // the salt used to generate the signature - bytes32 salt; - // the expiration timestamp (UTC) of the signature - uint256 expiry; - } + // @notice Struct that bundles together a signature and an expiration time for the signature. Used primarily for stack + // management. + struct SignatureWithExpiry { + // the signature itself, formatted as a single bytes object + bytes signature; + // the expiration timestamp (UTC) of the signature + uint256 expiry; + } + + // @notice Struct that bundles together a signature, a salt for uniqueness, and an expiration time for the signature. + // Used primarily for stack management. + struct SignatureWithSaltAndExpiry { + // the signature itself, formatted as a single bytes object + bytes signature; + // the salt used to generate the signature + bytes32 salt; + // the expiration timestamp (UTC) of the signature + uint256 expiry; + } } interface IDelegationManager is IEigenLayerUtils { - - /** - * @notice Caller delegates their stake to an operator. - * @param operator The account (`msg.sender`) is delegating its assets to for use in serving applications built on EigenLayer. - * @param approverSignatureAndExpiry Verifies the operator approves of this delegation - * @param approverSalt A unique single use value tied to an individual signature. - * @dev The approverSignatureAndExpiry is used in the event that: - * 1) the operator's `delegationApprover` address is set to a non-zero value. - * AND - * 2) neither the operator nor their `delegationApprover` is the `msg.sender`, since in the event that the operator - * or their delegationApprover is the `msg.sender`, then approval is assumed. - * @dev In the event that `approverSignatureAndExpiry` is not checked, its content is ignored entirely; it's recommended to use an empty input - * in this case to save on complexity + gas costs - */ - function delegateTo( - address operator, - SignatureWithExpiry memory approverSignatureAndExpiry, - bytes32 approverSalt - ) external; - - /** - * @notice Undelegates the staker from the operator who they are delegated to. Puts the staker into the "undelegation limbo" mode of the EigenPodManager - * and queues a withdrawal of all of the staker's shares in the StrategyManager (to the staker), if necessary. - * @param staker The account to be undelegated. - * @return withdrawalRoot The root of the newly queued withdrawal, if a withdrawal was queued. Otherwise just bytes32(0). - * - * @dev Reverts if the `staker` is also an operator, since operators are not allowed to undelegate from themselves. - * @dev Reverts if the caller is not the staker, nor the operator who the staker is delegated to, nor the operator's specified "delegationApprover" - * @dev Reverts if the `staker` is already undelegated. - */ - function undelegate(address staker) external returns (bytes32 withdrawalRoot); + /** + * @notice Caller delegates their stake to an operator. + * @param operator The account (`msg.sender`) is delegating its assets to for use in serving applications built on + * EigenLayer. + * @param approverSignatureAndExpiry Verifies the operator approves of this delegation + * @param approverSalt A unique single use value tied to an individual signature. + * @dev The approverSignatureAndExpiry is used in the event that: + * 1) the operator's `delegationApprover` address is set to a non-zero value. + * AND + * 2) neither the operator nor their `delegationApprover` is the `msg.sender`, since in the event that the + * operator + * or their delegationApprover is the `msg.sender`, then approval is assumed. + * @dev In the event that `approverSignatureAndExpiry` is not checked, its content is ignored entirely; it's + * recommended to use an empty input + * in this case to save on complexity + gas costs + */ + function delegateTo(address operator, SignatureWithExpiry memory approverSignatureAndExpiry, bytes32 approverSalt) + external; + + /** + * @notice Undelegates the staker from the operator who they are delegated to. Puts the staker into the "undelegation + * limbo" mode of the EigenPodManager + * and queues a withdrawal of all of the staker's shares in the StrategyManager (to the staker), if necessary. + * @param staker The account to be undelegated. + * @return withdrawalRoot The root of the newly queued withdrawal, if a withdrawal was queued. Otherwise just + * bytes32(0). + * + * @dev Reverts if the `staker` is also an operator, since operators are not allowed to undelegate from themselves. + * @dev Reverts if the caller is not the staker, nor the operator who the staker is delegated to, nor the operator's + * specified "delegationApprover" + * @dev Reverts if the `staker` is already undelegated. + */ + function undelegate(address staker) external returns (bytes32 withdrawalRoot); } interface IEigenPodManager { - /** - * @notice Creates an EigenPod for the sender. - * @dev Function will revert if the `msg.sender` already has an EigenPod. - * @dev Returns EigenPod address - */ - function createPod() external returns (address); - - /** - * @notice Stakes for a new beacon chain validator on the sender's EigenPod. - * Also creates an EigenPod for the sender if they don't have one already. - * @param pubkey The 48 bytes public key of the beacon chain validator. - * @param signature The validator's signature of the deposit data. - * @param depositDataRoot The root/hash of the deposit data for the validator's deposit. - */ - function stake(bytes calldata pubkey, bytes calldata signature, bytes32 depositDataRoot) external payable; - - /// @notice Returns the address of the `podOwner`'s EigenPod (whether it is deployed yet or not). - function getPod(address podOwner) external returns (address); + /** + * @notice Creates an EigenPod for the sender. + * @dev Function will revert if the `msg.sender` already has an EigenPod. + * @dev Returns EigenPod address + */ + function createPod() external returns (address); + + /** + * @notice Stakes for a new beacon chain validator on the sender's EigenPod. + * Also creates an EigenPod for the sender if they don't have one already. + * @param pubkey The 48 bytes public key of the beacon chain validator. + * @param signature The validator's signature of the deposit data. + * @param depositDataRoot The root/hash of the deposit data for the validator's deposit. + */ + function stake(bytes calldata pubkey, bytes calldata signature, bytes32 depositDataRoot) external payable; + + /// @notice Returns the address of the `podOwner`'s EigenPod (whether it is deployed yet or not). + function getPod(address podOwner) external returns (address); } - interface IDelayedWithdrawalRouter { - /** - * @notice Called in order to withdraw delayed withdrawals made to the `recipient` that have passed the `withdrawalDelayBlocks` period. - * @param recipient The address to claim delayedWithdrawals for. - * @param maxNumberOfDelayedWithdrawalsToClaim Used to limit the maximum number of delayedWithdrawals to loop through claiming. - * @dev - * WARNING: Note that the caller of this function cannot control where the funds are sent, but they can control when the - * funds are sent once the withdrawal becomes claimable. - */ - function claimDelayedWithdrawals( - address recipient, - uint256 maxNumberOfDelayedWithdrawalsToClaim - ) external; - - /** - * @notice Creates a delayed withdrawal for `msg.value` to the `recipient`. - * @dev Only callable by the `podOwner`'s EigenPod contract. - */ - function createDelayedWithdrawal( - address podOwner, - address recipient - ) external; - - /// @notice Owner-only function for modifying the value of the `withdrawalDelayBlocks` variable. - function setWithdrawalDelayBlocks(uint256 newValue) external; + /** + * @notice Called in order to withdraw delayed withdrawals made to the `recipient` that have passed the + * `withdrawalDelayBlocks` period. + * @param recipient The address to claim delayedWithdrawals for. + * @param maxNumberOfDelayedWithdrawalsToClaim Used to limit the maximum number of delayedWithdrawals to loop through + * claiming. + * @dev + * WARNING: Note that the caller of this function cannot control where the funds are sent, but they can control + * when the + * funds are sent once the withdrawal becomes claimable. + */ + function claimDelayedWithdrawals(address recipient, uint256 maxNumberOfDelayedWithdrawalsToClaim) external; + + /** + * @notice Creates a delayed withdrawal for `msg.value` to the `recipient`. + * @dev Only callable by the `podOwner`'s EigenPod contract. + */ + function createDelayedWithdrawal(address podOwner, address recipient) external; + + /// @notice Owner-only function for modifying the value of the `withdrawalDelayBlocks` variable. + function setWithdrawalDelayBlocks(uint256 newValue) external; } interface IEigenPod { + function activateRestaking() external; - function activateRestaking() external; + /// @notice Called by the pod owner to withdraw the balance of the pod when `hasRestaked` is set to false + function withdrawBeforeRestaking() external; - /// @notice Called by the pod owner to withdraw the balance of the pod when `hasRestaked` is set to false - function withdrawBeforeRestaking() external; + /// @notice Called by the pod owner to withdraw the nonBeaconChainETHBalanceWei + function withdrawNonBeaconChainETHBalanceWei(address recipient, uint256 amountToWithdraw) external; - /// @notice Called by the pod owner to withdraw the nonBeaconChainETHBalanceWei - function withdrawNonBeaconChainETHBalanceWei(address recipient, uint256 amountToWithdraw) external; + /// @notice called by owner of a pod to remove any ERC20s deposited in the pod + function recoverTokens(ERC20[] memory tokenList, uint256[] memory amountsToWithdraw, address recipient) external; - /// @notice called by owner of a pod to remove any ERC20s deposited in the pod - function recoverTokens(ERC20[] memory tokenList, uint256[] memory amountsToWithdraw, address recipient) external; + /// @notice The single EigenPodManager for EigenLayer + function eigenPodManager() external view returns (IEigenPodManager); - /// @notice The single EigenPodManager for EigenLayer - function eigenPodManager() external view returns (IEigenPodManager); + /// @notice The owner of this EigenPod + function podOwner() external view returns (address); - /// @notice The owner of this EigenPod - function podOwner() external view returns (address); + /// @notice an indicator of whether or not the podOwner has ever "fully restaked" by successfully calling + /// `verifyCorrectWithdrawalCredentials`. + function hasRestaked() external view returns (bool); - /// @notice an indicator of whether or not the podOwner has ever "fully restaked" by successfully calling `verifyCorrectWithdrawalCredentials`. - function hasRestaked() external view returns (bool); + /// @notice The max amount of eth, in gwei, that can be restaked per validator + function MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR() external view returns (uint64); - /// @notice The max amount of eth, in gwei, that can be restaked per validator - function MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR() external view returns (uint64); + /// @notice the amount of execution layer ETH in this contract that is staked in EigenLayer (i.e. withdrawn from + /// beaconchain but not EigenLayer), + function withdrawableRestakedExecutionLayerGwei() external view returns (uint64); - /// @notice the amount of execution layer ETH in this contract that is staked in EigenLayer (i.e. withdrawn from beaconchain but not EigenLayer), - function withdrawableRestakedExecutionLayerGwei() external view returns (uint64); + /// @notice any ETH deposited into the EigenPod contract via the `receive` fallback function + function nonBeaconChainETHBalanceWei() external view returns (uint256); - /// @notice any ETH deposited into the EigenPod contract via the `receive` fallback function - function nonBeaconChainETHBalanceWei() external view returns (uint256); + /// @notice Used to initialize the pointers to contracts crucial to the pod's functionality, in beacon proxy + /// construction from EigenPodManager + function initialize(address owner) external; - /// @notice Used to initialize the pointers to contracts crucial to the pod's functionality, in beacon proxy construction from EigenPodManager - function initialize(address owner) external; - - /// @notice Called by EigenPodManager when the owner wants to create another ETH validator. - function stake(bytes calldata pubkey, bytes calldata signature, bytes32 depositDataRoot) external payable; -} \ No newline at end of file + /// @notice Called by EigenPodManager when the owner wants to create another ETH validator. + function stake(bytes calldata pubkey, bytes calldata signature, bytes32 depositDataRoot) external payable; +} diff --git a/src/interfaces/IweETH.sol b/src/interfaces/IweETH.sol new file mode 100644 index 0000000..199a460 --- /dev/null +++ b/src/interfaces/IweETH.sol @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +interface IweETH { + function wrap(uint256 _eETHAmount) external returns (uint256); + function getEETHByWeETH(uint256 _weETHAmount) external view returns (uint256); + function getWeETHByeETH(uint256 _eETHAmount) external view returns (uint256); + function eETH() external view returns (address); +} diff --git a/src/interfaces/IwstETH.sol b/src/interfaces/IwstETH.sol index 7918250..db7446e 100644 --- a/src/interfaces/IwstETH.sol +++ b/src/interfaces/IwstETH.sol @@ -4,4 +4,4 @@ pragma solidity ^0.8.19; interface IwstETH { function wrap(uint256 amount) external returns (uint256); function getWstETHByStETH(uint256 _stETHAmount) external view returns (uint256); -} \ No newline at end of file +} diff --git a/src/lido/ObolLidoSplit.sol b/src/lido/ObolLidoSplit.sol index c27450b..2753899 100644 --- a/src/lido/ObolLidoSplit.sol +++ b/src/lido/ObolLidoSplit.sol @@ -5,113 +5,50 @@ import {ERC20} from "solmate/tokens/ERC20.sol"; import {SafeTransferLib} from "solmate/utils/SafeTransferLib.sol"; import {Clone} from "solady/utils/Clone.sol"; import {IwstETH} from "src/interfaces/IwstETH.sol"; +import {BaseSplit} from "../base/BaseSplit.sol"; /// @title ObolLidoSplit /// @author Obol /// @notice A wrapper for 0xsplits/split-contracts SplitWallet that transforms /// stETH token to wstETH token because stETH is a rebasing token /// @dev Wraps stETH to wstETH and transfers to defined SplitWallet address -contract ObolLidoSplit is Clone { - error Invalid_Address(); - error Invalid_FeeShare(uint256 fee); - error Invalid_FeeRecipient(); - - /// ----------------------------------------------------------------------- - /// libraries - /// ----------------------------------------------------------------------- - using SafeTransferLib for ERC20; - using SafeTransferLib for address; - - address internal constant ETH_ADDRESS = address(0); - uint256 internal constant PERCENTAGE_SCALE = 1e5; - - /// ----------------------------------------------------------------------- - /// storage - cwia offsets - /// ----------------------------------------------------------------------- - - // splitWallet (adress, 20 bytes) - // 0; first item - uint256 internal constant SPLIT_WALLET_ADDRESS_OFFSET = 0; - - /// ----------------------------------------------------------------------- - /// storage - /// ----------------------------------------------------------------------- - +contract ObolLidoSplit is BaseSplit { /// @notice stETH token ERC20 public immutable stETH; /// @notice wstETH token ERC20 public immutable wstETH; - /// @notice fee address - address public immutable feeRecipient; - - /// @notice fee share - uint256 public immutable feeShare; - /// @notice Constructor /// @param _feeRecipient address to receive fee /// @param _feeShare fee share scaled by PERCENTAGE_SCALE /// @param _stETH stETH address /// @param _wstETH wstETH address - constructor(address _feeRecipient, uint256 _feeShare, ERC20 _stETH, ERC20 _wstETH) { - if (_feeShare >= PERCENTAGE_SCALE) revert Invalid_FeeShare(_feeShare); - if (_feeShare > 0 && _feeRecipient == address(0)) revert Invalid_FeeRecipient(); - - feeRecipient = _feeRecipient; + constructor(address _feeRecipient, uint256 _feeShare, ERC20 _stETH, ERC20 _wstETH) BaseSplit(_feeRecipient, _feeShare) { stETH = _stETH; wstETH = _wstETH; - feeShare = _feeShare; } - /// Address of split wallet to send funds to to - /// @dev equivalent to address public immutable splitWallet - function splitWallet() public pure returns (address) { - return _getArgAddress(SPLIT_WALLET_ADDRESS_OFFSET); + function _beforeRescueFunds(address tokenAddress) internal view override { + // we check weETH here so rescueFunds can't be used + // to bypass fee + if (tokenAddress == address(stETH) || tokenAddress == address(wstETH)) revert Invalid_Address(); } /// Wraps the current stETH token balance to wstETH - /// transfers the wstETH balance to splitWallet for distribution - /// @return amount Amount of wstETH transferred to splitWallet - function distribute() external returns (uint256 amount) { + /// transfers the wstETH balance to withdrawalAddress for distribution + function _beforeDistribute() internal override returns (address tokenAddress, uint256 amount) { + tokenAddress = address(wstETH); + // get current balance uint256 balance = stETH.balanceOf(address(this)); // approve the wstETH stETH.approve(address(wstETH), balance); - // wrap into wseth + // wrap into wstETH // we ignore the return value IwstETH(address(wstETH)).wrap(balance); // we use balanceOf here in case some wstETH is stuck in the // contract we would be able to rescue it amount = ERC20(wstETH).balanceOf(address(this)); - - if (feeShare > 0) { - uint256 fee = (amount * feeShare) / PERCENTAGE_SCALE; - // transfer to split wallet - // update amount to reflect fee charged - ERC20(wstETH).safeTransfer(splitWallet(), amount -= fee); - // transfer to fee address - ERC20(wstETH).safeTransfer(feeRecipient, fee); - } else { - // transfer to split wallet - ERC20(wstETH).safeTransfer(splitWallet(), amount); - } - } - - /// @notice Rescue stuck ETH and tokens - /// Uses token == address(0) to represent ETH - /// @return balance Amount of ETH or tokens rescued - function rescueFunds(address token) external returns (uint256 balance) { - // we check wstETH here so rescueFunds can't be used - // to bypass fee - if (token == address(stETH) || token == address(wstETH)) revert Invalid_Address(); - - if (token == ETH_ADDRESS) { - balance = address(this).balance; - if (balance > 0) splitWallet().safeTransferETH(balance); - } else { - balance = ERC20(token).balanceOf(address(this)); - if (balance > 0) ERC20(token).safeTransfer(splitWallet(), balance); - } } } diff --git a/src/lido/ObolLidoSplitFactory.sol b/src/lido/ObolLidoSplitFactory.sol index 4d0514f..bdb872f 100644 --- a/src/lido/ObolLidoSplitFactory.sol +++ b/src/lido/ObolLidoSplitFactory.sol @@ -3,32 +3,20 @@ pragma solidity 0.8.19; import {LibClone} from "solady/utils/LibClone.sol"; import {ERC20} from "solmate/tokens/ERC20.sol"; +import {BaseSplitFactory} from "../base/BaseSplitFactory.sol"; import "./ObolLidoSplit.sol"; /// @title ObolLidoSplitFactory /// @author Obol /// @notice A factory contract for cheaply deploying ObolLidoSplit. /// @dev The address returned should be used to as reward address for Lido -contract ObolLidoSplitFactory { - /// ----------------------------------------------------------------------- - /// errors - /// ----------------------------------------------------------------------- - - /// Invalid wallet - error Invalid_Wallet(); +contract ObolLidoSplitFactory is BaseSplitFactory { /// ----------------------------------------------------------------------- /// libraries /// ----------------------------------------------------------------------- using LibClone for address; - /// ----------------------------------------------------------------------- - /// events - /// ----------------------------------------------------------------------- - - /// Emitted after lido split - event CreateObolLidoSplit(address split); - /// ----------------------------------------------------------------------- /// storage /// ----------------------------------------------------------------------- @@ -40,15 +28,17 @@ contract ObolLidoSplitFactory { lidoSplitImpl = new ObolLidoSplit(_feeRecipient, _feeShare, _stETH, _wstETH); } - /// Creates a wrapper for splitWallet that transforms stETH token into + // Creates a wrapper for splitWallet that transforms stETH token into /// wstETH - /// @param splitWallet Address of the splitWallet to transfer wstETH to - /// @return lidoSplit Address of the wrappper split - function createSplit(address splitWallet) external returns (address lidoSplit) { - if (splitWallet == address(0)) revert Invalid_Wallet(); + /// @dev Create a new collector + /// @dev address(0) is used to represent ETH + /// @param withdrawalAddress Address of the splitWallet to transfer wstETH to + /// @return collector Address of the wrappper split + function createCollector(address, address withdrawalAddress) external override returns (address collector) { + if (withdrawalAddress == address(0)) revert Invalid_Address(); - lidoSplit = address(lidoSplitImpl).clone(abi.encodePacked(splitWallet)); + collector = address(lidoSplitImpl).clone(abi.encodePacked(withdrawalAddress)); - emit CreateObolLidoSplit(lidoSplit); + emit CreateSplit(address(0), collector); } } diff --git a/src/owr/OptimisticWithdrawalRecipient.sol b/src/owr/OptimisticWithdrawalRecipient.sol index 7dfdf60..4bd6c5a 100644 --- a/src/owr/OptimisticWithdrawalRecipient.sol +++ b/src/owr/OptimisticWithdrawalRecipient.sol @@ -341,4 +341,4 @@ contract OptimisticWithdrawalRecipient is Clone { } } } -} +} \ No newline at end of file diff --git a/src/owr/OptimisticWithdrawalRecipientFactory.sol b/src/owr/OptimisticWithdrawalRecipientFactory.sol index 3d95565..0dee73f 100644 --- a/src/owr/OptimisticWithdrawalRecipientFactory.sol +++ b/src/owr/OptimisticWithdrawalRecipientFactory.sol @@ -122,4 +122,4 @@ contract OptimisticWithdrawalRecipientFactory { emit CreateOWRecipient(address(owr), token, recoveryAddress, principalRecipient, rewardRecipient, amountOfPrincipalStake); } -} +} \ No newline at end of file diff --git a/src/test/collector/ObolCollector.t.sol b/src/test/collector/ObolCollector.t.sol new file mode 100644 index 0000000..dd53844 --- /dev/null +++ b/src/test/collector/ObolCollector.t.sol @@ -0,0 +1,197 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.19; + +import "forge-std/Test.sol"; +import {ObolCollectorFactory, ObolCollector} from "src/collector/ObolCollectorFactory.sol"; +import {MockERC20} from "src/test/utils/mocks/MockERC20.sol"; +import {BaseSplit} from "src/base/BaseSplit.sol"; + +contract ObolCollectorTest is Test { + + uint256 internal constant PERCENTAGE_SCALE = 1e5; + + address feeRecipient; + address withdrawalAddress; + address ethWithdrawalAddress; + + uint256 feeShare; + MockERC20 mERC20; + MockERC20 rescueERC20; + + ObolCollectorFactory collectorFactoryWithFee; + + ObolCollector collectorWithFee; + ObolCollector ethCollectorWithFee; + + function setUp() public { + feeRecipient = makeAddr("feeRecipient"); + withdrawalAddress = makeAddr("withdrawalAddress"); + ethWithdrawalAddress = makeAddr("ethWithdrawalAddress"); + mERC20 = new MockERC20("Test Token", "TOK", 18); + rescueERC20 = new MockERC20("Rescue Test Token", "TOK", 18); + + feeShare = 1e4; // 10% + collectorFactoryWithFee = new ObolCollectorFactory(feeRecipient, feeShare); + + collectorWithFee = ObolCollector(collectorFactoryWithFee.createCollector(address(mERC20), withdrawalAddress)); + ethCollectorWithFee = ObolCollector(collectorFactoryWithFee.createCollector(address(0), ethWithdrawalAddress)); + + mERC20.mint(type(uint256).max); + rescueERC20.mint(type(uint256).max); + } + + function test_InvalidFeeShare() public { + vm.expectRevert(abi.encodeWithSelector(BaseSplit.Invalid_FeeShare.selector, 1e10)); + new ObolCollectorFactory(address(0), 1e10); + + vm.expectRevert(abi.encodeWithSelector(BaseSplit.Invalid_FeeShare.selector, 1e5)); + new ObolCollectorFactory(address(0), 1e5); + } + + function test_feeShare() public { + assertEq(collectorWithFee.feeShare(), feeShare, "invalid collector fee"); + + assertEq(ethCollectorWithFee.feeShare(), feeShare, "invalid collector value fee"); + } + + function test_feeRecipient() public { + assertEq(collectorWithFee.feeRecipient(), feeRecipient, "invalid collector feeRecipient"); + + assertEq(ethCollectorWithFee.feeRecipient(), feeRecipient, "invalid collector feeRecipient 2"); + } + + function test_withdrawalAddress() public { + assertEq(collectorWithFee.withdrawalAddress(), withdrawalAddress, "invalid split wallet"); + + assertEq(ethCollectorWithFee.withdrawalAddress(), ethWithdrawalAddress, "invalid eth split wallet"); + } + + function test_token() public { + assertEq(collectorWithFee.token(), address(mERC20), "invalid token"); + + assertEq(ethCollectorWithFee.token(), address(0), "ivnalid token eth"); + } + + function test_DistributeERC20WithFee() public { + uint256 amountToDistribute = 10 ether; + + mERC20.transfer(address(collectorWithFee), amountToDistribute); + + collectorWithFee.distribute(); + + uint256 fee = amountToDistribute * feeShare / PERCENTAGE_SCALE; + + assertEq(mERC20.balanceOf(feeRecipient), fee, "invalid fee share"); + + assertEq(mERC20.balanceOf(withdrawalAddress), amountToDistribute - fee, "invalid amount to split"); + } + + function testFuzz_DistributeERC20WithFee( + uint256 amountToDistribute, + uint256 fuzzFeeShare, + address fuzzFeeRecipient, + address fuzzWithdrawalAddress + ) public { + vm.assume(amountToDistribute > 0); + vm.assume(fuzzWithdrawalAddress != address(0)); + vm.assume(fuzzFeeRecipient != address(0)); + + amountToDistribute = bound(amountToDistribute, 1, type(uint128).max); + fuzzFeeShare = bound(fuzzFeeShare, 1, 8 * 1e4); + + ObolCollectorFactory fuzzCollectorFactoryWithFee = new ObolCollectorFactory(fuzzFeeRecipient, fuzzFeeShare); + ObolCollector fuzzCollectorWithFee = + ObolCollector(fuzzCollectorFactoryWithFee.createCollector(address(mERC20), fuzzWithdrawalAddress)); + + uint256 feeRecipientBalancePrev = mERC20.balanceOf(fuzzFeeRecipient); + uint256 fuzzWithdrawalAddressBalancePrev = mERC20.balanceOf(fuzzWithdrawalAddress); + + mERC20.transfer(address(fuzzCollectorWithFee), amountToDistribute); + + fuzzCollectorWithFee.distribute(); + + uint256 fee = amountToDistribute * fuzzFeeShare / PERCENTAGE_SCALE; + + assertEq(mERC20.balanceOf(fuzzFeeRecipient), feeRecipientBalancePrev + fee, "invalid fee share"); + + assertEq( + mERC20.balanceOf(fuzzWithdrawalAddress), + fuzzWithdrawalAddressBalancePrev + amountToDistribute - fee, + "invalid amount to split" + ); + } + + function test_DistributeETHWithFee() public { + uint256 amountToDistribute = 10 ether; + + vm.deal(address(ethCollectorWithFee), amountToDistribute); + + ethCollectorWithFee.distribute(); + + uint256 fee = amountToDistribute * feeShare / PERCENTAGE_SCALE; + + assertEq(address(feeRecipient).balance, fee, "invalid fee share"); + + assertEq(address(ethWithdrawalAddress).balance, amountToDistribute - fee, "invalid amount to split"); + } + + function testFuzz_DistributeETHWithFee(uint256 amountToDistribute, uint256 fuzzFeeShare) public { + vm.assume(amountToDistribute > 0); + vm.assume(fuzzFeeShare > 0); + + address fuzzWithdrawalAddress = makeAddr("fuzzWithdrawalAddress"); + address fuzzFeeRecipient = makeAddr("fuzzFeeRecipient"); + + amountToDistribute = bound(amountToDistribute, 1, type(uint96).max); + fuzzFeeShare = bound(fuzzFeeShare, 1, 9 * 1e4); + + ObolCollectorFactory fuzzCollectorFactoryWithFee = new ObolCollectorFactory(fuzzFeeRecipient, fuzzFeeShare); + ObolCollector fuzzETHCollectorWithFee = + ObolCollector(fuzzCollectorFactoryWithFee.createCollector(address(0), fuzzWithdrawalAddress)); + + vm.deal(address(fuzzETHCollectorWithFee), amountToDistribute); + + uint256 fuzzFeeRecipientBalance = address(fuzzFeeRecipient).balance; + uint256 fuzzWithdrawalAddressBalance = address(fuzzWithdrawalAddress).balance; + + fuzzETHCollectorWithFee.distribute(); + + uint256 fee = amountToDistribute * fuzzFeeShare / PERCENTAGE_SCALE; + + assertEq(address(fuzzFeeRecipient).balance, fuzzFeeRecipientBalance + fee, "invalid fee share"); + + assertEq( + address(fuzzWithdrawalAddress).balance, + fuzzWithdrawalAddressBalance + amountToDistribute - fee, + "invalid amount to split" + ); + } + + function testCannot_RescueControllerToken() public { + deal(address(ethCollectorWithFee), 1 ether); + vm.expectRevert(BaseSplit.Invalid_Address.selector); + ethCollectorWithFee.rescueFunds(address(0)); + + mERC20.transfer(address(collectorWithFee), 1 ether); + vm.expectRevert(BaseSplit.Invalid_Address.selector); + collectorWithFee.rescueFunds(address(mERC20)); + } + + function test_RescueTokens() public { + uint256 amountToRescue = 1 ether; + deal(address(collectorWithFee), amountToRescue); + collectorWithFee.rescueFunds(address(0)); + + assertEq(address(withdrawalAddress).balance, amountToRescue, "invalid amount"); + + rescueERC20.transfer(address(collectorWithFee), amountToRescue); + collectorWithFee.rescueFunds(address(rescueERC20)); + assertEq(rescueERC20.balanceOf(withdrawalAddress), amountToRescue, "invalid erc20 amount"); + + // ETH + rescueERC20.transfer(address(ethCollectorWithFee), amountToRescue); + ethCollectorWithFee.rescueFunds(address(rescueERC20)); + + assertEq(rescueERC20.balanceOf(ethWithdrawalAddress), amountToRescue, "invalid erc20 amount"); + } +} diff --git a/src/test/collector/ObolCollectorFactory.t.sol b/src/test/collector/ObolCollectorFactory.t.sol new file mode 100644 index 0000000..a7f376a --- /dev/null +++ b/src/test/collector/ObolCollectorFactory.t.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.19; + +import "forge-std/Test.sol"; +import {ObolCollectorFactory, ObolCollector} from "src/collector/ObolCollectorFactory.sol"; +import {MockERC20} from "src/test/utils/mocks/MockERC20.sol"; + +contract ObolCollectorFactoryTest is Test { + error Invalid_Address(); + + address feeRecipient; + uint256 feeShare; + address splitWallet; + + ObolCollectorFactory collectorFactory; + + function setUp() public { + feeRecipient = makeAddr("feeRecipient"); + splitWallet = makeAddr("splitWallet"); + feeShare = 1e4; // 10% + collectorFactory = new ObolCollectorFactory(feeRecipient, feeShare); + } + + function testCannot_CreateCollectorInvalidWithdrawalAddress() public { + vm.expectRevert(Invalid_Address.selector); + collectorFactory.createCollector(address(0), address(0)); + } + + function test_CreateCollector() public { + collectorFactory.createCollector(address(0), splitWallet); + } +} diff --git a/src/test/controllers/IMSC.t.sol b/src/test/controllers/IMSC.t.sol index b1c9d95..d6dd517 100644 --- a/src/test/controllers/IMSC.t.sol +++ b/src/test/controllers/IMSC.t.sol @@ -32,7 +32,7 @@ contract IMSC is Test { function setUp() public { uint256 goerliBlock = 8_529_931; - vm.createSelectFork(getChain("goerli").rpcUrl, goerliBlock); + vm.createSelectFork(getChain("goerli").rpcUrl); factory = new ImmutableSplitControllerFactory(SPLIT_MAIN_GOERLI); cntrlImpl = factory.controller(); diff --git a/src/test/controllers/IMSCFactory.t.sol b/src/test/controllers/IMSCFactory.t.sol index 2079b24..e9e79de 100644 --- a/src/test/controllers/IMSCFactory.t.sol +++ b/src/test/controllers/IMSCFactory.t.sol @@ -33,7 +33,7 @@ contract IMSCFactory is Test { function setUp() public { uint256 goerliBlock = 8_529_931; - vm.createSelectFork(getChain("goerli").rpcUrl, goerliBlock); + vm.createSelectFork(getChain("goerli").rpcUrl); factory = new ImmutableSplitControllerFactory(SPLIT_MAIN_GOERLI); cntrlImpl = factory.controller(); @@ -154,20 +154,10 @@ contract IMSCFactory is Test { uint32[] memory localAllocations = _generatePercentAlloc(size); vm.expectRevert( - abi.encodeWithSelector( - ImmutableSplitControllerFactory.InvalidSplit__TooManyAccounts.selector, - size - ) + abi.encodeWithSelector(ImmutableSplitControllerFactory.InvalidSplit__TooManyAccounts.selector, size) ); - factory.createController( - address(1), - owner, - localAccounts, - localAllocations, - 0, - deploymentSalt - ); + factory.createController(address(1), owner, localAccounts, localAllocations, 0, deploymentSalt); } function test_CanCreateController() public { diff --git a/src/test/eigenlayer/OELPCFactory.t.sol b/src/test/eigenlayer/OELPCFactory.t.sol index e46514f..78ecf40 100644 --- a/src/test/eigenlayer/OELPCFactory.t.sol +++ b/src/test/eigenlayer/OELPCFactory.t.sol @@ -25,7 +25,7 @@ contract ObolEigenLayerPodControllerFactoryTest is EigenLayerTestBase { function setUp() public { uint256 goerliBlock = 10_205_449; - vm.createSelectFork(getChain("goerli").rpcUrl, goerliBlock); + vm.createSelectFork(getChain("goerli").rpcUrl); owner = makeAddr("owner"); user1 = makeAddr("user1"); @@ -72,17 +72,10 @@ contract ObolEigenLayerPodControllerFactoryTest is EigenLayerTestBase { emit CreatePodController(address(0), withdrawalAddress, user1); - address predictedAddress = factory.predictControllerAddress( - user1, - withdrawalAddress - ); + address predictedAddress = factory.predictControllerAddress(user1, withdrawalAddress); address createdAddress = factory.createPodController(user1, withdrawalAddress); - assertEq( - predictedAddress, - createdAddress, - "predicted address is equivalent" - ); + assertEq(predictedAddress, createdAddress, "predicted address is equivalent"); } } diff --git a/src/test/eigenlayer/ObolEigenLayerPodController.t.sol b/src/test/eigenlayer/ObolEigenLayerPodController.t.sol index 7372d47..c07ae90 100644 --- a/src/test/eigenlayer/ObolEigenLayerPodController.t.sol +++ b/src/test/eigenlayer/ObolEigenLayerPodController.t.sol @@ -52,7 +52,7 @@ contract ObolEigenLayerPodControllerTest is EigenLayerTestBase { function setUp() public { uint256 goerliBlock = 10_205_449; - vm.createSelectFork(getChain("goerli").rpcUrl, goerliBlock); + vm.createSelectFork(getChain("goerli").rpcUrl); vm.mockCall( DEPOSIT_CONTRACT_GOERLI, abi.encodeWithSelector(IDepositContract.deposit.selector), bytes.concat(bytes32(0)) @@ -71,7 +71,7 @@ contract ObolEigenLayerPodControllerTest is EigenLayerTestBase { ); zeroFeeFactory = new ObolEigenLayerPodControllerFactory( - address(0), 0, DELEGATION_MANAGER_GOERLI, POD_MANAGER_GOERLI, DELAY_ROUTER_GOERLI + address(0), 0, DELEGATION_MANAGER_GOERLI, POD_MANAGER_GOERLI, DELAY_ROUTER_GOERLI ); controller = ObolEigenLayerPodController(factory.createPodController(owner, withdrawalAddress)); diff --git a/src/test/eigenlayer/integration/OELPCIntegration.t.sol b/src/test/eigenlayer/integration/OELPCIntegration.t.sol index 84a8722..83d10c4 100644 --- a/src/test/eigenlayer/integration/OELPCIntegration.t.sol +++ b/src/test/eigenlayer/integration/OELPCIntegration.t.sol @@ -42,8 +42,8 @@ contract OELPCIntegration is EigenLayerTestBase { uint256 feeShare; function setUp() public { - uint256 goerliBlock = 10_205_449; - vm.createSelectFork(getChain("goerli").rpcUrl, goerliBlock); + uint256 goerliBlock = 10_653_080; + vm.createSelectFork(getChain("goerli").rpcUrl); vm.mockCall( ENS_REVERSE_REGISTRAR_GOERLI, diff --git a/src/test/etherfi/ObolEtherfiSplit.t.sol b/src/test/etherfi/ObolEtherfiSplit.t.sol new file mode 100644 index 0000000..5fc9188 --- /dev/null +++ b/src/test/etherfi/ObolEtherfiSplit.t.sol @@ -0,0 +1,189 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.19; + +import "forge-std/Test.sol"; +import {ObolEtherfiSplitFactory, ObolEtherfiSplit, IweETH} from "src/etherfi/ObolEtherfiSplitFactory.sol"; +import {BaseSplit} from "src/base/BaseSplit.sol"; +import {ERC20} from "solmate/tokens/ERC20.sol"; +import {ObolEtherfiSplitTestHelper} from "./ObolEtherfiSplitTestHelper.sol"; +import {MockERC20} from "src/test/utils/mocks/MockERC20.sol"; + +contract ObolEtherfiSplitTest is ObolEtherfiSplitTestHelper, Test { + uint256 internal constant PERCENTAGE_SCALE = 1e5; + + ObolEtherfiSplitFactory internal etherfiSplitFactory; + ObolEtherfiSplitFactory internal etherfiSplitFactoryWithFee; + + ObolEtherfiSplit internal etherfiSplit; + ObolEtherfiSplit internal etherfiSplitWithFee; + + address demoSplit; + address feeRecipient; + uint256 feeShare; + + MockERC20 mERC20; + + function setUp() public { + uint256 mainnetBlock = 19_393_100; + vm.createSelectFork(getChain("mainnet").rpcUrl, mainnetBlock); + + feeRecipient = makeAddr("feeRecipient"); + feeShare = 1e4; + + etherfiSplitFactory = + new ObolEtherfiSplitFactory(address(0), 0, ERC20(EETH_MAINNET_ADDRESS), ERC20(WEETH_MAINNET_ADDRESS)); + + etherfiSplitFactoryWithFee = + new ObolEtherfiSplitFactory(feeRecipient, feeShare, ERC20(EETH_MAINNET_ADDRESS), ERC20(WEETH_MAINNET_ADDRESS)); + + demoSplit = makeAddr("demoSplit"); + + etherfiSplit = ObolEtherfiSplit(etherfiSplitFactory.createCollector(address(0), demoSplit)); + etherfiSplitWithFee = ObolEtherfiSplit(etherfiSplitFactoryWithFee.createCollector(address(0), demoSplit)); + + mERC20 = new MockERC20("Test Token", "TOK", 18); + mERC20.mint(type(uint256).max); + } + + function test_etherfi_CannotCreateInvalidFeeRecipient() public { + vm.expectRevert(BaseSplit.Invalid_FeeRecipient.selector); + new ObolEtherfiSplit(address(0), 10, ERC20(EETH_MAINNET_ADDRESS), ERC20(WEETH_MAINNET_ADDRESS)); + } + + function test_etherfi_CannotCreateInvalidFeeShare() public { + vm.expectRevert(abi.encodeWithSelector(BaseSplit.Invalid_FeeShare.selector, PERCENTAGE_SCALE + 1)); + new ObolEtherfiSplit(address(1), PERCENTAGE_SCALE + 1, ERC20(EETH_MAINNET_ADDRESS), ERC20(WEETH_MAINNET_ADDRESS)); + + vm.expectRevert(abi.encodeWithSelector(BaseSplit.Invalid_FeeShare.selector, PERCENTAGE_SCALE)); + new ObolEtherfiSplit(address(1), PERCENTAGE_SCALE, ERC20(EETH_MAINNET_ADDRESS), ERC20(WEETH_MAINNET_ADDRESS)); + } + + function test_etherfi_CloneArgsIsCorrect() public { + assertEq(etherfiSplit.withdrawalAddress(), demoSplit, "invalid address"); + assertEq(address(etherfiSplit.eETH()), EETH_MAINNET_ADDRESS, "invalid eETH address"); + assertEq(address(etherfiSplit.weETH()), WEETH_MAINNET_ADDRESS, "invalid weETH address"); + assertEq(etherfiSplit.feeRecipient(), address(0), "invalid fee recipient"); + assertEq(etherfiSplit.feeShare(), 0, "invalid fee amount"); + + assertEq(etherfiSplitWithFee.withdrawalAddress(), demoSplit, "invalid address"); + assertEq(address(etherfiSplitWithFee.eETH()), EETH_MAINNET_ADDRESS, "invalid eETH address"); + assertEq(address(etherfiSplitWithFee.weETH()), WEETH_MAINNET_ADDRESS, "invalid weETH address"); + assertEq(etherfiSplitWithFee.feeRecipient(), feeRecipient, "invalid fee recipient /2"); + assertEq(etherfiSplitWithFee.feeShare(), feeShare, "invalid fee share /2"); + } + + function test_etherfi_CanRescueFunds() public { + // rescue ETH + uint256 amountOfEther = 1 ether; + deal(address(etherfiSplit), amountOfEther); + + uint256 balance = etherfiSplit.rescueFunds(address(0)); + assertEq(balance, amountOfEther, "balance not rescued"); + assertEq(address(etherfiSplit).balance, 0, "balance is not zero"); + assertEq(address(etherfiSplit.withdrawalAddress()).balance, amountOfEther, "rescue not successful"); + + // rescue tokens + mERC20.transfer(address(etherfiSplit), amountOfEther); + uint256 tokenBalance = etherfiSplit.rescueFunds(address(mERC20)); + assertEq(tokenBalance, amountOfEther, "token - balance not rescued"); + assertEq(mERC20.balanceOf(address(etherfiSplit)), 0, "token - balance is not zero"); + assertEq(mERC20.balanceOf(etherfiSplit.withdrawalAddress()), amountOfEther, "token - rescue not successful"); + } + + function test_etherfi_Cannot_RescueEtherfiTokens() public { + vm.expectRevert(BaseSplit.Invalid_Address.selector); + etherfiSplit.rescueFunds(address(EETH_MAINNET_ADDRESS)); + + vm.expectRevert(BaseSplit.Invalid_Address.selector); + etherfiSplit.rescueFunds(address(WEETH_MAINNET_ADDRESS)); + } + + function test_etherfi_CanDistributeWithoutFee() public { + // we use a random account on Etherscan to credit the etherfiSplit address + // with 10 ether worth of eETH on mainnet + vm.prank(RANDOM_EETH_ACCOUNT_ADDRESS); + ERC20(EETH_MAINNET_ADDRESS).transfer(address(etherfiSplit), 100 ether); + + uint256 prevBalance = ERC20(WEETH_MAINNET_ADDRESS).balanceOf(demoSplit); + + uint256 amount = etherfiSplit.distribute(); + + assertTrue(amount > 0, "invalid amount"); + + uint256 afterBalance = ERC20(WEETH_MAINNET_ADDRESS).balanceOf(demoSplit); + + assertGe(afterBalance, prevBalance, "after balance greater"); + } + + function test_etherfi_CanDistributeWithFee() public { + // we use a random account on Etherscan to credit the etherfiSplit address + // with 10 ether worth of eETH on mainnet + vm.prank(RANDOM_EETH_ACCOUNT_ADDRESS); + uint256 amountToDistribute = 100 ether; + ERC20(EETH_MAINNET_ADDRESS).transfer(address(etherfiSplitWithFee), amountToDistribute); + + uint256 prevBalance = ERC20(WEETH_MAINNET_ADDRESS).balanceOf(demoSplit); + + uint256 balance = ERC20(EETH_MAINNET_ADDRESS).balanceOf(address(etherfiSplitWithFee)); + + uint256 weETHDistributed = IweETH(WEETH_MAINNET_ADDRESS).getWeETHByeETH(balance); + + uint256 amount = etherfiSplitWithFee.distribute(); + + assertTrue(amount > 0, "invalid amount"); + + uint256 afterBalance = ERC20(WEETH_MAINNET_ADDRESS).balanceOf(demoSplit); + + assertGe(afterBalance, prevBalance, "after balance greater"); + + uint256 expectedFee = (weETHDistributed * feeShare) / PERCENTAGE_SCALE; + + assertEq(ERC20(WEETH_MAINNET_ADDRESS).balanceOf(feeRecipient), expectedFee, "invalid fee transferred"); + + assertEq(ERC20(WEETH_MAINNET_ADDRESS).balanceOf(demoSplit), weETHDistributed - expectedFee, "invalid amount"); + } + + function testFuzz_etherfi_CanDistributeWithFee( + address anotherSplit, + uint256 amountToDistribute, + address fuzzFeeRecipient, + uint256 fuzzFeeShare + ) public { + vm.assume(anotherSplit != address(0)); + vm.assume(fuzzFeeRecipient != anotherSplit); + vm.assume(fuzzFeeShare > 0 && fuzzFeeShare < PERCENTAGE_SCALE); + vm.assume(fuzzFeeRecipient != address(0)); + vm.assume(amountToDistribute > 1 ether); + vm.assume(amountToDistribute < 10 ether); + + ObolEtherfiSplitFactory fuzzFactorySplitWithFee = new ObolEtherfiSplitFactory( + fuzzFeeRecipient, fuzzFeeShare, ERC20(EETH_MAINNET_ADDRESS), ERC20(WEETH_MAINNET_ADDRESS) + ); + + ObolEtherfiSplit fuzzSplitWithFee = ObolEtherfiSplit(fuzzFactorySplitWithFee.createCollector(address(0), anotherSplit)); + + vm.prank(RANDOM_EETH_ACCOUNT_ADDRESS); + + ERC20(EETH_MAINNET_ADDRESS).transfer(address(fuzzSplitWithFee), amountToDistribute); + + uint256 prevBalance = ERC20(WEETH_MAINNET_ADDRESS).balanceOf(anotherSplit); + + uint256 balance = ERC20(EETH_MAINNET_ADDRESS).balanceOf(address(fuzzSplitWithFee)); + + uint256 weETHDistributed = IweETH(WEETH_MAINNET_ADDRESS).getWeETHByeETH(balance); + + uint256 amount = fuzzSplitWithFee.distribute(); + + assertTrue(amount > 0, "invalid amount"); + + uint256 afterBalance = ERC20(WEETH_MAINNET_ADDRESS).balanceOf(anotherSplit); + + assertGe(afterBalance, prevBalance, "after balance greater"); + + uint256 expectedFee = (weETHDistributed * fuzzFeeShare) / PERCENTAGE_SCALE; + + assertEq(ERC20(WEETH_MAINNET_ADDRESS).balanceOf(fuzzFeeRecipient), expectedFee, "invalid fee transferred"); + + assertEq(ERC20(WEETH_MAINNET_ADDRESS).balanceOf(anotherSplit), weETHDistributed - expectedFee, "invalid amount"); + } +} diff --git a/src/test/etherfi/ObolEtherfiSplitFactory.t.sol b/src/test/etherfi/ObolEtherfiSplitFactory.t.sol new file mode 100644 index 0000000..08b3e9d --- /dev/null +++ b/src/test/etherfi/ObolEtherfiSplitFactory.t.sol @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.19; + +import "forge-std/Test.sol"; +import {ObolEtherfiSplitFactory} from "src/etherfi/ObolEtherfiSplitFactory.sol"; +import {BaseSplitFactory} from "src/base/BaseSplitFactory.sol"; +import {ERC20} from "solmate/tokens/ERC20.sol"; +import {ObolEtherfiSplitTestHelper} from "./ObolEtherfiSplitTestHelper.sol"; + +contract ObolEtherfiSplitFactoryTest is ObolEtherfiSplitTestHelper, Test { + ObolEtherfiSplitFactory internal etherfiSplitFactory; + ObolEtherfiSplitFactory internal etherfiSplitFactoryWithFee; + + address demoSplit; + + event CreateSplit(address token, address split); + + function setUp() public { + uint256 mainnetBlock = 19_228_949; + vm.createSelectFork(getChain("mainnet").rpcUrl, mainnetBlock); + + etherfiSplitFactory = + new ObolEtherfiSplitFactory(address(0), 0, ERC20(EETH_MAINNET_ADDRESS), ERC20(WEETH_MAINNET_ADDRESS)); + + etherfiSplitFactoryWithFee = + new ObolEtherfiSplitFactory(address(this), 1e3, ERC20(EETH_MAINNET_ADDRESS), ERC20(WEETH_MAINNET_ADDRESS)); + + demoSplit = makeAddr("demoSplit"); + } + + function testCan_CreateSplit() public { + vm.expectEmit(true, true, true, false, address(etherfiSplitFactory)); + emit CreateSplit(address(0), address(0x1)); + + etherfiSplitFactory.createCollector(address(0), demoSplit); + + vm.expectEmit(true, true, true, false, address(etherfiSplitFactoryWithFee)); + emit CreateSplit(address(0), address(0x1)); + + etherfiSplitFactoryWithFee.createCollector(address(0), demoSplit); + } + + function testCannot_CreateSplitInvalidAddress() public { + vm.expectRevert(BaseSplitFactory.Invalid_Address.selector); + etherfiSplitFactory.createCollector(address(0), address(0)); + + vm.expectRevert(BaseSplitFactory.Invalid_Address.selector); + etherfiSplitFactoryWithFee.createCollector(address(0), address(0)); + } +} diff --git a/src/test/etherfi/ObolEtherfiSplitTestHelper.sol b/src/test/etherfi/ObolEtherfiSplitTestHelper.sol new file mode 100644 index 0000000..fcb4b13 --- /dev/null +++ b/src/test/etherfi/ObolEtherfiSplitTestHelper.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.19; + +contract ObolEtherfiSplitTestHelper { + address internal EETH_MAINNET_ADDRESS = address(0x35fA164735182de50811E8e2E824cFb9B6118ac2); + address internal WEETH_MAINNET_ADDRESS = address(0xCd5fE23C85820F7B72D0926FC9b05b43E359b7ee); + address internal RANDOM_EETH_ACCOUNT_ADDRESS = address(0x30653c83162ff00918842D8bFe016935Fdd6Ab84); +} diff --git a/src/test/etherfi/integration/ObolEtherfiSplitIntegrationTest.sol b/src/test/etherfi/integration/ObolEtherfiSplitIntegrationTest.sol new file mode 100644 index 0000000..62e2e00 --- /dev/null +++ b/src/test/etherfi/integration/ObolEtherfiSplitIntegrationTest.sol @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.19; + +import "forge-std/Test.sol"; +import {ObolEtherfiSplitFactory, ObolEtherfiSplit} from "src/etherfi/ObolEtherfiSplitFactory.sol"; +import {ERC20} from "solmate/tokens/ERC20.sol"; +import {ObolEtherfiSplitTestHelper} from "../ObolEtherfiSplitTestHelper.sol"; +import {ISplitMain} from "src/interfaces/ISplitMain.sol"; + +contract ObolEtherfiSplitIntegrationTest is ObolEtherfiSplitTestHelper, Test { + ObolEtherfiSplitFactory internal etherfiSplitFactory; + ObolEtherfiSplit internal etherfiSplit; + + address splitter; + + address[] accounts; + uint32[] percentAllocations; + + address internal SPLIT_MAIN_MAINNET = 0x2ed6c4B5dA6378c7897AC67Ba9e43102Feb694EE; + + function setUp() public { + uint256 mainnetBlock = 19_228_949; + vm.createSelectFork(getChain("mainnet").rpcUrl, mainnetBlock); + + etherfiSplitFactory = + new ObolEtherfiSplitFactory(address(0), 0, ERC20(EETH_MAINNET_ADDRESS), ERC20(WEETH_MAINNET_ADDRESS)); + + accounts = new address[](2); + accounts[0] = makeAddr("accounts0"); + accounts[1] = makeAddr("accounts1"); + + percentAllocations = new uint32[](2); + percentAllocations[0] = 400_000; + percentAllocations[1] = 600_000; + + splitter = ISplitMain(SPLIT_MAIN_MAINNET).createSplit(accounts, percentAllocations, 0, address(0)); + + etherfiSplit = ObolEtherfiSplit(etherfiSplitFactory.createCollector(address(0), splitter)); + } + + function test_etherfi_integration_CanDistribute() public { + vm.prank(RANDOM_EETH_ACCOUNT_ADDRESS); + ERC20(EETH_MAINNET_ADDRESS).transfer(address(etherfiSplit), 100 ether); + + etherfiSplit.distribute(); + + ISplitMain(SPLIT_MAIN_MAINNET).distributeERC20( + splitter, ERC20(WEETH_MAINNET_ADDRESS), accounts, percentAllocations, 0, address(0) + ); + + ERC20[] memory tokens = new ERC20[](1); + tokens[0] = ERC20(WEETH_MAINNET_ADDRESS); + + ISplitMain(SPLIT_MAIN_MAINNET).withdraw(accounts[0], 0, tokens); + ISplitMain(SPLIT_MAIN_MAINNET).withdraw(accounts[1], 0, tokens); + + assertEq( + ERC20(WEETH_MAINNET_ADDRESS).balanceOf(accounts[0]), 38_787_430_925_418_583_374, "invalid account 0 balance" + ); + assertEq( + ERC20(WEETH_MAINNET_ADDRESS).balanceOf(accounts[1]), 58_181_146_388_127_875_061, "invalid account 1 balance" + ); + } +} diff --git a/src/test/lido/ObolLIdoSplitFactory.t.sol b/src/test/lido/ObolLIdoSplitFactory.t.sol index fb5d5cc..99bbc13 100644 --- a/src/test/lido/ObolLIdoSplitFactory.t.sol +++ b/src/test/lido/ObolLIdoSplitFactory.t.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.19; import "forge-std/Test.sol"; import {ObolLidoSplitFactory} from "src/lido/ObolLidoSplitFactory.sol"; import {ERC20} from "solmate/tokens/ERC20.sol"; +import {BaseSplitFactory} from "src/base/BaseSplitFactory.sol"; import {ObolLidoSplitTestHelper} from "./ObolLidoSplitTestHelper.sol"; contract ObolLidoSplitFactoryTest is ObolLidoSplitTestHelper, Test { @@ -12,46 +13,38 @@ contract ObolLidoSplitFactoryTest is ObolLidoSplitTestHelper, Test { address demoSplit; - event CreateObolLidoSplit(address split); + event CreateSplit(address token, address split); function setUp() public { uint256 mainnetBlock = 17_421_005; vm.createSelectFork(getChain("mainnet").rpcUrl, mainnetBlock); - lidoSplitFactory = new ObolLidoSplitFactory( - address(0), - 0, - ERC20(STETH_MAINNET_ADDRESS), - ERC20(WSTETH_MAINNET_ADDRESS) - ); + lidoSplitFactory = + new ObolLidoSplitFactory(address(0), 0, ERC20(STETH_MAINNET_ADDRESS), ERC20(WSTETH_MAINNET_ADDRESS)); - lidoSplitFactoryWithFee = new ObolLidoSplitFactory( - address(this), - 1e3, - ERC20(STETH_MAINNET_ADDRESS), - ERC20(WSTETH_MAINNET_ADDRESS) - ); + lidoSplitFactoryWithFee = + new ObolLidoSplitFactory(address(this), 1e3, ERC20(STETH_MAINNET_ADDRESS), ERC20(WSTETH_MAINNET_ADDRESS)); demoSplit = makeAddr("demoSplit"); } function testCan_CreateSplit() public { vm.expectEmit(true, true, true, false, address(lidoSplitFactory)); - emit CreateObolLidoSplit(address(0x1)); + emit CreateSplit(address(0), address(0x1)); - lidoSplitFactory.createSplit(demoSplit); + lidoSplitFactory.createCollector(address(0), demoSplit); vm.expectEmit(true, true, true, false, address(lidoSplitFactoryWithFee)); - emit CreateObolLidoSplit(address(0x1)); + emit CreateSplit(address(0), address(0x1)); - lidoSplitFactoryWithFee.createSplit(demoSplit); + lidoSplitFactoryWithFee.createCollector(address(0), demoSplit); } function testCannot_CreateSplitInvalidAddress() public { - vm.expectRevert(ObolLidoSplitFactory.Invalid_Wallet.selector); - lidoSplitFactory.createSplit(address(0)); + vm.expectRevert(BaseSplitFactory.Invalid_Address.selector); + lidoSplitFactory.createCollector(address(0), address(0)); - vm.expectRevert(ObolLidoSplitFactory.Invalid_Wallet.selector); - lidoSplitFactoryWithFee.createSplit(address(0)); + vm.expectRevert(BaseSplitFactory.Invalid_Address.selector); + lidoSplitFactoryWithFee.createCollector(address(0), address(0)); } } diff --git a/src/test/lido/ObolLidoSplit.t.sol b/src/test/lido/ObolLidoSplit.t.sol index c9b98a0..2a63b37 100644 --- a/src/test/lido/ObolLidoSplit.t.sol +++ b/src/test/lido/ObolLidoSplit.t.sol @@ -3,6 +3,7 @@ pragma solidity ^0.8.19; import "forge-std/Test.sol"; import {ObolLidoSplitFactory, ObolLidoSplit, IwstETH} from "src/lido/ObolLidoSplitFactory.sol"; +import {BaseSplit} from "src/base/BaseSplit.sol"; import {ERC20} from "solmate/tokens/ERC20.sol"; import {ObolLidoSplitTestHelper} from "./ObolLidoSplitTestHelper.sol"; import {MockERC20} from "src/test/utils/mocks/MockERC20.sol"; @@ -29,56 +30,42 @@ contract ObolLidoSplitTest is ObolLidoSplitTestHelper, Test { feeRecipient = makeAddr("feeRecipient"); feeShare = 1e4; - lidoSplitFactory = new ObolLidoSplitFactory( - address(0), - 0, - ERC20(STETH_MAINNET_ADDRESS), - ERC20(WSTETH_MAINNET_ADDRESS) - ); + lidoSplitFactory = + new ObolLidoSplitFactory(address(0), 0, ERC20(STETH_MAINNET_ADDRESS), ERC20(WSTETH_MAINNET_ADDRESS)); - lidoSplitFactoryWithFee = new ObolLidoSplitFactory( - feeRecipient, - feeShare, - ERC20(STETH_MAINNET_ADDRESS), - ERC20(WSTETH_MAINNET_ADDRESS) - ); + lidoSplitFactoryWithFee = + new ObolLidoSplitFactory(feeRecipient, feeShare, ERC20(STETH_MAINNET_ADDRESS), ERC20(WSTETH_MAINNET_ADDRESS)); demoSplit = makeAddr("demoSplit"); - lidoSplit = ObolLidoSplit(lidoSplitFactory.createSplit(demoSplit)); - lidoSplitWithFee = ObolLidoSplit(lidoSplitFactoryWithFee.createSplit(demoSplit)); + lidoSplit = ObolLidoSplit(lidoSplitFactory.createCollector(address(0), demoSplit)); + lidoSplitWithFee = ObolLidoSplit(lidoSplitFactoryWithFee.createCollector(address(0), demoSplit)); mERC20 = new MockERC20("Test Token", "TOK", 18); mERC20.mint(type(uint256).max); } function test_CannotCreateInvalidFeeRecipient() public { - vm.expectRevert( - ObolLidoSplit.Invalid_FeeRecipient.selector - ); + vm.expectRevert(BaseSplit.Invalid_FeeRecipient.selector); new ObolLidoSplit(address(0), 10, ERC20(STETH_MAINNET_ADDRESS), ERC20(WSTETH_MAINNET_ADDRESS)); } function test_CannotCreateInvalidFeeShare() public { - vm.expectRevert( - abi.encodeWithSelector(ObolLidoSplit.Invalid_FeeShare.selector, PERCENTAGE_SCALE + 1) - ); + vm.expectRevert(abi.encodeWithSelector(BaseSplit.Invalid_FeeShare.selector, PERCENTAGE_SCALE + 1)); new ObolLidoSplit(address(1), PERCENTAGE_SCALE + 1, ERC20(STETH_MAINNET_ADDRESS), ERC20(WSTETH_MAINNET_ADDRESS)); - vm.expectRevert( - abi.encodeWithSelector(ObolLidoSplit.Invalid_FeeShare.selector, PERCENTAGE_SCALE) - ); + vm.expectRevert(abi.encodeWithSelector(BaseSplit.Invalid_FeeShare.selector, PERCENTAGE_SCALE)); new ObolLidoSplit(address(1), PERCENTAGE_SCALE, ERC20(STETH_MAINNET_ADDRESS), ERC20(WSTETH_MAINNET_ADDRESS)); } function test_CloneArgsIsCorrect() public { - assertEq(lidoSplit.splitWallet(), demoSplit, "invalid address"); + assertEq(lidoSplit.withdrawalAddress(), demoSplit, "invalid address"); assertEq(address(lidoSplit.stETH()), STETH_MAINNET_ADDRESS, "invalid stETH address"); assertEq(address(lidoSplit.wstETH()), WSTETH_MAINNET_ADDRESS, "invalid wstETH address"); assertEq(lidoSplit.feeRecipient(), address(0), "invalid fee recipient"); assertEq(lidoSplit.feeShare(), 0, "invalid fee amount"); - assertEq(lidoSplitWithFee.splitWallet(), demoSplit, "invalid address"); + assertEq(lidoSplitWithFee.withdrawalAddress(), demoSplit, "invalid address"); assertEq(address(lidoSplitWithFee.stETH()), STETH_MAINNET_ADDRESS, "invalid stETH address"); assertEq(address(lidoSplitWithFee.wstETH()), WSTETH_MAINNET_ADDRESS, "invalid wstETH address"); assertEq(lidoSplitWithFee.feeRecipient(), feeRecipient, "invalid fee recipient /2"); @@ -93,21 +80,21 @@ contract ObolLidoSplitTest is ObolLidoSplitTestHelper, Test { uint256 balance = lidoSplit.rescueFunds(address(0)); assertEq(balance, amountOfEther, "balance not rescued"); assertEq(address(lidoSplit).balance, 0, "balance is not zero"); - assertEq(address(lidoSplit.splitWallet()).balance, amountOfEther, "rescue not successful"); + assertEq(address(lidoSplit.withdrawalAddress()).balance, amountOfEther, "rescue not successful"); // rescue tokens mERC20.transfer(address(lidoSplit), amountOfEther); uint256 tokenBalance = lidoSplit.rescueFunds(address(mERC20)); assertEq(tokenBalance, amountOfEther, "token - balance not rescued"); assertEq(mERC20.balanceOf(address(lidoSplit)), 0, "token - balance is not zero"); - assertEq(mERC20.balanceOf(lidoSplit.splitWallet()), amountOfEther, "token - rescue not successful"); + assertEq(mERC20.balanceOf(lidoSplit.withdrawalAddress()), amountOfEther, "token - rescue not successful"); } function testCannot_RescueLidoTokens() public { - vm.expectRevert(ObolLidoSplit.Invalid_Address.selector); + vm.expectRevert(BaseSplit.Invalid_Address.selector); lidoSplit.rescueFunds(address(STETH_MAINNET_ADDRESS)); - vm.expectRevert(ObolLidoSplit.Invalid_Address.selector); + vm.expectRevert(BaseSplit.Invalid_Address.selector); lidoSplit.rescueFunds(address(WSTETH_MAINNET_ADDRESS)); } @@ -170,13 +157,10 @@ contract ObolLidoSplitTest is ObolLidoSplitTestHelper, Test { vm.assume(amountToDistribute < 10 ether); ObolLidoSplitFactory fuzzFactorySplitWithFee = new ObolLidoSplitFactory( - fuzzFeeRecipient, - fuzzFeeShare, - ERC20(STETH_MAINNET_ADDRESS), - ERC20(WSTETH_MAINNET_ADDRESS) + fuzzFeeRecipient, fuzzFeeShare, ERC20(STETH_MAINNET_ADDRESS), ERC20(WSTETH_MAINNET_ADDRESS) ); - ObolLidoSplit fuzzSplitWithFee = ObolLidoSplit(fuzzFactorySplitWithFee.createSplit(anotherSplit)); + ObolLidoSplit fuzzSplitWithFee = ObolLidoSplit(fuzzFactorySplitWithFee.createCollector(address(0), anotherSplit)); vm.prank(0x2bf3937b8BcccE4B65650F122Bb3f1976B937B2f); diff --git a/src/test/lido/integration/LidoSplitIntegrationTest.sol b/src/test/lido/integration/LidoSplitIntegrationTest.sol index 24049a8..2f89c51 100644 --- a/src/test/lido/integration/LidoSplitIntegrationTest.sol +++ b/src/test/lido/integration/LidoSplitIntegrationTest.sol @@ -22,12 +22,8 @@ contract ObolLidoSplitIntegrationTest is ObolLidoSplitTestHelper, Test { uint256 mainnetBlock = 17_421_005; vm.createSelectFork(getChain("mainnet").rpcUrl, mainnetBlock); - lidoSplitFactory = new ObolLidoSplitFactory( - address(0), - 0, - ERC20(STETH_MAINNET_ADDRESS), - ERC20(WSTETH_MAINNET_ADDRESS) - ); + lidoSplitFactory = + new ObolLidoSplitFactory(address(0), 0, ERC20(STETH_MAINNET_ADDRESS), ERC20(WSTETH_MAINNET_ADDRESS)); accounts = new address[](2); accounts[0] = makeAddr("accounts0"); @@ -39,7 +35,7 @@ contract ObolLidoSplitIntegrationTest is ObolLidoSplitTestHelper, Test { splitter = ISplitMain(SPLIT_MAIN_MAINNET).createSplit(accounts, percentAllocations, 0, address(0)); - lidoSplit = ObolLidoSplit(lidoSplitFactory.createSplit(splitter)); + lidoSplit = ObolLidoSplit(lidoSplitFactory.createCollector(address(0), splitter)); } function test_CanDistribute() public { diff --git a/src/test/owr/OptimisticWithdrawalRecipient.t.sol b/src/test/owr/OptimisticWithdrawalRecipient.t.sol index 68cdb8a..c30d1d5 100644 --- a/src/test/owr/OptimisticWithdrawalRecipient.t.sol +++ b/src/test/owr/OptimisticWithdrawalRecipient.t.sol @@ -38,18 +38,18 @@ contract OptimisticWithdrawalRecipientTest is OWRTestHelper, Test { mERC20.mint(type(uint256).max); vm.mockCall( - ENS_REVERSE_REGISTRAR_GOERLI, abi.encodeWithSelector(IENSReverseRegistrar.setName.selector), bytes.concat(bytes32(0)) + ENS_REVERSE_REGISTRAR_GOERLI, + abi.encodeWithSelector(IENSReverseRegistrar.setName.selector), + bytes.concat(bytes32(0)) ); vm.mockCall( - ENS_REVERSE_REGISTRAR_GOERLI, abi.encodeWithSelector(IENSReverseRegistrar.claim.selector), bytes.concat(bytes32(0)) - ); - - owrFactory = new OptimisticWithdrawalRecipientFactory( - "demo.obol.eth", ENS_REVERSE_REGISTRAR_GOERLI, - address(this) + abi.encodeWithSelector(IENSReverseRegistrar.claim.selector), + bytes.concat(bytes32(0)) ); - + + owrFactory = new OptimisticWithdrawalRecipientFactory("demo.obol.eth", ENS_REVERSE_REGISTRAR_GOERLI, address(this)); + owrModule = owrFactory.owrImpl(); (principalRecipient, rewardRecipient) = generateTrancheRecipients(uint256(uint160(makeAddr("tranche")))); diff --git a/src/test/owr/OptimisticWithdrawalRecipientFactory.t.sol b/src/test/owr/OptimisticWithdrawalRecipientFactory.t.sol index f694d60..8743304 100644 --- a/src/test/owr/OptimisticWithdrawalRecipientFactory.t.sol +++ b/src/test/owr/OptimisticWithdrawalRecipientFactory.t.sol @@ -32,18 +32,19 @@ contract OptimisticWithdrawalRecipientFactoryTest is OWRTestHelper, Test { mERC20.mint(type(uint256).max); vm.mockCall( - ENS_REVERSE_REGISTRAR_GOERLI, abi.encodeWithSelector(IENSReverseRegistrar.setName.selector), bytes.concat(bytes32(0)) + ENS_REVERSE_REGISTRAR_GOERLI, + abi.encodeWithSelector(IENSReverseRegistrar.setName.selector), + bytes.concat(bytes32(0)) ); vm.mockCall( - ENS_REVERSE_REGISTRAR_GOERLI, abi.encodeWithSelector(IENSReverseRegistrar.claim.selector), bytes.concat(bytes32(0)) - ); - - owrFactoryModule = new OptimisticWithdrawalRecipientFactory( - "demo.obol.eth", ENS_REVERSE_REGISTRAR_GOERLI, - address(this) + abi.encodeWithSelector(IENSReverseRegistrar.claim.selector), + bytes.concat(bytes32(0)) ); + owrFactoryModule = + new OptimisticWithdrawalRecipientFactory("demo.obol.eth", ENS_REVERSE_REGISTRAR_GOERLI, address(this)); + recoveryAddress = makeAddr("recoveryAddress"); (principalRecipient, rewardRecipient) = generateTrancheRecipients(10); threshold = ETH_STAKE; @@ -61,6 +62,7 @@ contract OptimisticWithdrawalRecipientFactoryTest is OWRTestHelper, Test { function testCan_emitOnCreate() public { // don't check deploy address vm.expectEmit(false, true, true, true); + emit CreateOWRecipient( address(0xdead), ETH_ADDRESS, recoveryAddress, principalRecipient, rewardRecipient, threshold ); @@ -176,13 +178,11 @@ contract OptimisticWithdrawalRecipientFactoryTest is OWRTestHelper, Test { vm.expectRevert( abi.encodeWithSelector(OptimisticWithdrawalRecipientFactory.Invalid__ThresholdTooLarge.selector, _threshold) ); - owrFactoryModule.createOWRecipient(ETH_ADDRESS, recoveryAddress, principalRecipient, rewardRecipient, threshold); vm.expectRevert( abi.encodeWithSelector(OptimisticWithdrawalRecipientFactory.Invalid__ThresholdTooLarge.selector, _threshold) ); - owrFactoryModule.createOWRecipient(address(mERC20), recoveryAddress, principalRecipient, rewardRecipient, threshold); } }