diff --git a/.claude/skills/obol-ovm/SKILL.md b/.claude/skills/deploy-obol-ovm/SKILL.md similarity index 72% rename from .claude/skills/obol-ovm/SKILL.md rename to .claude/skills/deploy-obol-ovm/SKILL.md index 8c29247b..1eb193d2 100644 --- a/.claude/skills/obol-ovm/SKILL.md +++ b/.claude/skills/deploy-obol-ovm/SKILL.md @@ -1,5 +1,5 @@ --- -name: obol-ovm +name: deploy-obol-ovm description: | Manage Obol Validator Manager (OVM) smart contracts on Ethereum. Use this skill for any OVM operation: querying contract state, deploying new OVMs, managing roles (grant/revoke), distributing funds, setting beneficiaries or reward recipients, and requesting validator withdrawals. Trigger this skill whenever the user mentions OVM, Obol Validator Manager, validator management, distributed validators, or wants to interact with OVM contracts on mainnet/hoodi/sepolia. --- @@ -8,6 +8,8 @@ description: | This skill provides scripts and knowledge to manage OVM contracts on Ethereum. OVM contracts manage distributed validators — handling ETH deposits, withdrawals (EIP-7002), consolidations (EIP-7251), and fund distribution to principal and reward recipients. +OVM is the **current-generation** Obol smart-contract family and the default choice for new deployments. For legacy deployments using the older OptimisticWithdrawalRecipient (OWR) + 0xSplits pattern, see the `deploy-obol-splits-legacy` skill instead. + ## Prerequisites - **Foundry (`cast`)** must be installed and on PATH @@ -30,7 +32,7 @@ For write operations, always confirm with the user that `PRIVATE_KEY` is set. Ne ## Scripts -All scripts are in `.claude/skills/obol-ovm/scripts/`. Every script accepts an optional network argument (defaults to `mainnet`). Supported networks: `mainnet`, `hoodi`, `sepolia`. +All scripts are in `.claude/skills/deploy-obol-ovm/scripts/`. Every script accepts an optional network argument (defaults to `mainnet`). Supported networks: `mainnet`, `hoodi`, `sepolia`. Override the default RPC by setting `RPC_URL` env var. @@ -38,7 +40,7 @@ Override the default RPC by setting `RPC_URL` env var. Before performing write operations on an address, verify it was deployed by the factory: ```bash -.claude/skills/obol-ovm/scripts/check-is-ovm.sh
[network] +.claude/skills/deploy-obol-ovm/scripts/check-is-ovm.sh
[network] ``` Queries `CreateObolValidatorManager` event logs from the factory. Exits 0 if the address is an OVM, exits 1 if not. Run this before grant-roles, revoke-roles, distribute, set-beneficiary, set-reward-recipient, or withdraw to catch mistakes early. @@ -46,19 +48,19 @@ Queries `CreateObolValidatorManager` event logs from the factory. Exits 0 if the **Query OVM state:** ```bash -.claude/skills/obol-ovm/scripts/query-ovm.sh [network] +.claude/skills/deploy-obol-ovm/scripts/query-ovm.sh [network] ``` Returns owner, principal/reward recipients, threshold, balances, version. **Query roles for an address:** ```bash -.claude/skills/obol-ovm/scripts/query-roles.sh [network] +.claude/skills/deploy-obol-ovm/scripts/query-roles.sh [network] ``` Returns the decoded role bitmask showing which roles the target has. **Query EIP-7002/7251 system contract fees:** ```bash -.claude/skills/obol-ovm/scripts/query-fees.sh [network] +.claude/skills/deploy-obol-ovm/scripts/query-fees.sh [network] ``` Returns current withdrawal fee (EIP-7002) and consolidation fee (EIP-7251) in wei. Useful before calling withdraw or consolidate to know how much ETH to send. @@ -76,65 +78,65 @@ Each write script checks that `PRIVATE_KEY` is set, prints what it's about to do **Deploy a new OVM:** ```bash -.claude/skills/obol-ovm/scripts/deploy-ovm.sh [threshold_gwei] [network] +.claude/skills/deploy-obol-ovm/scripts/deploy-ovm.sh [threshold_gwei] [network] ``` Default threshold is 16 gwei. Deploys via the network's factory contract. **Grant roles:** ```bash -.claude/skills/obol-ovm/scripts/grant-roles.sh [network] +.claude/skills/deploy-obol-ovm/scripts/grant-roles.sh [network] ``` **Revoke roles:** ```bash -.claude/skills/obol-ovm/scripts/revoke-roles.sh [network] +.claude/skills/deploy-obol-ovm/scripts/revoke-roles.sh [network] ``` **Distribute funds:** ```bash -.claude/skills/obol-ovm/scripts/distribute-funds.sh [network] +.claude/skills/deploy-obol-ovm/scripts/distribute-funds.sh [network] ``` Anyone can call this — no special role required. **Set beneficiary:** ```bash -.claude/skills/obol-ovm/scripts/set-beneficiary.sh [network] +.claude/skills/deploy-obol-ovm/scripts/set-beneficiary.sh [network] ``` Requires SET_BENEFICIARY_ROLE (4). **Set reward recipient:** ```bash -.claude/skills/obol-ovm/scripts/set-reward-recipient.sh [network] +.claude/skills/deploy-obol-ovm/scripts/set-reward-recipient.sh [network] ``` Requires SET_REWARD_ROLE (16). **Request validator withdrawal (EIP-7002):** ```bash -.claude/skills/obol-ovm/scripts/withdraw.sh [network] +.claude/skills/deploy-obol-ovm/scripts/withdraw.sh [network] ``` Requires WITHDRAWAL_ROLE (1). Sends ETH = max_fee * num_validators for fees. **Consolidate validators (EIP-7251):** ```bash -.claude/skills/obol-ovm/scripts/consolidate.sh [network] +.claude/skills/deploy-obol-ovm/scripts/consolidate.sh [network] ``` Requires CONSOLIDATION_ROLE (2). Consolidates stake from source validator into destination. Sends max_fee as ETH. Query current fees with `query-fees.sh` first. **Deposit for validator(s):** ```bash -.claude/skills/obol-ovm/scripts/deposit.sh [network] +.claude/skills/deploy-obol-ovm/scripts/deposit.sh [network] ``` Requires DEPOSIT_ROLE (32). Reads a deposit data JSON file (standard format from deposit CLI) and executes deposits via `forge script`. Each deposit sends 32 ETH. **Set principal stake amount:** ```bash -.claude/skills/obol-ovm/scripts/set-principal-stake.sh [network] +.claude/skills/deploy-obol-ovm/scripts/set-principal-stake.sh [network] ``` Requires owner. Sets `amountOfPrincipalStake` which controls how much of distributed funds goes to the principal recipient. Queries and prints current value before changing. **Sweep pull balance:** ```bash -.claude/skills/obol-ovm/scripts/sweep.sh [network] +.claude/skills/deploy-obol-ovm/scripts/sweep.sh [network] ``` Extracts funds from `pullBalances[principalRecipient]`. Pass `0x0000000000000000000000000000000000000000` as beneficiary to sweep to principal recipient (anyone can call). Pass a custom address to sweep there (owner only). Amount=0 sweeps all. @@ -156,11 +158,13 @@ Example: grant WITHDRAWAL + DEPOSIT = pass `33` as the roles value. ## Factory Addresses -| Network | Factory Address | -|---------|----------------| -| mainnet | `0x2c26B5A373294CaccBd3DE817D9B7C6aea7De584` | -| hoodi | `0x5754C8665B7e7BF15E83fCdF6d9636684B782b12` | -| sepolia | `0xF32F8B563d8369d40C45D5d667C2B26937F2A3d3` | +| Network | Factory Address | Deploy Block | +|---------|----------------|--------------| +| mainnet | `0x2c26B5A373294CaccBd3DE817D9B7C6aea7De584` | 23919948 | +| hoodi | `0x5754C8665B7e7BF15E83fCdF6d9636684B782b12` | 1735335 | +| sepolia | `0xF32F8B563d8369d40C45D5d667C2B26937F2A3d3` | 9159573 | + +Deploy blocks are the starting point for `cast logs` when enumerating OVMs (see "Listing OVMs on a network" below). ## Default RPCs @@ -184,16 +188,16 @@ When `distributeFunds()` is called: ### Deploy and configure a new OVM ``` 1. User sets PRIVATE_KEY env var -2. Deploy: .claude/skills/obol-ovm/scripts/deploy-ovm.sh 16 hoodi +2. Deploy: .claude/skills/deploy-obol-ovm/scripts/deploy-ovm.sh 16 hoodi 3. Query tx receipt to get the new OVM address from logs -4. Grant roles: .claude/skills/obol-ovm/scripts/grant-roles.sh 33 hoodi -5. Verify: .claude/skills/obol-ovm/scripts/query-roles.sh hoodi +4. Grant roles: .claude/skills/deploy-obol-ovm/scripts/grant-roles.sh 33 hoodi +5. Verify: .claude/skills/deploy-obol-ovm/scripts/query-roles.sh hoodi ``` ### Check OVM state and distribute ``` -1. Query: .claude/skills/obol-ovm/scripts/query-ovm.sh mainnet -2. If balance > 0, distribute: .claude/skills/obol-ovm/scripts/distribute-funds.sh mainnet +1. Query: .claude/skills/deploy-obol-ovm/scripts/query-ovm.sh mainnet +2. If balance > 0, distribute: .claude/skills/deploy-obol-ovm/scripts/distribute-funds.sh mainnet ``` ### Listing OVMs on a network @@ -204,7 +208,7 @@ cast logs --from-block --to-block latest \ "CreateObolValidatorManager(address indexed,address indexed,address,address,uint64)" \ --rpc-url ``` -Deploy blocks: mainnet=23919948, hoodi=1735335, sepolia=9159573. +Deploy blocks are listed in the Factory Addresses table above. ## RPC Retry Rule diff --git a/.claude/skills/obol-ovm/scripts/check-is-ovm.sh b/.claude/skills/deploy-obol-ovm/scripts/check-is-ovm.sh similarity index 100% rename from .claude/skills/obol-ovm/scripts/check-is-ovm.sh rename to .claude/skills/deploy-obol-ovm/scripts/check-is-ovm.sh diff --git a/.claude/skills/obol-ovm/scripts/consolidate.sh b/.claude/skills/deploy-obol-ovm/scripts/consolidate.sh similarity index 100% rename from .claude/skills/obol-ovm/scripts/consolidate.sh rename to .claude/skills/deploy-obol-ovm/scripts/consolidate.sh diff --git a/.claude/skills/obol-ovm/scripts/deploy-ovm.sh b/.claude/skills/deploy-obol-ovm/scripts/deploy-ovm.sh similarity index 100% rename from .claude/skills/obol-ovm/scripts/deploy-ovm.sh rename to .claude/skills/deploy-obol-ovm/scripts/deploy-ovm.sh diff --git a/.claude/skills/obol-ovm/scripts/deposit.sh b/.claude/skills/deploy-obol-ovm/scripts/deposit.sh similarity index 100% rename from .claude/skills/obol-ovm/scripts/deposit.sh rename to .claude/skills/deploy-obol-ovm/scripts/deposit.sh diff --git a/.claude/skills/obol-ovm/scripts/distribute-funds.sh b/.claude/skills/deploy-obol-ovm/scripts/distribute-funds.sh similarity index 100% rename from .claude/skills/obol-ovm/scripts/distribute-funds.sh rename to .claude/skills/deploy-obol-ovm/scripts/distribute-funds.sh diff --git a/.claude/skills/obol-ovm/scripts/grant-roles.sh b/.claude/skills/deploy-obol-ovm/scripts/grant-roles.sh similarity index 100% rename from .claude/skills/obol-ovm/scripts/grant-roles.sh rename to .claude/skills/deploy-obol-ovm/scripts/grant-roles.sh diff --git a/.claude/skills/obol-ovm/scripts/query-fees.sh b/.claude/skills/deploy-obol-ovm/scripts/query-fees.sh similarity index 100% rename from .claude/skills/obol-ovm/scripts/query-fees.sh rename to .claude/skills/deploy-obol-ovm/scripts/query-fees.sh diff --git a/.claude/skills/obol-ovm/scripts/query-ovm.sh b/.claude/skills/deploy-obol-ovm/scripts/query-ovm.sh similarity index 100% rename from .claude/skills/obol-ovm/scripts/query-ovm.sh rename to .claude/skills/deploy-obol-ovm/scripts/query-ovm.sh diff --git a/.claude/skills/obol-ovm/scripts/query-roles.sh b/.claude/skills/deploy-obol-ovm/scripts/query-roles.sh similarity index 100% rename from .claude/skills/obol-ovm/scripts/query-roles.sh rename to .claude/skills/deploy-obol-ovm/scripts/query-roles.sh diff --git a/.claude/skills/obol-ovm/scripts/revoke-roles.sh b/.claude/skills/deploy-obol-ovm/scripts/revoke-roles.sh similarity index 100% rename from .claude/skills/obol-ovm/scripts/revoke-roles.sh rename to .claude/skills/deploy-obol-ovm/scripts/revoke-roles.sh diff --git a/.claude/skills/obol-ovm/scripts/set-beneficiary.sh b/.claude/skills/deploy-obol-ovm/scripts/set-beneficiary.sh similarity index 100% rename from .claude/skills/obol-ovm/scripts/set-beneficiary.sh rename to .claude/skills/deploy-obol-ovm/scripts/set-beneficiary.sh diff --git a/.claude/skills/obol-ovm/scripts/set-principal-stake.sh b/.claude/skills/deploy-obol-ovm/scripts/set-principal-stake.sh similarity index 100% rename from .claude/skills/obol-ovm/scripts/set-principal-stake.sh rename to .claude/skills/deploy-obol-ovm/scripts/set-principal-stake.sh diff --git a/.claude/skills/obol-ovm/scripts/set-reward-recipient.sh b/.claude/skills/deploy-obol-ovm/scripts/set-reward-recipient.sh similarity index 100% rename from .claude/skills/obol-ovm/scripts/set-reward-recipient.sh rename to .claude/skills/deploy-obol-ovm/scripts/set-reward-recipient.sh diff --git a/.claude/skills/obol-ovm/scripts/sweep.sh b/.claude/skills/deploy-obol-ovm/scripts/sweep.sh similarity index 100% rename from .claude/skills/obol-ovm/scripts/sweep.sh rename to .claude/skills/deploy-obol-ovm/scripts/sweep.sh diff --git a/.claude/skills/obol-ovm/scripts/withdraw.sh b/.claude/skills/deploy-obol-ovm/scripts/withdraw.sh similarity index 100% rename from .claude/skills/obol-ovm/scripts/withdraw.sh rename to .claude/skills/deploy-obol-ovm/scripts/withdraw.sh diff --git a/CLAUDE.md b/CLAUDE.md index 0f49ad72..97ef154e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2,144 +2,69 @@ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. -## Project Overview - -Obol Splits is a suite of Solidity smart contracts enabling safe creation and management of Distributed Validators for Ethereum Consensus-based networks. Built with Foundry, targeting Solidity 0.8.19 with Shanghai EVM compatibility. - ## Development Commands ```sh # Setup -forge install && cp .env.sample .env - -# Testing -forge test # All tests -forge test --match-contract C --match-test t # Specific test -forge test --gas-report # With gas reporting +forge install && cp .env.sample .env # fill in RPC URLs and ETHERSCAN_API_KEY -# Build & Deploy +# Build forge build -forge script script/DeployFactoryScript.s.sol -``` - -## Architecture Overview - -### Core Contract Types - -**ObolValidatorManager (OVM)** - Validator management with ETH2 deposits, withdrawals (EIP-7002), consolidations (EIP-7251) -- 6 role-based permissions: WITHDRAWAL (0x01), CONSOLIDATION (0x02), SET_BENEFICIARY (0x04), RECOVER_FUNDS (0x08), SET_REWARD (0x10), DEPOSIT (0x20) -- PUSH/PULL distribution modes; principal threshold (gwei) routes funds -- `sweep()` extracts from `pullBalances[principalRecipient]` - anyone can call with beneficiary=address(0), owner for custom address -- Non-proxy (deployed via `new`, not Clone) - -**OptimisticWithdrawalRecipient (OWR)** - ETH distribution via 16 ETH threshold, Clone proxy, PUSH/PULL modes - -**OptimisticTokenWithdrawalRecipient** - OWR for ERC20 with configurable threshold - -**ObolLidoSplit** - Wraps stETH→wstETH for 0xSplits (Clone + BaseSplit) - -**ObolEtherfiSplit** - Wraps eETH→weETH for 0xSplits (Clone + BaseSplit) -**ImmutableSplitController** - Immutable 0xSplits config (CWIA pattern) - -### Factory Pattern - -Clone factories (Solady LibClone): OptimisticWithdrawalRecipientFactory, OptimisticTokenWithdrawalRecipientFactory, ObolLidoSplitFactory, ObolEtherfiSplitFactory, ObolCollectorFactory, ImmutableSplitControllerFactory - -**Exception**: ObolValidatorManagerFactory deploys full instances via `new` (not clones) - -### Key Patterns - -- **Clone Proxy**: Minimal proxy with CWIA optimization (all except OVM) -- **Two-Phase Distribution**: PUSH (0) = direct transfer; PULL (1) = deferred via `withdrawPullBalance()`. Prevents malicious recipient DOS. -- **Sweep (OVM)**: Extract from `pullBalances[principalRecipient]`. Anyone if beneficiary=0, owner for custom address. Amount=0 sweeps all. -- **BaseSplit**: Abstract base with distribute(), rescueFunds(), fee mechanism (PERCENTAGE_SCALE=1e5) -- **Rebasing Wrapping**: stETH→wstETH, eETH→weETH for 0xSplits - -## Constants & External Integrations - -**Constants:** -- `BALANCE_CLASSIFICATION_THRESHOLD_GWEI = 16 ether / 1 gwei` (OVM tests), `BALANCE_CLASSIFICATION_THRESHOLD = 16 ether` (OWR) -- `PERCENTAGE_SCALE = 1e5`, `PUBLIC_KEY_LENGTH = 48`, Distribution modes: `PUSH = 0, PULL = 1` -- OVM Roles: WITHDRAWAL (0x01), CONSOLIDATION (0x02), SET_BENEFICIARY (0x04), RECOVER_FUNDS (0x08), SET_REWARD (0x10), DEPOSIT (0x20) - -**Integrations:** 0xSplits (SplitMain), Lido (stETH/wstETH), EtherFi (eETH/weETH), Deposit Contract (ETH2), EIP-7002 (0x09Fc...aAaA), EIP-7251 (0x0043...EFf6), ENS Reverse Registrar - -## Project Structure +# Test +forge test -vvv # all tests +forge test --match-contract ObolLidoSplitTest -vvv # specific contract +forge test --match-contract ObolLidoSplitTest --match-test testCanDistribute -vv # specific test +forge test --gas-report # with gas reporting +# Format +forge fmt ``` -src/ -├── base/ BaseSplit and BaseSplitFactory abstracts -├── collector/ ObolCollector for reward collection -├── controllers/ ImmutableSplitController -├── etherfi/ EtherFi integration (eETH → weETH) -├── interfaces/ All interface definitions (IObolValidatorManager, etc.) -├── lido/ Lido integration (stETH → wstETH) -├── ovm/ ObolValidatorManager and ObolValidatorManagerFactory -├── owr/ OptimisticWithdrawalRecipient (ETH distribution) -│ └── token/ Token-based withdrawal recipient -└── test/ Test suite organized by feature - ├── ovm/ OVM tests and mocks - ├── owr/ OWR tests - └── ... - -script/ Deployment and management scripts -├── ovm/ OVM-specific scripts (12 scripts) -│ ├── DeployFactoryScript.s.sol -│ ├── CreateOVMScript.s.sol -│ ├── DepositScript.s.sol -│ ├── DistributeFundsScript.s.sol -│ ├── ConsolidateScript.s.sol -│ ├── WithdrawScript.s.sol -│ ├── GrantRolesScript.s.sol -│ ├── SetBeneficiaryScript.s.sol -│ ├── SetRewardRecipientScript.s.sol -│ ├── SetAmountOfPrincipalStakeScript.s.sol -│ ├── SystemContractFeesScript.s.sol -│ └── Utils.s.sol -├── splits/ 0xSplits deployment scripts -└── data/ Sample configuration JSON files -``` -## Testing & Security +Integration tests require `MAINNET_RPC_URL` and/or `SEPOLIA_RPC_URL` in `.env` for forked tests. + +## Architecture -**Testing:** -- Unit tests (role checks, fees, distribution), integration tests (lido/, etherfi/, owr/token/integration/) -- Mocks: SystemContractMock (EIP-7002/7251), DepositContractMock, MockERC20/1155/NFT -- 100 fuzz runs, .t.sol suffix, 43+ OVM tests (PUSH/PULL, sweep, roles, edge cases) -- OVM test pattern: ≥16 ether → beneficiary, <16 ether → reward +Solidity 0.8.19 on Foundry, targeting Shanghai EVM. The codebase provides composable contracts for Distributed Validator fund distribution, integrating with 0xSplits, Lido, Ether.fi, and Ethereum consensus layer contracts. -**Security:** -- ReentrancyGuard on OVM distribute/sweep -- PUSH/PULL prevents DOS, role-based access (6 OVM roles) -- Fund recovery via `recoverFunds()`, 48-byte pubkey validation -- Sweep allows emergency extraction, fee validation with refunds -- `fundsPendingWithdrawal` prevents over-distribution +### Core Pattern: Factory + Clone -## Deployment Addresses +Nearly every contract has a corresponding factory that deploys minimal proxies using solady's `LibClone` (CWIA — Constructor With Immutable Arguments stored in code, not storage). **Exception**: `ObolValidatorManagerFactory` deploys full instances via `new`. -**Mainnet:** OWRFactory: 0x119acd7844cbdd5fc09b1c6a4408f490c8f7f522, OWR: 0xe11eabf19a49c389d3e8735c35f8f34f28bdcb22, ObolLidoSplitFactory: 0xA9d94139A310150Ca1163b5E23f3E1dbb7D9E2A6, ObolLidoSplit: 0x2fB59065F049e0D0E3180C6312FA0FeB5Bbf0FE3, IMSCFactory: 0x49e7cA187F1E94d9A0d1DFBd6CCCd69Ca17F56a4, IMSC: 0xaF129979b773374dD3025d3F97353e73B0A6Cc8d +### Contract Modules -**Sepolia:** OWRFactory: 0xca78f8fda7ec13ae246e4d4cd38b9ce25a12e64a, OWR: 0x99585e71ab1118682d51efefca0a170c70eef0d6 +| Module | Key Contract | Purpose | +|--------|-------------|---------| +| `src/base/` | `BaseSplit`, `BaseSplitFactory` | Abstract base for all splitting contracts. Fee mechanism: `PERCENTAGE_SCALE = 1e5` | +| `src/collector/` | `ObolCollector` | Generic ETH/ERC20 reward collector with fee + distribute | +| `src/lido/` | `ObolLidoSplit` | Wraps rebasing stETH→wstETH before distributing to SplitWallet | +| `src/etherfi/` | `ObolEtherfiSplit` | Same pattern: wraps rebasing eETH→weETH | +| `src/owr/` | `OptimisticWithdrawalRecipient` | ETH-only withdrawal with principal/reward threshold (16 ether). `src/owr/token/` adds ERC20 support | +| `src/ovm/` | `ObolValidatorManager` | Validator lifecycle management: deposit, consolidation (EIP-7251), withdrawal (EIP-7002), distribution. Role-based access via solady `OwnableRoles` | +| `src/controllers/` | `ImmutableSplitController` | Manages 0xSplits config updates with hardcoded recipients | -## Notes +### Key Design Decisions -Solidity 0.8.19, Shanghai EVM, gas reports enabled, audited (https://docs.obol.tech/docs/sec/smart_contract_audit), formatting: 2-space tabs, 120 char lines, no bracket spacing +- **ETH as `address(0)`**: Throughout the codebase, native ETH is represented as `address(0)` in token parameters. +- **PUSH/PULL distribution**: `PUSH (0)` transfers directly; `PULL (1)` defers via `withdrawPullBalance()`. Pull mode prevents malicious recipient DOS. +- **Binary recipients**: OWR and OVM support exactly 2 recipients (principal and reward), routed by balance threshold. +- **Rebasing wrapping**: Lido and Ether.fi integrations wrap rebasing tokens before sending to 0xSplits, which can't handle rebasing tokens natively. +- **SafeTransferLib**: All token transfers use solmate's `SafeTransferLib`. -## OVM Workflows +### Test Organization -**Lifecycle:** -1. Deploy: `ObolValidatorManagerFactory.createObolValidatorManager(owner, beneficiary, rewardRecipient, principalThreshold)` -2. Grant roles: `grantRoles(user, DEPOSIT_ROLE | WITHDRAWAL_ROLE)` -3. Deposit: `deposit(pubkey, withdrawal_credentials, signature, deposit_data_root)` with 32 ETH -4. Distribute: `distributeFunds()` (PUSH) or `distributeFundsPull()` (PULL), then `withdrawPullBalance(account)` -5. Emergency: `sweep(address(0), 0)` extracts all principal pull balance to beneficiary +Tests in `src/test/` mirror the source structure. Each module has: +- Unit tests (`.t.sol` suffix) +- Test helpers (e.g., `ObolLidoSplitTestHelper.sol`) +- `integration/` subdirectories for fork-based tests +- Mocks in `src/test/utils/mocks/` and `src/test/ovm/mocks/` -**Distribution:** If `balance - fundsPendingWithdrawal >= principalThreshold * 1e9` AND `amountOfPrincipalStake > 0`: pay principal first (up to `amountOfPrincipalStake`), overflow to reward. Else: all to reward. `amountOfPrincipalStake` decrements on payout. +Fuzz testing configured at 100 runs. -**Sweep:** `sweep(address(0), amount)` anyone→principalRecipient; `sweep(customAddr, amount)` owner→custom; `sweep(address(0), 0)` sweeps all +### Formatting -**EIP-7002 Withdrawals:** `withdraw(pubKeys, amounts, maxFeePerWithdrawal, excessFeeRecipient)` - requires WITHDRAWAL_ROLE, ETH for `fee * pubKeys.length` +Configured in `foundry.toml [fmt]`: 2-space indentation, 120 char line length, no bracket spacing, double quotes, `attributes_first` multiline func headers. -**EIP-7251 Consolidations:** `consolidate(requests, maxFeePerConsolidation, excessFeeRecipient)` - requires CONSOLIDATION_ROLE, max 63 source pubkeys per request +### Deployment Scripts +Scripts in `script/`, with OVM-specific scripts in `script/ovm/`. Lido deployment uses JSON config files from `script/data/`. diff --git a/README.md b/README.md index 23eea374..ad0c6af0 100644 --- a/README.md +++ b/README.md @@ -6,11 +6,11 @@ This repo contains Obol Splits smart contracts. This suite of smart contracts an ### Disclaimer -The following smart contracts are provided as is, without warranty. Details of their audit can be consulted [here](https://docs.obol.tech/docs/sec/smart_contract_audit). +The following smart contracts are provided as is, without warranty. Details of their audit can be consulted [here](https://docs.obol.org/docs/advanced-and-troubleshooting/security/overview). ## Quickstart -This repo is built with [foundry](https://github.com/foundry-rs/foundry), a rust-based solidity development environment, and relies on [solmate](https://github.com/Rari-Capital/solmate), an efficient solidity smart contract library. Read the docs on our [docs site](https://docs.obol.org/learn/intro/obol-splits) for more information on what Distributed Validators are, and their smart contract lifecycle. +This repo is built with [foundry](https://github.com/foundry-rs/foundry), a rust-based solidity development environment, and relies on [solmate](https://github.com/Rari-Capital/solmate), an efficient solidity smart contract library. Read the docs on our [docs site](https://docs.obol.org/docs/learn/introduction/obol-splits) for more information on what Distributed Validators are, and their smart contract lifecycle. ### Installation @@ -34,7 +34,7 @@ cp .env.sample .env forge test ``` -This command runs all tests. +This command starts runs all tests. > NOTE: To run a specific test: ```sh @@ -55,6 +55,17 @@ This command generates compilation output into the `out` directory. This repo can be deployed with `forge create` or running the deployment scripts. +#### Create a new Lido Split + +To create a new Lido Splitter, setup your environment variables (`cp .env.sample .env` and ajust as needed) and then `source .env` to load them, then run the following command: + +```sh +forge script script/ObolLidoSetupScript.sol:ObolLidoSetupScript --fork-url $RPC_URL -vvvv --sig "run(string,address,address)" "./script/data/lido-data.json" $SPLITMAIN $OBOL_LIDO_SPLIT_FACTORY +``` + +> [!TIP] +> The above is setup as a dry-run, you must add `--broadcast` to the end of the command for the transaction to be sent to the network. + #### Hoodi ObolValidatorManagerFactory: https://hoodi.etherscan.io/address/0x5754C8665B7e7BF15E83fCdF6d9636684B782b12 diff --git a/foundry.lock b/foundry.lock new file mode 100644 index 00000000..cb7ad1fe --- /dev/null +++ b/foundry.lock @@ -0,0 +1,23 @@ +{ + "lib/ds-test": { + "rev": "0a5da56b0d65960e6a994d2ec8245e6edd38c248" + }, + "lib/forge-std": { + "branch": { + "name": "v1.3.0", + "rev": "3b20d60d14b343ee4f908cb8079495c07f5e8981" + } + }, + "lib/solady": { + "branch": { + "name": "v0.0.123", + "rev": "77809c18e010b914dde9518956a4ae7cb507d383" + } + }, + "lib/solmate": { + "rev": "2001af43aedb46fdc2335d2a7714fb2dae7cfcd1" + }, + "lib/splits-utils": { + "rev": "fcab1b0359aec71cc4da0ee468ee1ae922584e42" + } +} \ No newline at end of file diff --git a/lib/forge-std b/lib/forge-std index 3b20d60d..e8a047e3 160000 --- a/lib/forge-std +++ b/lib/forge-std @@ -1 +1 @@ -Subproject commit 3b20d60d14b343ee4f908cb8079495c07f5e8981 +Subproject commit e8a047e3f40f13fa37af6fe14e6e06283d9a060e diff --git a/script/ObolLidoSetupScript.sol b/script/ObolLidoSetupScript.sol index 7b510f99..3503ff74 100644 --- a/script/ObolLidoSetupScript.sol +++ b/script/ObolLidoSetupScript.sol @@ -3,8 +3,8 @@ 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 {SplitterConfiguration} from "./SplitterConfiguration.sol"; +import { ObolLidoSplitFactory } from "src/lido/ObolLidoSplitFactory.sol"; + /// @title ObolLidoScript /// @author Obol @@ -16,7 +16,7 @@ import {SplitterConfiguration} from "./SplitterConfiguration.sol"; /// 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. +/// in the sample file else the json parse will fail. /// /// /// To Run @@ -29,50 +29,136 @@ import {SplitterConfiguration} from "./SplitterConfiguration.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, SplitterConfiguration { - 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); - JsonSplitData[] memory configuration = abi.decode(parsedJson, (JsonSplitData[])); - _validateSplitInputJson(configuration); +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; + } + + 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; - // 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]; - for (uint256 j = 0; j < configuration.length; j++) { - string memory objKey = vm.toString(j); - // deploy split - JsonSplitData memory currentConfiguration = configuration[j]; + vm.startBroadcast(privKey); - vm.startBroadcast(privKey); + address split = ISplitMain(splitMain).createSplit( + currentConfiguration.accounts, + currentConfiguration.percentAllocations, + currentConfiguration.distributorFee, + currentConfiguration.controller + ); - address split = ISplitMain(splitMain).createSplit( - currentConfiguration.accounts, - currentConfiguration.percentAllocations, - currentConfiguration.distributorFee, - currentConfiguration.controller - ); + // create obol split + address obolLidoSplitAdress = ObolLidoSplitFactory(obolLidoSplitFactory).createSplit( + split + ); - // create obol split - address obolLidoSplitAdress = ObolLidoSplitFactory(obolLidoSplitFactory).createCollector(address(0), split); + vm.stopBroadcast(); - vm.stopBroadcast(); + vm.serializeAddress(objKey, "splitAddress", split); + string memory repsonse = vm.serializeAddress(objKey, "obolLidoSplitAddress", obolLidoSplitAdress); - vm.serializeAddress(objKey, "splitAddress", split); - string memory repsonse = vm.serializeAddress(objKey, "obolLidoSplitAddress", obolLidoSplitAdress); + finalJSON = vm.serializeString( + jsonKey, + objKey, + repsonse + ); + } + + vm.writeJson(finalJSON, "./result.json"); + } - finalJSON = vm.serializeString(jsonKey, objKey, repsonse); + 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); + } } - vm.writeJson(finalJSON, "./result.json"); - } -} + 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]; + } + } +} \ No newline at end of file diff --git a/script/ObolLidoSplitFactoryScript.s.sol b/script/ObolLidoSplitFactoryScript.s.sol index ffa778da..b50e34d9 100644 --- a/script/ObolLidoSplitFactoryScript.s.sol +++ b/script/ObolLidoSplitFactoryScript.s.sol @@ -6,15 +6,25 @@ 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); + 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); + ERC20 stETH = ERC20(_stETH); + ERC20 wstETH = ERC20(_wstETH); - new ObolLidoSplitFactory{salt: keccak256("obol.lidoSplitFactory.v1")}(_feeRecipient, _feeShare, stETH, wstETH); + new ObolLidoSplitFactory{salt: keccak256("obol.lidoSplitFactory.v1")}( + _feeRecipient, + _feeShare, + stETH, + wstETH + ); - vm.stopBroadcast(); - } + vm.stopBroadcast(); + } } diff --git a/script/data/lido-data-supercluster.json b/script/data/lido-data-supercluster.json new file mode 100644 index 00000000..9042f049 --- /dev/null +++ b/script/data/lido-data-supercluster.json @@ -0,0 +1,19 @@ +[ + { + "accounts": [ + "0x0000000000000000000000000000000000000001", + "0x0000000000000000000000000000000000000002", + "0x0000000000000000000000000000000000000003", + "0x0000000000000000000000000000000000000004", + "0x0000000000000000000000000000000000000005", + "0x0000000000000000000000000000000000000006", + "0x0000000000000000000000000000000000000007", + "0x0000000000000000000000000000000000000008" + ], + "controller": "0x0000000000000000000000000000000000000000", + "distributorFee": 0, + "percentAllocations": [ + 102040, 285714, 102041, 102041, 102041, 102041, 102041, 102041 + ] + } +] diff --git a/script/data/lido-data.json b/script/data/lido-data.json new file mode 100644 index 00000000..4ad30b8b --- /dev/null +++ b/script/data/lido-data.json @@ -0,0 +1,18 @@ +[ + { + "accounts": [ + "0x0000000000000000000000000000000000000001", + "0x0000000000000000000000000000000000000002", + "0x0000000000000000000000000000000000000003", + "0x0000000000000000000000000000000000000004", + "0x0000000000000000000000000000000000000005", + "0x0000000000000000000000000000000000000006", + "0x0000000000000000000000000000000000000007" + ], + "controller": "0x0000000000000000000000000000000000000000", + "distributorFee": 0, + "percentAllocations": [ + 142858, 142857, 142857, 142857, 142857, 142857, 142857 + ] + } +] \ No newline at end of file diff --git a/src/base/BaseSplit.sol b/src/base/BaseSplit.sol deleted file mode 100644 index f2a8063f..00000000 --- a/src/base/BaseSplit.sol +++ /dev/null @@ -1,104 +0,0 @@ -// 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 deleted file mode 100644 index 01944da5..00000000 --- a/src/base/BaseSplitFactory.sol +++ /dev/null @@ -1,18 +0,0 @@ -// 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 deleted file mode 100644 index 756455a5..00000000 --- a/src/collector/ObolCollector.sol +++ /dev/null @@ -1,26 +0,0 @@ -// 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 deleted file mode 100644 index c76c6fd9..00000000 --- a/src/collector/ObolCollectorFactory.sol +++ /dev/null @@ -1,40 +0,0 @@ -// 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/etherfi/ObolEtherfiSplit.sol b/src/etherfi/ObolEtherfiSplit.sol deleted file mode 100644 index b9eebe0b..00000000 --- a/src/etherfi/ObolEtherfiSplit.sol +++ /dev/null @@ -1,55 +0,0 @@ -// 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 deleted file mode 100644 index bca33cfe..00000000 --- a/src/etherfi/ObolEtherfiSplitFactory.sol +++ /dev/null @@ -1,43 +0,0 @@ -// 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/IweETH.sol b/src/interfaces/IweETH.sol deleted file mode 100644 index 199a4605..00000000 --- a/src/interfaces/IweETH.sol +++ /dev/null @@ -1,9 +0,0 @@ -// 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/lido/ObolLidoSplit.sol b/src/lido/ObolLidoSplit.sol index 2753899d..c27450bc 100644 --- a/src/lido/ObolLidoSplit.sol +++ b/src/lido/ObolLidoSplit.sol @@ -5,50 +5,113 @@ 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 BaseSplit { +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 + /// ----------------------------------------------------------------------- + /// @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) BaseSplit(_feeRecipient, _feeShare) { + 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; stETH = _stETH; wstETH = _wstETH; + feeShare = _feeShare; } - 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(); + /// 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); } /// Wraps the current stETH token balance to wstETH - /// transfers the wstETH balance to withdrawalAddress for distribution - function _beforeDistribute() internal override returns (address tokenAddress, uint256 amount) { - tokenAddress = address(wstETH); - + /// transfers the wstETH balance to splitWallet for distribution + /// @return amount Amount of wstETH transferred to splitWallet + function distribute() external returns (uint256 amount) { // get current balance uint256 balance = stETH.balanceOf(address(this)); // approve the wstETH stETH.approve(address(wstETH), balance); - // wrap into wstETH + // wrap into wseth // 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 bdb872fc..4d0514f6 100644 --- a/src/lido/ObolLidoSplitFactory.sol +++ b/src/lido/ObolLidoSplitFactory.sol @@ -3,20 +3,32 @@ 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 is BaseSplitFactory { +contract ObolLidoSplitFactory { + /// ----------------------------------------------------------------------- + /// errors + /// ----------------------------------------------------------------------- + + /// Invalid wallet + error Invalid_Wallet(); /// ----------------------------------------------------------------------- /// libraries /// ----------------------------------------------------------------------- using LibClone for address; + /// ----------------------------------------------------------------------- + /// events + /// ----------------------------------------------------------------------- + + /// Emitted after lido split + event CreateObolLidoSplit(address split); + /// ----------------------------------------------------------------------- /// storage /// ----------------------------------------------------------------------- @@ -28,17 +40,15 @@ contract ObolLidoSplitFactory is BaseSplitFactory { 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 - /// @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(); + /// @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(); - collector = address(lidoSplitImpl).clone(abi.encodePacked(withdrawalAddress)); + lidoSplit = address(lidoSplitImpl).clone(abi.encodePacked(splitWallet)); - emit CreateSplit(address(0), collector); + emit CreateObolLidoSplit(lidoSplit); } } diff --git a/src/test/collector/ObolCollector.t.sol b/src/test/collector/ObolCollector.t.sol deleted file mode 100644 index f0cf7e85..00000000 --- a/src/test/collector/ObolCollector.t.sol +++ /dev/null @@ -1,198 +0,0 @@ -// 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)); - vm.assume(fuzzFeeRecipient != fuzzWithdrawalAddress); - - 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 deleted file mode 100644 index a7f376a1..00000000 --- a/src/test/collector/ObolCollectorFactory.t.sol +++ /dev/null @@ -1,32 +0,0 @@ -// 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/etherfi/ObolEtherfiSplit.t.sol b/src/test/etherfi/ObolEtherfiSplit.t.sol deleted file mode 100644 index 0eac8a1e..00000000 --- a/src/test/etherfi/ObolEtherfiSplit.t.sol +++ /dev/null @@ -1,189 +0,0 @@ -// 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, - uint96 amountToDistribute, - address fuzzFeeRecipient, - uint96 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 deleted file mode 100644 index 08b3e9d0..00000000 --- a/src/test/etherfi/ObolEtherfiSplitFactory.t.sol +++ /dev/null @@ -1,50 +0,0 @@ -// 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 deleted file mode 100644 index fcb4b131..00000000 --- a/src/test/etherfi/ObolEtherfiSplitTestHelper.sol +++ /dev/null @@ -1,8 +0,0 @@ -// 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 deleted file mode 100644 index 62e2e00e..00000000 --- a/src/test/etherfi/integration/ObolEtherfiSplitIntegrationTest.sol +++ /dev/null @@ -1,64 +0,0 @@ -// 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 99bbc13c..fb5d5cc1 100644 --- a/src/test/lido/ObolLIdoSplitFactory.t.sol +++ b/src/test/lido/ObolLIdoSplitFactory.t.sol @@ -4,7 +4,6 @@ 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 { @@ -13,38 +12,46 @@ contract ObolLidoSplitFactoryTest is ObolLidoSplitTestHelper, Test { address demoSplit; - event CreateSplit(address token, address split); + event CreateObolLidoSplit(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 CreateSplit(address(0), address(0x1)); + emit CreateObolLidoSplit(address(0x1)); - lidoSplitFactory.createCollector(address(0), demoSplit); + lidoSplitFactory.createSplit(demoSplit); vm.expectEmit(true, true, true, false, address(lidoSplitFactoryWithFee)); - emit CreateSplit(address(0), address(0x1)); + emit CreateObolLidoSplit(address(0x1)); - lidoSplitFactoryWithFee.createCollector(address(0), demoSplit); + lidoSplitFactoryWithFee.createSplit(demoSplit); } function testCannot_CreateSplitInvalidAddress() public { - vm.expectRevert(BaseSplitFactory.Invalid_Address.selector); - lidoSplitFactory.createCollector(address(0), address(0)); + vm.expectRevert(ObolLidoSplitFactory.Invalid_Wallet.selector); + lidoSplitFactory.createSplit(address(0)); - vm.expectRevert(BaseSplitFactory.Invalid_Address.selector); - lidoSplitFactoryWithFee.createCollector(address(0), address(0)); + vm.expectRevert(ObolLidoSplitFactory.Invalid_Wallet.selector); + lidoSplitFactoryWithFee.createSplit(address(0)); } } diff --git a/src/test/lido/ObolLidoSplit.t.sol b/src/test/lido/ObolLidoSplit.t.sol index d5797ac3..c9b98a08 100644 --- a/src/test/lido/ObolLidoSplit.t.sol +++ b/src/test/lido/ObolLidoSplit.t.sol @@ -3,7 +3,6 @@ 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"; @@ -30,42 +29,56 @@ 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.createCollector(address(0), demoSplit)); - lidoSplitWithFee = ObolLidoSplit(lidoSplitFactoryWithFee.createCollector(address(0), demoSplit)); + lidoSplit = ObolLidoSplit(lidoSplitFactory.createSplit(demoSplit)); + lidoSplitWithFee = ObolLidoSplit(lidoSplitFactoryWithFee.createSplit(demoSplit)); mERC20 = new MockERC20("Test Token", "TOK", 18); mERC20.mint(type(uint256).max); } function test_CannotCreateInvalidFeeRecipient() public { - vm.expectRevert(BaseSplit.Invalid_FeeRecipient.selector); + vm.expectRevert( + ObolLidoSplit.Invalid_FeeRecipient.selector + ); new ObolLidoSplit(address(0), 10, ERC20(STETH_MAINNET_ADDRESS), ERC20(WSTETH_MAINNET_ADDRESS)); } function test_CannotCreateInvalidFeeShare() public { - vm.expectRevert(abi.encodeWithSelector(BaseSplit.Invalid_FeeShare.selector, PERCENTAGE_SCALE + 1)); + vm.expectRevert( + abi.encodeWithSelector(ObolLidoSplit.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(BaseSplit.Invalid_FeeShare.selector, PERCENTAGE_SCALE)); + vm.expectRevert( + abi.encodeWithSelector(ObolLidoSplit.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.withdrawalAddress(), demoSplit, "invalid address"); + assertEq(lidoSplit.splitWallet(), 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.withdrawalAddress(), demoSplit, "invalid address"); + assertEq(lidoSplitWithFee.splitWallet(), 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"); @@ -80,21 +93,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.withdrawalAddress()).balance, amountOfEther, "rescue not successful"); + assertEq(address(lidoSplit.splitWallet()).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.withdrawalAddress()), amountOfEther, "token - rescue not successful"); + assertEq(mERC20.balanceOf(lidoSplit.splitWallet()), amountOfEther, "token - rescue not successful"); } function testCannot_RescueLidoTokens() public { - vm.expectRevert(BaseSplit.Invalid_Address.selector); + vm.expectRevert(ObolLidoSplit.Invalid_Address.selector); lidoSplit.rescueFunds(address(STETH_MAINNET_ADDRESS)); - vm.expectRevert(BaseSplit.Invalid_Address.selector); + vm.expectRevert(ObolLidoSplit.Invalid_Address.selector); lidoSplit.rescueFunds(address(WSTETH_MAINNET_ADDRESS)); } @@ -145,23 +158,25 @@ contract ObolLidoSplitTest is ObolLidoSplitTestHelper, Test { function testFuzz_CanDistributeWithFee( address anotherSplit, - uint8 amountToDistributeEth, + uint256 amountToDistribute, address fuzzFeeRecipient, - uint16 fuzzFeeShare + 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(amountToDistributeEth > 1 && amountToDistributeEth < 200); - - uint256 amountToDistribute = uint256(amountToDistributeEth) * 1 ether; + vm.assume(amountToDistribute > 1 ether); + 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.createCollector(address(0), anotherSplit)); + ObolLidoSplit fuzzSplitWithFee = ObolLidoSplit(fuzzFactorySplitWithFee.createSplit(anotherSplit)); vm.prank(0x2bf3937b8BcccE4B65650F122Bb3f1976B937B2f); diff --git a/src/test/lido/integration/LidoSplitIntegrationTest.sol b/src/test/lido/integration/LidoSplitIntegrationTest.sol index 2f89c511..24049a86 100644 --- a/src/test/lido/integration/LidoSplitIntegrationTest.sol +++ b/src/test/lido/integration/LidoSplitIntegrationTest.sol @@ -22,8 +22,12 @@ 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"); @@ -35,7 +39,7 @@ contract ObolLidoSplitIntegrationTest is ObolLidoSplitTestHelper, Test { splitter = ISplitMain(SPLIT_MAIN_MAINNET).createSplit(accounts, percentAllocations, 0, address(0)); - lidoSplit = ObolLidoSplit(lidoSplitFactory.createCollector(address(0), splitter)); + lidoSplit = ObolLidoSplit(lidoSplitFactory.createSplit(splitter)); } function test_CanDistribute() public {