From f5fa3ae3875ee8ce6a17def9f02eb32c1f8249a5 Mon Sep 17 00:00:00 2001 From: samparsky <8148384+samparsky@users.noreply.github.com> Date: Tue, 15 Aug 2023 13:33:41 +0300 Subject: [PATCH 01/82] chore: remove unused contracts --- src/archive/NFTDeposit.sol | 74 -------- src/archive/Splitter.sol | 73 -------- src/archive/WithdrawalRecipientNFT.sol | 60 ------ src/archive/WithdrawalRecipientOwnable.sol | 50 ----- src/factory/ValidatorRewardSplitFactory.sol | 70 ------- .../OptimisticWithdrawalRecipient.sol | 0 .../OptimisticWithdrawalRecipientFactory.sol | 0 src/test/archive/NFTDeposit.t.sol | 27 --- src/test/archive/Splitter.t.sol | 69 ------- src/test/archive/WithdrawalRecipientNFT.t.sol | 49 ----- .../archive/WithdrawalRecipientOwnable.t.sol | 46 ----- src/waterfall/LWFactory.sol | 85 --------- src/waterfall/token/LW1155.sol | 175 ------------------ 13 files changed, 778 deletions(-) delete mode 100644 src/archive/NFTDeposit.sol delete mode 100644 src/archive/Splitter.sol delete mode 100644 src/archive/WithdrawalRecipientNFT.sol delete mode 100644 src/archive/WithdrawalRecipientOwnable.sol delete mode 100644 src/factory/ValidatorRewardSplitFactory.sol rename src/{waterfall => owr}/OptimisticWithdrawalRecipient.sol (100%) rename src/{waterfall => owr}/OptimisticWithdrawalRecipientFactory.sol (100%) delete mode 100644 src/test/archive/NFTDeposit.t.sol delete mode 100644 src/test/archive/Splitter.t.sol delete mode 100644 src/test/archive/WithdrawalRecipientNFT.t.sol delete mode 100644 src/test/archive/WithdrawalRecipientOwnable.t.sol delete mode 100644 src/waterfall/LWFactory.sol delete mode 100644 src/waterfall/token/LW1155.sol diff --git a/src/archive/NFTDeposit.sol b/src/archive/NFTDeposit.sol deleted file mode 100644 index f9d0804..0000000 --- a/src/archive/NFTDeposit.sol +++ /dev/null @@ -1,74 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity =0.8.17; - -import "ds-test/test.sol"; -import "solmate/tokens/ERC721.sol"; - -error DoesNotExist(); - -/// @notice Deposit contract wrapper which mints an NFT on successful deposit. -/// @author Obol Labs Inc. (https://github.com/ObolNetwork) -contract NFTDeposit is ERC721 { - /*/////////////////////////////////////////////////////////////// - IMMUTABLES - //////////////////////////////////////////////////////////////*/ - - IDepositContract public immutable depositContract; - - /*/////////////////////////////////////////////////////////////// - VARIABLES - //////////////////////////////////////////////////////////////*/ - - uint256 public totalSupply; - string public baseURI; - - /*/////////////////////////////////////////////////////////////// - CONSTRUCTOR - //////////////////////////////////////////////////////////////*/ - - constructor(IDepositContract _depositContract, string memory name, string memory symbol, string memory _baseURI) - ERC721(name, symbol) - { - depositContract = _depositContract; - baseURI = _baseURI; - } - - /*/////////////////////////////////////////////////////////////// - DEPOSIT LOGIC - //////////////////////////////////////////////////////////////*/ - - function deposit( - bytes calldata pubkey, - bytes calldata withdrawal_credentials, - bytes calldata signature, - bytes32 deposit_data_root - ) external payable returns (uint256) { - depositContract.deposit{value: msg.value}(pubkey, withdrawal_credentials, signature, deposit_data_root); - - uint256 id = totalSupply; - _mint(msg.sender, id); - totalSupply++; - return id; - } - - function tokenURI(uint256 id) public view override returns (string memory) { - // if (ownerOf[id] == address(0)) revert DoesNotExist(); - - return string(abi.encodePacked(baseURI, id)); - } -} - -interface IDepositContract { - /// @notice Submit a Phase 0 DepositData object. - /// @param pubkey A BLS12-381 public key. - /// @param withdrawal_credentials Commitment to a public key for withdrawals. - /// @param signature A BLS12-381 signature. - /// @param deposit_data_root The SHA-256 hash of the SSZ-encoded DepositData object. - /// Used as a protection against malformed input. - function deposit( - bytes calldata pubkey, - bytes calldata withdrawal_credentials, - bytes calldata signature, - bytes32 deposit_data_root - ) external payable; -} diff --git a/src/archive/Splitter.sol b/src/archive/Splitter.sol deleted file mode 100644 index dc71596..0000000 --- a/src/archive/Splitter.sol +++ /dev/null @@ -1,73 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity =0.8.17; - -import "ds-test/test.sol"; - -/// @notice A deployer contract which deploys fixed-cut 0xSplitter contracts. -/// @author Obol Labs Inc. (https://github.com/ObolNetwork) -contract ObolSplitterDeployer { - /*/////////////////////////////////////////////////////////////// - IMMUTABLES - //////////////////////////////////////////////////////////////*/ - - ISplitMain public immutable splitterContract; - address public immutable obolWallet; - - /*/////////////////////////////////////////////////////////////// - CONSTRUCTOR - //////////////////////////////////////////////////////////////*/ - - constructor(ISplitMain _splitterContract, address _obolWallet) { - splitterContract = _splitterContract; - obolWallet = _obolWallet; - } - - /*/////////////////////////////////////////////////////////////// - DEPLOY LOGIC - //////////////////////////////////////////////////////////////*/ - - function deploy(address[] calldata accounts) public returns (address) { - // Revert if function is called without any accounts. - require(accounts.length != 0, "Deploy called without any recipients"); - - // Calculate percentAllocations. - uint32[] memory percentAllocations = new uint32[](accounts.length + 1); - // 4% standard share for Obol - uint32 obolShare = 40_000; - uint32 validatorShare = 960_000; - uint32 sharePerValidator = validatorShare / uint32(accounts.length); - uint32 totalValidatorShare = sharePerValidator * uint32(accounts.length); - - // Return the difference to Obol, if any. - if (totalValidatorShare < validatorShare) obolShare += validatorShare - totalValidatorShare; - - for (uint256 i = 0; i < accounts.length; i++) { - percentAllocations[i] = sharePerValidator; - } - - percentAllocations[accounts.length] = obolShare; - - // Inject obol address to accounts. - address[] memory fullAccountList = new address[](accounts.length + 1); - - for (uint256 i = 0; i < accounts.length; i++) { - fullAccountList[i] = accounts[i]; - } - - fullAccountList[accounts.length] = obolWallet; - return splitterContract.createSplit(fullAccountList, percentAllocations, 0, msg.sender); - } -} - -/** - * @title ISplitMain - * @author 0xSplits - */ -interface ISplitMain { - function createSplit( - address[] calldata accounts, - uint32[] calldata percentAllocations, - uint32 distributorFee, - address controller - ) external returns (address); -} diff --git a/src/archive/WithdrawalRecipientNFT.sol b/src/archive/WithdrawalRecipientNFT.sol deleted file mode 100644 index 9a3fc67..0000000 --- a/src/archive/WithdrawalRecipientNFT.sol +++ /dev/null @@ -1,60 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity =0.8.17; - -import "ds-test/test.sol"; -import "solmate/auth/Auth.sol"; -import "solmate/tokens/ERC721.sol"; - -/// @notice Withdrawal contract that allows sending NFT to withdrawal recipient account -/// @author Obol Labs Inc. (https://github.com/ObolNetwork) -contract WithdrawalRecipientNFT is Auth { - /*/////////////////////////////////////////////////////////////// - EVENTS - //////////////////////////////////////////////////////////////*/ - - event Withdrawal(address indexed user, address indexed recipient); - - event OwnerChanged(address indexed user, address indexed newOwner); - - /*/////////////////////////////////////////////////////////////// - IMMUTABLES - //////////////////////////////////////////////////////////////*/ - - ERC721 public nftContract; - uint256 public tokenID; - - /*/////////////////////////////////////////////////////////////// - CONSTRUCTOR - //////////////////////////////////////////////////////////////*/ - - constructor(ERC721 _nftContract, uint256 _tokenID, address _owner, Authority _authority) Auth(_owner, _authority) { - nftContract = _nftContract; - tokenID = _tokenID; - } - - /*/////////////////////////////////////////////////////////////// - WITHDRAWAL LOGIC - //////////////////////////////////////////////////////////////*/ - - function withdraw(address recipient) public requiresAuth { - nftContract.transferFrom(address(this), recipient, tokenID); - - emit Withdrawal(msg.sender, recipient); - } - - /*/////////////////////////////////////////////////////////////// - OWNER CHANGE LOGIC - //////////////////////////////////////////////////////////////*/ - - function changeOwner(address newOwner) public requiresAuth { - owner = newOwner; - - emit OwnerChanged(msg.sender, newOwner); - } - - /*/////////////////////////////////////////////////////////////// - RECEIVE LOGIC - //////////////////////////////////////////////////////////////*/ - - receive() external payable {} -} diff --git a/src/archive/WithdrawalRecipientOwnable.sol b/src/archive/WithdrawalRecipientOwnable.sol deleted file mode 100644 index 00aa327..0000000 --- a/src/archive/WithdrawalRecipientOwnable.sol +++ /dev/null @@ -1,50 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity >=0.8.10; - -import "solmate/auth/Auth.sol"; -import "ds-test/test.sol"; - -/// @notice Withdrawal contract that allows only the owner account to withdraw -/// @author Obol Labs Inc. (https://github.com/ObolNetwork) -contract WithdrawalRecipientOwnable is Auth { - /*/////////////////////////////////////////////////////////////// - EVENTS - //////////////////////////////////////////////////////////////*/ - - event Withdrawal(address indexed user, address indexed recipient); - - event OwnerChanged(address indexed user, address indexed newOwner); - - /*/////////////////////////////////////////////////////////////// - CONSTRUCTOR - //////////////////////////////////////////////////////////////*/ - - constructor(address _owner, Authority _authority) Auth(_owner, _authority) {} - - /*/////////////////////////////////////////////////////////////// - WITHDRAWAL LOGIC - //////////////////////////////////////////////////////////////*/ - - function withdraw(address payable recipient) public requiresAuth { - (bool sent, bytes memory data) = recipient.call{value: address(this).balance}(""); - require(sent, "Failed to withdraw balance"); - - emit Withdrawal(msg.sender, recipient); - } - - /*/////////////////////////////////////////////////////////////// - OWNER CHANGE LOGIC - //////////////////////////////////////////////////////////////*/ - - function changeOwner(address newOwner) public requiresAuth { - owner = newOwner; - - emit OwnerChanged(msg.sender, newOwner); - } - - /*/////////////////////////////////////////////////////////////// - RECEIVE LOGIC - //////////////////////////////////////////////////////////////*/ - - receive() external payable {} -} diff --git a/src/factory/ValidatorRewardSplitFactory.sol b/src/factory/ValidatorRewardSplitFactory.sol deleted file mode 100644 index cf337fb..0000000 --- a/src/factory/ValidatorRewardSplitFactory.sol +++ /dev/null @@ -1,70 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity =0.8.17; - -import {IWaterfallFactoryModule} from "../interfaces/IWaterfallFactoryModule.sol"; -import {ISplitMain, SplitConfiguration} from "../interfaces/ISplitMain.sol"; -import {IENSReverseRegistrar} from "../interfaces/IENSReverseRegistrar.sol"; - -/// @dev Creates multiple waterfall contracts and connects it to a splitter contract -contract ValidatorRewardSplitFactory { - /// @dev amount of ETH required to run a validator - uint256 internal constant ETH_STAKE = 32 ether; - - /// @dev waterfall eth token representation - address internal constant WATERFALL_ETH_TOKEN_ADDRESS = address(0x0); - - /// @dev non waterfall recipient - address internal constant NON_WATERFALL_TOKEN_RECIPIENT = address(0x0); - - /// @dev waterfall factory - IWaterfallFactoryModule public immutable waterfallFactoryModule; - - /// @dev splitMain factory - ISplitMain public immutable splitMain; - - constructor( - address _waterfallFactoryModule, - address _splitMain, - string memory _ensName, - address _ensReverseRegistrar, - address _ensOwner - ) { - waterfallFactoryModule = IWaterfallFactoryModule(_waterfallFactoryModule); - splitMain = ISplitMain(_splitMain); - IENSReverseRegistrar(_ensReverseRegistrar).setName(_ensName); - IENSReverseRegistrar(_ensReverseRegistrar).claim(_ensOwner); - } - - /// @dev Create reward split for ETH rewards - /// @param _split Split configuration data - /// @param _principal address to receive principal - /// @param _numberOfValidators number of validators being created - /// @return withdrawalAddresses array of withdrawal addresses - /// @return rewardSplitContract reward split contract - function createETHRewardSplit( - SplitConfiguration calldata _split, - address payable _principal, - uint256 _numberOfValidators - ) external returns (address[] memory withdrawalAddresses, address rewardSplitContract) { - rewardSplitContract = - splitMain.createSplit(_split.accounts, _split.percentAllocations, _split.distributorFee, _split.controller); - - address[] memory waterfallRecipients = new address[](2); - waterfallRecipients[0] = _principal; - waterfallRecipients[1] = rewardSplitContract; - - uint256[] memory thresholds = new uint256[](1); - thresholds[0] = ETH_STAKE; - - withdrawalAddresses = new address[](_numberOfValidators); - - for (uint256 i = 0; i < _numberOfValidators;) { - withdrawalAddresses[i] = waterfallFactoryModule.createWaterfallModule( - WATERFALL_ETH_TOKEN_ADDRESS, NON_WATERFALL_TOKEN_RECIPIENT, waterfallRecipients, thresholds - ); - unchecked { - i++; - } - } - } -} diff --git a/src/waterfall/OptimisticWithdrawalRecipient.sol b/src/owr/OptimisticWithdrawalRecipient.sol similarity index 100% rename from src/waterfall/OptimisticWithdrawalRecipient.sol rename to src/owr/OptimisticWithdrawalRecipient.sol diff --git a/src/waterfall/OptimisticWithdrawalRecipientFactory.sol b/src/owr/OptimisticWithdrawalRecipientFactory.sol similarity index 100% rename from src/waterfall/OptimisticWithdrawalRecipientFactory.sol rename to src/owr/OptimisticWithdrawalRecipientFactory.sol diff --git a/src/test/archive/NFTDeposit.t.sol b/src/test/archive/NFTDeposit.t.sol deleted file mode 100644 index bdab9b2..0000000 --- a/src/test/archive/NFTDeposit.t.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity >=0.8.0; - -import "ds-test/test.sol"; - -import {IDepositContract} from "src/archive/NFTDeposit.sol"; -import "../utils/mocks/MockNFTDeposit.sol"; -import "../utils/mocks/MockDepositContract.sol"; - -contract NFTDepositTest is DSTest { - MockNFTDeposit mockNFTDeposit; - - function setUp() public { - IDepositContract mockDepositContract = new MockDepositContract(); - mockNFTDeposit = new MockNFTDeposit(mockDepositContract); - } - - function testMintFromDeposit() public { - bytes memory pubkey = "0x12"; - bytes memory withdrawal_credentials = "0x32"; - bytes memory signature = "0x48"; - bytes32 deposit_data_root = "root"; - uint256 id = mockNFTDeposit.deposit{value: 32e18}(pubkey, withdrawal_credentials, signature, deposit_data_root); - - assertEq(mockNFTDeposit.ownerOf(id), address(this)); - } -} diff --git a/src/test/archive/Splitter.t.sol b/src/test/archive/Splitter.t.sol deleted file mode 100644 index e6f44b0..0000000 --- a/src/test/archive/Splitter.t.sol +++ /dev/null @@ -1,69 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity >=0.8.17; - -import "ds-test/test.sol"; - -import "../utils/mocks/MockSplitter.sol"; -import "../utils/mocks/MockSplitterDeployer.sol"; - -contract SplitterTest is DSTest { - MockSplitterDeployer mockSplitterDeployer; - MockSplitter mockSplitter; - - function setUp() public { - mockSplitter = new MockSplitter(); - mockSplitterDeployer = new MockSplitterDeployer(mockSplitter, address(0x0B01)); - } - - function testFailCallWithoutAccounts() public { - address[] memory accounts = new address[](0); - mockSplitterDeployer.deploy(accounts); - } - - function testCallDeploy() public { - address[] memory accounts = new address[](3); - accounts[0] = address(0xABEE); - accounts[1] = address(0xBEEF); - accounts[2] = address(0xCAFE); - address returnal = mockSplitterDeployer.deploy(accounts); - - assertEq(returnal, address(0xFFFF)); - - assertEq(mockSplitter.showSplitRecipient(0), accounts[0]); - assertEq(mockSplitter.showSplitRecipient(1), accounts[1]); - assertEq(mockSplitter.showSplitRecipient(2), accounts[2]); - assertEq(mockSplitter.showSplitRecipient(3), address(0x0B01)); - - assertEq(mockSplitter.showSplitAllocation(0), 320_000); - assertEq(mockSplitter.showSplitAllocation(1), 320_000); - assertEq(mockSplitter.showSplitAllocation(2), 320_000); - assertEq(mockSplitter.showSplitAllocation(3), 40_000); - - assertEq(mockSplitter.showController(), address(this)); - } - - function testCallDeploy7() public { - uint256 splitCnt = 7; - uint160 firstAddr = 0x0A00; - address[] memory accounts = new address[](splitCnt); - for (uint256 i = 0; i < splitCnt; i++) { - accounts[i] = address(firstAddr); - firstAddr += 1; - } - - address returnal = mockSplitterDeployer.deploy(accounts); - assertEq(returnal, address(0xFFFF)); - - for (uint256 i = 0; i < splitCnt; i++) { - assertEq(mockSplitter.showSplitRecipient(i), accounts[i]); - } - assertEq(mockSplitter.showSplitRecipient(splitCnt), address(0x0B01)); - - for (uint256 i = 0; i < splitCnt; i++) { - assertEq(mockSplitter.showSplitAllocation(i), 137_142); - } - assertEq(mockSplitter.showSplitAllocation(splitCnt), 40_006); - - assertEq(mockSplitter.showController(), address(this)); - } -} diff --git a/src/test/archive/WithdrawalRecipientNFT.t.sol b/src/test/archive/WithdrawalRecipientNFT.t.sol deleted file mode 100644 index a6f3067..0000000 --- a/src/test/archive/WithdrawalRecipientNFT.t.sol +++ /dev/null @@ -1,49 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity >=0.8.10; - -import "ds-test/test.sol"; -import "solmate/tokens/ERC721.sol"; - -import "../utils/mocks/MockWithdrawalRecipientNFT.sol"; -import "../utils/mocks/MockNFT.sol"; - -contract WithdrawalRecipientNFTTest is DSTest { - MockNFT nftContract; - MockWithdrawalRecipientNFT mockWithdrawalRecipientNFT; - - function setUp() public { - nftContract = new MockNFT(); - mockWithdrawalRecipientNFT = new MockWithdrawalRecipientNFT(nftContract); - - // ensure test contract is the withdrawal owner - address ownerAddr = mockWithdrawalRecipientNFT.owner(); - assertEq(ownerAddr, address(this)); - - // send an NFT to withdrawal contract - nftContract.mint(address(this), 0); - nftContract.transferFrom(address(this), address(mockWithdrawalRecipientNFT), 0); - } - - function testWithdrawAsOwner() public { - assertEq(nftContract.ownerOf(0), address(mockWithdrawalRecipientNFT)); - - mockWithdrawalRecipientNFT.withdraw(payable(address(0xABEE))); - - assertEq(nftContract.ownerOf(0), address(0xABEE)); - } - - function testFailWithdrawAsNonOwner() public { - mockWithdrawalRecipientNFT.changeOwner(address(0)); - mockWithdrawalRecipientNFT.withdraw(payable(address(0xABEE))); - } - - function testChangeOwnerAsOwner() public { - mockWithdrawalRecipientNFT.changeOwner(address(0xABEE)); - assertEq(mockWithdrawalRecipientNFT.owner(), address(0xABEE)); - } - - function testFailChangeOwnerAsNonOwner() public { - mockWithdrawalRecipientNFT.changeOwner(address(0)); - mockWithdrawalRecipientNFT.changeOwner(address(0xABEE)); - } -} diff --git a/src/test/archive/WithdrawalRecipientOwnable.t.sol b/src/test/archive/WithdrawalRecipientOwnable.t.sol deleted file mode 100644 index 1784472..0000000 --- a/src/test/archive/WithdrawalRecipientOwnable.t.sol +++ /dev/null @@ -1,46 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.10; - -import "ds-test/test.sol"; -import "../utils/mocks/MockWithdrawalRecipient.sol"; - - -contract WithdrawalRecipientOwnableTest is DSTest { - MockWithdrawalRecipient mockWithdrawalRecipient; - - function setUp() public { - mockWithdrawalRecipient = new MockWithdrawalRecipient(); - - // ensure test contract is the withdrawal owner - address ownerAddr = mockWithdrawalRecipient.owner(); - assertEq(ownerAddr, address(this)); - - // send some ether to withdrawal contract - (bool sent,) = address(mockWithdrawalRecipient).call{value: address(this).balance}(""); - require(sent, "Failed to send eth"); - } - - function testWithdrawAsOwner() public { - uint256 initialBalance = address(mockWithdrawalRecipient).balance; - - mockWithdrawalRecipient.withdraw(payable(address(0xABEE))); - - assertEq(address(mockWithdrawalRecipient).balance, 0); - assertEq(address(0xABEE).balance, initialBalance); - } - - function testFailWithdrawAsNonOwner() public { - mockWithdrawalRecipient.changeOwner(address(0)); - mockWithdrawalRecipient.withdraw(payable(address(0xABEE))); - } - - function testChangeOwnerAsOwner() public { - mockWithdrawalRecipient.changeOwner(address(0xABEE)); - assertEq(mockWithdrawalRecipient.owner(), address(0xABEE)); - } - - function testFailChangeOwnerAsNonOwner() public { - mockWithdrawalRecipient.changeOwner(address(0)); - mockWithdrawalRecipient.changeOwner(address(0xABEE)); - } -} diff --git a/src/waterfall/LWFactory.sol b/src/waterfall/LWFactory.sol deleted file mode 100644 index 5a9766e..0000000 --- a/src/waterfall/LWFactory.sol +++ /dev/null @@ -1,85 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity =0.8.17; - -import {LibClone} from "solady/utils/LibClone.sol"; -import {IWaterfallFactoryModule} from "../interfaces/IWaterfallFactoryModule.sol"; -import {ISplitMainV2, SplitConfiguration} from "../interfaces/ISplitMainV2.sol"; -import {ISplitFactory} from "../interfaces/ISplitFactory.sol"; -import {IENSReverseRegistrar} from "../interfaces/IENSReverseRegistrar.sol"; -import {LW1155} from "./token/LW1155.sol"; - -/// @dev Creates liquid waterfall and splitter contract contracts -contract LWFactory { - /// ----------------------------------------------------------------------- - /// storage - constants and immutables - /// ----------------------------------------------------------------------- - - /// @dev amount of ETH required to run a validator - uint256 internal constant ETH_STAKE = 32 ether; - - /// @dev waterfall eth token representation - address internal constant WATERFALL_ETH_TOKEN_ADDRESS = address(0x0); - - /// @dev non waterfall recipient - address internal constant NON_WATERFALL_TOKEN_RECIPIENT = address(0x0); - - /// @dev waterfall factory - IWaterfallFactoryModule public immutable waterfallFactoryModule; - - /// @dev splitMain factory - ISplitFactory public immutable splitFactory; - - /// @dev liquid waterfall implementation - LW1155 public immutable lw1155; - - constructor( - address _waterfallFactoryModule, - address _splitFactory, - string memory _ensName, - address _ensReverseRegistrar, - address _ensOwner, - address _recoveryWallet - ) { - waterfallFactoryModule = IWaterfallFactoryModule(_waterfallFactoryModule); - splitFactory = ISplitFactory(_splitFactory); - lw1155 = new LW1155(ISplitFactory(_splitFactory).splitMain(), _recoveryWallet); - IENSReverseRegistrar(_ensReverseRegistrar).setName(_ensName); - IENSReverseRegistrar(_ensReverseRegistrar).claim(_ensOwner); - } - - /// @dev Create reward split for ETH rewards - /// @param _split Split configuration data - /// @param _principal address to receive principal - /// @return withdrawalAddress withdrawal address - /// @return rewardSplitContract reward split contract - function createETHRewardSplit(bytes32 _splitWalletId, SplitConfiguration calldata _split, address payable _principal) - external - returns (address withdrawalAddress, address rewardSplitContract) - { - require(_split.accounts[0] == address(lw1155), "invalid_address"); - - // use factory to create split - rewardSplitContract = splitFactory.createSplit( - _splitWalletId, - _split.accounts, - _split.percentAllocations, - _split.distributorFee, - address(lw1155), - _split.controller - ); - - address[] memory waterfallRecipients = new address[](2); - waterfallRecipients[0] = address(lw1155); - waterfallRecipients[1] = rewardSplitContract; - - uint256[] memory thresholds = new uint256[](1); - thresholds[0] = ETH_STAKE; - - withdrawalAddress = waterfallFactoryModule.createWaterfallModule( - WATERFALL_ETH_TOKEN_ADDRESS, NON_WATERFALL_TOKEN_RECIPIENT, waterfallRecipients, thresholds - ); - - // mint tokens to principal account - lw1155.mint(_principal, rewardSplitContract, withdrawalAddress, _split); - } -} diff --git a/src/waterfall/token/LW1155.sol b/src/waterfall/token/LW1155.sol deleted file mode 100644 index 857947c..0000000 --- a/src/waterfall/token/LW1155.sol +++ /dev/null @@ -1,175 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.13; - -import {Ownable} from "solady/auth/Ownable.sol"; -import {ERC1155} from "solmate/tokens/ERC1155.sol"; -import {LibString} from "solmate/utils/LibString.sol"; -import {Base64} from "solady/utils/Base64.sol"; -import {ERC20} from "solmate/tokens/ERC20.sol"; -import {TokenUtils} from "splits-utils/TokenUtils.sol"; -import {utils} from "../../lib/Utils.sol"; -import {Renderer} from "../../lib/Renderer.sol"; -import {ISplitMainV2, SplitConfiguration} from "../../interfaces/ISplitMainV2.sol"; -import {IWaterfallModule} from "../../interfaces/IWaterfallModule.sol"; - -// @title LW1155 -/// @author Obol -/// @notice A minimal liquid waterfall and splits implementation -/// Ownership is represented by 1155s (each = 100% of waterfall tranche + split) -contract LW1155 is ERC1155, Ownable { - /// @dev invalid owner - error InvalidOwner(); - /// @dev zero address - error InvalidAddress(); - /// @dev zero number - error ZeroAmount(); - /// @dev claim exists - error ClaimExists(); - - /// ----------------------------------------------------------------------- - /// libraries - /// ----------------------------------------------------------------------- - using TokenUtils for address; - - /// ----------------------------------------------------------------------- - /// events - /// ----------------------------------------------------------------------- - event ReceiveETH(address indexed sender, uint256 amount); - event Recovered(address sender, address token, uint256 amount); - - /// ----------------------------------------------------------------------- - /// structs - /// ----------------------------------------------------------------------- - struct Claim { - ISplitMainV2 split; - IWaterfallModule waterfall; - SplitConfiguration configuration; - } - - /// ----------------------------------------------------------------------- - /// storage - /// ----------------------------------------------------------------------- - /// @dev ETH address representation - address internal constant ETH_TOKEN_ADDRESS = address(0x0); - /// @dev splitMain factory - ISplitMainV2 public immutable splitMain; - /// @dev obol treasury - address public immutable recoveryWallet; - - /// ----------------------------------------------------------------------- - /// storage - mutables - /// ----------------------------------------------------------------------- - - /// @dev nft claim information - mapping(uint256 => Claim) public claimData; - - constructor(ISplitMainV2 _splitMain, address _recoveryWallet) { - if (_recoveryWallet == address(0)) revert InvalidAddress(); - - splitMain = _splitMain; - recoveryWallet = _recoveryWallet; - _initializeOwner(msg.sender); - } - - /// @dev Mint NFT - /// @param _recipient address to receive minted NFT - /// @param _configuration split configuration - function mint(address _recipient, address _split, address _waterfall, SplitConfiguration calldata _configuration) - external - onlyOwner - { - // waterfall is unique per validator - uint256 id = uint256(keccak256(abi.encodePacked(_recipient, _waterfall))); - Claim memory claiminfo = Claim(ISplitMainV2(_split), IWaterfallModule(_waterfall), _configuration); - - if (address(claimData[id].split) != address(0)) revert ClaimExists(); - - claimData[id] = claiminfo; - _mint({to: _recipient, id: id, amount: 1, data: ""}); - } - - /// @dev send tokens and ETH to receiver - /// @notice Ensures the receiver is the right address to receive the tokens - /// @param _tokenIds address of tokens, address(0) represents ETH - /// @param _receiver address holding the NFT - function claim(uint256[] calldata _tokenIds, address _receiver) external { - uint256 size = _tokenIds.length; - - for (uint256 i = 0; i < size;) { - uint256 tokenId = _tokenIds[i]; - - if (balanceOf[_receiver][tokenId] == 0) revert InvalidOwner(); - - // fetch claim information - Claim memory tokenClaim = claimData[tokenId]; - - // claim from waterfall - tokenClaim.waterfall.waterfallFunds(); - - // claim from splitter - splitMain.distributeETH( - address(tokenClaim.split), - tokenClaim.configuration.accounts, - tokenClaim.configuration.percentAllocations, - tokenClaim.configuration.distributorFee, - address(0) - ); - ERC20[] memory emptyTokens = new ERC20[](0); - splitMain.withdraw(address(this), 1, emptyTokens); - - // transfer claimed eth to receiver - ETH_TOKEN_ADDRESS._safeTransfer(_receiver, ETH_TOKEN_ADDRESS._balanceOf(address(this))); - - unchecked { - ++i; - } - } - } - - /// Transfers a given `_amount` of an ERC20-token where address(0) is ETH - /// @param _token an ERC20-compatible token - /// @param _amount token amount - function recover(ERC20 _token, uint256 _amount) external { - if (_amount == 0) revert ZeroAmount(); - - emit Recovered(msg.sender, address(_token), _amount); - - address(_token)._safeTransfer(recoveryWallet, _amount); - } - - /// @dev Returns token uri - function uri(uint256) public view override returns (string memory) { - return string.concat( - "data:application/json;base64,", - Base64.encode( - bytes( - string.concat( - '{"name": "Obol Liquid Waterfall + Split ', - utils.shortAddressToString(address(this)), - '", "description": ', - '"Each token represents 32 ETH staked plus rewards", ', - '"external_url": ', - '"https://app.0xsplits.xyz/accounts/', - utils.addressToString(address(this)), - "/?chainId=", - utils.uint2str(block.chainid), - '", ', - '"image": "data:image/svg+xml;base64,', - Base64.encode(bytes(Renderer.render(address(this)))), - '"}' - ) - ) - ) - ); - } - - /// @dev Returns ERC1155 name - function name() external view returns (string memory) { - return string.concat("Obol Liquid Waterfall + Split ", utils.shortAddressToString(address(this))); - } - - /// @dev Enables ERC1155 to receive ETH - receive() external payable { - emit ReceiveETH(msg.sender, msg.value); - } -} From 25930df9e689bbee7f7784ddca32bb7d9a5e2545 Mon Sep 17 00:00:00 2001 From: samparsky <8148384+samparsky@users.noreply.github.com> Date: Tue, 15 Aug 2023 16:40:22 +0300 Subject: [PATCH 02/82] chore: remove unused contracts --- script/LWFactory.s.sol | 30 --- script/OWRFactoryScript.s.sol | 2 +- script/ValidatorRewardSplitFactory.s.sol | 25 -- src/lib/Renderer.sol | 96 -------- src/lib/SVG.sol | 101 -------- src/lib/Utils.sol | 122 ---------- src/test/ValidatorRewardSplitFactory.t.sol | 75 ------ src/test/utils/mocks/MockDepositContract.sol | 13 - src/test/utils/mocks/MockNFTDeposit.sol | 8 - src/test/utils/mocks/MockSplitter.sol | 38 --- src/test/utils/mocks/MockSplitterDeployer.sol | 8 - .../utils/mocks/MockWithdrawalRecipient.sol | 8 - .../mocks/MockWithdrawalRecipientNFT.sol | 11 - src/test/waterfall/LW1155.t.sol | 227 ------------------ src/test/waterfall/LWFactory.t.sol | 79 ------ .../OptimisticWithdrawalRecipient.t.sol | 4 +- ...OptimisticWithdrawalRecipientFactory.t.sol | 4 +- 17 files changed, 5 insertions(+), 846 deletions(-) delete mode 100644 script/LWFactory.s.sol delete mode 100644 script/ValidatorRewardSplitFactory.s.sol delete mode 100644 src/lib/Renderer.sol delete mode 100644 src/lib/SVG.sol delete mode 100644 src/lib/Utils.sol delete mode 100644 src/test/ValidatorRewardSplitFactory.t.sol delete mode 100644 src/test/utils/mocks/MockDepositContract.sol delete mode 100644 src/test/utils/mocks/MockNFTDeposit.sol delete mode 100644 src/test/utils/mocks/MockSplitter.sol delete mode 100644 src/test/utils/mocks/MockSplitterDeployer.sol delete mode 100644 src/test/utils/mocks/MockWithdrawalRecipient.sol delete mode 100644 src/test/utils/mocks/MockWithdrawalRecipientNFT.sol delete mode 100644 src/test/waterfall/LW1155.t.sol delete mode 100644 src/test/waterfall/LWFactory.t.sol diff --git a/script/LWFactory.s.sol b/script/LWFactory.s.sol deleted file mode 100644 index 5192b11..0000000 --- a/script/LWFactory.s.sol +++ /dev/null @@ -1,30 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity =0.8.17; - -import "forge-std/Script.sol"; -import {LWFactory} from "src/waterfall/LWFactory.sol"; - -contract LWFactoryScript is Script { - function run( - address waterfallFactoryModule, - address splitMain, - address ensReverseRegistrar, - address ensOnwer, - address recoveryWallet - ) external { - uint256 privKey = vm.envUint("PRIVATE_KEY"); - vm.startBroadcast(privKey); - - string memory ensName = "lwfactory.obol.eth"; - - new LWFactory{salt: keccak256("obol.lwFactory.v1")}( - waterfallFactoryModule, - splitMain, - ensName, - ensReverseRegistrar, - ensOnwer, - recoveryWallet - ); - vm.stopBroadcast(); - } -} diff --git a/script/OWRFactoryScript.s.sol b/script/OWRFactoryScript.s.sol index 6a8969b..008988e 100644 --- a/script/OWRFactoryScript.s.sol +++ b/script/OWRFactoryScript.s.sol @@ -2,7 +2,7 @@ pragma solidity =0.8.17; import "forge-std/Script.sol"; -import {OptimisticWithdrawalRecipientFactory} from "src/waterfall/OptimisticWithdrawalRecipientFactory.sol"; +import {OptimisticWithdrawalRecipientFactory} from "src/owr/OptimisticWithdrawalRecipientFactory.sol"; contract OWRFactoryScript is Script { function run() external { diff --git a/script/ValidatorRewardSplitFactory.s.sol b/script/ValidatorRewardSplitFactory.s.sol deleted file mode 100644 index a6134af..0000000 --- a/script/ValidatorRewardSplitFactory.s.sol +++ /dev/null @@ -1,25 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity =0.8.17; - -import "forge-std/Script.sol"; -import {ValidatorRewardSplitFactory} from "src/factory/ValidatorRewardSplitFactory.sol"; - -contract ValidatorRewardSplitFactoryScript is Script { - function run(address waterfallFactoryModule, address splitMain, address ensReverseRegistrar, address ensOnwer) - external - { - uint256 privKey = vm.envUint("PRIVATE_KEY"); - vm.startBroadcast(privKey); - - string memory ensName = "launchpad.obol.eth"; - - new ValidatorRewardSplitFactory{salt: keccak256("obol.validatorRewardSplitFactory.v1")}( - waterfallFactoryModule, - splitMain, - ensName, - ensReverseRegistrar, - ensOnwer - ); - vm.stopBroadcast(); - } -} diff --git a/src/lib/Renderer.sol b/src/lib/Renderer.sol deleted file mode 100644 index 2230371..0000000 --- a/src/lib/Renderer.sol +++ /dev/null @@ -1,96 +0,0 @@ -//SPDX-License-Identifier: MIT -pragma solidity =0.8.17; - -import "./SVG.sol"; -import "./Utils.sol"; - -// adapted from https://github.com/w1nt3r-eth/hot-chain-svg - -library Renderer { - uint256 internal constant size = 600; - uint256 internal constant rowSpacing = size / 8; - uint256 internal constant colSpacing = size / 14; - uint256 internal constant MAX_R = colSpacing * 65 / 100; - uint256 internal constant MIN_R = colSpacing * 3 / 10; - uint256 internal constant maxDur = 60; - uint256 internal constant minDur = 30; - uint256 internal constant durRandomnessDiscord = 5; // (60 - 30) < 2^5 - - function render(address addr) internal pure returns (string memory) { - string memory logo; - uint256 seed = uint256(uint160(addr)); - string memory color = utils.getHslColor(seed); - uint8[5] memory xs = [5, 4, 3, 4, 5]; - uint256 y = rowSpacing * 2; - for (uint256 i; i < 5; i++) { - uint256 x = colSpacing * xs[i]; - for (uint256 j; j < (8 - xs[i]); j++) { - logo = string.concat(logo, drawRandomOrb(x, y, color, seed = newSeed(seed))); - x += colSpacing * 2; - } - y += rowSpacing; - } - - return string.concat( - '', - logo, - "" - ); - } - - function randomR(uint256 seed) internal pure returns (uint256 r) { - r = utils.bound(seed, MAX_R, MIN_R); - } - - function randomDur(uint256 seed) internal pure returns (uint256 dur) { - dur = utils.bound(seed, maxDur, minDur); - } - - function newSeed(uint256 seed) internal pure returns (uint256) { - return uint256(keccak256(abi.encodePacked(seed))); - } - - function drawRandomOrb(uint256 cx, uint256 cy, string memory color, uint256 seed) - internal - pure - returns (string memory) - { - uint256 dur = randomDur(seed); - uint256 r = randomR(seed >> durRandomnessDiscord); - return drawOrb(cx, cy, r, dur, color); - } - - function drawOrb(uint256 cx, uint256 cy, uint256 r, uint256 dur, string memory color) - internal - pure - returns (string memory _values) - { - string memory animate; - string memory durStr = string.concat(utils.uint2str(dur / 10), ".", utils.uint2str(dur % 10)); - string memory valStr = string.concat( - utils.uint2str(r), "; ", utils.uint2str(MAX_R), "; ", utils.uint2str(MIN_R), "; ", utils.uint2str(r) - ); - animate = svg.animate( - string.concat( - svg.prop("attributeName", "r"), - svg.prop("dur", durStr), - svg.prop("repeatCount", "indefinite"), - svg.prop("calcMode", "paced"), - svg.prop("values", valStr) - ) - ); - _values = svg.circle( - string.concat( - svg.prop("cx", utils.uint2str(cx)), - svg.prop("cy", utils.uint2str(cy)), - svg.prop("r", utils.uint2str(r)), - svg.prop("fill", color) - ), - animate - ); - } -} diff --git a/src/lib/SVG.sol b/src/lib/SVG.sol deleted file mode 100644 index b6a3e9a..0000000 --- a/src/lib/SVG.sol +++ /dev/null @@ -1,101 +0,0 @@ -//SPDX-License-Identifier: MIT -pragma solidity =0.8.17; - -import "./Utils.sol"; - -// from https://github.com/w1nt3r-eth/hot-chain-svg - -// Core SVG utilitiy library which helps us construct -// onchain SVG's with a simple, web-like API. -library svg { - /* MAIN ELEMENTS */ - function g(string memory _props, string memory _children) internal pure returns (string memory) { - return el("g", _props, _children); - } - - function path(string memory _props, string memory _children) internal pure returns (string memory) { - return el("path", _props, _children); - } - - function text(string memory _props, string memory _children) internal pure returns (string memory) { - return el("text", _props, _children); - } - - function line(string memory _props, string memory _children) internal pure returns (string memory) { - return el("line", _props, _children); - } - - function circle(string memory _props, string memory _children) internal pure returns (string memory) { - return el("circle", _props, _children); - } - - function circle(string memory _props) internal pure returns (string memory) { - return el("circle", _props); - } - - function rect(string memory _props, string memory _children) internal pure returns (string memory) { - return el("rect", _props, _children); - } - - function rect(string memory _props) internal pure returns (string memory) { - return el("rect", _props); - } - - function filter(string memory _props, string memory _children) internal pure returns (string memory) { - return el("filter", _props, _children); - } - - function cdata(string memory _content) internal pure returns (string memory) { - return string.concat(""); - } - - /* GRADIENTS */ - function radialGradient(string memory _props, string memory _children) internal pure returns (string memory) { - return el("radialGradient", _props, _children); - } - - function linearGradient(string memory _props, string memory _children) internal pure returns (string memory) { - return el("linearGradient", _props, _children); - } - - function gradientStop(uint256 offset, string memory stopColor, string memory _props) - internal - pure - returns (string memory) - { - return el( - "stop", - string.concat( - prop("stop-color", stopColor), " ", prop("offset", string.concat(utils.uint2str(offset), "%")), " ", _props - ) - ); - } - - function animate(string memory _props) internal pure returns (string memory) { - return el("animate", _props); - } - - function animateTransform(string memory _props) internal pure returns (string memory) { - return el("animateTransform", _props); - } - - function image(string memory _href, string memory _props) internal pure returns (string memory) { - return el("image", string.concat(prop("href", _href), " ", _props)); - } - - /* COMMON */ - // A generic element, can be used to construct any SVG (or HTML) element - function el(string memory _tag, string memory _props, string memory _children) internal pure returns (string memory) { - return string.concat("<", _tag, " ", _props, ">", _children, ""); - } - - // A generic element, can be used to construct any SVG (or HTML) element without children - function el(string memory _tag, string memory _props) internal pure returns (string memory) { - return string.concat("<", _tag, " ", _props, "/>"); - } - - // an SVG attribute - function prop(string memory _key, string memory _val) internal pure returns (string memory) { - return string.concat(_key, "=", '"', _val, '" '); - } -} diff --git a/src/lib/Utils.sol b/src/lib/Utils.sol deleted file mode 100644 index 8ae015d..0000000 --- a/src/lib/Utils.sol +++ /dev/null @@ -1,122 +0,0 @@ -//SPDX-License-Identifier: MIT -pragma solidity =0.8.17; - -// adapted from https://github.com/w1nt3r-eth/hot-chain-svg - -// Core utils used extensively to format CSS and numbers. -library utils { - // used to simulate empty strings - string internal constant NULL = ""; - - // formats a CSS variable line. includes a semicolon for formatting. - function setCssVar(string memory _key, string memory _val) internal pure returns (string memory) { - return string.concat("--", _key, ":", _val, ";"); - } - - // formats getting a css variable - function getCssVar(string memory _key) internal pure returns (string memory) { - return string.concat("var(--", _key, ")"); - } - - // formats getting a def URL - function getDefURL(string memory _id) internal pure returns (string memory) { - return string.concat("url(#", _id, ")"); - } - - // formats rgba white with a specified opacity / alpha - function white_a(uint256 _a) internal pure returns (string memory) { - return rgba(255, 255, 255, _a); - } - - // formats rgba black with a specified opacity / alpha - function black_a(uint256 _a) internal pure returns (string memory) { - return rgba(0, 0, 0, _a); - } - - // formats generic rgba color in css - function rgba(uint256 _r, uint256 _g, uint256 _b, uint256 _a) internal pure returns (string memory) { - string memory formattedA = _a < 100 ? string.concat("0.", utils.uint2str(_a)) : "1"; - return - string.concat("rgba(", utils.uint2str(_r), ",", utils.uint2str(_g), ",", utils.uint2str(_b), ",", formattedA, ")"); - } - - // checks if two strings are equal - function stringsEqual(string memory _a, string memory _b) internal pure returns (bool) { - return keccak256(abi.encodePacked(_a)) == keccak256(abi.encodePacked(_b)); - } - - // returns the length of a string in characters - function utfStringLength(string memory _str) internal pure returns (uint256 length) { - uint256 i = 0; - bytes memory string_rep = bytes(_str); - - while (i < string_rep.length) { - if (string_rep[i] >> 7 == 0) i += 1; - else if (string_rep[i] >> 5 == bytes1(uint8(0x6))) i += 2; - else if (string_rep[i] >> 4 == bytes1(uint8(0xE))) i += 3; - else if (string_rep[i] >> 3 == bytes1(uint8(0x1E))) i += 4; - //For safety - else i += 1; - - length++; - } - } - - // converts an unsigned integer to a string - function uint2str(uint256 _i) internal pure returns (string memory _uintAsString) { - if (_i == 0) return "0"; - uint256 j = _i; - uint256 len; - while (j != 0) { - len++; - j /= 10; - } - bytes memory bstr = new bytes(len); - uint256 k = len; - while (_i != 0) { - k = k - 1; - uint8 temp = (48 + uint8(_i - (_i / 10) * 10)); - bytes1 b1 = bytes1(temp); - bstr[k] = b1; - _i /= 10; - } - return string(bstr); - } - - // generate hsla color from seed - function getHslColor(uint256 seed) internal pure returns (string memory _hsla) { - uint256 hue = seed % 360; - _hsla = string.concat("hsla(", utils.uint2str(hue), ", 88%, 56%, 1)"); - } - - function bound(uint256 value, uint256 max, uint256 min) internal pure returns (uint256 _value) { - /* require(max >= min, "INVALID_BOUND"); */ - _value = value % (max - min) + min; - } - - function addressToString(address _address) internal pure returns (string memory) { - bytes32 _bytes = bytes32(uint256(uint160(_address))); - bytes memory HEX = "0123456789abcdef"; - bytes memory _string = new bytes(42); - _string[0] = "0"; - _string[1] = "x"; - for (uint256 i = 0; i < 20; i++) { - _string[2 + i * 2] = HEX[uint8(_bytes[i + 12] >> 4)]; - _string[3 + i * 2] = HEX[uint8(_bytes[i + 12] & 0x0f)]; - } - return string(_string); - } - - function shortAddressToString(address _address) internal pure returns (string memory) { - bytes32 _bytes = bytes32(uint256(uint160(_address))); - bytes memory HEX = "0123456789abcdef"; - bytes memory _string = new bytes(6); - _string[0] = "0"; - _string[1] = "x"; - for (uint256 i = 0; i < 2; i++) { - _string[2 + i * 2] = HEX[uint8(_bytes[i + 12] >> 4)]; - _string[3 + i * 2] = HEX[uint8(_bytes[i + 12] & 0x0f)]; - } - return string(_string); - } -} diff --git a/src/test/ValidatorRewardSplitFactory.t.sol b/src/test/ValidatorRewardSplitFactory.t.sol deleted file mode 100644 index 7bdd589..0000000 --- a/src/test/ValidatorRewardSplitFactory.t.sol +++ /dev/null @@ -1,75 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity >=0.8.0; - -import "forge-std/Test.sol"; -import {ISplitMain, SplitConfiguration} from "../interfaces/ISplitMain.sol"; -import {IENSReverseRegistrar} from "../interfaces/IENSReverseRegistrar.sol"; -import {ValidatorRewardSplitFactory} from "../factory/ValidatorRewardSplitFactory.sol"; -import {IWaterfallFactoryModule} from "../interfaces/IWaterfallFactoryModule.sol"; -import {IWaterfallModule} from "../interfaces/IWaterfallModule.sol"; - -contract ValidatorRewardSplitFactoryTest is Test { - ValidatorRewardSplitFactory public factory; - address public ensReverseRegistrar = 0x084b1c3C81545d370f3634392De611CaaBFf8148; - - address internal WATERFALL_FACTORY_MODULE_GOERLI = 0xd647B9bE093Ec237be72bB17f54b0C5Ada886A25; - address internal SPLIT_MAIN_GOERLI = 0x2ed6c4B5dA6378c7897AC67Ba9e43102Feb694EE; - - function setUp() public { - uint256 goerliBlock = 8_529_931; - - vm.createSelectFork(getChain("goerli").rpcUrl, goerliBlock); - // for local tests, mock the ENS reverse registrar at its goerli address. - vm.mockCall( - ensReverseRegistrar, abi.encodeWithSelector(IENSReverseRegistrar.setName.selector), bytes.concat(bytes32(0)) - ); - vm.mockCall( - ensReverseRegistrar, abi.encodeWithSelector(IENSReverseRegistrar.claim.selector), bytes.concat(bytes32(0)) - ); - - factory = new ValidatorRewardSplitFactory( - WATERFALL_FACTORY_MODULE_GOERLI, - SPLIT_MAIN_GOERLI, - "launchpad.obol.tech", - ensReverseRegistrar, - address(0) - ); - } - - function testCreateRewardSplit() external { - address[] memory accounts = new address[](2); - accounts[0] = makeAddr("accounts0"); - accounts[1] = makeAddr("accounts1"); - - uint32[] memory percentAllocations = new uint32[](2); - percentAllocations[0] = 400_000; - percentAllocations[1] = 600_000; - - SplitConfiguration memory splitConfig = SplitConfiguration(accounts, percentAllocations, 0, address(0x0)); - - address payable principal = payable(makeAddr("accounts2")); - uint256 numberOfValidators = 10; - - (address[] memory withdrawAddresses, address splitRecipient) = - factory.createETHRewardSplit(splitConfig, principal, numberOfValidators); - - // confirm expected splitrecipient address - address expectedSplitRecipient = - ISplitMain(SPLIT_MAIN_GOERLI).predictImmutableSplitAddress(accounts, percentAllocations, 0); - assertEq(splitRecipient, expectedSplitRecipient, "invalid split configuration"); - - address[] memory expectedRecipients = new address[](2); - expectedRecipients[0] = principal; - expectedRecipients[1] = splitRecipient; - - uint256[] memory expectedThresholds = new uint256[](1); - expectedThresholds[0] = 32 ether; - - for (uint256 i = 0; i < withdrawAddresses.length; i++) { - (address[] memory recipients, uint256[] memory thresholds) = IWaterfallModule(withdrawAddresses[i]).getTranches(); - - assertEq(recipients, expectedRecipients, "invalid recipients"); - assertEq(thresholds, expectedThresholds, "invalid thresholds"); - } - } -} diff --git a/src/test/utils/mocks/MockDepositContract.sol b/src/test/utils/mocks/MockDepositContract.sol deleted file mode 100644 index e41674a..0000000 --- a/src/test/utils/mocks/MockDepositContract.sol +++ /dev/null @@ -1,13 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity =0.8.17; - -import {IDepositContract} from "src/archive/NFTDeposit.sol"; - -contract MockDepositContract is IDepositContract { - function deposit( - bytes calldata pubkey, - bytes calldata withdrawal_credentials, - bytes calldata signature, - bytes32 deposit_data_root - ) external payable {} -} diff --git a/src/test/utils/mocks/MockNFTDeposit.sol b/src/test/utils/mocks/MockNFTDeposit.sol deleted file mode 100644 index dfd0577..0000000 --- a/src/test/utils/mocks/MockNFTDeposit.sol +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity >=0.8.0; - -import {IDepositContract, NFTDeposit} from "src/archive/NFTDeposit.sol"; - -contract MockNFTDeposit is NFTDeposit { - constructor(IDepositContract depositContract) NFTDeposit(depositContract, "Obol", "OBOL", "URI") {} -} diff --git a/src/test/utils/mocks/MockSplitter.sol b/src/test/utils/mocks/MockSplitter.sol deleted file mode 100644 index 6f1bb7f..0000000 --- a/src/test/utils/mocks/MockSplitter.sol +++ /dev/null @@ -1,38 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity =0.8.17; - -import {ISplitMain} from "src/archive/Splitter.sol"; - -contract MockSplitter is ISplitMain { - struct SplitData { - address[] recipients; - uint32[] percentAllocations; - uint32 distributorFee; - address controller; - } - - SplitData[] public splitData; - - function createSplit( - address[] calldata accounts, - uint32[] calldata percentAllocations, - uint32 distributorFee, - address controller - ) external returns (address) { - SplitData memory data = SplitData(accounts, percentAllocations, distributorFee, controller); - splitData.push(data); - return address(0xFFFF); - } - - function showSplitRecipient(uint256 index) external view returns (address) { - return splitData[0].recipients[index]; - } - - function showSplitAllocation(uint256 index) external view returns (uint32) { - return splitData[0].percentAllocations[index]; - } - - function showController() external view returns (address) { - return splitData[0].controller; - } -} diff --git a/src/test/utils/mocks/MockSplitterDeployer.sol b/src/test/utils/mocks/MockSplitterDeployer.sol deleted file mode 100644 index 7e69b21..0000000 --- a/src/test/utils/mocks/MockSplitterDeployer.sol +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity >=0.8.17; - -import {ObolSplitterDeployer, ISplitMain} from "src/archive/Splitter.sol"; - -contract MockSplitterDeployer is ObolSplitterDeployer { - constructor(ISplitMain splitterContract, address obolWallet) ObolSplitterDeployer(splitterContract, obolWallet) {} -} diff --git a/src/test/utils/mocks/MockWithdrawalRecipient.sol b/src/test/utils/mocks/MockWithdrawalRecipient.sol deleted file mode 100644 index de7ab2c..0000000 --- a/src/test/utils/mocks/MockWithdrawalRecipient.sol +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity >=0.8.10; - -import "solmate/auth/Auth.sol"; - -import {WithdrawalRecipientOwnable} from "src/archive/WithdrawalRecipientOwnable.sol"; - -contract MockWithdrawalRecipient is WithdrawalRecipientOwnable(msg.sender, Authority(address(0))) {} diff --git a/src/test/utils/mocks/MockWithdrawalRecipientNFT.sol b/src/test/utils/mocks/MockWithdrawalRecipientNFT.sol deleted file mode 100644 index 5178bc6..0000000 --- a/src/test/utils/mocks/MockWithdrawalRecipientNFT.sol +++ /dev/null @@ -1,11 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity =0.8.17; - -import "solmate/tokens/ERC721.sol"; -import "solmate/auth/Auth.sol"; - -import {WithdrawalRecipientNFT} from "src/archive/WithdrawalRecipientNFT.sol"; - -contract MockWithdrawalRecipientNFT is WithdrawalRecipientNFT { - constructor(ERC721 nftContract) WithdrawalRecipientNFT(nftContract, 0, msg.sender, Authority(address(0))) {} -} diff --git a/src/test/waterfall/LW1155.t.sol b/src/test/waterfall/LW1155.t.sol deleted file mode 100644 index db18a91..0000000 --- a/src/test/waterfall/LW1155.t.sol +++ /dev/null @@ -1,227 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.17; - -import "forge-std/Test.sol"; -import {ERC1155} from "solmate/tokens/ERC1155.sol"; -import {ERC20} from "solmate/tokens/ERC20.sol"; -import {LW1155} from "src/waterfall/token/LW1155.sol"; -import {SplitWallet} from "src/splitter/SplitWallet.sol"; -import {utils} from "../../lib/Utils.sol"; -import {ISplitMainV2, SplitConfiguration} from "../../interfaces/ISplitMainV2.sol"; -import {SplitMainV2} from "src/splitter/SplitMainV2.sol"; -import {IWaterfallModule} from "../../interfaces/IWaterfallModule.sol"; -import {IWaterfallFactoryModule} from "../../interfaces/IWaterfallFactoryModule.sol"; -import {MockERC20} from "../utils/mocks/MockERC20.sol"; - -contract AddressBook { - address public ensReverseRegistrar = 0x084b1c3C81545d370f3634392De611CaaBFf8148; - uint256 internal constant ETH_STAKE = 32 ether; - address internal WATERFALL_FACTORY_MODULE_GOERLI = 0xd647B9bE093Ec237be72bB17f54b0C5Ada886A25; - // address internal SPLIT_MAIN_GOERLI = 0x2ed6c4B5dA6378c7897AC67Ba9e43102Feb694EE; -} - -contract BaseTest is AddressBook, Test { - LW1155 public lw1155; - - address user1; - address user2; - - address rewardSplit; - address waterfallModule; - address recoveryWallet; - SplitMainV2 splitMainV2; - SplitWallet splitWallet; - - SplitConfiguration configuration; - MockERC20 mockERC20; - - error Unauthorized(); - error InvalidOwner(); - - event TransferSingle(address indexed operator, address indexed from, address indexed to, uint256 id, uint256 amount); - - function setUp() public { - uint256 goerliBlock = 8_529_931; - - vm.createSelectFork(getChain("goerli").rpcUrl, goerliBlock); - - user1 = makeAddr("1"); - user2 = makeAddr("2"); - recoveryWallet = makeAddr("3"); - - splitMainV2 = new SplitMainV2(); - splitWallet = new SplitWallet(address(splitMainV2)); - lw1155 = new LW1155(ISplitMainV2(address(splitMainV2)), recoveryWallet); - - mockERC20 = new MockERC20("demo", "DMT", 18); - - uint32[] memory percentAllocations = new uint32[](2); - percentAllocations[0] = 500_000; - percentAllocations[1] = 500_000; - - address[] memory accounts = new address[](2); - accounts[0] = user1; - accounts[1] = address(lw1155); - - configuration = SplitConfiguration(accounts, percentAllocations, 0, address(0), address(0)); - - rewardSplit = splitMainV2.createSplit(address(splitWallet), accounts, percentAllocations, address(0), address(0), 0 ); - - address[] memory waterfallRecipients = new address[](2); - waterfallRecipients[0] = address(lw1155); - waterfallRecipients[1] = address(rewardSplit); - - uint256[] memory thresholds = new uint256[](1); - thresholds[0] = ETH_STAKE; - - waterfallModule = IWaterfallFactoryModule(WATERFALL_FACTORY_MODULE_GOERLI).createWaterfallModule( - address(0), address(0), waterfallRecipients, thresholds - ); - } -} - -contract LW1155OwnerTest is BaseTest { - function testOwner() public { - assertEq(lw1155.owner(), address(this)); - } -} - -contract LW1155UriTest is BaseTest { - function testCanFetchUri() public { - vm.expectCall(address(lw1155), abi.encodeCall(lw1155.uri, (0))); - lw1155.uri(0); - } -} - -contract LW1155NameTest is BaseTest { - function testCanFetchName() public { - assertEq( - lw1155.name(), string.concat("Obol Liquid Waterfall + Split ", utils.shortAddressToString(address(lw1155))) - ); - } -} - -contract LW1155MintTest is BaseTest { - error ClaimExists(); - - function testOnlyOwnerCanMint() public { - vm.prank(user1); - vm.expectRevert(Unauthorized.selector); - - lw1155.mint(user1, rewardSplit, waterfallModule, configuration); - } - - function testCannotMintDoubleIds() public { - lw1155.mint(user1, rewardSplit, waterfallModule, configuration); - vm.expectRevert(ClaimExists.selector); - lw1155.mint(user1, rewardSplit, waterfallModule, configuration); - } - - function testCanMint() public { - uint256 id = uint256(keccak256(abi.encodePacked(user1, waterfallModule))); - vm.expectEmit(true, true, true, true, address(lw1155)); - emit TransferSingle(address(this), address(0), user1, id, 1); - - lw1155.mint(user1, rewardSplit, waterfallModule, configuration); - - // assert claim information - ( - ISplitMainV2 receivedRewardSplit, - IWaterfallModule receivedWaterfallModule, - SplitConfiguration memory receivedConfig - ) = lw1155.claimData(id); - assertEq(address(receivedRewardSplit), rewardSplit); - assertEq(address(receivedWaterfallModule), waterfallModule); - assertEq(configuration.accounts, receivedConfig.accounts); - // assertEq(configuration.percentAllocations, receivedConfig.percentAllocations); - assertEq(configuration.distributorFee, receivedConfig.distributorFee); - assertEq(configuration.controller, receivedConfig.controller); - } -} - -contract LW1155ClaimTest is BaseTest { - function testOnlyCorrectReceiverCanClaim() public { - vm.expectRevert(InvalidOwner.selector); - - uint256[] memory tokenIds = new uint256[](10); - lw1155.claim(tokenIds, user2); - } - - function testReceiverCanClaim() public { - // mint to user1 - lw1155.mint(user1, rewardSplit, waterfallModule, configuration); - - // credit waterfall with 50 ETH - vm.deal(waterfallModule, 50 ether); - uint256 id = uint256(keccak256(abi.encodePacked(user1, waterfallModule))); - - uint256[] memory tokenIds = new uint256[](1); - tokenIds[0] = id; - - lw1155.claim(tokenIds, user1); - - // check the user1 balance is 41 ETH - assertApproxEqAbs(user1.balance, 41 ether, 1); - } -} - -contract LW1155TransferTest is BaseTest { - uint256 id; - - function testTransferAndNewOwnerClaim() public { - // credit waterfall with 50 ETH - vm.deal(waterfallModule, 50 ether); - - // mint to user1 - lw1155.mint(user1, rewardSplit, waterfallModule, configuration); - - id = uint256(keccak256(abi.encodePacked(user1, waterfallModule))); - - vm.expectEmit(true, true, true, true, address(lw1155)); - emit TransferSingle(user1, user1, user2, id, 1); - - vm.prank(user1); - lw1155.safeTransferFrom(user1, user2, id, 1, ""); - - uint256[] memory tokenIds = new uint256[](1); - tokenIds[0] = id; - - vm.prank(user2); - lw1155.claim(tokenIds, user2); - - // reject if previous owner tries to claim - vm.expectRevert(InvalidOwner.selector); - vm.prank(user1); - lw1155.claim(tokenIds, user1); - } -} - -contract LW1155RecoverTest is BaseTest { - function testReoveryWalletAddress() external { - assertEq(lw1155.recoveryWallet(), recoveryWallet); - } - - function testRecoverETH() public { - vm.deal(address(lw1155), 10 ether); - - lw1155.recover(ERC20(address(0)), 5 ether); - - assertEq(recoveryWallet.balance, 5 ether); - - lw1155.recover(ERC20(address(0)), 5 ether); - - assertEq(recoveryWallet.balance, 10 ether); - } - - function testRecoverToken() public { - deal(address(mockERC20), address(lw1155), 10_000); - - lw1155.recover(ERC20(address(mockERC20)), 5000); - - assertEq(mockERC20.balanceOf(recoveryWallet), 5000); - - lw1155.recover(ERC20(address(mockERC20)), 5000); - - assertEq(mockERC20.balanceOf(recoveryWallet), 10_000); - } -} diff --git a/src/test/waterfall/LWFactory.t.sol b/src/test/waterfall/LWFactory.t.sol deleted file mode 100644 index 24327b2..0000000 --- a/src/test/waterfall/LWFactory.t.sol +++ /dev/null @@ -1,79 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.17; - -import "forge-std/Test.sol"; -import {SplitFactory} from "src/splitter/SplitFactory.sol"; -import {SplitWallet} from "src/splitter/SplitWallet.sol"; -import {AddressBook} from "./LW1155.t.sol"; -import {LWFactory} from "../../waterfall/LWFactory.sol"; -import {IENSReverseRegistrar} from "../../interfaces/IENSReverseRegistrar.sol"; -import {SplitConfiguration} from "../../interfaces/ISplitMainV2.sol"; -import {IWaterfallModule} from "../../interfaces/IWaterfallModule.sol"; - -contract LWFactoryTest is Test, AddressBook { - LWFactory lwFactory; - SplitFactory splitFactory; - SplitWallet splitWallet; - bytes32 splitWalletId; - - function setUp() public { - uint256 goerliBlock = 8_529_931; - - vm.createSelectFork(getChain("goerli").rpcUrl, goerliBlock); - // for local tests, mock the ENS reverse registrar at its goerli address. - vm.mockCall( - ensReverseRegistrar, abi.encodeWithSelector(IENSReverseRegistrar.setName.selector), bytes.concat(bytes32(0)) - ); - vm.mockCall( - ensReverseRegistrar, abi.encodeWithSelector(IENSReverseRegistrar.claim.selector), bytes.concat(bytes32(0)) - ); - splitFactory = new SplitFactory(address(this)); - splitWallet = new SplitWallet(address(splitFactory.splitMain())); - splitWalletId = keccak256("demofactroy"); - splitFactory.addSplitWallet(splitWalletId, address(splitWallet)); - lwFactory = new LWFactory( - WATERFALL_FACTORY_MODULE_GOERLI, - address(splitFactory), - "demo.obol.eth", - ensReverseRegistrar, - address(this), - address(this) - ); - } - - function testCreateETHRewardSplit() external { - address[] memory accounts = new address[](2); - accounts[0] = address(lwFactory.lw1155()); - accounts[1] = makeAddr("accounts1"); - - uint32[] memory percentAllocations = new uint32[](2); - percentAllocations[0] = 400_000; - percentAllocations[1] = 600_000; - - SplitConfiguration memory splitConfig = SplitConfiguration(accounts, percentAllocations, 0, address(0x0), address(0)); - - address payable principal = payable(makeAddr("accounts2")); - - (address withdrawAddress, address splitRecipient) = lwFactory.createETHRewardSplit( - splitWalletId, - splitConfig, - principal - ); - - // confirm expected splitrecipient address - address expectedSplitRecipient = splitFactory.predictImmutableSplitAddress(splitWalletId, accounts, percentAllocations, 0); - assertEq(splitRecipient, expectedSplitRecipient, "invalid split configuration"); - - address[] memory expectedRecipients = new address[](2); - expectedRecipients[0] = address(lwFactory.lw1155()); - expectedRecipients[1] = splitRecipient; - - uint256[] memory expectedThresholds = new uint256[](1); - expectedThresholds[0] = 32 ether; - - (address[] memory recipients, uint256[] memory thresholds) = IWaterfallModule(withdrawAddress).getTranches(); - - assertEq(recipients, expectedRecipients, "invalid recipients"); - assertEq(thresholds, expectedThresholds, "invalid thresholds"); - } -} diff --git a/src/test/waterfall/OptimisticWithdrawalRecipient.t.sol b/src/test/waterfall/OptimisticWithdrawalRecipient.t.sol index dd73c60..5d195b1 100644 --- a/src/test/waterfall/OptimisticWithdrawalRecipient.t.sol +++ b/src/test/waterfall/OptimisticWithdrawalRecipient.t.sol @@ -2,8 +2,8 @@ pragma solidity 0.8.17; import "forge-std/Test.sol"; -import { OptimisticWithdrawalRecipient } from "src/waterfall/OptimisticWithdrawalRecipient.sol"; -import { OptimisticWithdrawalRecipientFactory } from "src/waterfall/OptimisticWithdrawalRecipientFactory.sol"; +import { OptimisticWithdrawalRecipient } from "src/owr/OptimisticWithdrawalRecipient.sol"; +import { OptimisticWithdrawalRecipientFactory } from "src/owr/OptimisticWithdrawalRecipientFactory.sol"; import {MockERC20} from "../utils/mocks/MockERC20.sol"; import {SafeTransferLib} from "solady/utils/SafeTransferLib.sol"; import {OWRReentrancy} from "./OWRReentrancy.sol"; diff --git a/src/test/waterfall/OptimisticWithdrawalRecipientFactory.t.sol b/src/test/waterfall/OptimisticWithdrawalRecipientFactory.t.sol index c77c5d2..75ab164 100644 --- a/src/test/waterfall/OptimisticWithdrawalRecipientFactory.t.sol +++ b/src/test/waterfall/OptimisticWithdrawalRecipientFactory.t.sol @@ -2,8 +2,8 @@ pragma solidity 0.8.17; import "forge-std/Test.sol"; -import { OptimisticWithdrawalRecipient } from "src/waterfall/OptimisticWithdrawalRecipient.sol"; -import { OptimisticWithdrawalRecipientFactory } from "src/waterfall/OptimisticWithdrawalRecipientFactory.sol"; +import { OptimisticWithdrawalRecipient } from "src/owr/OptimisticWithdrawalRecipient.sol"; +import { OptimisticWithdrawalRecipientFactory } from "src/owr/OptimisticWithdrawalRecipientFactory.sol"; import {MockERC20} from "../utils/mocks/MockERC20.sol"; import {OWRTestHelper} from "./OWRTestHelper.t.sol"; From cf8f49c6c6cd9d9293fb4c0dfc8e508dcc967732 Mon Sep 17 00:00:00 2001 From: samparsky <8148384+samparsky@users.noreply.github.com> Date: Tue, 15 Aug 2023 16:41:52 +0300 Subject: [PATCH 03/82] chore: rename folder waterfall => owr --- src/test/waterfall/OWRReentrancy.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/waterfall/OWRReentrancy.sol b/src/test/waterfall/OWRReentrancy.sol index 544ca59..0ca4b12 100644 --- a/src/test/waterfall/OWRReentrancy.sol +++ b/src/test/waterfall/OWRReentrancy.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity ^0.8.17; import "forge-std/Test.sol"; -import {OptimisticWithdrawalRecipient} from "src/waterfall/OptimisticWithdrawalRecipient.sol"; +import {OptimisticWithdrawalRecipient} from "src/owr/OptimisticWithdrawalRecipient.sol"; contract OWRReentrancy is Test { receive() external payable { From 342e503b4bb40b8634fbb43ae3bb31066a98f312 Mon Sep 17 00:00:00 2001 From: samparsky <8148384+samparsky@users.noreply.github.com> Date: Wed, 16 Aug 2023 12:01:43 +0300 Subject: [PATCH 04/82] chore: add comments --- src/lido/LidoSplit.sol | 5 +++++ src/lido/LidoSplitFactory.sol | 4 ++++ 2 files changed, 9 insertions(+) diff --git a/src/lido/LidoSplit.sol b/src/lido/LidoSplit.sol index f68d33a..d938aa4 100644 --- a/src/lido/LidoSplit.sol +++ b/src/lido/LidoSplit.sol @@ -8,6 +8,11 @@ interface IwSTETH { function wrap(uint256 amount) external returns (uint256); } +/// @title LidoSplit +/// @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 LidoSplit is Clone { /// ----------------------------------------------------------------------- /// libraries diff --git a/src/lido/LidoSplitFactory.sol b/src/lido/LidoSplitFactory.sol index 7185ecf..a4b2373 100644 --- a/src/lido/LidoSplitFactory.sol +++ b/src/lido/LidoSplitFactory.sol @@ -4,6 +4,10 @@ import {LibClone} from "solady/utils/LibClone.sol"; import {ERC20} from 'solmate/tokens/ERC20.sol'; import "./LidoSplit.sol"; +/// @title LidoSplitFactory +/// @author Obol +/// @notice A factory contract for cheaply deploying LidoSplit. +/// @dev The address returned should be used to as reward address for Lido contract LidoSplitFactory { /// ----------------------------------------------------------------------- From 25cc60396dc3a5f1a912a5e626c952cd826261e5 Mon Sep 17 00:00:00 2001 From: samparsky <8148384+samparsky@users.noreply.github.com> Date: Wed, 16 Aug 2023 13:07:48 +0300 Subject: [PATCH 05/82] chore: add solidity-metrics audit doc --- docs/audit/LidoSplit.md | 275 ++++++++++++++++ docs/audit/LidoSplitFactory.md | 268 ++++++++++++++++ docs/audit/OptimisticWithdrawalRecipient.md | 278 ++++++++++++++++ .../OptimisticWithdrawalRecipientFactory.md | 267 ++++++++++++++++ docs/audit/SplitMainV2.md | 297 ++++++++++++++++++ docs/audit/SplitWallet.md | 269 ++++++++++++++++ 6 files changed, 1654 insertions(+) create mode 100644 docs/audit/LidoSplit.md create mode 100644 docs/audit/LidoSplitFactory.md create mode 100644 docs/audit/OptimisticWithdrawalRecipient.md create mode 100644 docs/audit/OptimisticWithdrawalRecipientFactory.md create mode 100644 docs/audit/SplitMainV2.md create mode 100644 docs/audit/SplitWallet.md diff --git a/docs/audit/LidoSplit.md b/docs/audit/LidoSplit.md new file mode 100644 index 0000000..bf0ebe4 --- /dev/null +++ b/docs/audit/LidoSplit.md @@ -0,0 +1,275 @@ + +[get in touch with Consensys Diligence](https://diligence.consensys.net)
+ +[[ ๐ŸŒ ](https://diligence.consensys.net) [ ๐Ÿ“ฉ ](mailto:diligence@consensys.net) [ ๐Ÿ”ฅ ](https://consensys.github.io/diligence/)] +

+ + + +# Solidity Metrics for 'CLI' + +## Table of contents + +- [Scope](#t-scope) + - [Source Units in Scope](#t-source-Units-in-Scope) + - [Out of Scope](#t-out-of-scope) + - [Excluded Source Units](#t-out-of-scope-excluded-source-units) + - [Duplicate Source Units](#t-out-of-scope-duplicate-source-units) + - [Doppelganger Contracts](#t-out-of-scope-doppelganger-contracts) +- [Report Overview](#t-report) + - [Risk Summary](#t-risk) + - [Source Lines](#t-source-lines) + - [Inline Documentation](#t-inline-documentation) + - [Components](#t-components) + - [Exposed Functions](#t-exposed-functions) + - [StateVariables](#t-statevariables) + - [Capabilities](#t-capabilities) + - [Dependencies](#t-package-imports) + - [Totals](#t-totals) + +## Scope + +This section lists files that are in scope for the metrics report. + +- **Project:** `'CLI'` +- **Included Files:** + - `` +- **Excluded Paths:** + - `` +- **File Limit:** `undefined` + - **Exclude File list Limit:** `undefined` + +- **Workspace Repository:** `unknown` (`undefined`@`undefined`) + +### Source Units in Scope + +Source Units Analyzed: **`1`**
+Source Units in Scope: **`1`** (**100%**) + +| Type | File | Logic Contracts | Interfaces | Lines | nLines | nSLOC | Comment Lines | Complex. Score | Capabilities | +| ---- | ------ | --------------- | ---------- | ----- | ------ | ----- | ------------- | -------------- | ------------ | +| ๐Ÿ“๐Ÿ” | src/lido/LidoSplit.sol | 1 | 1 | 72 | 64 | 29 | 31 | 34 | **** | +| ๐Ÿ“๐Ÿ” | **Totals** | **1** | **1** | **72** | **64** | **29** | **31** | **34** | **** | + + +Legend: [โž•] + + + + +#### Out of Scope + +##### Excluded Source Units + +Source Units Excluded: **`0`** + +[โž•] + + + +##### Duplicate Source Units + +Duplicate Source Units Excluded: **`0`** + +[โž•] + + +##### Doppelganger Contracts + +Doppelganger Contracts: **`0`** + +[โž•] + + + +## Report + +### Overview + +The analysis finished with **`0`** errors and **`0`** duplicate files. + + + + + +#### Risk + +
+ +
+ +#### Source Lines (sloc vs. nsloc) + +
+ +
+ +#### Inline Documentation + +- **Comment-to-Source Ratio:** On average there are`1` code lines per comment (lower=better). +- **ToDo's:** `0` + +#### Components + +| ๐Ÿ“Contracts | ๐Ÿ“šLibraries | ๐Ÿ”Interfaces | ๐ŸŽจAbstract | +| ------------- | ----------- | ------------ | ---------- | +| 1 | 0 | 1 | 0 | + +#### Exposed Functions + +This section lists functions that are explicitly declared public or payable. Please note that getter methods for public stateVars are not included. + +| ๐ŸŒPublic | ๐Ÿ’ฐPayable | +| ---------- | --------- | +| 5 | 0 | + +| External | Internal | Private | Pure | View | +| ---------- | -------- | ------- | ---- | ---- | +| 2 | 3 | 0 | 3 | 0 | + +#### StateVariables + +| Total | ๐ŸŒPublic | +| ---------- | --------- | +| 3 | 0 | + +#### Capabilities + +| Solidity Versions observed | ๐Ÿงช Experimental Features | ๐Ÿ’ฐ Can Receive Funds | ๐Ÿ–ฅ Uses Assembly | ๐Ÿ’ฃ Has Destroyable Contracts | +| -------------------------- | ------------------------ | -------------------- | ---------------- | ---------------------------- | +| `=0.8.17` | | **** | **** | **** | + +| ๐Ÿ“ค Transfers ETH | โšก Low-Level Calls | ๐Ÿ‘ฅ DelegateCall | ๐Ÿงฎ Uses Hash Functions | ๐Ÿ”– ECRecover | ๐ŸŒ€ New/Create/Create2 | +| ---------------- | ----------------- | --------------- | ---------------------- | ------------ | --------------------- | +| **** | **** | **** | **** | **** | **** | + +| โ™ป๏ธ TryCatch | ฮฃ Unchecked | +| ---------- | ----------- | +| **** | **** | + +#### Dependencies / External Imports + +| Dependency / Import Path | Count | +| ------------------------ | ------ | +| solady/utils/Clone.sol | 1 | +| solmate/tokens/ERC20.sol | 1 | +| solmate/utils/SafeTransferLib.sol | 1 | + +#### Totals + +##### Summary + +
+ +
+ +##### AST Node Statistics + +###### Function Calls + +
+ +
+ +###### Assembly Calls + +
+ +
+ +###### AST Total + +
+ +
+ +##### Inheritance Graph + +[โž•] + + +##### CallGraph + +[โž•] + + +###### Contract Summary + +[โž•] + +____ + +Thinking about smart contract security? We can provide training, ongoing advice, and smart contract auditing. [Contact us](https://diligence.consensys.net/contact/). + + + diff --git a/docs/audit/LidoSplitFactory.md b/docs/audit/LidoSplitFactory.md new file mode 100644 index 0000000..4749e3a --- /dev/null +++ b/docs/audit/LidoSplitFactory.md @@ -0,0 +1,268 @@ + +[get in touch with Consensys Diligence](https://diligence.consensys.net)
+ +[[ ๐ŸŒ ](https://diligence.consensys.net) [ ๐Ÿ“ฉ ](mailto:diligence@consensys.net) [ ๐Ÿ”ฅ ](https://consensys.github.io/diligence/)] +

+ + + +# Solidity Metrics for 'CLI' + +## Table of contents + +- [Scope](#t-scope) + - [Source Units in Scope](#t-source-Units-in-Scope) + - [Out of Scope](#t-out-of-scope) + - [Excluded Source Units](#t-out-of-scope-excluded-source-units) + - [Duplicate Source Units](#t-out-of-scope-duplicate-source-units) + - [Doppelganger Contracts](#t-out-of-scope-doppelganger-contracts) +- [Report Overview](#t-report) + - [Risk Summary](#t-risk) + - [Source Lines](#t-source-lines) + - [Inline Documentation](#t-inline-documentation) + - [Components](#t-components) + - [Exposed Functions](#t-exposed-functions) + - [StateVariables](#t-statevariables) + - [Capabilities](#t-capabilities) + - [Dependencies](#t-package-imports) + - [Totals](#t-totals) + +## Scope + +This section lists files that are in scope for the metrics report. + +- **Project:** `'CLI'` +- **Included Files:** + - `` +- **Excluded Paths:** + - `` +- **File Limit:** `undefined` + - **Exclude File list Limit:** `undefined` + +- **Workspace Repository:** `unknown` (`undefined`@`undefined`) + +### Source Units in Scope + +Source Units Analyzed: **`1`**
+Source Units in Scope: **`1`** (**100%**) + +| Type | File | Logic Contracts | Interfaces | Lines | nLines | nSLOC | Comment Lines | Complex. Score | Capabilities | +| ---- | ------ | --------------- | ---------- | ----- | ------ | ----- | ------------- | -------------- | ------------ | +| ๐Ÿ“ | src/lido/LidoSplitFactory.sol | 1 | **** | 73 | 73 | 31 | 25 | 24 | **๐ŸŒ€** | +| ๐Ÿ“ | **Totals** | **1** | **** | **73** | **73** | **31** | **25** | **24** | **๐ŸŒ€** | + + +Legend: [โž•] + + + + +#### Out of Scope + +##### Excluded Source Units + +Source Units Excluded: **`0`** + +[โž•] + + + +##### Duplicate Source Units + +Duplicate Source Units Excluded: **`0`** + +[โž•] + + +##### Doppelganger Contracts + +Doppelganger Contracts: **`0`** + +[โž•] + + + +## Report + +### Overview + +The analysis finished with **`0`** errors and **`0`** duplicate files. + + + + + +#### Risk + +
+ +
+ +#### Source Lines (sloc vs. nsloc) + +
+ +
+ +#### Inline Documentation + +- **Comment-to-Source Ratio:** On average there are`1.24` code lines per comment (lower=better). +- **ToDo's:** `0` + +#### Components + +| ๐Ÿ“Contracts | ๐Ÿ“šLibraries | ๐Ÿ”Interfaces | ๐ŸŽจAbstract | +| ------------- | ----------- | ------------ | ---------- | +| 1 | 0 | 0 | 0 | + +#### Exposed Functions + +This section lists functions that are explicitly declared public or payable. Please note that getter methods for public stateVars are not included. + +| ๐ŸŒPublic | ๐Ÿ’ฐPayable | +| ---------- | --------- | +| 1 | 0 | + +| External | Internal | Private | Pure | View | +| ---------- | -------- | ------- | ---- | ---- | +| 1 | 2 | 0 | 0 | 0 | + +#### StateVariables + +| Total | ๐ŸŒPublic | +| ---------- | --------- | +| 3 | 3 | + +#### Capabilities + +| Solidity Versions observed | ๐Ÿงช Experimental Features | ๐Ÿ’ฐ Can Receive Funds | ๐Ÿ–ฅ Uses Assembly | ๐Ÿ’ฃ Has Destroyable Contracts | +| -------------------------- | ------------------------ | -------------------- | ---------------- | ---------------------------- | +| `=0.8.17` | | **** | **** | **** | + +| ๐Ÿ“ค Transfers ETH | โšก Low-Level Calls | ๐Ÿ‘ฅ DelegateCall | ๐Ÿงฎ Uses Hash Functions | ๐Ÿ”– ECRecover | ๐ŸŒ€ New/Create/Create2 | +| ---------------- | ----------------- | --------------- | ---------------------- | ------------ | --------------------- | +| **** | **** | **** | **** | **** | `yes`
โ†’ `NewContract:LidoSplit` | + +| โ™ป๏ธ TryCatch | ฮฃ Unchecked | +| ---------- | ----------- | +| **** | **** | + +#### Dependencies / External Imports + +| Dependency / Import Path | Count | +| ------------------------ | ------ | +| solady/utils/LibClone.sol | 1 | +| solmate/tokens/ERC20.sol | 1 | + +#### Totals + +##### Summary + +
+ +
+ +##### AST Node Statistics + +###### Function Calls + +
+ +
+ +###### Assembly Calls + +
+ +
+ +###### AST Total + +
+ +
+ +##### Inheritance Graph + +[โž•] + + +##### CallGraph + +[โž•] + + +###### Contract Summary + +[โž•] + +____ + +Thinking about smart contract security? We can provide training, ongoing advice, and smart contract auditing. [Contact us](https://diligence.consensys.net/contact/). + + + diff --git a/docs/audit/OptimisticWithdrawalRecipient.md b/docs/audit/OptimisticWithdrawalRecipient.md new file mode 100644 index 0000000..c27d826 --- /dev/null +++ b/docs/audit/OptimisticWithdrawalRecipient.md @@ -0,0 +1,278 @@ + +[get in touch with Consensys Diligence](https://diligence.consensys.net)
+ +[[ ๐ŸŒ ](https://diligence.consensys.net) [ ๐Ÿ“ฉ ](mailto:diligence@consensys.net) [ ๐Ÿ”ฅ ](https://consensys.github.io/diligence/)] +

+ + + +# Solidity Metrics for 'CLI' + +## Table of contents + +- [Scope](#t-scope) + - [Source Units in Scope](#t-source-Units-in-Scope) + - [Out of Scope](#t-out-of-scope) + - [Excluded Source Units](#t-out-of-scope-excluded-source-units) + - [Duplicate Source Units](#t-out-of-scope-duplicate-source-units) + - [Doppelganger Contracts](#t-out-of-scope-doppelganger-contracts) +- [Report Overview](#t-report) + - [Risk Summary](#t-risk) + - [Source Lines](#t-source-lines) + - [Inline Documentation](#t-inline-documentation) + - [Components](#t-components) + - [Exposed Functions](#t-exposed-functions) + - [StateVariables](#t-statevariables) + - [Capabilities](#t-capabilities) + - [Dependencies](#t-package-imports) + - [Totals](#t-totals) + +## Scope + +This section lists files that are in scope for the metrics report. + +- **Project:** `'CLI'` +- **Included Files:** + - `` +- **Excluded Paths:** + - `` +- **File Limit:** `undefined` + - **Exclude File list Limit:** `undefined` + +- **Workspace Repository:** `unknown` (`undefined`@`undefined`) + +### Source Units in Scope + +Source Units Analyzed: **`1`**
+Source Units in Scope: **`1`** (**100%**) + +| Type | File | Logic Contracts | Interfaces | Lines | nLines | nSLOC | Comment Lines | Complex. Score | Capabilities | +| ---- | ------ | --------------- | ---------- | ----- | ------ | ----- | ------------- | -------------- | ------------ | +| ๐Ÿ“ | src/owr/OptimisticWithdrawalRecipient.sol | 1 | **** | 385 | 378 | 171 | 142 | 139 | **๐Ÿ’ฐฮฃ** | +| ๐Ÿ“ | **Totals** | **1** | **** | **385** | **378** | **171** | **142** | **139** | **๐Ÿ’ฐฮฃ** | + + +Legend: [โž•] + + + + +#### Out of Scope + +##### Excluded Source Units + +Source Units Excluded: **`0`** + +[โž•] + + + +##### Duplicate Source Units + +Duplicate Source Units Excluded: **`0`** + +[โž•] + + +##### Doppelganger Contracts + +Doppelganger Contracts: **`0`** + +[โž•] + + + +## Report + +### Overview + +The analysis finished with **`0`** errors and **`0`** duplicate files. + + + + + +#### Risk + +
+ +
+ +#### Source Lines (sloc vs. nsloc) + +
+ +
+ +#### Inline Documentation + +- **Comment-to-Source Ratio:** On average there are`1.25` code lines per comment (lower=better). +- **ToDo's:** `0` + +#### Components + +| ๐Ÿ“Contracts | ๐Ÿ“šLibraries | ๐Ÿ”Interfaces | ๐ŸŽจAbstract | +| ------------- | ----------- | ------------ | ---------- | +| 1 | 0 | 0 | 0 | + +#### Exposed Functions + +This section lists functions that are explicitly declared public or payable. Please note that getter methods for public stateVars are not included. + +| ๐ŸŒPublic | ๐Ÿ’ฐPayable | +| ---------- | --------- | +| 8 | 3 | + +| External | Internal | Private | Pure | View | +| ---------- | -------- | ------- | ---- | ---- | +| 5 | 5 | 0 | 4 | 1 | + +#### StateVariables + +| Total | ๐ŸŒPublic | +| ---------- | --------- | +| 16 | 3 | + +#### Capabilities + +| Solidity Versions observed | ๐Ÿงช Experimental Features | ๐Ÿ’ฐ Can Receive Funds | ๐Ÿ–ฅ Uses Assembly | ๐Ÿ’ฃ Has Destroyable Contracts | +| -------------------------- | ------------------------ | -------------------- | ---------------- | ---------------------------- | +| `^0.8.17` | | `yes` | **** | **** | + +| ๐Ÿ“ค Transfers ETH | โšก Low-Level Calls | ๐Ÿ‘ฅ DelegateCall | ๐Ÿงฎ Uses Hash Functions | ๐Ÿ”– ECRecover | ๐ŸŒ€ New/Create/Create2 | +| ---------------- | ----------------- | --------------- | ---------------------- | ------------ | --------------------- | +| **** | **** | **** | **** | **** | **** | + +| โ™ป๏ธ TryCatch | ฮฃ Unchecked | +| ---------- | ----------- | +| **** | `yes` | + +#### Dependencies / External Imports + +| Dependency / Import Path | Count | +| ------------------------ | ------ | +| solady/utils/Clone.sol | 1 | +| solady/utils/SafeTransferLib.sol | 1 | +| solmate/tokens/ERC20.sol | 1 | + +#### Totals + +##### Summary + +
+ +
+ +##### AST Node Statistics + +###### Function Calls + +
+ +
+ +###### Assembly Calls + +
+ +
+ +###### AST Total + +
+ +
+ +##### Inheritance Graph + +[โž•] + + +##### CallGraph + +[โž•] + + +###### Contract Summary + +[โž•] + +____ + +Thinking about smart contract security? We can provide training, ongoing advice, and smart contract auditing. [Contact us](https://diligence.consensys.net/contact/). + + + diff --git a/docs/audit/OptimisticWithdrawalRecipientFactory.md b/docs/audit/OptimisticWithdrawalRecipientFactory.md new file mode 100644 index 0000000..92c77b5 --- /dev/null +++ b/docs/audit/OptimisticWithdrawalRecipientFactory.md @@ -0,0 +1,267 @@ + +[get in touch with Consensys Diligence](https://diligence.consensys.net)
+ +[[ ๐ŸŒ ](https://diligence.consensys.net) [ ๐Ÿ“ฉ ](mailto:diligence@consensys.net) [ ๐Ÿ”ฅ ](https://consensys.github.io/diligence/)] +

+ + + +# Solidity Metrics for 'CLI' + +## Table of contents + +- [Scope](#t-scope) + - [Source Units in Scope](#t-source-Units-in-Scope) + - [Out of Scope](#t-out-of-scope) + - [Excluded Source Units](#t-out-of-scope-excluded-source-units) + - [Duplicate Source Units](#t-out-of-scope-duplicate-source-units) + - [Doppelganger Contracts](#t-out-of-scope-doppelganger-contracts) +- [Report Overview](#t-report) + - [Risk Summary](#t-risk) + - [Source Lines](#t-source-lines) + - [Inline Documentation](#t-inline-documentation) + - [Components](#t-components) + - [Exposed Functions](#t-exposed-functions) + - [StateVariables](#t-statevariables) + - [Capabilities](#t-capabilities) + - [Dependencies](#t-package-imports) + - [Totals](#t-totals) + +## Scope + +This section lists files that are in scope for the metrics report. + +- **Project:** `'CLI'` +- **Included Files:** + - `` +- **Excluded Paths:** + - `` +- **File Limit:** `undefined` + - **Exclude File list Limit:** `undefined` + +- **Workspace Repository:** `unknown` (`undefined`@`undefined`) + +### Source Units in Scope + +Source Units Analyzed: **`1`**
+Source Units in Scope: **`1`** (**100%**) + +| Type | File | Logic Contracts | Interfaces | Lines | nLines | nSLOC | Comment Lines | Complex. Score | Capabilities | +| ---- | ------ | --------------- | ---------- | ----- | ------ | ----- | ------------- | -------------- | ------------ | +| ๐Ÿ“ | src/owr/OptimisticWithdrawalRecipientFactory.sol | 1 | **** | 134 | 128 | 45 | 60 | 47 | **๐ŸŒ€** | +| ๐Ÿ“ | **Totals** | **1** | **** | **134** | **128** | **45** | **60** | **47** | **๐ŸŒ€** | + + +Legend: [โž•] + + + + +#### Out of Scope + +##### Excluded Source Units + +Source Units Excluded: **`0`** + +[โž•] + + + +##### Duplicate Source Units + +Duplicate Source Units Excluded: **`0`** + +[โž•] + + +##### Doppelganger Contracts + +Doppelganger Contracts: **`0`** + +[โž•] + + + +## Report + +### Overview + +The analysis finished with **`0`** errors and **`0`** duplicate files. + + + + + +#### Risk + +
+ +
+ +#### Source Lines (sloc vs. nsloc) + +
+ +
+ +#### Inline Documentation + +- **Comment-to-Source Ratio:** On average there are`0.85` code lines per comment (lower=better). +- **ToDo's:** `0` + +#### Components + +| ๐Ÿ“Contracts | ๐Ÿ“šLibraries | ๐Ÿ”Interfaces | ๐ŸŽจAbstract | +| ------------- | ----------- | ------------ | ---------- | +| 1 | 0 | 0 | 0 | + +#### Exposed Functions + +This section lists functions that are explicitly declared public or payable. Please note that getter methods for public stateVars are not included. + +| ๐ŸŒPublic | ๐Ÿ’ฐPayable | +| ---------- | --------- | +| 1 | 0 | + +| External | Internal | Private | Pure | View | +| ---------- | -------- | ------- | ---- | ---- | +| 1 | 2 | 0 | 0 | 0 | + +#### StateVariables + +| Total | ๐ŸŒPublic | +| ---------- | --------- | +| 3 | 1 | + +#### Capabilities + +| Solidity Versions observed | ๐Ÿงช Experimental Features | ๐Ÿ’ฐ Can Receive Funds | ๐Ÿ–ฅ Uses Assembly | ๐Ÿ’ฃ Has Destroyable Contracts | +| -------------------------- | ------------------------ | -------------------- | ---------------- | ---------------------------- | +| `^0.8.17` | | **** | **** | **** | + +| ๐Ÿ“ค Transfers ETH | โšก Low-Level Calls | ๐Ÿ‘ฅ DelegateCall | ๐Ÿงฎ Uses Hash Functions | ๐Ÿ”– ECRecover | ๐ŸŒ€ New/Create/Create2 | +| ---------------- | ----------------- | --------------- | ---------------------- | ------------ | --------------------- | +| **** | **** | **** | **** | **** | `yes`
โ†’ `NewContract:OptimisticWithdrawalRecipient` | + +| โ™ป๏ธ TryCatch | ฮฃ Unchecked | +| ---------- | ----------- | +| **** | **** | + +#### Dependencies / External Imports + +| Dependency / Import Path | Count | +| ------------------------ | ------ | +| solady/utils/LibClone.sol | 1 | + +#### Totals + +##### Summary + +
+ +
+ +##### AST Node Statistics + +###### Function Calls + +
+ +
+ +###### Assembly Calls + +
+ +
+ +###### AST Total + +
+ +
+ +##### Inheritance Graph + +[โž•] + + +##### CallGraph + +[โž•] + + +###### Contract Summary + +[โž•] + +____ + +Thinking about smart contract security? We can provide training, ongoing advice, and smart contract auditing. [Contact us](https://diligence.consensys.net/contact/). + + + diff --git a/docs/audit/SplitMainV2.md b/docs/audit/SplitMainV2.md new file mode 100644 index 0000000..8630a95 --- /dev/null +++ b/docs/audit/SplitMainV2.md @@ -0,0 +1,297 @@ + +[get in touch with Consensys Diligence](https://diligence.consensys.net)
+ +[[ ๐ŸŒ ](https://diligence.consensys.net) [ ๐Ÿ“ฉ ](mailto:diligence@consensys.net) [ ๐Ÿ”ฅ ](https://consensys.github.io/diligence/)] +

+ + + +# Solidity Metrics for 'CLI' + +## Table of contents + +- [Scope](#t-scope) + - [Source Units in Scope](#t-source-Units-in-Scope) + - [Out of Scope](#t-out-of-scope) + - [Excluded Source Units](#t-out-of-scope-excluded-source-units) + - [Duplicate Source Units](#t-out-of-scope-duplicate-source-units) + - [Doppelganger Contracts](#t-out-of-scope-doppelganger-contracts) +- [Report Overview](#t-report) + - [Risk Summary](#t-risk) + - [Source Lines](#t-source-lines) + - [Inline Documentation](#t-inline-documentation) + - [Components](#t-components) + - [Exposed Functions](#t-exposed-functions) + - [StateVariables](#t-statevariables) + - [Capabilities](#t-capabilities) + - [Dependencies](#t-package-imports) + - [Totals](#t-totals) + +## Scope + +This section lists files that are in scope for the metrics report. + +- **Project:** `'CLI'` +- **Included Files:** + - `` +- **Excluded Paths:** + - `` +- **File Limit:** `undefined` + - **Exclude File list Limit:** `undefined` + +- **Workspace Repository:** `unknown` (`undefined`@`undefined`) + +### Source Units in Scope + +Source Units Analyzed: **`1`**
+Source Units in Scope: **`1`** (**100%**) + +| Type | File | Logic Contracts | Interfaces | Lines | nLines | nSLOC | Comment Lines | Complex. Score | Capabilities | +| ---- | ------ | --------------- | ---------- | ----- | ------ | ----- | ------------- | -------------- | ------------ | +| ๐Ÿ“ | src/splitter/SplitMainV2.sol | 1 | **** | 720 | 645 | 245 | 346 | 209 | **๐Ÿ–ฅ๐Ÿ’ฐ๐Ÿงฎฮฃ** | +| ๐Ÿ“ | **Totals** | **1** | **** | **720** | **645** | **245** | **346** | **209** | **๐Ÿ–ฅ๐Ÿ’ฐ๐Ÿงฎฮฃ** | + + +Legend: [โž•] + + + + +#### Out of Scope + +##### Excluded Source Units + +Source Units Excluded: **`0`** + +[โž•] + + + +##### Duplicate Source Units + +Duplicate Source Units Excluded: **`0`** + +[โž•] + + +##### Doppelganger Contracts + +Doppelganger Contracts: **`0`** + +[โž•] + + + +## Report + +### Overview + +The analysis finished with **`0`** errors and **`0`** duplicate files. + + + + + +#### Risk + +
+ +
+ +#### Source Lines (sloc vs. nsloc) + +
+ +
+ +#### Inline Documentation + +- **Comment-to-Source Ratio:** On average there are`0.92` code lines per comment (lower=better). +- **ToDo's:** `0` + +#### Components + +| ๐Ÿ“Contracts | ๐Ÿ“šLibraries | ๐Ÿ”Interfaces | ๐ŸŽจAbstract | +| ------------- | ----------- | ------------ | ---------- | +| 1 | 0 | 0 | 0 | + +#### Exposed Functions + +This section lists functions that are explicitly declared public or payable. Please note that getter methods for public stateVars are not included. + +| ๐ŸŒPublic | ๐Ÿ’ฐPayable | +| ---------- | --------- | +| 19 | 1 | + +| External | Internal | Private | Pure | View | +| ---------- | -------- | ------- | ---- | ---- | +| 19 | 26 | 0 | 3 | 8 | + +#### StateVariables + +| Total | ๐ŸŒPublic | +| ---------- | --------- | +| 6 | 2 | + +#### Capabilities + +| Solidity Versions observed | ๐Ÿงช Experimental Features | ๐Ÿ’ฐ Can Receive Funds | ๐Ÿ–ฅ Uses Assembly | ๐Ÿ’ฃ Has Destroyable Contracts | +| -------------------------- | ------------------------ | -------------------- | ---------------- | ---------------------------- | +| `=0.8.17` | | `yes` | `yes`
(1 asm blocks) | **** | + +| ๐Ÿ“ค Transfers ETH | โšก Low-Level Calls | ๐Ÿ‘ฅ DelegateCall | ๐Ÿงฎ Uses Hash Functions | ๐Ÿ”– ECRecover | ๐ŸŒ€ New/Create/Create2 | +| ---------------- | ----------------- | --------------- | ---------------------- | ------------ | --------------------- | +| **** | **** | **** | `yes` | **** | **** | + +| โ™ป๏ธ TryCatch | ฮฃ Unchecked | +| ---------- | ----------- | +| **** | `yes` | + +#### Dependencies / External Imports + +| Dependency / Import Path | Count | +| ------------------------ | ------ | +| solady/auth/Ownable.sol | 1 | +| solady/utils/LibClone.sol | 1 | +| solmate/tokens/ERC20.sol | 1 | +| solmate/utils/SafeTransferLib.sol | 1 | + +#### Totals + +##### Summary + +
+ +
+ +##### AST Node Statistics + +###### Function Calls + +
+ +
+ +###### Assembly Calls + +
+ +
+ +###### AST Total + +
+ +
+ +##### Inheritance Graph + +[โž•] + + +##### CallGraph + +[โž•] + + +###### Contract Summary + +[โž•] + +____ + +Thinking about smart contract security? We can provide training, ongoing advice, and smart contract auditing. [Contact us](https://diligence.consensys.net/contact/). + + + diff --git a/docs/audit/SplitWallet.md b/docs/audit/SplitWallet.md new file mode 100644 index 0000000..13b1391 --- /dev/null +++ b/docs/audit/SplitWallet.md @@ -0,0 +1,269 @@ + +[get in touch with Consensys Diligence](https://diligence.consensys.net)
+ +[[ ๐ŸŒ ](https://diligence.consensys.net) [ ๐Ÿ“ฉ ](mailto:diligence@consensys.net) [ ๐Ÿ”ฅ ](https://consensys.github.io/diligence/)] +

+ + + +# Solidity Metrics for 'CLI' + +## Table of contents + +- [Scope](#t-scope) + - [Source Units in Scope](#t-source-Units-in-Scope) + - [Out of Scope](#t-out-of-scope) + - [Excluded Source Units](#t-out-of-scope-excluded-source-units) + - [Duplicate Source Units](#t-out-of-scope-duplicate-source-units) + - [Doppelganger Contracts](#t-out-of-scope-doppelganger-contracts) +- [Report Overview](#t-report) + - [Risk Summary](#t-risk) + - [Source Lines](#t-source-lines) + - [Inline Documentation](#t-inline-documentation) + - [Components](#t-components) + - [Exposed Functions](#t-exposed-functions) + - [StateVariables](#t-statevariables) + - [Capabilities](#t-capabilities) + - [Dependencies](#t-package-imports) + - [Totals](#t-totals) + +## Scope + +This section lists files that are in scope for the metrics report. + +- **Project:** `'CLI'` +- **Included Files:** + - `` +- **Excluded Paths:** + - `` +- **File Limit:** `undefined` + - **Exclude File list Limit:** `undefined` + +- **Workspace Repository:** `unknown` (`undefined`@`undefined`) + +### Source Units in Scope + +Source Units Analyzed: **`1`**
+Source Units in Scope: **`1`** (**100%**) + +| Type | File | Logic Contracts | Interfaces | Lines | nLines | nSLOC | Comment Lines | Complex. Score | Capabilities | +| ---- | ------ | --------------- | ---------- | ----- | ------ | ----- | ------------- | -------------- | ------------ | +| ๐Ÿ“ | src/splitter/SplitWallet.sol | 1 | **** | 86 | 86 | 27 | 43 | 25 | **๐Ÿ’ฐ** | +| ๐Ÿ“ | **Totals** | **1** | **** | **86** | **86** | **27** | **43** | **25** | **๐Ÿ’ฐ** | + + +Legend: [โž•] + + + + +#### Out of Scope + +##### Excluded Source Units + +Source Units Excluded: **`0`** + +[โž•] + + + +##### Duplicate Source Units + +Duplicate Source Units Excluded: **`0`** + +[โž•] + + +##### Doppelganger Contracts + +Doppelganger Contracts: **`0`** + +[โž•] + + + +## Report + +### Overview + +The analysis finished with **`0`** errors and **`0`** duplicate files. + + + + + +#### Risk + +
+ +
+ +#### Source Lines (sloc vs. nsloc) + +
+ +
+ +#### Inline Documentation + +- **Comment-to-Source Ratio:** On average there are`0.63` code lines per comment (lower=better). +- **ToDo's:** `0` + +#### Components + +| ๐Ÿ“Contracts | ๐Ÿ“šLibraries | ๐Ÿ”Interfaces | ๐ŸŽจAbstract | +| ------------- | ----------- | ------------ | ---------- | +| 1 | 0 | 0 | 0 | + +#### Exposed Functions + +This section lists functions that are explicitly declared public or payable. Please note that getter methods for public stateVars are not included. + +| ๐ŸŒPublic | ๐Ÿ’ฐPayable | +| ---------- | --------- | +| 2 | 2 | + +| External | Internal | Private | Pure | View | +| ---------- | -------- | ------- | ---- | ---- | +| 2 | 1 | 0 | 0 | 0 | + +#### StateVariables + +| Total | ๐ŸŒPublic | +| ---------- | --------- | +| 1 | 1 | + +#### Capabilities + +| Solidity Versions observed | ๐Ÿงช Experimental Features | ๐Ÿ’ฐ Can Receive Funds | ๐Ÿ–ฅ Uses Assembly | ๐Ÿ’ฃ Has Destroyable Contracts | +| -------------------------- | ------------------------ | -------------------- | ---------------- | ---------------------------- | +| `0.8.17` | | `yes` | **** | **** | + +| ๐Ÿ“ค Transfers ETH | โšก Low-Level Calls | ๐Ÿ‘ฅ DelegateCall | ๐Ÿงฎ Uses Hash Functions | ๐Ÿ”– ECRecover | ๐ŸŒ€ New/Create/Create2 | +| ---------------- | ----------------- | --------------- | ---------------------- | ------------ | --------------------- | +| **** | **** | **** | **** | **** | **** | + +| โ™ป๏ธ TryCatch | ฮฃ Unchecked | +| ---------- | ----------- | +| **** | **** | + +#### Dependencies / External Imports + +| Dependency / Import Path | Count | +| ------------------------ | ------ | +| solmate/tokens/ERC20.sol | 1 | +| solmate/utils/SafeTransferLib.sol | 1 | + +#### Totals + +##### Summary + +
+ +
+ +##### AST Node Statistics + +###### Function Calls + +
+ +
+ +###### Assembly Calls + +
+ +
+ +###### AST Total + +
+ +
+ +##### Inheritance Graph + +[โž•] + + +##### CallGraph + +[โž•] + + +###### Contract Summary + +[โž•] + +____ + +Thinking about smart contract security? We can provide training, ongoing advice, and smart contract auditing. [Contact us](https://diligence.consensys.net/contact/). + + + From 3f7eeffeb961966dd807b3a9901977d1e01d1617 Mon Sep 17 00:00:00 2001 From: samparsky <8148384+samparsky@users.noreply.github.com> Date: Mon, 21 Aug 2023 13:55:23 +0300 Subject: [PATCH 06/82] chore: remove unused splitter contracts --- script/SplitFactoryScript.s.sol | 16 - script/SplitMainV2Script.s.sol | 17 - script/SplitWalletScript.s.sol | 16 - src/splitter/SplitFactory.sol | 76 --- src/splitter/SplitMainV2.sol | 720 -------------------------- src/splitter/SplitWallet.sol | 86 ---- src/test/splitter/SplitFactory.t.sol | 98 ---- src/test/splitter/SplitMainV2.t.sol | 744 --------------------------- src/test/splitter/SplitWallet.t.sol | 111 ---- 9 files changed, 1884 deletions(-) delete mode 100644 script/SplitFactoryScript.s.sol delete mode 100644 script/SplitMainV2Script.s.sol delete mode 100644 script/SplitWalletScript.s.sol delete mode 100644 src/splitter/SplitFactory.sol delete mode 100644 src/splitter/SplitMainV2.sol delete mode 100644 src/splitter/SplitWallet.sol delete mode 100644 src/test/splitter/SplitFactory.t.sol delete mode 100644 src/test/splitter/SplitMainV2.t.sol delete mode 100644 src/test/splitter/SplitWallet.t.sol diff --git a/script/SplitFactoryScript.s.sol b/script/SplitFactoryScript.s.sol deleted file mode 100644 index 1e30f03..0000000 --- a/script/SplitFactoryScript.s.sol +++ /dev/null @@ -1,16 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity =0.8.17; - -import "forge-std/Script.sol"; -import {SplitFactory} from "src/splitter/SplitFactory.sol"; - -contract SplitFactoryScript is Script { - function run(address owner) external { - uint256 privKey = vm.envUint("PRIVATE_KEY"); - vm.startBroadcast(privKey); - - new SplitFactory{salt: keccak256("obol.splitFactory.v1")}(owner); - - vm.stopBroadcast(); - } -} diff --git a/script/SplitMainV2Script.s.sol b/script/SplitMainV2Script.s.sol deleted file mode 100644 index f571a6c..0000000 --- a/script/SplitMainV2Script.s.sol +++ /dev/null @@ -1,17 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity =0.8.17; - -import "forge-std/Script.sol"; -import {SplitMainV2} from "src/splitter/SplitMainV2.sol"; - -/// @notice this is already deploy via splitfactory -contract SplitMainV2Script is Script { - function run() external { - uint256 privKey = vm.envUint("PRIVATE_KEY"); - vm.startBroadcast(privKey); - - new SplitMainV2{salt: keccak256("obol.splitMainV2.v1")}(); - - vm.stopBroadcast(); - } -} diff --git a/script/SplitWalletScript.s.sol b/script/SplitWalletScript.s.sol deleted file mode 100644 index 165b227..0000000 --- a/script/SplitWalletScript.s.sol +++ /dev/null @@ -1,16 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity =0.8.17; - -import "forge-std/Script.sol"; -import {SplitWallet} from "src/splitter/SplitWallet.sol"; - -contract SplitWalletScript is Script { - function run(address splitMainV2) external { - uint256 privKey = vm.envUint("PRIVATE_KEY"); - vm.startBroadcast(privKey); - - new SplitWallet{salt: keccak256("obol.splitWallet.v1")}(splitMainV2); - - vm.stopBroadcast(); - } -} diff --git a/src/splitter/SplitFactory.sol b/src/splitter/SplitFactory.sol deleted file mode 100644 index e6f82ff..0000000 --- a/src/splitter/SplitFactory.sol +++ /dev/null @@ -1,76 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity =0.8.17; - -import {Ownable} from "solady/auth/Ownable.sol"; -import {SplitMainV2} from "./SplitMainV2.sol"; -import {ISplitFactory} from "../interfaces/ISplitFactory.sol"; -import {ISplitMainV2} from "../interfaces/ISplitMainV2.sol"; - - -error IdExists(bytes32 id); -error InvalidConfig(bytes32 id, address implementation); -error InvalidSplitWalletId(bytes32 id); - -// @title SplitFactory -/// @author Obol -/// @notice SplitFactory to create splits -contract SplitFactory is Ownable, ISplitFactory { - /// @dev splitmain v2 - ISplitMainV2 public immutable splitMain; - - /// @dev split wallet id to split implmentation address - mapping(bytes32 => address) internal splitWalletImplementations; - - /// @dev Emitted on create new split wallet - /// @param id split wallet id - /// @param implementation split implementation address - event NewSplitWallet(bytes32 indexed id, address implementation); - - constructor(address owner) { - splitMain = new SplitMainV2(); - _initializeOwner(owner); - } - - /// @dev addSplitWallet - /// @param id split id - /// @param implementation split implemenation - function addSplitWallet(bytes32 id, address implementation) external onlyOwner { - if (implementation == address(0) || id == bytes32(0)) revert InvalidConfig(id, implementation); - if (splitWalletImplementations[id] != address(0)) revert IdExists(id); - splitWalletImplementations[id] = implementation; - emit NewSplitWallet(id, implementation); - } - - /// @dev createSplit - function createSplit( - bytes32 splitWalletId, - address[] calldata accounts, - uint32[] calldata percentAllocations, - uint32 distributorFee, - address distributor, - address controller - ) external override returns (address split) { - address splitWalletImplementation = splitWalletImplementations[splitWalletId]; - if (splitWalletImplementation == address(0)) revert InvalidSplitWalletId(splitWalletId); - split = splitMain.createSplit( - splitWalletImplementation, accounts, percentAllocations, controller, distributor, distributorFee - ); - } - - /// @notice Predicts the address for an immutable split created with recipients `accounts` with ownerships - /// `percentAllocations` and a keeper fee for splitting of `distributorFee` - /// @param accounts Ordered, unique list of addresses with ownership in the split - /// @param percentAllocations Percent allocations associated with each address - /// @param distributorFee Keeper fee paid by split to cover gas costs of distribution - /// @return split Predicted address of such an immutable split - function predictImmutableSplitAddress( - bytes32 splitWalletId, - address[] calldata accounts, - uint32[] calldata percentAllocations, - uint32 distributorFee - ) external view override returns (address split) { - address splitWalletImplementation = splitWalletImplementations[splitWalletId]; - split = - splitMain.predictImmutableSplitAddress(splitWalletImplementation, accounts, percentAllocations, distributorFee); - } -} diff --git a/src/splitter/SplitMainV2.sol b/src/splitter/SplitMainV2.sol deleted file mode 100644 index dc40496..0000000 --- a/src/splitter/SplitMainV2.sol +++ /dev/null @@ -1,720 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity =0.8.17; - -import {Ownable} from "solady/auth/Ownable.sol"; -import {ERC20} from "solmate/tokens/ERC20.sol"; -import {SafeTransferLib} from "solmate/utils/SafeTransferLib.sol"; -import {LibClone} from "solady/utils/LibClone.sol"; -import {ISplitMainV2} from "../interfaces/ISplitMainV2.sol"; -import {SplitWallet} from "./SplitWallet.sol"; - -/** - * ERRORS - */ - -/// @notice Unauthorized sender `sender` -/// @param sender Transaction sender -error Unauthorized(address sender); -/// @notice Invalid number of accounts `accountsLength`, must have at least 2 -/// @param accountsLength Length of accounts array -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 Invalid hash `hash` from split data (accounts, percentAllocations, distributorFee) -/// @param hash Invalid hash -error InvalidSplit__InvalidHash(bytes32 hash); -/// @notice Invalid new controlling address `newController` for mutable split -/// @param newController Invalid new controller -error InvalidNewController(address newController); - -/** - * @title SplitMain - * @author Obol - * @notice A composable and gas-efficient protocol for deploying splitter contracts. - * @dev Split recipients, ownerships, and keeper fees are stored onchain as calldata & re-passed as args / validated - * via hashing when needed. Each split gets its own address & proxy for maximum composability with other contracts - * onchain. - * For these proxies, we extended EIP-1167 Minimal Proxy Contract to avoid `DELEGATECALL` inside `receive()` to accept - * hard gas-capped `sends` & `transfers`. - */ -contract SplitMainV2 is ISplitMainV2 { - using SafeTransferLib for address; - using SafeTransferLib for ERC20; - using LibClone for address; - - /** - * STRUCTS - */ - - /// @notice holds Split metadata - struct Split { - bytes32 hash; - address controller; - address distributor; - address newPotentialController; - } - - /** - * STORAGE - */ - - /** - * STORAGE - CONSTANTS & IMMUTABLES - */ - - /// @notice constant to scale uints into percentages (1e6 == 100%) - uint256 public constant PERCENTAGE_SCALE = 1e6; - /// @notice maximum distributor fee; 1e5 = 10% * PERCENTAGE_SCALE - uint256 internal constant MAX_DISTRIBUTOR_FEE = 1e5; - /// @notice split factory address - address public immutable splitFactory; - - /** - * STORAGE - VARIABLES - PRIVATE & INTERNAL - */ - - /// @notice mapping to account ETH balances - mapping(address => uint256) internal ethBalances; - /// @notice mapping to account ERC20 balances - mapping(ERC20 => mapping(address => uint256)) internal erc20Balances; - /// @notice mapping to Split metadata - mapping(address => Split) internal splits; - - /** - * MODIFIERS - */ - - /** - * @notice Reverts if the sender doesn't own the split `split` - * @param split Address to check for control - */ - modifier onlySplitController(address split) { - if (msg.sender != splits[split].controller) revert Unauthorized(msg.sender); - _; - } - - /** - * @notice Reverts if the sender isn't the new potential controller of split `split` - * @param split Address to check for new potential control - */ - modifier onlySplitNewPotentialController(address split) { - if (msg.sender != splits[split].newPotentialController) revert Unauthorized(msg.sender); - _; - } - - /** - * @notice Reverts if the sender isn't splitFactory - */ - modifier onlySplitFactory() { - if (msg.sender != splitFactory) revert Unauthorized(msg.sender); - _; - } - - /** - * @notice Reverts if the split with recipients represented by `accounts` and `percentAllocations` is malformed - * @param accounts Ordered, unique list of addresses with ownership in the split - * @param percentAllocations Percent allocations associated with each address - * @param distributorFee Keeper fee paid by split to cover gas costs of distribution - */ - modifier validSplit(address[] memory accounts, uint32[] memory percentAllocations, uint32 distributorFee) { - 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); - _; - } - - /** - * @notice Reverts if `newController` is the zero address - * @param newController Proposed new controlling address - */ - modifier validNewController(address newController) { - if (newController == address(0)) revert InvalidNewController(newController); - _; - } - - modifier checkIfRequiresDistributor(address split) { - address distributor = splits[split].distributor; - if (distributor != address(0) && distributor != msg.sender) revert Unauthorized(msg.sender); - _; - } - - /** - * CONSTRUCTOR - */ - - constructor() { - splitFactory = msg.sender; - } - - /** - * FUNCTIONS - */ - - /** - * FUNCTIONS - PUBLIC & EXTERNAL - */ - - /** - * @notice Receive ETH - * @dev Used by split proxies in `distributeETH` to transfer ETH to `SplitMain` - * Funds sent outside of `distributeETH` will be unrecoverable - */ - receive() external payable {} - - - /// @notice Creates a new split with recipients `accounts` with ownerships `percentAllocations`, a keeper fee for - /// splitting of `distributorFee` and the controlling address `controller` - /// @param accounts Ordered, unique list of addresses with ownership in the split - /// @param percentAllocations Percent allocations associated with each address - /// @param distributorFee Keeper fee paid by split to cover gas costs of distribution - /// @param controller Controlling address (0x0 if immutable) - /// @param distributor priviledge distributor - /// @return split Address of newly created split - function createSplit( - address splitWalletImplementation, - address[] calldata accounts, - uint32[] calldata percentAllocations, - address controller, - address distributor, - uint32 distributorFee - ) external override validSplit(accounts, percentAllocations, distributorFee) onlySplitFactory returns (address split) { - bytes32 splitHash = _hashSplit(accounts, percentAllocations, distributorFee); - if (controller == address(0)) { - // create immutable split - split = splitWalletImplementation.cloneDeterministic("", splitHash); - } else { - // create mutable split - split = splitWalletImplementation.clone(""); - splits[split].controller = controller; - } - if (distributor != address(0)) splits[split].distributor = distributor; - // store split's hash in storage for future verification - splits[split].hash = splitHash; - - emit CreateSplit(split); - } - - /** - * @notice Predicts the address for an immutable split created with recipients `accounts` with ownerships - * `percentAllocations` and a keeper fee for splitting of `distributorFee` - * @param accounts Ordered, unique list of addresses with ownership in the split - * @param percentAllocations Percent allocations associated with each address - * @param distributorFee Keeper fee paid by split to cover gas costs of distribution - * @return split Predicted address of such an immutable split - */ - function predictImmutableSplitAddress( - address splitWalletImplementation, - address[] calldata accounts, - uint32[] calldata percentAllocations, - uint32 distributorFee - ) external view override validSplit(accounts, percentAllocations, distributorFee) returns (address split) { - bytes32 splitHash = _hashSplit(accounts, percentAllocations, distributorFee); - split = splitWalletImplementation.predictDeterministicAddress("", splitHash, address(this)); - } - - /** - * @notice Updates an existing split with recipients `accounts` with ownerships `percentAllocations` and a keeper fee - * for splitting of `distributorFee` - * @param split Address of mutable split to update - * @param accounts Ordered, unique list of addresses with ownership in the split - * @param percentAllocations Percent allocations associated with each address - * @param distributorFee Keeper fee paid by split to cover gas costs of distribution - */ - function updateSplit( - address split, - address[] calldata accounts, - uint32[] calldata percentAllocations, - uint32 distributorFee - ) external override onlySplitController(split) validSplit(accounts, percentAllocations, distributorFee) { - _updateSplit(split, accounts, percentAllocations, distributorFee); - } - - /** - * @notice Begins transfer of the controlling address of mutable split `split` to `newController` - * @dev Two-step control transfer inspired by - * [dharma](https://github.com/dharma-eng/dharma-smart-wallet/blob/master/contracts/helpers/TwoStepOwnable.sol) - * @param split Address of mutable split to transfer control for - * @param newController Address to begin transferring control to - */ - function transferControl(address split, address newController) - external - override - onlySplitController(split) - validNewController(newController) - { - splits[split].newPotentialController = newController; - emit InitiateControlTransfer(split, newController); - } - - /** - * @notice Cancels transfer of the controlling address of mutable split `split` - * @param split Address of mutable split to cancel control transfer for - */ - function cancelControlTransfer(address split) external override onlySplitController(split) { - delete splits[split].newPotentialController; - emit CancelControlTransfer(split); - } - - /** - * @notice Accepts transfer of the controlling address of mutable split `split` - * @param split Address of mutable split to accept control transfer for - */ - function acceptControl(address split) external override onlySplitNewPotentialController(split) { - delete splits[split].newPotentialController; - emit ControlTransfer(split, splits[split].controller, msg.sender); - splits[split].controller = msg.sender; - } - - /** - * @notice Turns mutable split `split` immutable - * @param split Address of mutable split to turn immutable - */ - function makeSplitImmutable(address split) external override onlySplitController(split) { - delete splits[split].newPotentialController; - emit ControlTransfer(split, splits[split].controller, address(0)); - splits[split].controller = address(0); - } - - /** - * @notice Distributes the ETH balance for split `split` - * @dev `accounts`, `percentAllocations`, and `distributorFee` are verified by hashing - * & comparing to the hash in storage associated with split `split` - * @param split Address of split to distribute balance for - * @param accounts Ordered, unique list of addresses with ownership in the split - * @param percentAllocations Percent allocations associated with each address - * @param distributorFee Keeper fee paid by split to cover gas costs of distribution - * @param distributorAddress Address to pay `distributorFee` to - */ - function distributeETH( - address split, - address[] calldata accounts, - uint32[] calldata percentAllocations, - uint32 distributorFee, - address distributorAddress - ) external override validSplit(accounts, percentAllocations, distributorFee) { - // use internal fn instead of modifier to avoid stack depth compiler errors - _validSplitHash(split, accounts, percentAllocations, distributorFee); - _distributeETH(split, accounts, percentAllocations, distributorFee, distributorAddress); - } - - /** - * @notice Updates & distributes the ETH balance for split `split` - * @dev only callable by SplitController - * @param split Address of split to distribute balance for - * @param accounts Ordered, unique list of addresses with ownership in the split - * @param percentAllocations Percent allocations associated with each address - * @param distributorFee Keeper fee paid by split to cover gas costs of distribution - * @param distributorAddress Address to pay `distributorFee` to - */ - function updateAndDistributeETH( - address split, - address[] calldata accounts, - uint32[] calldata percentAllocations, - uint32 distributorFee, - address distributorAddress - ) external override onlySplitController(split) validSplit(accounts, percentAllocations, distributorFee) { - _updateSplit(split, accounts, percentAllocations, distributorFee); - // know splitHash is valid immediately after updating; only accessible via controller - _distributeETH(split, accounts, percentAllocations, distributorFee, distributorAddress); - } - - /** - * @notice Distributes the ERC20 `token` balance for split `split` - * @dev `accounts`, `percentAllocations`, and `distributorFee` are verified by hashing - * & comparing to the hash in storage associated with split `split` - * @dev pernicious ERC20s may cause overflow in this function inside - * _scaleAmountByPercentage, but results do not affect ETH & other ERC20 balances - * @param split Address of split to distribute balance for - * @param token Address of ERC20 to distribute balance for - * @param accounts Ordered, unique list of addresses with ownership in the split - * @param percentAllocations Percent allocations associated with each address - * @param distributorFee Keeper fee paid by split to cover gas costs of distribution - * @param distributorAddress Address to pay `distributorFee` to - */ - function distributeERC20( - address split, - ERC20 token, - address[] calldata accounts, - uint32[] calldata percentAllocations, - uint32 distributorFee, - address distributorAddress - ) external override validSplit(accounts, percentAllocations, distributorFee) { - // use internal fn instead of modifier to avoid stack depth compiler errors - _validSplitHash(split, accounts, percentAllocations, distributorFee); - _distributeERC20(split, token, accounts, percentAllocations, distributorFee, distributorAddress); - } - - /** - * @notice Updates & distributes the ERC20 `token` balance for split `split` - * @dev only callable by SplitController - * @dev pernicious ERC20s may cause overflow in this function inside - * _scaleAmountByPercentage, but results do not affect ETH & other ERC20 balances - * @param split Address of split to distribute balance for - * @param token Address of ERC20 to distribute balance for - * @param accounts Ordered, unique list of addresses with ownership in the split - * @param percentAllocations Percent allocations associated with each address - * @param distributorFee Keeper fee paid by split to cover gas costs of distribution - * @param distributorAddress Address to pay `distributorFee` to - */ - function updateAndDistributeERC20( - address split, - ERC20 token, - address[] calldata accounts, - uint32[] calldata percentAllocations, - uint32 distributorFee, - address distributorAddress - ) external override onlySplitController(split) validSplit(accounts, percentAllocations, distributorFee) { - _updateSplit(split, accounts, percentAllocations, distributorFee); - // know splitHash is valid immediately after updating; only accessible via controller - _distributeERC20(split, token, accounts, percentAllocations, distributorFee, distributorAddress); - } - - /** - * @notice Withdraw ETH &/ ERC20 balances for account `account` - * @param account Address to withdraw on behalf of - * @param withdrawETH Withdraw all ETH if nonzero - * @param tokens Addresses of ERC20s to withdraw - */ - function withdraw(address account, uint256 withdrawETH, ERC20[] calldata tokens) external override { - uint256[] memory tokenAmounts = new uint256[](tokens.length); - uint256 ethAmount; - if (withdrawETH != 0) ethAmount = _withdraw(account); - unchecked { - // overflow should be impossible in for-loop index - for (uint256 i = 0; i < tokens.length; ++i) { - // overflow should be impossible in array length math - tokenAmounts[i] = _withdrawERC20(account, tokens[i]); - } - emit Withdrawal(account, ethAmount, tokens, tokenAmounts); - } - } - - /** - * FUNCTIONS - VIEWS - */ - - /** - * @notice Returns the current hash of split `split` - * @param split Split to return hash for - * @return Split's hash - */ - function getHash(address split) external view returns (bytes32) { - return splits[split].hash; - } - - /** - * @notice Returns the current controller of split `split` - * @param split Split to return controller for - * @return Split's controller - */ - function getController(address split) external view returns (address) { - return splits[split].controller; - } - - /** - * @notice Returns the current distributor of split `split` - * @param split Split to return distributor for - * @return Split's distributor - */ - function getDistributor(address split) external view returns (address) { - return splits[split].distributor; - } - - /** - * @notice Returns the current newPotentialController of split `split` - * @param split Split to return newPotentialController for - * @return Split's newPotentialController - */ - function getNewPotentialController(address split) external view returns (address) { - return splits[split].newPotentialController; - } - - /** - * @notice Returns the current ETH balance of account `account` - * @param account Account to return ETH balance for - * @return Account's balance of ETH - */ - function getETHBalance(address account) external view returns (uint256) { - return ethBalances[account] + (splits[account].hash != 0 ? account.balance : 0); - } - - /** - * @notice Returns the ERC20 balance of token `token` for account `account` - * @param account Account to return ERC20 `token` balance for - * @param token Token to return balance for - * @return Account's balance of `token` - */ - function getERC20Balance(address account, ERC20 token) external view returns (uint256) { - return erc20Balances[token][account] + (splits[account].hash != 0 ? token.balanceOf(account) : 0); - } - - /** - * FUNCTIONS - PRIVATE & INTERNAL - */ - - /** - * @notice Sums array of uint32s - * @param numbers Array of uint32s to sum - * @return sum Sum of `numbers`. - */ - function _getSum(uint32[] memory numbers) internal pure returns (uint32 sum) { - // overflow should be impossible in for-loop index - uint256 numbersLength = numbers.length; - for (uint256 i = 0; i < numbersLength;) { - sum += numbers[i]; - unchecked { - // overflow should be impossible in for-loop index - ++i; - } - } - } - - /** - * @notice Hashes a split - * @param accounts Ordered, unique list of addresses with ownership in the split - * @param percentAllocations Percent allocations associated with each address - * @param distributorFee Keeper fee paid by split to cover gas costs of distribution - * @return computedHash Hash of the split. - */ - function _hashSplit(address[] memory accounts, uint32[] memory percentAllocations, uint32 distributorFee) - internal - pure - returns (bytes32) - { - return keccak256(abi.encodePacked(accounts, percentAllocations, distributorFee)); - } - - /** - * @notice Updates an existing split with recipients `accounts` with ownerships `percentAllocations` and a keeper fee - * for splitting of `distributorFee` - * @param split Address of mutable split to update - * @param accounts Ordered, unique list of addresses with ownership in the split - * @param percentAllocations Percent allocations associated with each address - * @param distributorFee Keeper fee paid by split to cover gas costs of distribution - */ - function _updateSplit( - address split, - address[] calldata accounts, - uint32[] calldata percentAllocations, - uint32 distributorFee - ) internal { - bytes32 splitHash = _hashSplit(accounts, percentAllocations, distributorFee); - // store new hash in storage for future verification - splits[split].hash = splitHash; - emit UpdateSplit(split); - } - - /** - * @notice Checks hash from `accounts`, `percentAllocations`, and `distributorFee` against the hash stored for `split` - * @param split Address of hash to check - * @param accounts Ordered, unique list of addresses with ownership in the split - * @param percentAllocations Percent allocations associated with each address - * @param distributorFee Keeper fee paid by split to cover gas costs of distribution - */ - function _validSplitHash( - address split, - address[] memory accounts, - uint32[] memory percentAllocations, - uint32 distributorFee - ) internal view { - bytes32 hash = _hashSplit(accounts, percentAllocations, distributorFee); - if (splits[split].hash != hash) revert InvalidSplit__InvalidHash(hash); - } - - /** - * @notice Distributes the ETH balance for split `split` - * @dev `accounts`, `percentAllocations`, and `distributorFee` must be verified before calling - * @param split Address of split to distribute balance for - * @param accounts Ordered, unique list of addresses with ownership in the split - * @param percentAllocations Percent allocations associated with each address - * @param distributorFee Keeper fee paid by split to cover gas costs of distribution - * @param distributorAddress Address to pay `distributorFee` to - */ - function _distributeETH( - address split, - address[] memory accounts, - uint32[] memory percentAllocations, - uint32 distributorFee, - address distributorAddress - ) internal checkIfRequiresDistributor(split) { - uint256 mainBalance = ethBalances[split]; - // flush proxy ETH balance to SplitMain - // split proxy should be guaranteed to exist at this address after validating splitHash - // (attacker can't deploy own contract to address with high balance & empty sendETHToMain - // to drain ETH from SplitMain) - // could technically check if (change in proxy balance == change in SplitMain balance) - // before/after external call, but seems like extra gas for no practical benefit - uint256 proxyBalance = SplitWallet(split).sendETHToMain(); - - // if mainBalance is positive, leave 1 in SplitMain for gas efficiency - uint256 amountToSplit; - unchecked { - // underflow should be impossible - if (mainBalance > 0) mainBalance -= 1; - // overflow should be impossible - amountToSplit = mainBalance + proxyBalance; - } - if (mainBalance > 0) ethBalances[split] = 1; - // emit event with gross amountToSplit (before deducting distributorFee) - emit DistributeETH(split, amountToSplit, distributorAddress); - if (distributorFee != 0) { - // given `amountToSplit`, calculate keeper fee - uint256 distributorFeeAmount = _scaleAmountByPercentage(amountToSplit, distributorFee); - unchecked { - // credit keeper with fee - // overflow should be impossible with validated distributorFee - ethBalances[distributorAddress != address(0) ? distributorAddress : msg.sender] += distributorFeeAmount; - // given keeper fee, calculate how much to distribute to split recipients - // underflow should be impossible with validated distributorFee - amountToSplit -= distributorFeeAmount; - } - } - unchecked { - // distribute remaining balance - // overflow should be impossible in for-loop index - // cache accounts length to save gas - uint256 accountsLength = accounts.length; - for (uint256 i = 0; i < accountsLength; ++i) { - // overflow should be impossible with validated allocations - ethBalances[accounts[i]] += _scaleAmountByPercentage(amountToSplit, percentAllocations[i]); - } - } - } - - /** - * @notice Distributes the ERC20 `token` balance for split `split` - * @dev `accounts`, `percentAllocations`, and `distributorFee` must be verified before calling - * @dev pernicious ERC20s may cause overflow in this function inside - * _scaleAmountByPercentage, but results do not affect ETH & other ERC20 balances - * @param split Address of split to distribute balance for - * @param token Address of ERC20 to distribute balance for - * @param accounts Ordered, unique list of addresses with ownership in the split - * @param percentAllocations Percent allocations associated with each address - * @param distributorFee Keeper fee paid by split to cover gas costs of distribution - * @param distributorAddress Address to pay `distributorFee` to - */ - function _distributeERC20( - address split, - ERC20 token, - address[] memory accounts, - uint32[] memory percentAllocations, - uint32 distributorFee, - address distributorAddress - ) internal checkIfRequiresDistributor(split) { - uint256 amountToSplit; - uint256 mainBalance = erc20Balances[token][split]; - // split proxy should be guaranteed to exist at this address after validating splitHash - // (attacker can't deploy own contract to address with high ERC20 balance & empty - // sendERC20ToMain to drain ERC20 from SplitMain) - // doesn't support rebasing or fee-on-transfer tokens - // flush extra proxy ERC20 balance to SplitMain - uint256 proxyBalance = SplitWallet(split).sendERC20ToMain(token); - unchecked { - // if mainBalance &/ proxyBalance are positive, leave 1 for gas efficiency - // underflow should be impossible - if (proxyBalance > 0) proxyBalance -= 1; - // underflow should be impossible - if (mainBalance > 0) mainBalance -= 1; - // overflow should be impossible - amountToSplit = mainBalance + proxyBalance; - } - if (mainBalance > 0) erc20Balances[token][split] = 1; - // emit event with gross amountToSplit (before deducting distributorFee) - emit DistributeERC20(split, token, amountToSplit, distributorAddress); - if (distributorFee != 0) { - // given `amountToSplit`, calculate keeper fee - uint256 distributorFeeAmount = _scaleAmountByPercentage(amountToSplit, distributorFee); - // overflow should be impossible with validated distributorFee - unchecked { - // credit keeper with fee - erc20Balances[token][distributorAddress != address(0) ? distributorAddress : msg.sender] += distributorFeeAmount; - // given keeper fee, calculate how much to distribute to split recipients - amountToSplit -= distributorFeeAmount; - } - } - // distribute remaining balance - // overflows should be impossible in for-loop with validated allocations - unchecked { - // cache accounts length to save gas - uint256 accountsLength = accounts.length; - for (uint256 i = 0; i < accountsLength; ++i) { - erc20Balances[token][accounts[i]] += _scaleAmountByPercentage(amountToSplit, percentAllocations[i]); - } - } - } - - /** - * @notice Multiplies an amount by a scaled percentage - * @param amount Amount to get `scaledPercentage` of - * @param scaledPercent Percent scaled by PERCENTAGE_SCALE - * @return scaledAmount Percent of `amount`. - */ - function _scaleAmountByPercentage(uint256 amount, uint256 scaledPercent) internal pure returns (uint256 scaledAmount) { - // use assembly to bypass checking for overflow & division by 0 - // scaledPercent has been validated to be < PERCENTAGE_SCALE) - // & PERCENTAGE_SCALE will never be 0 - // pernicious ERC20s may cause overflow, but results do not affect ETH & other ERC20 balances - assembly { - /* eg (100 * 2*1e4) / (1e6) */ - scaledAmount := div(mul(amount, scaledPercent), PERCENTAGE_SCALE) - } - } - - /** - * @notice Withdraw ETH for account `account` - * @param account Account to withdrawn ETH for - * @return withdrawn Amount of ETH withdrawn - */ - function _withdraw(address account) internal returns (uint256 withdrawn) { - // leave balance of 1 for gas efficiency - // underflow if ethBalance is 0 - withdrawn = ethBalances[account] - 1; - ethBalances[account] = 1; - account.safeTransferETH(withdrawn); - } - - /** - * @notice Withdraw ERC20 `token` for account `account` - * @param account Account to withdrawn ERC20 `token` for - * @return withdrawn Amount of ERC20 `token` withdrawn - */ - function _withdrawERC20(address account, ERC20 token) internal returns (uint256 withdrawn) { - // leave balance of 1 for gas efficiency - // underflow if erc20Balance is 0 - withdrawn = erc20Balances[token][account] - 1; - erc20Balances[token][account] = 1; - token.safeTransfer(account, withdrawn); - } -} diff --git a/src/splitter/SplitWallet.sol b/src/splitter/SplitWallet.sol deleted file mode 100644 index 3d85ef0..0000000 --- a/src/splitter/SplitWallet.sol +++ /dev/null @@ -1,86 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity 0.8.17; - -import {ISplitMainV2} from "../interfaces/ISplitMainV2.sol"; -import {ERC20} from "solmate/tokens/ERC20.sol"; -import {SafeTransferLib} from "solmate/utils/SafeTransferLib.sol"; - -/** - * ERRORS - */ - -/// @notice Unauthorized sender -error Unauthorized(); - -/** - * @title SplitWallet - * @author 0xSplits - * @notice The implementation logic for `SplitProxy`. - * @dev `SplitProxy` handles `receive()` itself to avoid the gas cost with `DELEGATECALL`. - */ -contract SplitWallet { - using SafeTransferLib for address; - using SafeTransferLib for ERC20; - - /** - * EVENTS - */ - - /** - * @notice emitted after each successful ETH transfer to proxy - * @param split Address of the split that received ETH - * @param amount Amount of ETH received - */ - event ReceiveETH(address indexed split, uint256 amount); - - /** - * STORAGE - */ - - /** - * STORAGE - CONSTANTS & IMMUTABLES - */ - - /// @notice address of SplitMain for split distributions & EOA/SC withdrawals - ISplitMainV2 public immutable splitMain; - - /** - * MODIFIERS - */ - - /// @notice Reverts if the sender isn't SplitMain - modifier onlySplitMain() { - if (msg.sender != address(splitMain)) revert Unauthorized(); - _; - } - - /** - * CONSTRUCTOR - */ - - constructor(address splitMainV2) { - splitMain = ISplitMainV2(splitMainV2); - } - - /** - * FUNCTIONS - PUBLIC & EXTERNAL - */ - - /// @notice Sends amount `amount` of ETH in proxy to SplitMain - /// @dev payable reduces gas cost; no vulnerability to accidentally lock - /// ETH introduced since fn call is restricted to SplitMain - function sendETHToMain() external payable onlySplitMain returns (uint256 amount) { - amount = address(this).balance; - address(splitMain).safeTransferETH(amount); - } - - - /// @notice Sends amount `amount` of ERC20 `token` in proxy to SplitMain - /// @dev payable reduces gas cost; no vulnerability to accidentally lock - /// ETH introduced since fn call is restricted to SplitMain - /// @param token Token to send - function sendERC20ToMain(ERC20 token) external payable onlySplitMain returns (uint256 amount) { - amount = token.balanceOf(address(this)); - token.safeTransfer(address(splitMain), amount); - } -} diff --git a/src/test/splitter/SplitFactory.t.sol b/src/test/splitter/SplitFactory.t.sol deleted file mode 100644 index eb80fed..0000000 --- a/src/test/splitter/SplitFactory.t.sol +++ /dev/null @@ -1,98 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity >=0.8.0; - -import "forge-std/Test.sol"; -import {SplitFactory, InvalidConfig} from "src/splitter/SplitFactory.sol"; -import {SplitMainV2} from "src/splitter/SplitMainV2.sol"; -import {SplitWallet, Unauthorized} from "src/splitter/SplitWallet.sol"; - -contract XSplitFactoryTest is Test { - - error IdExists(bytes32 id); - - event NewSplitWallet(bytes32 indexed id, address implementation); - - SplitFactory public splitFactory; - SplitMainV2 public splitMainV2; - SplitWallet public splitWallet; - - address user1; - - address[] accounts; - uint32[] percentAllocations; - - bytes32 internal splitWalletId = keccak256("splitWallet"); - - function setUp() public { - splitFactory = new SplitFactory(address(this)); - // fetch splitMain from splitFactory - splitMainV2 = SplitMainV2(payable(address(splitFactory.splitMain()))); - splitWallet = new SplitWallet(address(splitMainV2)); - - user1 = makeAddr("user1"); - - accounts = new address[](2); - accounts[0] = makeAddr("accounts0"); - accounts[1] = makeAddr("accounts1"); - - percentAllocations = new uint32[](2); - percentAllocations[0] = 400_000; - percentAllocations[1] = 600_000; - } - - function testAddSplitWalletInvalidImplementation() public { - vm.expectRevert(); - splitFactory.addSplitWallet(splitWalletId, address(0)); - } - - function testAddSplitWallet() public { - vm.expectEmit(true, false, false, false, address(splitFactory)); - emit NewSplitWallet(splitWalletId, address(splitWallet)); - - splitFactory.addSplitWallet(splitWalletId, address(splitWallet)); - - vm.expectRevert(); - splitFactory.addSplitWallet(splitWalletId, address(splitWallet)); - } - - function testCheckSplitFactoryOwner() public { - assertEq( - splitFactory.owner(), - address(this) - ); - } - - function testCanChangeSplitFactoryOwner() public { - vm.prank(user1); - splitFactory.requestOwnershipHandover(); - - splitFactory.completeOwnershipHandover(user1); - } - - function testNonOwnerCannotAddSplitWallet() public { - vm.expectRevert(Unauthorized.selector); - vm.prank(user1); - splitFactory.addSplitWallet(splitWalletId, address(splitWallet)); - } - - function testCreateSplit() public { - // add split wallet - splitFactory.addSplitWallet(splitWalletId, address(splitWallet)); - - address predictedSplitAddress = splitFactory.predictImmutableSplitAddress( - splitWalletId, accounts, percentAllocations, 0 - ); - - address splitter = splitFactory.createSplit(splitWalletId, accounts, percentAllocations, 0, address(this), address(0)); - - - assertEq(predictedSplitAddress, splitter); - } - - function testCreateSplitInvalidSplutWalletId() public { - vm.expectRevert(); - - splitFactory.createSplit(bytes32("2"), accounts, percentAllocations, 0, address(this), address(0)); - } - -} diff --git a/src/test/splitter/SplitMainV2.t.sol b/src/test/splitter/SplitMainV2.t.sol deleted file mode 100644 index 779c083..0000000 --- a/src/test/splitter/SplitMainV2.t.sol +++ /dev/null @@ -1,744 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity >=0.8.0; - -import "forge-std/Test.sol"; -import {ERC20} from "solmate/tokens/ERC20.sol"; -import { - SplitMainV2, - Unauthorized, - InvalidSplit__TooFewAccounts, - InvalidSplit__AccountsAndAllocationsMismatch, - InvalidSplit__InvalidAllocationsSum, - InvalidSplit__AccountsOutOfOrder, - InvalidSplit__AllocationMustBePositive, - InvalidSplit__InvalidDistributorFee -} from "src/splitter/SplitMainV2.sol"; -import {SplitWallet} from "src/splitter/SplitWallet.sol"; -import {MockERC20} from "../utils/mocks/MockERC20.sol"; - - -contract SplitMainV2Test is Test { - SplitMainV2 public splitMainV2; - address public splitWallet; - uint32 public constant SPLIT_MAIN_PERCENTAGE_SCALE = 1e6; - - event CreateSplit(address indexed split); - - address[] accounts; - uint32[] percentAllocations; - address user1; - address user2; - address user3; - - ERC20 mockERC20; - - function setUp() public virtual { - splitMainV2 = new SplitMainV2(); - splitWallet = address(new SplitWallet(address(splitMainV2))); - - mockERC20 = ERC20(address(new MockERC20("demo", "DMT", 18))); - - accounts = new address[](2); - accounts[0] = makeAddr("accounts0"); - accounts[1] = makeAddr("accounts1"); - - percentAllocations = new uint32[](2); - percentAllocations[0] = 400_000; - percentAllocations[1] = 600_000; - - user1 = makeAddr("account2"); - user2 = makeAddr("account3"); - user3 = makeAddr("account4"); - } - - function createTestMutableSplit() internal returns (address split) { - split = splitMainV2.createSplit( - address(splitWallet), - accounts, - percentAllocations, - address(this), - address(this), - 0 - ); - } - -} - -contract SplitMainV2CreateSplitConfiguration is SplitMainV2Test { - address internal split; - - function testRevertIfAccountSizeIsOne() public { - address[] memory newAccounts = new address[](1); - newAccounts[0] = makeAddr("testRevertIfAccountSizeIsOne"); - - vm.expectRevert( - abi.encodeWithSelector(InvalidSplit__TooFewAccounts.selector, newAccounts.length) - ); - - splitMainV2.createSplit( - splitWallet, - newAccounts, - percentAllocations, - address(0), - address(0), - 0 - ); - } - - function testRevertIfIncorrectAccountsAndAllocationSize() public { - uint32[] memory newPercentAllocations = new uint32[](3); - newPercentAllocations[0] = 200_000; - newPercentAllocations[1] = 200_000; - newPercentAllocations[2] = 600_000; - - vm.expectRevert( - abi.encodeWithSelector( - InvalidSplit__AccountsAndAllocationsMismatch.selector, - accounts.length, - newPercentAllocations.length - ) - ); - - splitMainV2.createSplit( - splitWallet, - accounts, - newPercentAllocations, - address(0), - address(0), - 0 - ); - } - - function testRevertIfIncorrectPercentAllocations() public { - uint32[] memory newPercentAllocations = new uint32[](2); - newPercentAllocations[0] = 700_000; - newPercentAllocations[1] = 500_000; - - vm.expectRevert( - abi.encodeWithSelector( - InvalidSplit__InvalidAllocationsSum.selector, - 1_200_000 - ) - ); - - splitMainV2.createSplit( - splitWallet, - accounts, - newPercentAllocations, - address(0), - address(0), - 0 - ); - } - - function testRevertIfAccountOutOfOrder() public { - address[] memory newAccounts = new address[](2); - newAccounts[0] = address(0x4); - newAccounts[1] = address(0x1); - - vm.expectRevert( - abi.encodeWithSelector( - InvalidSplit__AccountsOutOfOrder.selector, - 0 - ) - ); - - splitMainV2.createSplit( - splitWallet, - newAccounts, - percentAllocations, - address(0), - address(0), - 0 - ); - } - - function testRevertIfZeroPercentAllocation() public { - uint32[] memory newPercentAllocations = new uint32[](2); - newPercentAllocations[0] = SPLIT_MAIN_PERCENTAGE_SCALE; - newPercentAllocations[1] = 0; - - vm.expectRevert( - abi.encodeWithSelector( - InvalidSplit__AllocationMustBePositive.selector, - 1 - ) - ); - - splitMainV2.createSplit( - splitWallet, - accounts, - newPercentAllocations, - address(0), - address(0), - 0 - ); - } - - function testRevertIfInvalidDistributorFee() public { - uint32 invalidDistributorFee = 1e6; - - vm.expectRevert( - abi.encodeWithSelector( - InvalidSplit__InvalidDistributorFee.selector, - invalidDistributorFee - ) - ); - - splitMainV2.createSplit( - splitWallet, - accounts, - percentAllocations, - address(0), - address(0), - invalidDistributorFee - ); - } - -} - -contract SplitMainV2CreateImmutableSplit is SplitMainV2Test { - address internal split; - - function setUp() public override { - super.setUp(); - address predictedSplitAddress = splitMainV2.predictImmutableSplitAddress( - address(splitWallet), accounts, percentAllocations, 0 - ); - - vm.expectEmit(true, true, true, true, address(splitMainV2)); - emit CreateSplit(predictedSplitAddress); - - split = splitMainV2.createSplit(address(splitWallet), accounts, percentAllocations, address(0), address(this), 0); - - assertEq(predictedSplitAddress, split, "invalid predicted split address"); - } - - function testGetSplitHash() public { - bytes32 splitHash = splitMainV2.getHash(split); - assertEq(splitHash != bytes32(0), true, "invalid split hash"); - } - - function testGetSplitController() public { - assertEq(splitMainV2.getController(split), address(0), "invalid split controller"); - } - - function testRevertIfUpdateImmutableSplit() public { - uint32[] memory newPercentAllocations = new uint32[](2); - newPercentAllocations[0] = percentAllocations[1]; - newPercentAllocations[1] = percentAllocations[0]; - - vm.expectRevert(); - splitMainV2.updateSplit(split, accounts, newPercentAllocations, 0); - } - -} - -contract SplitMainV2CreateMutableSplit is SplitMainV2Test { - - event UpdateSplit(address indexed split); - event InitiateControlTransfer(address indexed split, address indexed newPotentialController); - event CancelControlTransfer(address indexed split); - event ControlTransfer(address indexed split, address indexed previousController, address indexed newController); - - function testGetHash() public { - address split = createTestMutableSplit(); - bytes32 splitHash = splitMainV2.getHash(split); - assertEq(splitHash != bytes32(0), true, "invalid split hassh"); - } - - function testCanUpdateSplit() public { - address split = createTestMutableSplit(); - - uint32[] memory newPercentAllocations = new uint32[](2); - newPercentAllocations[0] = percentAllocations[1]; - newPercentAllocations[1] = percentAllocations[0]; - - vm.expectEmit(true, true, true, true, address(splitMainV2)); - emit UpdateSplit(split); - - splitMainV2.updateSplit(split, accounts, newPercentAllocations, 0); - } - - function testCanGetSplitController() public { - address split = createTestMutableSplit(); - assertEq(splitMainV2.getController(split), address(this), "invalid split controler"); - } - - function testTransferControlMutableSplit() public { - address split = createTestMutableSplit(); - vm.expectEmit(true, true, true, true, address(splitMainV2)); - emit InitiateControlTransfer(split, user1); - - splitMainV2.transferControl(split, user1); - - assertEq(splitMainV2.getNewPotentialController(split), user1, "invalid new controller"); - } - - function testRevertIfTransferControlNotController() public { - address split = createTestMutableSplit(); - vm.expectRevert(); - vm.prank(user1); - splitMainV2.transferControl(split, user1); - } - - function testCancelControlTransfer() public { - address split = createTestMutableSplit(); - - splitMainV2.transferControl(split, user1); - - vm.expectEmit(true, true, true, true, address(splitMainV2)); - emit CancelControlTransfer(split); - - splitMainV2.cancelControlTransfer(split); - } - - function testRevertIfCancelControlTransfer() public { - address split = createTestMutableSplit(); - vm.expectRevert(); - vm.prank(user1); - splitMainV2.cancelControlTransfer(split); - } - - function testAcceptControl() public { - address split = createTestMutableSplit(); - - console.logString("getcontroller"); - console.log(splitMainV2.getController(split)); - vm.prank(address(this)); - splitMainV2.transferControl(split, user1); - - vm.expectEmit(true, true, true, true, address(splitMainV2)); - emit ControlTransfer(split, address(this), user1); - - vm.prank(user1); - splitMainV2.acceptControl(split); - } - - function testRevertIfAcceptControlNotNewPotentialController() public { - - address split = createTestMutableSplit(); - - splitMainV2.transferControl(split, user1); - vm.prank(user2); - - vm.expectRevert(); - splitMainV2.acceptControl(split); - } - - function testMakeSplitImmutable() public { - address split = createTestMutableSplit(); - - vm.expectEmit(true, true, true, true, address(splitMainV2)); - emit ControlTransfer(split, address(this), address(0)); - - splitMainV2.makeSplitImmutable(split); - } - - function testRevertIfMakeImmutableNotController() public { - address split = createTestMutableSplit(); - - vm.expectRevert(); - vm.prank(user1); - - splitMainV2.makeSplitImmutable(split); - } -} - -contract SplitMainV2DistributeETH is SplitMainV2Test { - - address internal split; - - function setUp() public override { - super.setUp(); - - split = splitMainV2.createSplit( - address(splitWallet), - accounts, - percentAllocations, - address(0), - address(0), - 0 - ); - } - - function testDistributeETHNoDistribtuor() public { - - vm.deal(address(split), 10 ether); - - splitMainV2.distributeETH(split, accounts, percentAllocations, 0, address(0)); - - assertEq(splitMainV2.getETHBalance(accounts[0]), 4 ether); - assertEq(splitMainV2.getETHBalance(accounts[1]), 6 ether); - } - - function testRevertIfIncorrectSplitData() public { - address[] memory incorrectAccounts = new address[](2); - incorrectAccounts[0] = makeAddr("user1"); - incorrectAccounts[1] = makeAddr("user2"); - - - vm.expectRevert(); - splitMainV2.distributeETH(split, incorrectAccounts, percentAllocations, 0, address(0)); - } - - function testDistributeETHWithDistributor() public { - address[] memory newAccounts = new address[](2); - newAccounts[0] = makeAddr("user1"); - newAccounts[1] = makeAddr("user2"); - - address splitWithDistributor = splitMainV2.createSplit( - address(splitWallet), - newAccounts, - percentAllocations, - address(0), - address(this), - 0 - ); - - vm.deal(splitWithDistributor, 10 ether); - - assertEq(splitMainV2.getDistributor(splitWithDistributor), address(this), "invalid distributor"); - - // expect to revert if called by non distributor - vm.expectRevert(); - vm.prank(user1); - splitMainV2.distributeETH( - splitWithDistributor, - newAccounts, - percentAllocations, - 0, - address(this) - ); - - // should not revert - splitMainV2.distributeETH( - splitWithDistributor, - newAccounts, - percentAllocations, - 0, - address(this) - ); - - } - - function testDistributeETH0WithDistributorFee() public { - // @TODO fuzzing for the distributor fee - uint256 amountToDistribute = 10 ether; - - address[] memory newAccounts = new address[](2); - newAccounts[0] = makeAddr("user1"); - newAccounts[1] = makeAddr("user2"); - - address splitWithDistributorFee = splitMainV2.createSplit( - address(splitWallet), - newAccounts, - percentAllocations, - address(0), - address(this), - 1e5 - ); - - vm.deal(splitWithDistributorFee, amountToDistribute); - - splitMainV2.distributeETH( - splitWithDistributorFee, - newAccounts, - percentAllocations, - 1e5, - address(this) - ); - - assertEq(splitMainV2.getETHBalance(newAccounts[0]), 36e17); - assertEq(splitMainV2.getETHBalance(newAccounts[1]), 54e17); - } - -} - -contract SplitMainV2UpdateAndDistributeERC20 is SplitMainV2Test { - - function testUpdateAndDistributeERC20() public { - address split = createTestMutableSplit(); - - uint32[] memory newPercentAllocations = new uint32[](2); - newPercentAllocations[0] = 200_000; - newPercentAllocations[1] = 800_000; - - bytes32 currentSplitHash = splitMainV2.getHash(split); - - splitMainV2.updateAndDistributeERC20( - split, - mockERC20, - accounts, - newPercentAllocations, - 0, - address(this) - ); - - bytes32 newSplitHash = splitMainV2.getHash(split); - - assertEq(currentSplitHash != newSplitHash, true, "invalid split hash"); - } - - function testRevertsIfUpdateAndDistributeERC20NonController() public { - address split = createTestMutableSplit(); - - uint32[] memory newPercentAllocations = new uint32[](2); - newPercentAllocations[0] = 200_000; - newPercentAllocations[1] = 800_000; - - vm.expectRevert( - abi.encodeWithSelector(Unauthorized.selector, user1) - ); - - vm.prank(user1); - splitMainV2.updateAndDistributeERC20( - split, - mockERC20, - accounts, - newPercentAllocations, - 0, - address(this) - ); - } - -} - -contract SplitMainV2UpdateAndDistributeETH is SplitMainV2Test { - - function testUpdateAndDistributeETH() public { - address split = createTestMutableSplit(); - - uint32[] memory newPercentAllocations = new uint32[](2); - newPercentAllocations[0] = 200_000; - newPercentAllocations[1] = 800_000; - - bytes32 currentSplitHash = splitMainV2.getHash(split); - - splitMainV2.updateAndDistributeETH( - split, - accounts, - newPercentAllocations, - 0, - address(this) - ); - - bytes32 newSplitHash = splitMainV2.getHash(split); - - assertEq(currentSplitHash != newSplitHash, true, "invalid split hash"); - } - - function testRevertIfUpdateAndDistributeETHNonController() public { - address split = createTestMutableSplit(); - - uint32[] memory newPercentAllocations = new uint32[](2); - newPercentAllocations[0] = 200_000; - newPercentAllocations[1] = 800_000; - - vm.expectRevert( - abi.encodeWithSelector(Unauthorized.selector, user1) - ); - - vm.prank(user1); - - splitMainV2.updateAndDistributeETH( - split, - accounts, - newPercentAllocations, - 0, - address(this) - ); - } - -} - - -contract SplitMainV2DistributeERC20 is SplitMainV2Test { - - address internal split; - - function setUp() public override { - super.setUp(); - split = splitMainV2.createSplit( - address(splitWallet), - accounts, - percentAllocations, - address(0), - address(0), - 0 - ); - } - - function testDistributeERC20() public { - uint256 amountToDistribute = 10 ether; - - deal(address(mockERC20), split, amountToDistribute); - - splitMainV2.distributeERC20(split, mockERC20, accounts, percentAllocations, 0, address(0)); - - assertApproxEqAbs( - splitMainV2.getERC20Balance(accounts[0], mockERC20), - 4 ether, - 1, - "invalid distribution" - ); - - assertApproxEqAbs( - splitMainV2.getERC20Balance(accounts[1], mockERC20), - 6 ether, - 1, - "invalid distritbution" - ); - - } - - function testDistributeERC20WithDistributor() public { - uint256 amountToDistribute = 10 ether; - - address[] memory newAccounts = new address[](2); - newAccounts[0] = makeAddr("user1"); - newAccounts[1] = makeAddr("user2"); - - address splitWithDistributor = splitMainV2.createSplit( - address(splitWallet), - newAccounts, - percentAllocations, - address(0), - address(this), - 0 - ); - - deal(address(mockERC20), splitWithDistributor, amountToDistribute); - - // expect to revert if called by non distributor - vm.expectRevert(); - - vm.prank(user1); - - splitMainV2.distributeERC20( - splitWithDistributor, - ERC20(address(mockERC20)), - newAccounts, - percentAllocations, - 0, - address(this) - ); - - // should not revert - splitMainV2.distributeERC20( - splitWithDistributor, - ERC20(address(mockERC20)), - newAccounts, - percentAllocations, - 0, - address(this) - ); - } - - function testDistributeERC20WithDistributorFee() public { - // @TODO fuzzing for the distributor fee - uint256 amountToDistribute = 10 ether; - - address[] memory newAccounts = new address[](2); - newAccounts[0] = makeAddr("user1"); - newAccounts[1] = makeAddr("user2"); - - address splitWithDistributorFee = splitMainV2.createSplit( - address(splitWallet), - newAccounts, - percentAllocations, - address(0), - address(this), - 1e5 - ); - - deal(address(mockERC20), splitWithDistributorFee, amountToDistribute); - - splitMainV2.distributeERC20( - splitWithDistributorFee, - ERC20(address(mockERC20)), - newAccounts, - percentAllocations, - 1e5, - address(this) - ); - - assertEq(splitMainV2.getERC20Balance(newAccounts[0], mockERC20), 36e17); - assertEq(splitMainV2.getERC20Balance(newAccounts[1], mockERC20), 54e17); - } -} - - -contract SplitMainV2Withdraw is SplitMainV2Test { - - address internal split; - - function setUp() public override { - super.setUp(); - - split = splitMainV2.createSplit( - address(splitWallet), - accounts, - percentAllocations, - address(0), - address(0), - 0 - ); - } - - - function testWithdrawETH() public { - uint256 amountToDistribute = 10 ether; - vm.deal(split, amountToDistribute); - - // distribute - splitMainV2.distributeETH( - split, - accounts, - percentAllocations, - 0, - address(0) - ); - - assertApproxEqAbs(4 ether, splitMainV2.getETHBalance(accounts[0]), 1, "incorrect split amount"); - assertApproxEqAbs(6 ether, splitMainV2.getETHBalance(accounts[1]), 1, "incorrect withdraw amount"); - - // withdraw - ERC20[] memory tokens = new ERC20[](0); - splitMainV2.withdraw(accounts[0], 1, tokens); - splitMainV2.withdraw(accounts[1], 1, tokens); - - assertApproxEqAbs(accounts[0].balance, 4 ether, 1, "invalid amount"); - assertApproxEqAbs(accounts[1].balance, 6 ether, 1, "invalid amount"); - - } - - function testWithdrawERC20() public { - uint256 amountToDistribute = 10 ether; - - deal(address(mockERC20), split, amountToDistribute); - - splitMainV2.distributeERC20( - split, - mockERC20, - accounts, - percentAllocations, - 0, - address(0) - ); - - // withdraw - ERC20[] memory tokens = new ERC20[](1); - tokens[0] = mockERC20; - - assertApproxEqAbs(4 ether, splitMainV2.getERC20Balance(accounts[0], mockERC20), 2, "invalid amount"); - assertApproxEqAbs(6 ether, splitMainV2.getERC20Balance(accounts[1], mockERC20), 2, "invalid amount"); - - splitMainV2.withdraw(accounts[0], 0, tokens); - splitMainV2.withdraw(accounts[1], 0, tokens); - - assertApproxEqAbs(mockERC20.balanceOf(accounts[0]), 4 ether, 2, "invalid split"); - assertApproxEqAbs(mockERC20.balanceOf(accounts[1]), 6 ether, 2, "invalid split"); - } - -} \ No newline at end of file diff --git a/src/test/splitter/SplitWallet.t.sol b/src/test/splitter/SplitWallet.t.sol deleted file mode 100644 index 3429deb..0000000 --- a/src/test/splitter/SplitWallet.t.sol +++ /dev/null @@ -1,111 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity >=0.8.17; - -import "forge-std/Test.sol"; -import {SplitFactory} from "src/splitter/SplitFactory.sol"; -import {SplitMainV2} from "src/splitter/SplitMainV2.sol"; -import {SplitWallet, Unauthorized} from "src/splitter/SplitWallet.sol"; -import {MockERC20} from "../utils/mocks/MockERC20.sol"; -import {ERC20} from "solmate/tokens/ERC20.sol"; - - -contract SplitWalletTest is Test { - SplitFactory public splitFactory; - SplitMainV2 public splitMainV2; - SplitWallet public splitWallet; - MockERC20 mockERC20; - - bytes32 internal splitWalletId = keccak256("splitWallet"); - - address splitter; - - address user1; - - event ReceiveETH(uint256 amount); - - function setUp() public { - user1 = makeAddr("user1"); - - mockERC20 = new MockERC20("demo", "DMT", 18); - - splitFactory = new SplitFactory(address(this)); - splitMainV2 = SplitMainV2(payable(address(splitFactory.splitMain()))); - splitWallet = new SplitWallet(address(splitMainV2)); - // add split wallet - splitFactory.addSplitWallet(splitWalletId, address(splitWallet)); - - address[] memory accounts = new address[](2); - accounts[0] = makeAddr("accounts0"); - accounts[1] = makeAddr("accounts1"); - - uint32[] memory percentAllocations = new uint32[](2); - percentAllocations[0] = 400_000; - percentAllocations[1] = 600_000; - - splitter = splitFactory.createSplit( - splitWalletId, - accounts, - percentAllocations, - 0, - address(this), - address(0) - ); - - } - - function testCanReceiveETH() public { - uint256 amountOfEth = 10 ether; - deal(payable(splitter), amountOfEth); - assertEq(splitter.balance , amountOfEth); - } - - function testEmitCorrectEventOnETHReceive() public { - uint256 amountOfEth = 10 ether; - vm.expectEmit(false, false, false, false, address(splitter)); - emit ReceiveETH(amountOfEth); - - deal(payable(address(this)), amountOfEth); - - payable(address(splitter)).transfer(amountOfEth); - } - - function testShouldStoreSplitMainAddress() public { - assertEq( - address(SplitWallet(splitter).splitMain()), - address(splitMainV2) - ); - } - - function testNonSplitMainCallSendETH() public { - vm.expectRevert(Unauthorized.selector); - SplitWallet(splitter).sendETHToMain(); - } - - function testSendETHToSplitMain() public { - vm.prank(address(splitMainV2)); - - uint256 amountOfETHSent = splitter.balance; - SplitWallet(splitter).sendETHToMain(); - assertEq(address(splitMainV2).balance, amountOfETHSent); - } - - function testSendERC20ToMain() public { - uint256 amountOfTokens = 10 ether; - deal(address(mockERC20), splitter, amountOfTokens); - - vm.prank(address(splitMainV2)); - - SplitWallet(splitter).sendERC20ToMain(ERC20(address(mockERC20))); - - assertEq( - mockERC20.balanceOf(address(splitMainV2)), - amountOfTokens - ); - } - - function testNonSplitMainCallSendERC20ToMain() public { - vm.expectRevert(Unauthorized.selector); - SplitWallet(splitter).sendERC20ToMain(ERC20(address(mockERC20))); - } - -} \ No newline at end of file From 2c4aebcac734350c43252c1933061a0dc76d2a07 Mon Sep 17 00:00:00 2001 From: samparsky <8148384+samparsky@users.noreply.github.com> Date: Thu, 24 Aug 2023 17:30:51 +0300 Subject: [PATCH 07/82] chore: apply forge fmt --- src/interfaces/ISplitMain.sol | 1 - src/lido/LidoSplit.sol | 105 +- src/lido/LidoSplitFactory.sol | 90 +- src/owr/OptimisticWithdrawalRecipient.sol | 659 ++++---- .../OptimisticWithdrawalRecipientFactory.sol | 236 ++- src/test/lido/LIdoSplitFactory.t.sol | 50 +- src/test/lido/LidoSplit.t.sol | 70 +- src/test/lido/LidoSplitTestHelper.sol | 9 +- .../integration/LidoSplitIntegrationTest.sol | 103 +- src/test/waterfall/OWRReentrancy.sol | 9 +- src/test/waterfall/OWRTestHelper.t.sol | 82 +- .../OptimisticWithdrawalRecipient.t.sol | 1437 ++++++++--------- ...OptimisticWithdrawalRecipientFactory.t.sol | 443 ++--- 13 files changed, 1486 insertions(+), 1808 deletions(-) diff --git a/src/interfaces/ISplitMain.sol b/src/interfaces/ISplitMain.sol index 1a49d3c..1fe2e31 100644 --- a/src/interfaces/ISplitMain.sol +++ b/src/interfaces/ISplitMain.sol @@ -53,7 +53,6 @@ interface ISplitMain { address distributorAddress ) external; - /// @notice Distributes the ERC20 `token` balance for split `split` /// @dev `accounts`, `percentAllocations`, and `distributorFee` are verified by hashing /// & comparing to the hash in storage associated with split `split` diff --git a/src/lido/LidoSplit.sol b/src/lido/LidoSplit.sol index d938aa4..96e2b54 100644 --- a/src/lido/LidoSplit.sol +++ b/src/lido/LidoSplit.sol @@ -1,72 +1,73 @@ // SPDX-License-Identifier: MIT pragma solidity =0.8.17; -import {ERC20} from 'solmate/tokens/ERC20.sol'; -import {SafeTransferLib} from 'solmate/utils/SafeTransferLib.sol'; + +import {ERC20} from "solmate/tokens/ERC20.sol"; +import {SafeTransferLib} from "solmate/utils/SafeTransferLib.sol"; import {Clone} from "solady/utils/Clone.sol"; interface IwSTETH { - function wrap(uint256 amount) external returns (uint256); + function wrap(uint256 amount) external returns (uint256); } /// @title LidoSplit /// @author Obol -/// @notice A wrapper for 0xsplits/split-contracts SplitWallet that transforms +/// @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 LidoSplit is Clone { - /// ----------------------------------------------------------------------- - /// libraries - /// ----------------------------------------------------------------------- - using SafeTransferLib for ERC20; + /// ----------------------------------------------------------------------- + /// libraries + /// ----------------------------------------------------------------------- + using SafeTransferLib for ERC20; - /// ----------------------------------------------------------------------- - /// storage - cwia offsets - /// ----------------------------------------------------------------------- + /// ----------------------------------------------------------------------- + /// storage - cwia offsets + /// ----------------------------------------------------------------------- - // stETH (address, 20 bytes), - // 0; first item - uint256 internal constant ST_ETH_ADDRESS_OFFSET = 0; - // wstETH (address, 20 bytees) - // 20 = st_eth_offset(0) + st_eth_address_size(address, 20 bytes) - uint256 internal constant WST_ETH_ADDRESS_OFFSET = 20; - // splitWallet (adress, 20 bytes) - // 40 = wst_eth_offset(20) + wst_eth_size(address, 20 bytes) - uint256 internal constant SPLIT_WALLET_ADDRESS_OFFSET = 40; + // stETH (address, 20 bytes), + // 0; first item + uint256 internal constant ST_ETH_ADDRESS_OFFSET = 0; + // wstETH (address, 20 bytees) + // 20 = st_eth_offset(0) + st_eth_address_size(address, 20 bytes) + uint256 internal constant WST_ETH_ADDRESS_OFFSET = 20; + // splitWallet (adress, 20 bytes) + // 40 = wst_eth_offset(20) + wst_eth_size(address, 20 bytes) + uint256 internal constant SPLIT_WALLET_ADDRESS_OFFSET = 40; - constructor() {} + constructor() {} - /// 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); - } + /// 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); + } - /// Address of stETH token - /// @dev equivalent to address public immutable stETHAddress - function stETHAddress() public pure returns (address) { - return _getArgAddress(ST_ETH_ADDRESS_OFFSET); - } + /// Address of stETH token + /// @dev equivalent to address public immutable stETHAddress + function stETHAddress() public pure returns (address) { + return _getArgAddress(ST_ETH_ADDRESS_OFFSET); + } - /// Address of wstETH token - /// @dev equivalent to address public immutable wstETHAddress - function wstETHAddress() public pure returns (address) { - return _getArgAddress(WST_ETH_ADDRESS_OFFSET); - } + /// Address of wstETH token + /// @dev equivalent to address public immutable wstETHAddress + function wstETHAddress() public pure returns (address) { + return _getArgAddress(WST_ETH_ADDRESS_OFFSET); + } - /// 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) { - ERC20 stETH = ERC20(stETHAddress()); - ERC20 wstETH = ERC20(wstETHAddress()); + /// 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) { + ERC20 stETH = ERC20(stETHAddress()); + ERC20 wstETH = ERC20(wstETHAddress()); - // get current balance - uint256 balance = stETH.balanceOf(address(this)); - // approve the wstETH - stETH.approve(address(wstETH), balance); - // wrap into wseth - amount = IwSTETH(address(wstETH)).wrap(balance); - // transfer to split wallet - ERC20(wstETH).safeTransfer(splitWallet(), amount); - } -} \ No newline at end of file + // get current balance + uint256 balance = stETH.balanceOf(address(this)); + // approve the wstETH + stETH.approve(address(wstETH), balance); + // wrap into wseth + amount = IwSTETH(address(wstETH)).wrap(balance); + // transfer to split wallet + ERC20(wstETH).safeTransfer(splitWallet(), amount); + } +} diff --git a/src/lido/LidoSplitFactory.sol b/src/lido/LidoSplitFactory.sol index a4b2373..14f9d53 100644 --- a/src/lido/LidoSplitFactory.sol +++ b/src/lido/LidoSplitFactory.sol @@ -1,7 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity =0.8.17; + import {LibClone} from "solady/utils/LibClone.sol"; -import {ERC20} from 'solmate/tokens/ERC20.sol'; +import {ERC20} from "solmate/tokens/ERC20.sol"; import "./LidoSplit.sol"; /// @title LidoSplitFactory @@ -9,65 +10,54 @@ import "./LidoSplit.sol"; /// @notice A factory contract for cheaply deploying LidoSplit. /// @dev The address returned should be used to as reward address for Lido contract LidoSplitFactory { + /// ----------------------------------------------------------------------- + /// errors + /// ----------------------------------------------------------------------- - /// ----------------------------------------------------------------------- - /// errors - /// ----------------------------------------------------------------------- - - /// Invalid wallet - error Invalid_Wallet(); + /// Invalid wallet + error Invalid_Wallet(); + /// ----------------------------------------------------------------------- + /// libraries + /// ----------------------------------------------------------------------- + using LibClone for address; - /// ----------------------------------------------------------------------- - /// libraries - /// ----------------------------------------------------------------------- - using LibClone for address; + /// ----------------------------------------------------------------------- + /// events + /// ----------------------------------------------------------------------- - /// ----------------------------------------------------------------------- - /// events - /// ----------------------------------------------------------------------- + /// Emitted after lido split + event CreateLidoSplit(address split); - /// Emitted after lido split - event CreateLidoSplit( - address split - ); + /// ----------------------------------------------------------------------- + /// storage + /// ----------------------------------------------------------------------- - /// ----------------------------------------------------------------------- - /// storage - /// ----------------------------------------------------------------------- + /// @notice stETH token address + ERC20 public immutable stETH; - /// @notice stETH token address - ERC20 public immutable stETH; + /// @notice wstETH token address + ERC20 public immutable wstETH; - /// @notice wstETH token address - ERC20 public immutable wstETH; + /// @dev lido split implementation + LidoSplit public immutable lidoSplitImpl; - /// @dev lido split implementation - LidoSplit public immutable lidoSplitImpl; + constructor(ERC20 _stETH, ERC20 _wstETH) { + stETH = _stETH; + wstETH = _wstETH; + lidoSplitImpl = new LidoSplit(); + } - constructor(ERC20 _stETH, ERC20 _wstETH) { - stETH = _stETH; - wstETH = _wstETH; - lidoSplitImpl = new LidoSplit(); - } + /// 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(); - /// 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(); - } + bytes memory data = abi.encodePacked(stETH, wstETH, splitWallet); - bytes memory data = abi.encodePacked( - stETH, - wstETH, - splitWallet - ); - - lidoSplit = address(lidoSplitImpl).clone(data); + lidoSplit = address(lidoSplitImpl).clone(data); - emit CreateLidoSplit(lidoSplit); - } -} \ No newline at end of file + emit CreateLidoSplit(lidoSplit); + } +} diff --git a/src/owr/OptimisticWithdrawalRecipient.sol b/src/owr/OptimisticWithdrawalRecipient.sol index ad4ab46..1f818f1 100644 --- a/src/owr/OptimisticWithdrawalRecipient.sol +++ b/src/owr/OptimisticWithdrawalRecipient.sol @@ -14,372 +14,347 @@ import {SafeTransferLib} from "solady/utils/SafeTransferLib.sol"; /// Target ERC20s with very large decimals may overflow & cause issues. /// This contract uses token = address(0) to refer to ETH. contract OptimisticWithdrawalRecipient is Clone { - /// ----------------------------------------------------------------------- - /// libraries - /// ----------------------------------------------------------------------- - - using SafeTransferLib for address; - - /// ----------------------------------------------------------------------- - /// errors - /// ----------------------------------------------------------------------- - - /// Invalid token recovery; cannot recover the OWRecipient token - error InvalidTokenRecovery_OWRToken(); - - /// Invalid token recovery recipient - error InvalidTokenRecovery_InvalidRecipient(); - - /// Invalid distribution - error InvalidDistribution_TooLarge(); - - /// ----------------------------------------------------------------------- - /// events - /// ----------------------------------------------------------------------- - - /// Emitted after each successful ETH transfer to proxy - /// @param amount Amount of ETH received - /// @dev embedded in & emitted from clone bytecode - event ReceiveETH(uint256 amount); - - /// Emitted after funds are distributed to recipients - /// @param payouts Amount of payout - /// @param pullFlowFlag Flag for pushing funds to recipients or storing for pulling - event DistributeFunds( - uint256[] payouts, uint256 pullFlowFlag - ); - - /// Emitted after non-OWRecipient tokens are recovered to a recipient - /// @param recoveryAddressToken Recovered token (cannot be OptimisticWithdrawalRecipient token) - /// @param recipient Address receiving recovered token - /// @param amount Amount of recovered token - event RecoverNonOWRecipientFunds( - address recoveryAddressToken, address recipient, uint256 amount - ); - - /// Emitted after funds withdrawn using pull flow - /// @param account Account withdrawing funds for - /// @param amount Amount withdrawn - event Withdrawal(address account, uint256 amount); - - /// ----------------------------------------------------------------------- - /// storage - /// ----------------------------------------------------------------------- - - /// ----------------------------------------------------------------------- - /// storage - constants - /// ----------------------------------------------------------------------- - - address internal constant ETH_ADDRESS = address(0); - - uint256 internal constant PUSH = 0; - uint256 internal constant PULL = 1; - - uint256 internal constant ONE_WORD = 32; - uint256 internal constant ADDRESS_BITS = 160; - uint256 internal constant TRANCHE_SIZE = 2; - - /// @dev threshold for pushing balance update as reward or principal - uint256 internal constant BALANCE_CLASSIFICATION_THRESHOLD = 16 ether; - uint256 internal constant PRINCIPAL_RECIPIENT_INDEX = 0; - uint256 internal constant REWARD_RECIPIENT_INDEX = 1; - - /// ----------------------------------------------------------------------- - /// storage - cwia offsets - /// ----------------------------------------------------------------------- - - // token (address, 20 bytes), recoveryAddress (address, 20 bytes), - // tranches (uint256[], numTranches * 32 bytes) - - // 0; first item - uint256 internal constant TOKEN_OFFSET = 0; - // 20 = token_offset (0) + token_size (address, 20 bytes) - uint256 internal constant RECOVERY_ADDRESS_OFFSET = 20; - // 40 = recoveryAddress_offset (20) + recoveryAddress_size (address, 20 bytes) - uint256 internal constant TRANCHES_OFFSET = 40; - - /// Address of ERC20 to distribute (0x0 used for ETH) - /// @dev equivalent to address public immutable token; - function token() public pure returns (address) { - return _getArgAddress(TOKEN_OFFSET); + /// ----------------------------------------------------------------------- + /// libraries + /// ----------------------------------------------------------------------- + + using SafeTransferLib for address; + + /// ----------------------------------------------------------------------- + /// errors + /// ----------------------------------------------------------------------- + + /// Invalid token recovery; cannot recover the OWRecipient token + error InvalidTokenRecovery_OWRToken(); + + /// Invalid token recovery recipient + error InvalidTokenRecovery_InvalidRecipient(); + + /// Invalid distribution + error InvalidDistribution_TooLarge(); + + /// ----------------------------------------------------------------------- + /// events + /// ----------------------------------------------------------------------- + + /// Emitted after each successful ETH transfer to proxy + /// @param amount Amount of ETH received + /// @dev embedded in & emitted from clone bytecode + event ReceiveETH(uint256 amount); + + /// Emitted after funds are distributed to recipients + /// @param payouts Amount of payout + /// @param pullFlowFlag Flag for pushing funds to recipients or storing for pulling + event DistributeFunds(uint256[] payouts, uint256 pullFlowFlag); + + /// Emitted after non-OWRecipient tokens are recovered to a recipient + /// @param recoveryAddressToken Recovered token (cannot be OptimisticWithdrawalRecipient token) + /// @param recipient Address receiving recovered token + /// @param amount Amount of recovered token + event RecoverNonOWRecipientFunds(address recoveryAddressToken, address recipient, uint256 amount); + + /// Emitted after funds withdrawn using pull flow + /// @param account Account withdrawing funds for + /// @param amount Amount withdrawn + event Withdrawal(address account, uint256 amount); + + /// ----------------------------------------------------------------------- + /// storage + /// ----------------------------------------------------------------------- + + /// ----------------------------------------------------------------------- + /// storage - constants + /// ----------------------------------------------------------------------- + + address internal constant ETH_ADDRESS = address(0); + + uint256 internal constant PUSH = 0; + uint256 internal constant PULL = 1; + + uint256 internal constant ONE_WORD = 32; + uint256 internal constant ADDRESS_BITS = 160; + uint256 internal constant TRANCHE_SIZE = 2; + + /// @dev threshold for pushing balance update as reward or principal + uint256 internal constant BALANCE_CLASSIFICATION_THRESHOLD = 16 ether; + uint256 internal constant PRINCIPAL_RECIPIENT_INDEX = 0; + uint256 internal constant REWARD_RECIPIENT_INDEX = 1; + + /// ----------------------------------------------------------------------- + /// storage - cwia offsets + /// ----------------------------------------------------------------------- + + // token (address, 20 bytes), recoveryAddress (address, 20 bytes), + // tranches (uint256[], numTranches * 32 bytes) + + // 0; first item + uint256 internal constant TOKEN_OFFSET = 0; + // 20 = token_offset (0) + token_size (address, 20 bytes) + uint256 internal constant RECOVERY_ADDRESS_OFFSET = 20; + // 40 = recoveryAddress_offset (20) + recoveryAddress_size (address, 20 bytes) + uint256 internal constant TRANCHES_OFFSET = 40; + + /// Address of ERC20 to distribute (0x0 used for ETH) + /// @dev equivalent to address public immutable token; + function token() public pure returns (address) { + return _getArgAddress(TOKEN_OFFSET); + } + + /// Address to recover non-OWR tokens to + /// @dev equivalent to address public immutable recoveryAddress; + function recoveryAddress() public pure returns (address) { + return _getArgAddress(RECOVERY_ADDRESS_OFFSET); + } + + /// Get OWR tranche `i` + /// @dev emulates to uint256[] internal immutable tranche; + function _getTranche(uint256 i) internal pure returns (uint256) { + unchecked { + // shouldn't overflow + return _getArgUint256(TRANCHES_OFFSET + i * ONE_WORD); } - - /// Address to recover non-OWR tokens to - /// @dev equivalent to address public immutable recoveryAddress; - function recoveryAddress() public pure returns (address) { - return _getArgAddress(RECOVERY_ADDRESS_OFFSET); - } - - /// Get OWR tranche `i` - /// @dev emulates to uint256[] internal immutable tranche; - function _getTranche(uint256 i) internal pure returns (uint256) { + } + + /// ----------------------------------------------------------------------- + /// storage - mutables + /// ----------------------------------------------------------------------- + + /// Amount of distributed OWRecipient token + /// @dev ERC20s with very large decimals may overflow & cause issues + uint128 public distributedFunds; + + /// Amount of active balance set aside for pulls + /// @dev ERC20s with very large decimals may overflow & cause issues + uint128 public fundsPendingWithdrawal; + + /// Amount of distributed OWRecipient token for first tranche (principal) + /// @dev ERC20s with very large decimals may overflow & cause issues + uint256 public claimedFirstTrancheFunds; + + /// Mapping to account balances for pulling + mapping(address => uint256) internal pullBalances; + + /// ----------------------------------------------------------------------- + /// constructor + /// ----------------------------------------------------------------------- + + // solhint-disable-next-line no-empty-blocks + /// clone implementation doesn't use constructor + constructor() {} + + /// ----------------------------------------------------------------------- + /// functions + /// ----------------------------------------------------------------------- + + /// ----------------------------------------------------------------------- + /// functions - public & external + /// ----------------------------------------------------------------------- + + /// emit event when receiving ETH + /// @dev implemented w/i clone bytecode + /* receive() external payable { */ + /* emit ReceiveETH(msg.value); */ + /* } */ + + /// Distributes target token inside the contract to recipients + /// @dev pushes funds to recipients + function distributeFunds() external payable { + _distributeFunds(PUSH); + } + + /// Distributes target token inside the contract to recipients + /// @dev backup recovery if any recipient tries to brick the OWRecipient for + /// remaining recipients + function distributeFundsPull() external payable { + _distributeFunds(PULL); + } + + /// Recover non-OWR tokens to a recipient + /// @param nonOWRToken Token to recover (cannot be OWR token) + /// @param recipient Address to receive recovered token + function recoverFunds(address nonOWRToken, address recipient) external payable { + /// checks + + // revert if caller tries to recover OWRecipient token + if (nonOWRToken == token()) revert InvalidTokenRecovery_OWRToken(); + + // if recoveryAddress is set, recipient must match it + // else, recipient must be one of the OWR recipients + + address _recoveryAddress = recoveryAddress(); + if (_recoveryAddress == address(0)) { + // ensure txn recipient is a valid OWR recipient + (address[] memory recipients,) = getTranches(); + bool validRecipient = false; + uint256 _numTranches = TRANCHE_SIZE; + for (uint256 i; i < _numTranches;) { + if (recipients[i] == recipient) { + validRecipient = true; + break; + } unchecked { - // shouldn't overflow - return _getArgUint256(TRANCHES_OFFSET + i * ONE_WORD); + // shouldn't overflow + ++i; } + } + if (!validRecipient) revert InvalidTokenRecovery_InvalidRecipient(); + } else if (recipient != _recoveryAddress) { + revert InvalidTokenRecovery_InvalidRecipient(); } - /// ----------------------------------------------------------------------- - /// storage - mutables - /// ----------------------------------------------------------------------- - - /// Amount of distributed OWRecipient token - /// @dev ERC20s with very large decimals may overflow & cause issues - uint128 public distributedFunds; - - /// Amount of active balance set aside for pulls - /// @dev ERC20s with very large decimals may overflow & cause issues - uint128 public fundsPendingWithdrawal; - - /// Amount of distributed OWRecipient token for first tranche (principal) - /// @dev ERC20s with very large decimals may overflow & cause issues - uint256 public claimedFirstTrancheFunds; - - /// Mapping to account balances for pulling - mapping(address => uint256) internal pullBalances; - - /// ----------------------------------------------------------------------- - /// constructor - /// ----------------------------------------------------------------------- - - // solhint-disable-next-line no-empty-blocks - /// clone implementation doesn't use constructor - constructor() {} - - /// ----------------------------------------------------------------------- - /// functions - /// ----------------------------------------------------------------------- + /// effects - /// ----------------------------------------------------------------------- - /// functions - public & external - /// ----------------------------------------------------------------------- + /// interactions - /// emit event when receiving ETH - /// @dev implemented w/i clone bytecode - /* receive() external payable { */ - /* emit ReceiveETH(msg.value); */ - /* } */ - - /// Distributes target token inside the contract to recipients - /// @dev pushes funds to recipients - function distributeFunds() external payable { - _distributeFunds(PUSH); + // recover non-target token + uint256 amount; + if (nonOWRToken == ETH_ADDRESS) { + amount = address(this).balance; + recipient.safeTransferETH(amount); + } else { + amount = ERC20(nonOWRToken).balanceOf(address(this)); + nonOWRToken.safeTransfer(recipient, amount); } - /// Distributes target token inside the contract to recipients - /// @dev backup recovery if any recipient tries to brick the OWRecipient for - /// remaining recipients - function distributeFundsPull() external payable { - _distributeFunds(PULL); + emit RecoverNonOWRecipientFunds(nonOWRToken, recipient, amount); + } + + /// Withdraw token balance for account `account` + /// @param account Address to withdraw on behalf of + function withdraw(address account) external { + address _token = token(); + uint256 tokenAmount = pullBalances[account]; + unchecked { + // shouldn't underflow; fundsPendingWithdrawal = sum(pullBalances) + fundsPendingWithdrawal -= uint128(tokenAmount); + } + pullBalances[account] = 0; + if (_token == ETH_ADDRESS) account.safeTransferETH(tokenAmount); + else _token.safeTransfer(account, tokenAmount); + + emit Withdrawal(account, tokenAmount); + } + + /// ----------------------------------------------------------------------- + /// functions - view & pure + /// ----------------------------------------------------------------------- + + /// Return unpacked tranches + /// @return recipients Addresses to distribute payments to + /// @return threshold Absolute payment threshold for principal + function getTranches() public pure returns (address[] memory recipients, uint256 threshold) { + recipients = new address[](TRANCHE_SIZE); + + uint256 tranche = _getTranche(PRINCIPAL_RECIPIENT_INDEX); + recipients[0] = address(uint160(tranche)); + threshold = tranche >> ADDRESS_BITS; + + // recipients has one more entry than thresholds + recipients[1] = address(uint160(_getTranche(REWARD_RECIPIENT_INDEX))); + } + + /// Returns the balance for account `account` + /// @param account Account to return balance for + /// @return Account's balance OWR token + function getPullBalance(address account) external view returns (uint256) { + return pullBalances[account]; + } + + /// ----------------------------------------------------------------------- + /// functions - private & internal + /// ----------------------------------------------------------------------- + + /// Distributes target token inside the contract to next-in-line recipients + /// @dev can PUSH or PULL funds to recipients + function _distributeFunds(uint256 pullFlowFlag) internal { + /// checks + + /// effects + + // load storage into memory + // fetch the token we want to distribute + address _token = token(); + // the amount of funds distributed so far + uint256 _startingDistributedFunds = uint256(distributedFunds); + uint256 _endingDistributedFunds; + uint256 _fundsToBeDistributed; + uint256 _claimedFirstTrancheFunds = uint256(claimedFirstTrancheFunds); + uint256 _memoryFundsPendingWithdrawal = uint256(fundsPendingWithdrawal); + unchecked { + // shouldn't overflow + _endingDistributedFunds = _startingDistributedFunds + // fundsPendingWithdrawal is always <= _startingDistributedFunds + - _memoryFundsPendingWithdrawal + // recognizes 0x0 as ETH + // shouldn't need to worry about re-entrancy from ERC20 view fn + + (_token == ETH_ADDRESS ? address(this).balance : ERC20(_token).balanceOf(address(this))); + _fundsToBeDistributed = _endingDistributedFunds - _startingDistributedFunds; } - /// Recover non-OWR tokens to a recipient - /// @param nonOWRToken Token to recover (cannot be OWR token) - /// @param recipient Address to receive recovered token - function recoverFunds( - address nonOWRToken, - address recipient - ) external payable { - /// checks - - // revert if caller tries to recover OWRecipient token - if (nonOWRToken == token()) { - revert InvalidTokenRecovery_OWRToken(); - } - - // if recoveryAddress is set, recipient must match it - // else, recipient must be one of the OWR recipients - - address _recoveryAddress = recoveryAddress(); - if (_recoveryAddress == address(0)) { - // ensure txn recipient is a valid OWR recipient - (address[] memory recipients,) = getTranches(); - bool validRecipient = false; - uint256 _numTranches = TRANCHE_SIZE; - for (uint256 i; i < _numTranches;) { - if (recipients[i] == recipient) { - validRecipient = true; - break; - } - unchecked { - // shouldn't overflow - ++i; - } - } - if (!validRecipient) { - revert InvalidTokenRecovery_InvalidRecipient(); - } - } else if (recipient != _recoveryAddress) { - revert InvalidTokenRecovery_InvalidRecipient(); - } - - /// effects + (address[] memory recipients, uint256 threshold) = getTranches(); - /// interactions + // determine which tranche is getting paid based on funds to be distributed + // 0 = first tranche + // 1 = second tranche - // recover non-target token - uint256 amount; - if (nonOWRToken == ETH_ADDRESS) { - amount = address(this).balance; - recipient.safeTransferETH(amount); - } else { - amount = ERC20(nonOWRToken).balanceOf(address(this)); - nonOWRToken.safeTransfer(recipient, amount); - } + // construct the payout arrays + uint256 _payoutsLength = TRANCHE_SIZE; + uint256[] memory _payouts = new uint256[](_payoutsLength); - emit RecoverNonOWRecipientFunds(nonOWRToken, recipient, amount); - } + unchecked { + // _claimedFirstTrancheFunds should always be <= threshold + uint256 firstTrancheRemaining = threshold - _claimedFirstTrancheFunds; - /// Withdraw token balance for account `account` - /// @param account Address to withdraw on behalf of - function withdraw(address account) external { - address _token = token(); - uint256 tokenAmount = pullBalances[account]; - unchecked { - // shouldn't underflow; fundsPendingWithdrawal = sum(pullBalances) - fundsPendingWithdrawal -= uint128(tokenAmount); - } - pullBalances[account] = 0; - if (_token == ETH_ADDRESS) { - account.safeTransferETH(tokenAmount); + if (_fundsToBeDistributed >= BALANCE_CLASSIFICATION_THRESHOLD && firstTrancheRemaining > 0) { + if (_fundsToBeDistributed > firstTrancheRemaining) { + // this means there is reward part of the funds to be distributed + _payouts[PRINCIPAL_RECIPIENT_INDEX] = firstTrancheRemaining; + // shouldn't underflow + _payouts[REWARD_RECIPIENT_INDEX] = _fundsToBeDistributed - firstTrancheRemaining; } else { - _token.safeTransfer(account, tokenAmount); + // this means there is no reward part of the funds to be distributed + _payouts[PRINCIPAL_RECIPIENT_INDEX] = _fundsToBeDistributed; } - - emit Withdrawal(account, tokenAmount); + } else { + _payouts[REWARD_RECIPIENT_INDEX] = _fundsToBeDistributed; + } } - /// ----------------------------------------------------------------------- - /// functions - view & pure - /// ----------------------------------------------------------------------- - - /// Return unpacked tranches - /// @return recipients Addresses to distribute payments to - /// @return threshold Absolute payment threshold for principal - function getTranches() - public - pure - returns (address[] memory recipients, uint256 threshold) { - recipients = new address[](TRANCHE_SIZE); - - uint256 tranche = _getTranche(PRINCIPAL_RECIPIENT_INDEX); - recipients[0] = address(uint160(tranche)); - threshold = tranche >> ADDRESS_BITS; - - // recipients has one more entry than thresholds - recipients[1] = address(uint160(_getTranche(REWARD_RECIPIENT_INDEX))); - } - - /// Returns the balance for account `account` - /// @param account Account to return balance for - /// @return Account's balance OWR token - function getPullBalance(address account) external view returns (uint256) { - return pullBalances[account]; + if (_endingDistributedFunds > type(uint128).max) revert InvalidDistribution_TooLarge(); + // Write to storage + distributedFunds = uint128(_endingDistributedFunds); + // the principal value + claimedFirstTrancheFunds += _payouts[PRINCIPAL_RECIPIENT_INDEX]; } - /// ----------------------------------------------------------------------- - /// functions - private & internal - /// ----------------------------------------------------------------------- - - /// Distributes target token inside the contract to next-in-line recipients - /// @dev can PUSH or PULL funds to recipients - function _distributeFunds(uint256 pullFlowFlag) internal { - /// checks - - /// effects - - // load storage into memory - // fetch the token we want to distribute - address _token = token(); - // the amount of funds distributed so far - uint256 _startingDistributedFunds = uint256(distributedFunds); - uint256 _endingDistributedFunds; - uint256 _fundsToBeDistributed; - uint256 _claimedFirstTrancheFunds = uint256(claimedFirstTrancheFunds); - uint256 _memoryFundsPendingWithdrawal = uint256(fundsPendingWithdrawal); - unchecked { - // shouldn't overflow - _endingDistributedFunds = _startingDistributedFunds - // fundsPendingWithdrawal is always <= _startingDistributedFunds - - _memoryFundsPendingWithdrawal - // recognizes 0x0 as ETH - // shouldn't need to worry about re-entrancy from ERC20 view fn - + ( - _token == ETH_ADDRESS - ? address(this).balance - : ERC20(_token).balanceOf(address(this)) - ); - _fundsToBeDistributed = _endingDistributedFunds - _startingDistributedFunds; - } - - (address[] memory recipients, uint256 threshold) = - getTranches(); - - // determine which tranche is getting paid based on funds to be distributed - // 0 = first tranche - // 1 = second tranche - - // construct the payout arrays - uint256 _payoutsLength = TRANCHE_SIZE; - uint256[] memory _payouts = new uint256[](_payoutsLength); - - unchecked { - // _claimedFirstTrancheFunds should always be <= threshold - uint256 firstTrancheRemaining = threshold - _claimedFirstTrancheFunds; - - if (_fundsToBeDistributed >= BALANCE_CLASSIFICATION_THRESHOLD && firstTrancheRemaining > 0) { - if (_fundsToBeDistributed > firstTrancheRemaining) { - // this means there is reward part of the funds to be distributed - _payouts[PRINCIPAL_RECIPIENT_INDEX] = firstTrancheRemaining; - // shouldn't underflow - _payouts[REWARD_RECIPIENT_INDEX] = _fundsToBeDistributed - firstTrancheRemaining; - } else { - // this means there is no reward part of the funds to be distributed - _payouts[PRINCIPAL_RECIPIENT_INDEX] = _fundsToBeDistributed; - } - } else { - _payouts[REWARD_RECIPIENT_INDEX] = _fundsToBeDistributed; - } - } - - { - if (_endingDistributedFunds > type(uint128).max) { - revert InvalidDistribution_TooLarge(); - } - // Write to storage - distributedFunds = uint128(_endingDistributedFunds); - // the principal value - claimedFirstTrancheFunds += _payouts[PRINCIPAL_RECIPIENT_INDEX]; - } - - /// interactions - - // pay outs - // earlier tranche recipients may try to re-enter but will cause fn to revert - // when later external calls fail (bc balance is emptied early) - for (uint256 i; i < _payoutsLength;) { - if (_payouts[i] > 0) { - if (pullFlowFlag == PULL) { - pullBalances[recipients[i]] += _payouts[i]; - _memoryFundsPendingWithdrawal += _payouts[i]; - } else if (_token == ETH_ADDRESS) { - (recipients[i]).safeTransferETH(_payouts[i]); - } else { - _token.safeTransfer(recipients[i], _payouts[i]); - } - } - unchecked { - // shouldn't overflow - ++i; - } - } + /// interactions + // pay outs + // earlier tranche recipients may try to re-enter but will cause fn to revert + // when later external calls fail (bc balance is emptied early) + for (uint256 i; i < _payoutsLength;) { + if (_payouts[i] > 0) { if (pullFlowFlag == PULL) { - // Write to storage - fundsPendingWithdrawal = uint128(_memoryFundsPendingWithdrawal); + pullBalances[recipients[i]] += _payouts[i]; + _memoryFundsPendingWithdrawal += _payouts[i]; + } else if (_token == ETH_ADDRESS) { + (recipients[i]).safeTransferETH(_payouts[i]); + } else { + _token.safeTransfer(recipients[i], _payouts[i]); } + } + unchecked { + // shouldn't overflow + ++i; + } + } - emit DistributeFunds(_payouts, pullFlowFlag); + if (pullFlowFlag == PULL) { + // Write to storage + fundsPendingWithdrawal = uint128(_memoryFundsPendingWithdrawal); } -} \ No newline at end of file + + emit DistributeFunds(_payouts, pullFlowFlag); + } +} diff --git a/src/owr/OptimisticWithdrawalRecipientFactory.sol b/src/owr/OptimisticWithdrawalRecipientFactory.sol index b1a63ff..de26be4 100644 --- a/src/owr/OptimisticWithdrawalRecipientFactory.sol +++ b/src/owr/OptimisticWithdrawalRecipientFactory.sol @@ -9,126 +9,116 @@ import {LibClone} from "solady/utils/LibClone.sol"; /// @notice A factory contract for cheaply deploying OptimisticWithdrawalRecipient. /// @dev This contract uses token = address(0) to refer to ETH. contract OptimisticWithdrawalRecipientFactory { - /// ----------------------------------------------------------------------- - /// errors - /// ----------------------------------------------------------------------- - - /// Invalid token - error Invalid_Token(); - - /// Invalid number of recipients, must be 2 - error Invalid__Recipients(); - - /// Thresholds must be positive - error Invalid__ZeroThreshold(); - - /// Invalid threshold at `index`; must be < 2^96 - /// @param threshold threshold of too-large threshold - error Invalid__ThresholdTooLarge(uint256 threshold); - - /// ----------------------------------------------------------------------- - /// libraries - /// ----------------------------------------------------------------------- - - using LibClone for address; - - /// ----------------------------------------------------------------------- - /// events - /// ----------------------------------------------------------------------- - - /// Emitted after a new OptimisticWithdrawalRecipient module is deployed - /// @param owr Address of newly created OptimisticWithdrawalRecipient clone - /// @param token Address of ERC20 to distribute (0x0 used for ETH) - /// @param recoveryAddress Address to recover non-OWR tokens to - /// @param principalRecipient Address to distribute principal payment to - /// @param rewardRecipient Address to distribute reward payment to - /// @param threshold Absolute payment threshold for OWR first recipient - /// (reward recipient has no threshold & receives all residual flows) - event CreateOWRecipient( - address indexed owr, - address token, - address recoveryAddress, - address principalRecipient, - address rewardRecipient, - uint256 threshold - ); - - /// ----------------------------------------------------------------------- - /// storage - /// ----------------------------------------------------------------------- - - uint256 internal constant ADDRESS_BITS = 160; - uint256 internal constant RECIPIENT_SIZE = 2; - - /// OptimisticWithdrawalRecipient implementation address - OptimisticWithdrawalRecipient public immutable owrImpl; - - /// ----------------------------------------------------------------------- - /// constructor - /// ----------------------------------------------------------------------- - - constructor() { - owrImpl = new OptimisticWithdrawalRecipient(); - } - - /// ----------------------------------------------------------------------- - /// functions - /// ----------------------------------------------------------------------- - - /// ----------------------------------------------------------------------- - /// functions - public & external - /// ----------------------------------------------------------------------- - - /// Create a new OptimisticWithdrawalRecipient clone - /// @param token Address of ERC20 to distribute (0x0 used for ETH) - /// @param recoveryAddress Address to recover non-OWR tokens to - /// If this address is 0x0, recovery of unrelated tokens can be completed by - /// either the principal or reward recipients. If this address is set, only this address can recover - /// tokens (or ether) that isn't the token of the OWRecipient contract - /// @param principalRecipient Address to distribute principal payments to - /// @param rewardRecipient Address to distribute reward payments to - /// @param threshold Absolute payment threshold for principal recipient - /// (reward recipient has no threshold & receives all residual flows) - /// it cannot be greater than uint96 - /// @return owr Address of new OptimisticWithdrawalRecipient clone - function createOWRecipient( - address token, - address recoveryAddress, - address principalRecipient, - address rewardRecipient, - uint256 threshold - ) external returns (OptimisticWithdrawalRecipient owr) { - /// checks - - // ensure doesn't have address(0) - if (principalRecipient == address(0) || rewardRecipient == address(0)) { - revert Invalid__Recipients(); - } - // ensure threshold isn't zero - if (threshold == 0) { - revert Invalid__ZeroThreshold(); - } - // ensure threshold isn't too large - if (threshold > type(uint96).max) { - revert Invalid__ThresholdTooLarge(threshold); - } - - /// effects - - // copy recipients & threshold into storage - uint256[] memory tranches = new uint256[](RECIPIENT_SIZE); - // tranches size == recipients array size - tranches[0] = (threshold << ADDRESS_BITS) | uint256(uint160(principalRecipient)); - tranches[1] = uint256(uint160(rewardRecipient)); - - // would not exceed contract size limits - bytes memory data = abi.encodePacked( - token, recoveryAddress, tranches - ); - owr = OptimisticWithdrawalRecipient(address(owrImpl).clone(data)); - - emit CreateOWRecipient( - address(owr), token, recoveryAddress, principalRecipient, rewardRecipient, threshold - ); - } -} \ No newline at end of file + /// ----------------------------------------------------------------------- + /// errors + /// ----------------------------------------------------------------------- + + /// Invalid token + error Invalid_Token(); + + /// Invalid number of recipients, must be 2 + error Invalid__Recipients(); + + /// Thresholds must be positive + error Invalid__ZeroThreshold(); + + /// Invalid threshold at `index`; must be < 2^96 + /// @param threshold threshold of too-large threshold + error Invalid__ThresholdTooLarge(uint256 threshold); + + /// ----------------------------------------------------------------------- + /// libraries + /// ----------------------------------------------------------------------- + + using LibClone for address; + + /// ----------------------------------------------------------------------- + /// events + /// ----------------------------------------------------------------------- + + /// Emitted after a new OptimisticWithdrawalRecipient module is deployed + /// @param owr Address of newly created OptimisticWithdrawalRecipient clone + /// @param token Address of ERC20 to distribute (0x0 used for ETH) + /// @param recoveryAddress Address to recover non-OWR tokens to + /// @param principalRecipient Address to distribute principal payment to + /// @param rewardRecipient Address to distribute reward payment to + /// @param threshold Absolute payment threshold for OWR first recipient + /// (reward recipient has no threshold & receives all residual flows) + event CreateOWRecipient( + address indexed owr, + address token, + address recoveryAddress, + address principalRecipient, + address rewardRecipient, + uint256 threshold + ); + + /// ----------------------------------------------------------------------- + /// storage + /// ----------------------------------------------------------------------- + + uint256 internal constant ADDRESS_BITS = 160; + uint256 internal constant RECIPIENT_SIZE = 2; + + /// OptimisticWithdrawalRecipient implementation address + OptimisticWithdrawalRecipient public immutable owrImpl; + + /// ----------------------------------------------------------------------- + /// constructor + /// ----------------------------------------------------------------------- + + constructor() { + owrImpl = new OptimisticWithdrawalRecipient(); + } + + /// ----------------------------------------------------------------------- + /// functions + /// ----------------------------------------------------------------------- + + /// ----------------------------------------------------------------------- + /// functions - public & external + /// ----------------------------------------------------------------------- + + /// Create a new OptimisticWithdrawalRecipient clone + /// @param token Address of ERC20 to distribute (0x0 used for ETH) + /// @param recoveryAddress Address to recover non-OWR tokens to + /// If this address is 0x0, recovery of unrelated tokens can be completed by + /// either the principal or reward recipients. If this address is set, only this address can recover + /// tokens (or ether) that isn't the token of the OWRecipient contract + /// @param principalRecipient Address to distribute principal payments to + /// @param rewardRecipient Address to distribute reward payments to + /// @param threshold Absolute payment threshold for principal recipient + /// (reward recipient has no threshold & receives all residual flows) + /// it cannot be greater than uint96 + /// @return owr Address of new OptimisticWithdrawalRecipient clone + function createOWRecipient( + address token, + address recoveryAddress, + address principalRecipient, + address rewardRecipient, + uint256 threshold + ) external returns (OptimisticWithdrawalRecipient owr) { + /// checks + + // ensure doesn't have address(0) + if (principalRecipient == address(0) || rewardRecipient == address(0)) revert Invalid__Recipients(); + // ensure threshold isn't zero + if (threshold == 0) revert Invalid__ZeroThreshold(); + // ensure threshold isn't too large + if (threshold > type(uint96).max) revert Invalid__ThresholdTooLarge(threshold); + + /// effects + + // copy recipients & threshold into storage + uint256[] memory tranches = new uint256[](RECIPIENT_SIZE); + // tranches size == recipients array size + tranches[0] = (threshold << ADDRESS_BITS) | uint256(uint160(principalRecipient)); + tranches[1] = uint256(uint160(rewardRecipient)); + + // would not exceed contract size limits + bytes memory data = abi.encodePacked(token, recoveryAddress, tranches); + owr = OptimisticWithdrawalRecipient(address(owrImpl).clone(data)); + + emit CreateOWRecipient(address(owr), token, recoveryAddress, principalRecipient, rewardRecipient, threshold); + } +} diff --git a/src/test/lido/LIdoSplitFactory.t.sol b/src/test/lido/LIdoSplitFactory.t.sol index 9f9e9b2..b46749f 100644 --- a/src/test/lido/LIdoSplitFactory.t.sol +++ b/src/test/lido/LIdoSplitFactory.t.sol @@ -3,41 +3,37 @@ pragma solidity 0.8.17; import "forge-std/Test.sol"; import {LidoSplitFactory} from "src/lido/LidoSplitFactory.sol"; -import {ERC20} from 'solmate/tokens/ERC20.sol'; -import {LidoSplitTestHelper} from './LidoSplitTestHelper.sol'; +import {ERC20} from "solmate/tokens/ERC20.sol"; +import {LidoSplitTestHelper} from "./LidoSplitTestHelper.sol"; contract LidoSplitFactoryTest is LidoSplitTestHelper, Test { + LidoSplitFactory internal lidoSplitFactory; - LidoSplitFactory internal lidoSplitFactory; + address demoSplit; - address demoSplit; + event CreateLidoSplit(address split); - event CreateLidoSplit( - address split - ); + function setUp() public { + uint256 mainnetBlock = 17_421_005; + vm.createSelectFork(getChain("mainnet").rpcUrl, mainnetBlock); - function setUp() public { - uint256 mainnetBlock = 17421005; - vm.createSelectFork(getChain("mainnet").rpcUrl, mainnetBlock); - - lidoSplitFactory = new LidoSplitFactory( + lidoSplitFactory = new LidoSplitFactory( ERC20(STETH_MAINNET_ADDRESS), ERC20(WSTETH_MAINNET_ADDRESS) ); - demoSplit = makeAddr("demoSplit"); - } - - function testCan_CreateSplit() public { - - vm.expectEmit(true, true, true, false, address(lidoSplitFactory)); - emit CreateLidoSplit(address(0x1)); - - lidoSplitFactory.createSplit(demoSplit); - } - - function testCannot_CreateSplitInvalidAddress() public { - vm.expectRevert(LidoSplitFactory.Invalid_Wallet.selector); - lidoSplitFactory.createSplit(address(0)); - } + demoSplit = makeAddr("demoSplit"); + } + + function testCan_CreateSplit() public { + vm.expectEmit(true, true, true, false, address(lidoSplitFactory)); + emit CreateLidoSplit(address(0x1)); + + lidoSplitFactory.createSplit(demoSplit); + } + + function testCannot_CreateSplitInvalidAddress() public { + vm.expectRevert(LidoSplitFactory.Invalid_Wallet.selector); + lidoSplitFactory.createSplit(address(0)); + } } diff --git a/src/test/lido/LidoSplit.t.sol b/src/test/lido/LidoSplit.t.sol index 69dfe2f..751d086 100644 --- a/src/test/lido/LidoSplit.t.sol +++ b/src/test/lido/LidoSplit.t.sol @@ -3,59 +3,53 @@ pragma solidity 0.8.17; import "forge-std/Test.sol"; import {LidoSplitFactory, LidoSplit} from "src/lido/LidoSplitFactory.sol"; -import {ERC20} from 'solmate/tokens/ERC20.sol'; -import {LidoSplitTestHelper} from './LidoSplitTestHelper.sol'; - +import {ERC20} from "solmate/tokens/ERC20.sol"; +import {LidoSplitTestHelper} from "./LidoSplitTestHelper.sol"; contract LidoSplitTest is LidoSplitTestHelper, Test { + LidoSplitFactory internal lidoSplitFactory; + LidoSplit internal lidoSplit; - LidoSplitFactory internal lidoSplitFactory; - LidoSplit internal lidoSplit; - - address demoSplit; + address demoSplit; - function setUp() public { - uint256 mainnetBlock = 17421005; - vm.createSelectFork(getChain("mainnet").rpcUrl, mainnetBlock); + function setUp() public { + uint256 mainnetBlock = 17_421_005; + vm.createSelectFork(getChain("mainnet").rpcUrl, mainnetBlock); - lidoSplitFactory = new LidoSplitFactory( + lidoSplitFactory = new LidoSplitFactory( ERC20(STETH_MAINNET_ADDRESS), ERC20(WSTETH_MAINNET_ADDRESS) ); - demoSplit = makeAddr("demoSplit"); + demoSplit = makeAddr("demoSplit"); - lidoSplit = LidoSplit( - lidoSplitFactory.createSplit( - demoSplit - ) - ); - } + lidoSplit = LidoSplit(lidoSplitFactory.createSplit(demoSplit)); + } - function test_CloneArgsIsCorrect() public { - assertEq(lidoSplit.splitWallet(), demoSplit, "invalid address"); - assertEq(lidoSplit.stETHAddress(), STETH_MAINNET_ADDRESS, "invalid stETH address"); - assertEq(lidoSplit.wstETHAddress(), WSTETH_MAINNET_ADDRESS, "invalid wstETH address"); - } + function test_CloneArgsIsCorrect() public { + assertEq(lidoSplit.splitWallet(), demoSplit, "invalid address"); + assertEq(lidoSplit.stETHAddress(), STETH_MAINNET_ADDRESS, "invalid stETH address"); + assertEq(lidoSplit.wstETHAddress(), WSTETH_MAINNET_ADDRESS, "invalid wstETH address"); + } - function test_CanDistribute() public { - // we use a random account on Etherscan to credit the lidoSplit address - // with 10 ether worth of stETH on mainnet - vm.prank(0x2bf3937b8BcccE4B65650F122Bb3f1976B937B2f); - ERC20(STETH_MAINNET_ADDRESS).transfer(address(lidoSplit), 100 ether); + function test_CanDistribute() public { + // we use a random account on Etherscan to credit the lidoSplit address + // with 10 ether worth of stETH on mainnet + vm.prank(0x2bf3937b8BcccE4B65650F122Bb3f1976B937B2f); + ERC20(STETH_MAINNET_ADDRESS).transfer(address(lidoSplit), 100 ether); - uint256 prevBalance = ERC20(WSTETH_MAINNET_ADDRESS).balanceOf(demoSplit); + uint256 prevBalance = ERC20(WSTETH_MAINNET_ADDRESS).balanceOf(demoSplit); - uint256 amount = lidoSplit.distribute(); + uint256 amount = lidoSplit.distribute(); - assertTrue(amount > 0, "invalid amount"); + assertTrue(amount > 0, "invalid amount"); - uint256 afterBalance = ERC20(WSTETH_MAINNET_ADDRESS).balanceOf(demoSplit); + uint256 afterBalance = ERC20(WSTETH_MAINNET_ADDRESS).balanceOf(demoSplit); - console.log("checking"); - console.log(afterBalance); - console.log(prevBalance); + console.log("checking"); + console.log(afterBalance); + console.log(prevBalance); - assertGe(afterBalance, prevBalance, "after balance greater"); - } -} \ No newline at end of file + assertGe(afterBalance, prevBalance, "after balance greater"); + } +} diff --git a/src/test/lido/LidoSplitTestHelper.sol b/src/test/lido/LidoSplitTestHelper.sol index 061d686..4db029b 100644 --- a/src/test/lido/LidoSplitTestHelper.sol +++ b/src/test/lido/LidoSplitTestHelper.sol @@ -1,9 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.17; - contract LidoSplitTestHelper { - address internal STETH_MAINNET_ADDRESS = address(0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84); - address internal WSTETH_MAINNET_ADDRESS = address(0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0); - address internal RANDOM_stETH_ACCOUNT_ADDRESS = address(0x2bf3937b8BcccE4B65650F122Bb3f1976B937B2f); -} \ No newline at end of file + address internal STETH_MAINNET_ADDRESS = address(0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84); + address internal WSTETH_MAINNET_ADDRESS = address(0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0); + address internal RANDOM_stETH_ACCOUNT_ADDRESS = address(0x2bf3937b8BcccE4B65650F122Bb3f1976B937B2f); +} diff --git a/src/test/lido/integration/LidoSplitIntegrationTest.sol b/src/test/lido/integration/LidoSplitIntegrationTest.sol index 45b0082..64c656b 100644 --- a/src/test/lido/integration/LidoSplitIntegrationTest.sol +++ b/src/test/lido/integration/LidoSplitIntegrationTest.sol @@ -3,85 +3,64 @@ pragma solidity 0.8.17; import "forge-std/Test.sol"; import {LidoSplitFactory, LidoSplit} from "src/lido/LidoSplitFactory.sol"; -import {ERC20} from 'solmate/tokens/ERC20.sol'; -import {LidoSplitTestHelper} from '../LidoSplitTestHelper.sol'; +import {ERC20} from "solmate/tokens/ERC20.sol"; +import {LidoSplitTestHelper} from "../LidoSplitTestHelper.sol"; import {ISplitMain} from "src/interfaces/ISplitMain.sol"; contract LidoSplitIntegrationTest is LidoSplitTestHelper, Test { + LidoSplitFactory internal lidoSplitFactory; + LidoSplit internal lidoSplit; - LidoSplitFactory internal lidoSplitFactory; - LidoSplit internal lidoSplit; + address splitter; - address splitter; + address[] accounts; + uint32[] percentAllocations; - address[] accounts; - uint32[] percentAllocations; - - address internal SPLIT_MAIN_MAINNET = 0x2ed6c4B5dA6378c7897AC67Ba9e43102Feb694EE; + address internal SPLIT_MAIN_MAINNET = 0x2ed6c4B5dA6378c7897AC67Ba9e43102Feb694EE; - function setUp() public { - uint256 mainnetBlock = 17421005; - vm.createSelectFork(getChain("mainnet").rpcUrl, mainnetBlock); + function setUp() public { + uint256 mainnetBlock = 17_421_005; + vm.createSelectFork(getChain("mainnet").rpcUrl, mainnetBlock); - lidoSplitFactory = new LidoSplitFactory( + lidoSplitFactory = new LidoSplitFactory( ERC20(STETH_MAINNET_ADDRESS), ERC20(WSTETH_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) - ); - lidoSplit = LidoSplit( - lidoSplitFactory.createSplit( - splitter - ) - ); + accounts = new address[](2); + accounts[0] = makeAddr("accounts0"); + accounts[1] = makeAddr("accounts1"); - } + percentAllocations = new uint32[](2); + percentAllocations[0] = 400_000; + percentAllocations[1] = 600_000; - function test_CanDistribute() public { - vm.prank(RANDOM_stETH_ACCOUNT_ADDRESS); - ERC20(STETH_MAINNET_ADDRESS).transfer(address(lidoSplit), 100 ether); + splitter = ISplitMain(SPLIT_MAIN_MAINNET).createSplit(accounts, percentAllocations, 0, address(0)); - lidoSplit.distribute(); + lidoSplit = LidoSplit(lidoSplitFactory.createSplit(splitter)); + } - ISplitMain(SPLIT_MAIN_MAINNET).distributeERC20( - splitter, - ERC20(WSTETH_MAINNET_ADDRESS), - accounts, - percentAllocations, - 0, - address(0) - ); + function test_CanDistribute() public { + vm.prank(RANDOM_stETH_ACCOUNT_ADDRESS); + ERC20(STETH_MAINNET_ADDRESS).transfer(address(lidoSplit), 100 ether); - ERC20[] memory tokens = new ERC20[](1); - tokens[0] = ERC20(WSTETH_MAINNET_ADDRESS); + lidoSplit.distribute(); - ISplitMain(SPLIT_MAIN_MAINNET).withdraw(accounts[0], 0, tokens); - ISplitMain(SPLIT_MAIN_MAINNET).withdraw(accounts[1], 0, tokens); + ISplitMain(SPLIT_MAIN_MAINNET).distributeERC20( + splitter, ERC20(WSTETH_MAINNET_ADDRESS), accounts, percentAllocations, 0, address(0) + ); - assertEq( - ERC20(WSTETH_MAINNET_ADDRESS).balanceOf(accounts[0]), - 35483996363190140092, - "invalid account 0 balance" - ); - assertEq( - ERC20(WSTETH_MAINNET_ADDRESS).balanceOf(accounts[1]), - 53225994544785210138, - "invalid account 1 balance" - ); - } -} + ERC20[] memory tokens = new ERC20[](1); + tokens[0] = ERC20(WSTETH_MAINNET_ADDRESS); + ISplitMain(SPLIT_MAIN_MAINNET).withdraw(accounts[0], 0, tokens); + ISplitMain(SPLIT_MAIN_MAINNET).withdraw(accounts[1], 0, tokens); + + assertEq( + ERC20(WSTETH_MAINNET_ADDRESS).balanceOf(accounts[0]), 35_483_996_363_190_140_092, "invalid account 0 balance" + ); + assertEq( + ERC20(WSTETH_MAINNET_ADDRESS).balanceOf(accounts[1]), 53_225_994_544_785_210_138, "invalid account 1 balance" + ); + } +} diff --git a/src/test/waterfall/OWRReentrancy.sol b/src/test/waterfall/OWRReentrancy.sol index 0ca4b12..4fded65 100644 --- a/src/test/waterfall/OWRReentrancy.sol +++ b/src/test/waterfall/OWRReentrancy.sol @@ -1,12 +1,11 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity ^0.8.17; + import "forge-std/Test.sol"; import {OptimisticWithdrawalRecipient} from "src/owr/OptimisticWithdrawalRecipient.sol"; contract OWRReentrancy is Test { - receive() external payable { - if (address(this).balance <= 1 ether) { - OptimisticWithdrawalRecipient(msg.sender).distributeFunds(); - } - } + receive() external payable { + if (address(this).balance <= 1 ether) OptimisticWithdrawalRecipient(msg.sender).distributeFunds(); + } } diff --git a/src/test/waterfall/OWRTestHelper.t.sol b/src/test/waterfall/OWRTestHelper.t.sol index 0ffe36c..2b66a09 100644 --- a/src/test/waterfall/OWRTestHelper.t.sol +++ b/src/test/waterfall/OWRTestHelper.t.sol @@ -2,50 +2,40 @@ pragma solidity 0.8.17; contract OWRTestHelper { + address internal constant ETH_ADDRESS = address(0); - address internal constant ETH_ADDRESS = address(0); - - uint256 internal constant MAX_TRANCHE_SIZE = 2; - - uint256 internal constant ETH_STAKE = 32 ether; - - uint256 internal constant BALANCE_CLASSIFICATION_THRESHOLD = 16 ether; - - /// ----------------------------------------------------------------------- - /// helper fns - /// ----------------------------------------------------------------------- - - function generateTranches(uint256 rSeed, uint256 tSeed) - internal - pure - returns (address principal, address reward, uint256 threshold) - { - (principal, reward) = generateTrancheRecipients(rSeed); - threshold = generateTrancheThreshold(tSeed); - } - - function generateTrancheRecipients(uint256 _seed) - internal - pure - returns (address principal, address reward) - { - bytes32 seed = bytes32(_seed); - - seed = keccak256(abi.encodePacked(seed)); - principal = address(bytes20(seed)); - - seed = keccak256(abi.encodePacked(seed)); - reward = address(bytes20(seed)); - } - - function generateTrancheThreshold(uint256 _seed) - internal - pure - returns (uint256 threshold) - { - uint256 seed = _seed; - seed = uint256(keccak256(abi.encodePacked(seed))); - threshold = uint96(seed); - } - -} \ No newline at end of file + uint256 internal constant MAX_TRANCHE_SIZE = 2; + + uint256 internal constant ETH_STAKE = 32 ether; + + uint256 internal constant BALANCE_CLASSIFICATION_THRESHOLD = 16 ether; + + /// ----------------------------------------------------------------------- + /// helper fns + /// ----------------------------------------------------------------------- + + function generateTranches(uint256 rSeed, uint256 tSeed) + internal + pure + returns (address principal, address reward, uint256 threshold) + { + (principal, reward) = generateTrancheRecipients(rSeed); + threshold = generateTrancheThreshold(tSeed); + } + + function generateTrancheRecipients(uint256 _seed) internal pure returns (address principal, address reward) { + bytes32 seed = bytes32(_seed); + + seed = keccak256(abi.encodePacked(seed)); + principal = address(bytes20(seed)); + + seed = keccak256(abi.encodePacked(seed)); + reward = address(bytes20(seed)); + } + + function generateTrancheThreshold(uint256 _seed) internal pure returns (uint256 threshold) { + uint256 seed = _seed; + seed = uint256(keccak256(abi.encodePacked(seed))); + threshold = uint96(seed); + } +} diff --git a/src/test/waterfall/OptimisticWithdrawalRecipient.t.sol b/src/test/waterfall/OptimisticWithdrawalRecipient.t.sol index 5d195b1..246ac7b 100644 --- a/src/test/waterfall/OptimisticWithdrawalRecipient.t.sol +++ b/src/test/waterfall/OptimisticWithdrawalRecipient.t.sol @@ -2,930 +2,807 @@ pragma solidity 0.8.17; import "forge-std/Test.sol"; -import { OptimisticWithdrawalRecipient } from "src/owr/OptimisticWithdrawalRecipient.sol"; -import { OptimisticWithdrawalRecipientFactory } from "src/owr/OptimisticWithdrawalRecipientFactory.sol"; +import {OptimisticWithdrawalRecipient} from "src/owr/OptimisticWithdrawalRecipient.sol"; +import {OptimisticWithdrawalRecipientFactory} from "src/owr/OptimisticWithdrawalRecipientFactory.sol"; import {MockERC20} from "../utils/mocks/MockERC20.sol"; import {SafeTransferLib} from "solady/utils/SafeTransferLib.sol"; import {OWRReentrancy} from "./OWRReentrancy.sol"; import {OWRTestHelper} from "./OWRTestHelper.t.sol"; contract OptimisticWithdrawalRecipientTest is OWRTestHelper, Test { - using SafeTransferLib for address; + using SafeTransferLib for address; - event ReceiveETH(uint256 amount); + event ReceiveETH(uint256 amount); - event DistributeFunds( - uint256[] payouts, uint256 pullFlowFlag - ); - - event RecoverNonOWRecipientFunds( - address nonOWRToken, address recipient, uint256 amount - ); - - OptimisticWithdrawalRecipient public owrModule; - OptimisticWithdrawalRecipientFactory public owrFactory; - address internal recoveryAddress; - - OptimisticWithdrawalRecipient owrETH; - OptimisticWithdrawalRecipient owrERC20; - OptimisticWithdrawalRecipient owrETH_OR; - OptimisticWithdrawalRecipient owrERC20_OR; - MockERC20 mERC20; - - address public principalRecipient; - address public rewardRecipient; - uint256 internal trancheThreshold; - - function setUp() public { - owrFactory = new OptimisticWithdrawalRecipientFactory(); - owrModule = owrFactory.owrImpl(); - - mERC20 = new MockERC20("demo", "DMT", 18); - mERC20.mint(type(uint256).max); - - (principalRecipient, rewardRecipient) = generateTrancheRecipients(uint256(uint160(makeAddr("tranche")))); - // use 1 validator as default tranche threshold - trancheThreshold = ETH_STAKE; - - recoveryAddress = makeAddr("recoveryAddress"); - - owrETH = owrFactory.createOWRecipient( - ETH_ADDRESS, - recoveryAddress, - principalRecipient, rewardRecipient, - trancheThreshold - ); - - owrERC20 = owrFactory.createOWRecipient( - address(mERC20), - recoveryAddress, - principalRecipient, rewardRecipient, - trancheThreshold - ); - - owrETH_OR = owrFactory.createOWRecipient( - ETH_ADDRESS, address(0), principalRecipient, rewardRecipient, trancheThreshold - ); - owrERC20_OR = owrFactory.createOWRecipient( - address(mERC20), address(0), principalRecipient, rewardRecipient, trancheThreshold - ); - } - - function testGetTranches() public { - // eth - (address[] memory recipients, uint256 wtrancheThreshold) - = owrETH.getTranches(); - - assertEq(recipients[0], principalRecipient, "invalid principal recipient"); - assertEq(recipients[1], rewardRecipient, "invalid reward recipient"); - assertEq(wtrancheThreshold, ETH_STAKE, "invalid eth tranche threshold"); - - // erc20 - (recipients, wtrancheThreshold) = owrERC20.getTranches(); - - assertEq(recipients[0], principalRecipient, "invalid erc20 principal recipient"); - assertEq(recipients[1], rewardRecipient, "invalid erc20 reward recipient"); - assertEq(wtrancheThreshold, ETH_STAKE, "invalid erc20 tranche threshold"); - } - - function testReceiveETH() public { - address(owrETH).safeTransferETH(1 ether); - assertEq(address(owrETH).balance, 1 ether); - - address(owrERC20).safeTransferETH(1 ether); - assertEq(address(owrERC20).balance, 1 ether); - } - - function testReceiveTransfer() public { - payable(address(owrETH)).transfer(1 ether); - assertEq(address(owrETH).balance, 1 ether); + event DistributeFunds(uint256[] payouts, uint256 pullFlowFlag); - payable(address(owrERC20)).transfer(1 ether); - assertEq(address(owrERC20).balance, 1 ether); - } - - function testEmitOnReceiveETH() public { - vm.expectEmit(true, true, true, true); - emit ReceiveETH(1 ether); - - address(owrETH).safeTransferETH(1 ether); - } - - function testReceiveERC20() public { - address(mERC20).safeTransfer(address(owrETH), 1 ether); - assertEq(mERC20.balanceOf(address(owrETH)), 1 ether); - - address(mERC20).safeTransfer(address(owrERC20), 1 ether); - assertEq(mERC20.balanceOf(address(owrERC20)), 1 ether); - } - - function testCan_recoverNonOWRFundsToRecipient() public { - address(owrETH).safeTransferETH(1 ether); - address(mERC20).safeTransfer(address(owrETH), 1 ether); - address(owrETH_OR).safeTransferETH(1 ether); - address(mERC20).safeTransfer(address(owrETH_OR), 1 ether); - - vm.expectEmit(true, true, true, true); - emit RecoverNonOWRecipientFunds( - address(mERC20), recoveryAddress, 1 ether - ); - owrETH.recoverFunds(address(mERC20), recoveryAddress); - assertEq(address(owrETH).balance, 1 ether); - assertEq(mERC20.balanceOf(address(owrETH)), 0 ether); - assertEq(mERC20.balanceOf(recoveryAddress), 1 ether); - - vm.expectEmit(true, true, true, true); - emit RecoverNonOWRecipientFunds( - address(mERC20), principalRecipient, 1 ether - ); - owrETH_OR.recoverFunds(address(mERC20), principalRecipient); - assertEq(address(owrETH_OR).balance, 1 ether); - assertEq(mERC20.balanceOf(address(owrETH_OR)), 0 ether); - assertEq(mERC20.balanceOf(principalRecipient), 1 ether); - - address(mERC20).safeTransfer(address(owrETH_OR), 1 ether); - - vm.expectEmit(true, true, true, true); - emit RecoverNonOWRecipientFunds( - address(mERC20), rewardRecipient, 1 ether - ); - owrETH_OR.recoverFunds(address(mERC20), rewardRecipient); - assertEq(address(owrETH_OR).balance, 1 ether); - assertEq(mERC20.balanceOf(address(owrETH_OR)), 0 ether); - assertEq(mERC20.balanceOf(rewardRecipient), 1 ether); - - - address(owrERC20).safeTransferETH(1 ether); - address(mERC20).safeTransfer(address(owrERC20), 1 ether); - - - vm.expectEmit(true, true, true, true); - emit RecoverNonOWRecipientFunds( - ETH_ADDRESS, recoveryAddress, 1 ether - ); - owrERC20.recoverFunds(ETH_ADDRESS, recoveryAddress); - assertEq(mERC20.balanceOf(address(owrERC20)), 1 ether); - assertEq(address(owrERC20).balance, 0 ether); - assertEq(recoveryAddress.balance, 1 ether); + event RecoverNonOWRecipientFunds(address nonOWRToken, address recipient, uint256 amount); + OptimisticWithdrawalRecipient public owrModule; + OptimisticWithdrawalRecipientFactory public owrFactory; + address internal recoveryAddress; - address(owrERC20_OR).safeTransferETH(1 ether); - address(mERC20).safeTransfer(address(owrERC20_OR), 1 ether); + OptimisticWithdrawalRecipient owrETH; + OptimisticWithdrawalRecipient owrERC20; + OptimisticWithdrawalRecipient owrETH_OR; + OptimisticWithdrawalRecipient owrERC20_OR; + MockERC20 mERC20; - vm.expectEmit(true, true, true, true); - emit RecoverNonOWRecipientFunds( - ETH_ADDRESS, principalRecipient, 1 ether - ); - owrERC20_OR.recoverFunds(ETH_ADDRESS, principalRecipient); - assertEq(mERC20.balanceOf(address(owrERC20_OR)), 1 ether); - assertEq(address(owrERC20_OR).balance, 0 ether); - assertEq(principalRecipient.balance, 1 ether); - - address(owrERC20_OR).safeTransferETH(1 ether); + address public principalRecipient; + address public rewardRecipient; + uint256 internal trancheThreshold; - owrERC20_OR.recoverFunds(ETH_ADDRESS, rewardRecipient); - assertEq(mERC20.balanceOf(address(owrERC20_OR)), 1 ether); - assertEq(address(owrERC20_OR).balance, 0 ether, "invalid erc20 balance"); - assertEq(rewardRecipient.balance, 1 ether, "invalid eth balance"); - } + function setUp() public { + owrFactory = new OptimisticWithdrawalRecipientFactory(); + owrModule = owrFactory.owrImpl(); - function testCannot_recoverFundsToNonRecipient() public { - vm.expectRevert( - OptimisticWithdrawalRecipient.InvalidTokenRecovery_InvalidRecipient.selector - ); - owrETH.recoverFunds(address(mERC20), address(1)); + mERC20 = new MockERC20("demo", "DMT", 18); + mERC20.mint(type(uint256).max); - vm.expectRevert( - OptimisticWithdrawalRecipient.InvalidTokenRecovery_InvalidRecipient.selector - ); - owrERC20_OR.recoverFunds(ETH_ADDRESS, address(1)); + (principalRecipient, rewardRecipient) = generateTrancheRecipients(uint256(uint160(makeAddr("tranche")))); + // use 1 validator as default tranche threshold + trancheThreshold = ETH_STAKE; - vm.expectRevert( - OptimisticWithdrawalRecipient.InvalidTokenRecovery_InvalidRecipient.selector - ); - owrETH_OR.recoverFunds(address(mERC20), address(2)); + recoveryAddress = makeAddr("recoveryAddress"); - vm.expectRevert( - OptimisticWithdrawalRecipient.InvalidTokenRecovery_InvalidRecipient.selector - ); - owrERC20_OR.recoverFunds(ETH_ADDRESS, address(2)); - } + owrETH = + owrFactory.createOWRecipient(ETH_ADDRESS, recoveryAddress, principalRecipient, rewardRecipient, trancheThreshold); - function testCannot_recoverOWRFunds() public { - vm.expectRevert( - OptimisticWithdrawalRecipient.InvalidTokenRecovery_OWRToken.selector - ); - owrETH.recoverFunds(ETH_ADDRESS, recoveryAddress); - - vm.expectRevert( - OptimisticWithdrawalRecipient.InvalidTokenRecovery_OWRToken.selector - ); - owrERC20_OR.recoverFunds(address(mERC20), recoveryAddress); - - vm.expectRevert( - OptimisticWithdrawalRecipient.InvalidTokenRecovery_OWRToken.selector - ); - owrETH_OR.recoverFunds(ETH_ADDRESS, address(1)); + owrERC20 = owrFactory.createOWRecipient( + address(mERC20), recoveryAddress, principalRecipient, rewardRecipient, trancheThreshold + ); - vm.expectRevert( - OptimisticWithdrawalRecipient.InvalidTokenRecovery_OWRToken.selector - ); - owrERC20_OR.recoverFunds(address(mERC20), address(1)); - } + owrETH_OR = + owrFactory.createOWRecipient(ETH_ADDRESS, address(0), principalRecipient, rewardRecipient, trancheThreshold); + owrERC20_OR = + owrFactory.createOWRecipient(address(mERC20), address(0), principalRecipient, rewardRecipient, trancheThreshold); + } + + function testGetTranches() public { + // eth + (address[] memory recipients, uint256 wtrancheThreshold) = owrETH.getTranches(); + + assertEq(recipients[0], principalRecipient, "invalid principal recipient"); + assertEq(recipients[1], rewardRecipient, "invalid reward recipient"); + assertEq(wtrancheThreshold, ETH_STAKE, "invalid eth tranche threshold"); + + // erc20 + (recipients, wtrancheThreshold) = owrERC20.getTranches(); + + assertEq(recipients[0], principalRecipient, "invalid erc20 principal recipient"); + assertEq(recipients[1], rewardRecipient, "invalid erc20 reward recipient"); + assertEq(wtrancheThreshold, ETH_STAKE, "invalid erc20 tranche threshold"); + } + + function testReceiveETH() public { + address(owrETH).safeTransferETH(1 ether); + assertEq(address(owrETH).balance, 1 ether); + + address(owrERC20).safeTransferETH(1 ether); + assertEq(address(owrERC20).balance, 1 ether); + } + + function testReceiveTransfer() public { + payable(address(owrETH)).transfer(1 ether); + assertEq(address(owrETH).balance, 1 ether); + + payable(address(owrERC20)).transfer(1 ether); + assertEq(address(owrERC20).balance, 1 ether); + } + + function testEmitOnReceiveETH() public { + vm.expectEmit(true, true, true, true); + emit ReceiveETH(1 ether); + + address(owrETH).safeTransferETH(1 ether); + } + + function testReceiveERC20() public { + address(mERC20).safeTransfer(address(owrETH), 1 ether); + assertEq(mERC20.balanceOf(address(owrETH)), 1 ether); + + address(mERC20).safeTransfer(address(owrERC20), 1 ether); + assertEq(mERC20.balanceOf(address(owrERC20)), 1 ether); + } + + function testCan_recoverNonOWRFundsToRecipient() public { + address(owrETH).safeTransferETH(1 ether); + address(mERC20).safeTransfer(address(owrETH), 1 ether); + address(owrETH_OR).safeTransferETH(1 ether); + address(mERC20).safeTransfer(address(owrETH_OR), 1 ether); + + vm.expectEmit(true, true, true, true); + emit RecoverNonOWRecipientFunds(address(mERC20), recoveryAddress, 1 ether); + owrETH.recoverFunds(address(mERC20), recoveryAddress); + assertEq(address(owrETH).balance, 1 ether); + assertEq(mERC20.balanceOf(address(owrETH)), 0 ether); + assertEq(mERC20.balanceOf(recoveryAddress), 1 ether); + + vm.expectEmit(true, true, true, true); + emit RecoverNonOWRecipientFunds(address(mERC20), principalRecipient, 1 ether); + owrETH_OR.recoverFunds(address(mERC20), principalRecipient); + assertEq(address(owrETH_OR).balance, 1 ether); + assertEq(mERC20.balanceOf(address(owrETH_OR)), 0 ether); + assertEq(mERC20.balanceOf(principalRecipient), 1 ether); + + address(mERC20).safeTransfer(address(owrETH_OR), 1 ether); + + vm.expectEmit(true, true, true, true); + emit RecoverNonOWRecipientFunds(address(mERC20), rewardRecipient, 1 ether); + owrETH_OR.recoverFunds(address(mERC20), rewardRecipient); + assertEq(address(owrETH_OR).balance, 1 ether); + assertEq(mERC20.balanceOf(address(owrETH_OR)), 0 ether); + assertEq(mERC20.balanceOf(rewardRecipient), 1 ether); + + address(owrERC20).safeTransferETH(1 ether); + address(mERC20).safeTransfer(address(owrERC20), 1 ether); + + vm.expectEmit(true, true, true, true); + emit RecoverNonOWRecipientFunds(ETH_ADDRESS, recoveryAddress, 1 ether); + owrERC20.recoverFunds(ETH_ADDRESS, recoveryAddress); + assertEq(mERC20.balanceOf(address(owrERC20)), 1 ether); + assertEq(address(owrERC20).balance, 0 ether); + assertEq(recoveryAddress.balance, 1 ether); + + address(owrERC20_OR).safeTransferETH(1 ether); + address(mERC20).safeTransfer(address(owrERC20_OR), 1 ether); + + vm.expectEmit(true, true, true, true); + emit RecoverNonOWRecipientFunds(ETH_ADDRESS, principalRecipient, 1 ether); + owrERC20_OR.recoverFunds(ETH_ADDRESS, principalRecipient); + assertEq(mERC20.balanceOf(address(owrERC20_OR)), 1 ether); + assertEq(address(owrERC20_OR).balance, 0 ether); + assertEq(principalRecipient.balance, 1 ether); + + address(owrERC20_OR).safeTransferETH(1 ether); + + owrERC20_OR.recoverFunds(ETH_ADDRESS, rewardRecipient); + assertEq(mERC20.balanceOf(address(owrERC20_OR)), 1 ether); + assertEq(address(owrERC20_OR).balance, 0 ether, "invalid erc20 balance"); + assertEq(rewardRecipient.balance, 1 ether, "invalid eth balance"); + } + + function testCannot_recoverFundsToNonRecipient() public { + vm.expectRevert(OptimisticWithdrawalRecipient.InvalidTokenRecovery_InvalidRecipient.selector); + owrETH.recoverFunds(address(mERC20), address(1)); + + vm.expectRevert(OptimisticWithdrawalRecipient.InvalidTokenRecovery_InvalidRecipient.selector); + owrERC20_OR.recoverFunds(ETH_ADDRESS, address(1)); + + vm.expectRevert(OptimisticWithdrawalRecipient.InvalidTokenRecovery_InvalidRecipient.selector); + owrETH_OR.recoverFunds(address(mERC20), address(2)); + + vm.expectRevert(OptimisticWithdrawalRecipient.InvalidTokenRecovery_InvalidRecipient.selector); + owrERC20_OR.recoverFunds(ETH_ADDRESS, address(2)); + } + + function testCannot_recoverOWRFunds() public { + vm.expectRevert(OptimisticWithdrawalRecipient.InvalidTokenRecovery_OWRToken.selector); + owrETH.recoverFunds(ETH_ADDRESS, recoveryAddress); + + vm.expectRevert(OptimisticWithdrawalRecipient.InvalidTokenRecovery_OWRToken.selector); + owrERC20_OR.recoverFunds(address(mERC20), recoveryAddress); + + vm.expectRevert(OptimisticWithdrawalRecipient.InvalidTokenRecovery_OWRToken.selector); + owrETH_OR.recoverFunds(ETH_ADDRESS, address(1)); + + vm.expectRevert(OptimisticWithdrawalRecipient.InvalidTokenRecovery_OWRToken.selector); + owrERC20_OR.recoverFunds(address(mERC20), address(1)); + } + + function testCan_OWRIsPayable() public { + owrETH.distributeFunds{value: 2 ether}(); + + assertEq(address(owrETH).balance, 0 ether); + assertEq(principalRecipient.balance, 0); + assertEq(rewardRecipient.balance, 2 ether); + } + + function testCan_distributeToNoRecipients() public { + owrETH.distributeFunds(); + assertEq(principalRecipient.balance, 0 ether); + + owrERC20_OR.distributeFunds(); + assertEq(mERC20.balanceOf(principalRecipient), 0 ether); + } + + function testCan_emitOnDistributeToNoRecipients() public { + uint256[] memory payouts = new uint256[](2); + payouts[0] = 0 ether; + payouts[1] = 0 ether; + + vm.expectEmit(true, true, true, true); + emit DistributeFunds(payouts, 0); + owrETH.distributeFunds(); + } + + function testCan_distributeToSecondRecipient() public { + address(owrETH).safeTransferETH(1 ether); + + uint256[] memory payouts = new uint256[](2); + payouts[1] = 1 ether; + + vm.expectEmit(true, true, true, true); + emit DistributeFunds(payouts, 0); + owrETH.distributeFunds(); + assertEq(address(owrETH).balance, 0 ether); + assertEq(rewardRecipient.balance, 1 ether); + + payouts[1] = 0; + vm.expectEmit(true, true, true, true); + emit DistributeFunds(payouts, 0); + owrETH.distributeFunds(); + assertEq(address(owrETH).balance, 0 ether); + assertEq(principalRecipient.balance, 0 ether); + assertEq(rewardRecipient.balance, 1 ether); + + address(mERC20).safeTransfer(address(owrERC20_OR), 1 ether); + + payouts[1] = 1 ether; + vm.expectEmit(true, true, true, true); + emit DistributeFunds(payouts, 0); + owrERC20_OR.distributeFunds(); + assertEq(mERC20.balanceOf(address(owrERC20_OR)), 0 ether); + assertEq(mERC20.balanceOf(rewardRecipient), 1 ether); + + payouts[1] = 0; + vm.expectEmit(true, true, true, true); + emit DistributeFunds(payouts, 0); + owrERC20_OR.distributeFunds(); + assertEq(mERC20.balanceOf(address(owrERC20_OR)), 0 ether); + assertEq(principalRecipient.balance, 0 ether); + assertEq(rewardRecipient.balance, 1 ether); + } + + function testCan_distributeMultipleDepositsToRewardRecipient() public { + address(owrETH).safeTransferETH(0.5 ether); + owrETH.distributeFunds(); + assertEq(address(owrETH).balance, 0 ether); + assertEq(rewardRecipient.balance, 0.5 ether); + + address(owrETH).safeTransferETH(0.5 ether); + owrETH.distributeFunds(); + assertEq(address(owrETH).balance, 0 ether); + assertEq(rewardRecipient.balance, 1 ether); + + address(mERC20).safeTransfer(address(owrERC20_OR), 0.5 ether); + owrERC20_OR.distributeFunds(); + assertEq(mERC20.balanceOf(address(owrERC20_OR)), 0 ether); + assertEq(mERC20.balanceOf(rewardRecipient), 0.5 ether); + + address(mERC20).safeTransfer(address(owrERC20_OR), 0.5 ether); + owrERC20_OR.distributeFunds(); + assertEq(mERC20.balanceOf(address(owrERC20_OR)), 0 ether); + assertEq(mERC20.balanceOf(rewardRecipient), 1 ether); + } + + function testCan_distributeToBothRecipients() public { + address(owrETH).safeTransferETH(36 ether); + + uint256[] memory payouts = new uint256[](2); + payouts[0] = 32 ether; + payouts[1] = 4 ether; + + vm.expectEmit(true, true, true, true); + emit DistributeFunds(payouts, 0); + owrETH.distributeFunds(); + assertEq(address(owrETH).balance, 0 ether); + assertEq(principalRecipient.balance, 32 ether); + assertEq(rewardRecipient.balance, 4 ether); + + address(mERC20).safeTransfer(address(owrERC20_OR), 36 ether); + + vm.expectEmit(true, true, true, true); + emit DistributeFunds(payouts, 0); + owrERC20_OR.distributeFunds(); + assertEq(mERC20.balanceOf(address(owrERC20_OR)), 0 ether); + assertEq(principalRecipient.balance, 32 ether); + assertEq(rewardRecipient.balance, 4 ether); + } + + function testCan_distributeMultipleDepositsToPrincipalRecipient() public { + address(owrETH).safeTransferETH(16 ether); + owrETH.distributeFunds(); + + address(owrETH).safeTransferETH(16 ether); + owrETH.distributeFunds(); + + assertEq(address(owrETH).balance, 0 ether); + assertEq(principalRecipient.balance, 32 ether); + assertEq(rewardRecipient.balance, 0 ether); - function testCan_OWRIsPayable() public { - owrETH.distributeFunds{value: 2 ether}(); + address(mERC20).safeTransfer(address(owrERC20_OR), 16 ether); + owrERC20_OR.distributeFunds(); - assertEq(address(owrETH).balance, 0 ether); - assertEq(principalRecipient.balance, 0); - assertEq(rewardRecipient.balance, 2 ether); - } + address(mERC20).safeTransfer(address(owrERC20_OR), 16 ether); + owrERC20_OR.distributeFunds(); - function testCan_distributeToNoRecipients() public { - owrETH.distributeFunds(); - assertEq(principalRecipient.balance, 0 ether); + assertEq(mERC20.balanceOf(address(owrERC20_OR)), 0 ether); + assertEq(mERC20.balanceOf(principalRecipient), 32 ether); + assertEq(mERC20.balanceOf(rewardRecipient), 0); + } - owrERC20_OR.distributeFunds(); - assertEq(mERC20.balanceOf(principalRecipient), 0 ether); - } + function testCannot_distributeTooMuch() public { + vm.deal(address(owrETH), type(uint128).max); + owrETH.distributeFunds(); + vm.deal(address(owrETH), 1); - function testCan_emitOnDistributeToNoRecipients() public { - uint256[] memory payouts = new uint256[](2); - payouts[0] = 0 ether; - payouts[1] = 0 ether; + vm.expectRevert(OptimisticWithdrawalRecipient.InvalidDistribution_TooLarge.selector); + owrETH.distributeFunds(); - vm.expectEmit(true, true, true, true); - emit DistributeFunds(payouts, 0); - owrETH.distributeFunds(); - } + vm.expectRevert(OptimisticWithdrawalRecipient.InvalidDistribution_TooLarge.selector); + owrETH.distributeFundsPull(); - function testCan_distributeToSecondRecipient() public { - address(owrETH).safeTransferETH(1 ether); - - uint256[] memory payouts = new uint256[](2); - payouts[1] = 1 ether; - - vm.expectEmit(true, true, true, true); - emit DistributeFunds(payouts, 0); - owrETH.distributeFunds(); - assertEq(address(owrETH).balance, 0 ether); - assertEq(rewardRecipient.balance, 1 ether); - - payouts[1] = 0; - vm.expectEmit(true, true, true, true); - emit DistributeFunds(payouts, 0); - owrETH.distributeFunds(); - assertEq(address(owrETH).balance, 0 ether); - assertEq(principalRecipient.balance, 0 ether); - assertEq(rewardRecipient.balance, 1 ether); - - address(mERC20).safeTransfer(address(owrERC20_OR), 1 ether); - - payouts[1] = 1 ether; - vm.expectEmit(true, true, true, true); - emit DistributeFunds(payouts, 0); - owrERC20_OR.distributeFunds(); - assertEq(mERC20.balanceOf(address(owrERC20_OR)), 0 ether); - assertEq(mERC20.balanceOf(rewardRecipient), 1 ether); - - payouts[1] = 0; - vm.expectEmit(true, true, true, true); - emit DistributeFunds(payouts, 0); - owrERC20_OR.distributeFunds(); - assertEq(mERC20.balanceOf(address(owrERC20_OR)), 0 ether); - assertEq(principalRecipient.balance, 0 ether); - assertEq(rewardRecipient.balance, 1 ether); - } + address(mERC20).safeTransfer(address(owrERC20_OR), type(uint128).max); + owrERC20_OR.distributeFunds(); + address(mERC20).safeTransfer(address(owrERC20_OR), 1); - function testCan_distributeMultipleDepositsToRewardRecipient() public { - address(owrETH).safeTransferETH(0.5 ether); - owrETH.distributeFunds(); - assertEq(address(owrETH).balance, 0 ether); - assertEq(rewardRecipient.balance, 0.5 ether); - - address(owrETH).safeTransferETH(0.5 ether); - owrETH.distributeFunds(); - assertEq(address(owrETH).balance, 0 ether); - assertEq(rewardRecipient.balance, 1 ether); - - address(mERC20).safeTransfer(address(owrERC20_OR), 0.5 ether); - owrERC20_OR.distributeFunds(); - assertEq(mERC20.balanceOf(address(owrERC20_OR)), 0 ether); - assertEq(mERC20.balanceOf(rewardRecipient), 0.5 ether); - - address(mERC20).safeTransfer(address(owrERC20_OR), 0.5 ether); - owrERC20_OR.distributeFunds(); - assertEq(mERC20.balanceOf(address(owrERC20_OR)), 0 ether); - assertEq(mERC20.balanceOf(rewardRecipient), 1 ether); - } + vm.expectRevert(OptimisticWithdrawalRecipient.InvalidDistribution_TooLarge.selector); + owrERC20_OR.distributeFunds(); - function testCan_distributeToBothRecipients() public { - address(owrETH).safeTransferETH(36 ether); - - uint256[] memory payouts = new uint256[](2); - payouts[0] = 32 ether; - payouts[1] = 4 ether; - - vm.expectEmit(true, true, true, true); - emit DistributeFunds(payouts, 0); - owrETH.distributeFunds(); - assertEq(address(owrETH).balance, 0 ether); - assertEq(principalRecipient.balance, 32 ether); - assertEq(rewardRecipient.balance, 4 ether); - - address(mERC20).safeTransfer(address(owrERC20_OR), 36 ether); - - vm.expectEmit(true, true, true, true); - emit DistributeFunds(payouts, 0); - owrERC20_OR.distributeFunds(); - assertEq(mERC20.balanceOf(address(owrERC20_OR)), 0 ether); - assertEq(principalRecipient.balance, 32 ether); - assertEq(rewardRecipient.balance, 4 ether); - } + vm.expectRevert(OptimisticWithdrawalRecipient.InvalidDistribution_TooLarge.selector); + owrERC20_OR.distributeFundsPull(); + } - function testCan_distributeMultipleDepositsToPrincipalRecipient() public { - address(owrETH).safeTransferETH(16 ether); - owrETH.distributeFunds(); + function testCannot_reenterOWR() public { + OWRReentrancy wr = new OWRReentrancy(); - address(owrETH).safeTransferETH(16 ether); - owrETH.distributeFunds(); + owrETH = owrFactory.createOWRecipient(ETH_ADDRESS, recoveryAddress, address(wr), rewardRecipient, 1 ether); + address(owrETH).safeTransferETH(33 ether); - assertEq(address(owrETH).balance, 0 ether); - assertEq(principalRecipient.balance, 32 ether); - assertEq(rewardRecipient.balance, 0 ether); + vm.expectRevert(SafeTransferLib.ETHTransferFailed.selector); + owrETH.distributeFunds(); - address(mERC20).safeTransfer(address(owrERC20_OR), 16 ether); - owrERC20_OR.distributeFunds(); + assertEq(address(owrETH).balance, 33 ether); + assertEq(address(wr).balance, 0 ether); + assertEq(address(0).balance, 0 ether); + } - address(mERC20).safeTransfer(address(owrERC20_OR), 16 ether); - owrERC20_OR.distributeFunds(); + function testCan_distributeToPullFlow() public { + // test eth + address(owrETH).safeTransferETH(36 ether); + owrETH.distributeFundsPull(); - assertEq(mERC20.balanceOf(address(owrERC20_OR)), 0 ether); - assertEq(mERC20.balanceOf(principalRecipient), 32 ether); - assertEq(mERC20.balanceOf(rewardRecipient), 0); - } + assertEq(address(owrETH).balance, 36 ether); + assertEq(principalRecipient.balance, 0 ether); + assertEq(rewardRecipient.balance, 0 ether); - function testCannot_distributeTooMuch() public { - vm.deal(address(owrETH), type(uint128).max); - owrETH.distributeFunds(); - vm.deal(address(owrETH), 1); + assertEq(owrETH.getPullBalance(principalRecipient), 32 ether); + assertEq(owrETH.getPullBalance(rewardRecipient), 4 ether); - vm.expectRevert(OptimisticWithdrawalRecipient.InvalidDistribution_TooLarge.selector); - owrETH.distributeFunds(); + assertEq(owrETH.distributedFunds(), 36 ether); + assertEq(owrETH.fundsPendingWithdrawal(), 36 ether); - vm.expectRevert(OptimisticWithdrawalRecipient.InvalidDistribution_TooLarge.selector); - owrETH.distributeFundsPull(); + owrETH.withdraw(rewardRecipient); - address(mERC20).safeTransfer(address(owrERC20_OR), type(uint128).max); - owrERC20_OR.distributeFunds(); - address(mERC20).safeTransfer(address(owrERC20_OR), 1); + assertEq(address(owrETH).balance, 32 ether); + assertEq(principalRecipient.balance, 0); + assertEq(rewardRecipient.balance, 4 ether); - vm.expectRevert(OptimisticWithdrawalRecipient.InvalidDistribution_TooLarge.selector); - owrERC20_OR.distributeFunds(); + assertEq(owrETH.getPullBalance(principalRecipient), 32 ether); + assertEq(owrETH.getPullBalance(rewardRecipient), 0); - vm.expectRevert(OptimisticWithdrawalRecipient.InvalidDistribution_TooLarge.selector); - owrERC20_OR.distributeFundsPull(); - } + assertEq(owrETH.distributedFunds(), 36 ether); + assertEq(owrETH.fundsPendingWithdrawal(), 32 ether); - function testCannot_reenterOWR() public { - OWRReentrancy wr = new OWRReentrancy(); + owrETH.withdraw(principalRecipient); - owrETH = owrFactory.createOWRecipient( - ETH_ADDRESS, - recoveryAddress, - address(wr), - rewardRecipient, - 1 ether - ); - address(owrETH).safeTransferETH(33 ether); + assertEq(address(owrETH).balance, 0 ether); + assertEq(principalRecipient.balance, 32 ether); + assertEq(rewardRecipient.balance, 4 ether); - vm.expectRevert(SafeTransferLib.ETHTransferFailed.selector); - owrETH.distributeFunds(); + assertEq(owrETH.getPullBalance(principalRecipient), 0); + assertEq(owrETH.getPullBalance(rewardRecipient), 0); - assertEq(address(owrETH).balance, 33 ether); - assertEq(address(wr).balance, 0 ether); - assertEq(address(0).balance, 0 ether); - } + assertEq(owrETH.distributedFunds(), 36 ether); + assertEq(owrETH.fundsPendingWithdrawal(), 0 ether); - function testCan_distributeToPullFlow() public { - // test eth - address(owrETH).safeTransferETH(36 ether); - owrETH.distributeFundsPull(); + // test erc20 + address(mERC20).safeTransfer(address(owrERC20_OR), 36 ether); + owrERC20_OR.distributeFundsPull(); - assertEq(address(owrETH).balance, 36 ether); - assertEq(principalRecipient.balance, 0 ether); - assertEq(rewardRecipient.balance, 0 ether); + assertEq(mERC20.balanceOf(address(owrERC20_OR)), 36 ether); + assertEq(mERC20.balanceOf(principalRecipient), 0); + assertEq(mERC20.balanceOf(rewardRecipient), 0); - assertEq(owrETH.getPullBalance(principalRecipient), 32 ether); - assertEq(owrETH.getPullBalance(rewardRecipient), 4 ether); + assertEq(owrERC20_OR.getPullBalance(principalRecipient), 32 ether); + assertEq(owrERC20_OR.getPullBalance(rewardRecipient), 4 ether); - assertEq(owrETH.distributedFunds(), 36 ether); - assertEq(owrETH.fundsPendingWithdrawal(), 36 ether); + assertEq(owrERC20_OR.distributedFunds(), 36 ether); + assertEq(owrERC20_OR.fundsPendingWithdrawal(), 36 ether); - owrETH.withdraw(rewardRecipient); + owrERC20_OR.withdraw(rewardRecipient); - assertEq(address(owrETH).balance, 32 ether); - assertEq(principalRecipient.balance, 0); - assertEq(rewardRecipient.balance, 4 ether); + assertEq(mERC20.balanceOf(address(owrERC20_OR)), 32 ether); + assertEq(mERC20.balanceOf(principalRecipient), 0 ether); + assertEq(mERC20.balanceOf(rewardRecipient), 4 ether); - assertEq(owrETH.getPullBalance(principalRecipient), 32 ether); - assertEq(owrETH.getPullBalance(rewardRecipient), 0); + assertEq(owrERC20_OR.getPullBalance(principalRecipient), 32 ether); + assertEq(owrERC20_OR.getPullBalance(rewardRecipient), 0 ether); - assertEq(owrETH.distributedFunds(), 36 ether); - assertEq(owrETH.fundsPendingWithdrawal(), 32 ether); + assertEq(owrERC20_OR.distributedFunds(), 36 ether); + assertEq(owrERC20_OR.fundsPendingWithdrawal(), 32 ether); - owrETH.withdraw(principalRecipient); + owrERC20_OR.withdraw(principalRecipient); - assertEq(address(owrETH).balance, 0 ether); - assertEq(principalRecipient.balance, 32 ether); - assertEq(rewardRecipient.balance, 4 ether); + assertEq(mERC20.balanceOf(address(owrERC20_OR)), 0 ether); + assertEq(mERC20.balanceOf(principalRecipient), 32 ether); + assertEq(mERC20.balanceOf(rewardRecipient), 4 ether); - assertEq(owrETH.getPullBalance(principalRecipient), 0); - assertEq(owrETH.getPullBalance(rewardRecipient), 0); + assertEq(owrERC20_OR.getPullBalance(principalRecipient), 0 ether); + assertEq(owrERC20_OR.getPullBalance(rewardRecipient), 0 ether); - assertEq(owrETH.distributedFunds(), 36 ether); - assertEq(owrETH.fundsPendingWithdrawal(), 0 ether); + assertEq(owrERC20_OR.distributedFunds(), 36 ether); + assertEq(owrERC20_OR.fundsPendingWithdrawal(), 0 ether); + } - // test erc20 - address(mERC20).safeTransfer(address(owrERC20_OR), 36 ether); - owrERC20_OR.distributeFundsPull(); + function testCan_distributePushAndPull() public { + // test eth + address(owrETH).safeTransferETH(0.5 ether); + assertEq(address(owrETH).balance, 0.5 ether, "2/incorrect balance"); - assertEq(mERC20.balanceOf(address(owrERC20_OR)), 36 ether); - assertEq(mERC20.balanceOf(principalRecipient), 0); - assertEq(mERC20.balanceOf(rewardRecipient), 0); + owrETH.distributeFunds(); - assertEq(owrERC20_OR.getPullBalance(principalRecipient), 32 ether); - assertEq(owrERC20_OR.getPullBalance(rewardRecipient), 4 ether); + assertEq(address(owrETH).balance, 0, "3/incorrect balance"); + assertEq(principalRecipient.balance, 0 ether); + assertEq(rewardRecipient.balance, 0.5 ether); - assertEq(owrERC20_OR.distributedFunds(), 36 ether); - assertEq(owrERC20_OR.fundsPendingWithdrawal(), 36 ether); + assertEq(owrETH.getPullBalance(principalRecipient), 0 ether); + assertEq(owrETH.getPullBalance(rewardRecipient), 0 ether); - owrERC20_OR.withdraw(rewardRecipient); + assertEq(owrETH.distributedFunds(), 0.5 ether); + assertEq(owrETH.fundsPendingWithdrawal(), 0 ether); - assertEq(mERC20.balanceOf(address(owrERC20_OR)), 32 ether); - assertEq(mERC20.balanceOf(principalRecipient), 0 ether); - assertEq(mERC20.balanceOf(rewardRecipient), 4 ether); + address(owrETH).safeTransferETH(1 ether); + assertEq(address(owrETH).balance, 1 ether); - assertEq(owrERC20_OR.getPullBalance(principalRecipient), 32 ether); - assertEq(owrERC20_OR.getPullBalance(rewardRecipient), 0 ether); + owrETH.distributeFundsPull(); - assertEq(owrERC20_OR.distributedFunds(), 36 ether); - assertEq(owrERC20_OR.fundsPendingWithdrawal(), 32 ether); + assertEq(address(owrETH).balance, 1 ether); + assertEq(principalRecipient.balance, 0 ether); + assertEq(rewardRecipient.balance, 0.5 ether); - owrERC20_OR.withdraw(principalRecipient); + assertEq(owrETH.getPullBalance(principalRecipient), 0 ether); + assertEq(owrETH.getPullBalance(rewardRecipient), 1 ether); - assertEq(mERC20.balanceOf(address(owrERC20_OR)), 0 ether); - assertEq(mERC20.balanceOf(principalRecipient), 32 ether); - assertEq(mERC20.balanceOf(rewardRecipient), 4 ether); + assertEq(owrETH.distributedFunds(), 1.5 ether); + assertEq(owrETH.fundsPendingWithdrawal(), 1 ether); - assertEq(owrERC20_OR.getPullBalance(principalRecipient), 0 ether); - assertEq(owrERC20_OR.getPullBalance(rewardRecipient), 0 ether); + owrETH.distributeFunds(); - assertEq(owrERC20_OR.distributedFunds(), 36 ether); - assertEq(owrERC20_OR.fundsPendingWithdrawal(), 0 ether); - } + assertEq(address(owrETH).balance, 1 ether); + assertEq(principalRecipient.balance, 0 ether); + assertEq(rewardRecipient.balance, 0.5 ether); - function testCan_distributePushAndPull() public { - // test eth - address(owrETH).safeTransferETH(0.5 ether); - assertEq(address(owrETH).balance, 0.5 ether, "2/incorrect balance"); + assertEq(owrETH.getPullBalance(principalRecipient), 0); + assertEq(owrETH.getPullBalance(rewardRecipient), 1 ether); - owrETH.distributeFunds(); + assertEq(owrETH.distributedFunds(), 1.5 ether); + assertEq(owrETH.fundsPendingWithdrawal(), 1 ether); - assertEq(address(owrETH).balance, 0, "3/incorrect balance"); - assertEq(principalRecipient.balance, 0 ether); - assertEq(rewardRecipient.balance, 0.5 ether); + owrETH.distributeFundsPull(); - assertEq(owrETH.getPullBalance(principalRecipient), 0 ether); - assertEq(owrETH.getPullBalance(rewardRecipient), 0 ether); + assertEq(address(owrETH).balance, 1 ether); + assertEq(principalRecipient.balance, 0 ether); + assertEq(rewardRecipient.balance, 0.5 ether); - assertEq(owrETH.distributedFunds(), 0.5 ether); - assertEq(owrETH.fundsPendingWithdrawal(), 0 ether); + assertEq(owrETH.getPullBalance(principalRecipient), 0); + assertEq(owrETH.getPullBalance(rewardRecipient), 1 ether); - address(owrETH).safeTransferETH(1 ether); - assertEq(address(owrETH).balance, 1 ether); + assertEq(owrETH.distributedFunds(), 1.5 ether); + assertEq(owrETH.fundsPendingWithdrawal(), 1 ether); - owrETH.distributeFundsPull(); + address(owrETH).safeTransferETH(1 ether); + assertEq(address(owrETH).balance, 2 ether); - assertEq(address(owrETH).balance, 1 ether); - assertEq(principalRecipient.balance, 0 ether); - assertEq(rewardRecipient.balance, 0.5 ether); + owrETH.distributeFunds(); - assertEq(owrETH.getPullBalance(principalRecipient), 0 ether); - assertEq(owrETH.getPullBalance(rewardRecipient), 1 ether); + assertEq(address(owrETH).balance, 1 ether); + assertEq(principalRecipient.balance, 0); + assertEq(rewardRecipient.balance, 1.5 ether); - assertEq(owrETH.distributedFunds(), 1.5 ether); - assertEq(owrETH.fundsPendingWithdrawal(), 1 ether); + assertEq(owrETH.getPullBalance(principalRecipient), 0 ether); + assertEq(owrETH.getPullBalance(rewardRecipient), 1 ether); - owrETH.distributeFunds(); + assertEq(owrETH.distributedFunds(), 2.5 ether); + assertEq(owrETH.fundsPendingWithdrawal(), 1 ether); - assertEq(address(owrETH).balance, 1 ether); - assertEq(principalRecipient.balance, 0 ether); - assertEq(rewardRecipient.balance, 0.5 ether); + owrETH.withdraw(rewardRecipient); - assertEq(owrETH.getPullBalance(principalRecipient), 0); - assertEq(owrETH.getPullBalance(rewardRecipient), 1 ether); + assertEq(address(owrETH).balance, 0 ether); + assertEq(principalRecipient.balance, 0); + assertEq(rewardRecipient.balance, 2.5 ether); - assertEq(owrETH.distributedFunds(), 1.5 ether); - assertEq(owrETH.fundsPendingWithdrawal(), 1 ether); + assertEq(owrETH.getPullBalance(principalRecipient), 0 ether); + assertEq(owrETH.getPullBalance(rewardRecipient), 0 ether); - owrETH.distributeFundsPull(); + assertEq(owrETH.distributedFunds(), 2.5 ether); + assertEq(owrETH.fundsPendingWithdrawal(), 0); - assertEq(address(owrETH).balance, 1 ether); - assertEq(principalRecipient.balance, 0 ether); - assertEq(rewardRecipient.balance, 0.5 ether); + address(owrETH).safeTransferETH(1 ether); + owrETH.withdraw(rewardRecipient); - assertEq(owrETH.getPullBalance(principalRecipient), 0); - assertEq(owrETH.getPullBalance(rewardRecipient), 1 ether); + assertEq(address(owrETH).balance, 1 ether); + assertEq(principalRecipient.balance, 0 ether); + assertEq(rewardRecipient.balance, 2.5 ether); - assertEq(owrETH.distributedFunds(), 1.5 ether); - assertEq(owrETH.fundsPendingWithdrawal(), 1 ether); + assertEq(owrETH.getPullBalance(principalRecipient), 0 ether); + assertEq(owrETH.getPullBalance(rewardRecipient), 0 ether); - address(owrETH).safeTransferETH(1 ether); - assertEq(address(owrETH).balance, 2 ether); + assertEq(owrETH.distributedFunds(), 2.5 ether); + assertEq(owrETH.fundsPendingWithdrawal(), 0 ether); - owrETH.distributeFunds(); + // TEST ERC20 - assertEq(address(owrETH).balance, 1 ether); - assertEq(principalRecipient.balance, 0); - assertEq(rewardRecipient.balance, 1.5 ether); + address(mERC20).safeTransfer(address(owrERC20_OR), 0.5 ether); + assertEq(mERC20.balanceOf(address(owrERC20_OR)), 0.5 ether); - assertEq(owrETH.getPullBalance(principalRecipient), 0 ether); - assertEq(owrETH.getPullBalance(rewardRecipient), 1 ether); + owrERC20_OR.distributeFunds(); - assertEq(owrETH.distributedFunds(), 2.5 ether); - assertEq(owrETH.fundsPendingWithdrawal(), 1 ether); + assertEq(mERC20.balanceOf(address(owrERC20_OR)), 0 ether, "1/invalid balance"); + assertEq(mERC20.balanceOf(principalRecipient), 0 ether, "2/invalid tranche 1 recipient balance"); + assertEq(mERC20.balanceOf(rewardRecipient), 0.5 ether, "3/invalid tranche 2 recipient balance - 1"); - owrETH.withdraw(rewardRecipient); + assertEq(owrERC20_OR.getPullBalance(principalRecipient), 0 ether, "4/invalid pull balance"); + assertEq(owrERC20_OR.getPullBalance(rewardRecipient), 0 ether, "5/invalid pull balance"); - assertEq(address(owrETH).balance, 0 ether); - assertEq(principalRecipient.balance, 0); - assertEq(rewardRecipient.balance, 2.5 ether); + assertEq(owrERC20_OR.distributedFunds(), 0.5 ether, "6/invalid distributed funds"); + assertEq(owrERC20_OR.fundsPendingWithdrawal(), 0 ether, "7/invalid funds pending withdrawal"); - assertEq(owrETH.getPullBalance(principalRecipient), 0 ether); - assertEq(owrETH.getPullBalance(rewardRecipient), 0 ether); + address(mERC20).safeTransfer(address(owrERC20_OR), 1 ether); + assertEq(mERC20.balanceOf(address(owrERC20_OR)), 1 ether, "8/invalid balance"); - assertEq(owrETH.distributedFunds(), 2.5 ether); - assertEq(owrETH.fundsPendingWithdrawal(), 0); - - address(owrETH).safeTransferETH(1 ether); - owrETH.withdraw(rewardRecipient); + owrERC20_OR.distributeFundsPull(); - assertEq(address(owrETH).balance, 1 ether); - assertEq(principalRecipient.balance, 0 ether); - assertEq(rewardRecipient.balance, 2.5 ether); + assertEq(mERC20.balanceOf(address(owrERC20_OR)), 1 ether, "9/invalid balance"); + assertEq(mERC20.balanceOf(principalRecipient), 0 ether, "10/invalid recipeint balance"); + assertEq(mERC20.balanceOf(rewardRecipient), 0.5 ether, "11/invalid recipient balance"); - assertEq(owrETH.getPullBalance(principalRecipient), 0 ether); - assertEq(owrETH.getPullBalance(rewardRecipient), 0 ether); + assertEq(owrERC20_OR.getPullBalance(principalRecipient), 0, "12/invalid recipient pull balance"); + assertEq(owrERC20_OR.getPullBalance(rewardRecipient), 1 ether, "13/invalid recipient pull balance"); - assertEq(owrETH.distributedFunds(), 2.5 ether); - assertEq(owrETH.fundsPendingWithdrawal(), 0 ether); + assertEq(owrERC20_OR.distributedFunds(), 1.5 ether, "14/invalid distributed funds balance"); + assertEq(owrERC20_OR.fundsPendingWithdrawal(), 1 ether, "15/invalid funds pending balance"); - // TEST ERC20 + owrERC20_OR.distributeFundsPull(); - address(mERC20).safeTransfer(address(owrERC20_OR), 0.5 ether); - assertEq(mERC20.balanceOf(address(owrERC20_OR)), 0.5 ether); + assertEq(mERC20.balanceOf(address(owrERC20_OR)), 1 ether, "16/invalid balance"); + assertEq(mERC20.balanceOf(principalRecipient), 0 ether, "17/invalid recipient balance"); + assertEq(mERC20.balanceOf(rewardRecipient), 0.5 ether, "18/invalid recipient balance"); - owrERC20_OR.distributeFunds(); + assertEq(owrERC20_OR.getPullBalance(principalRecipient), 0 ether, "19/invalid pull balance"); + assertEq(owrERC20_OR.getPullBalance(rewardRecipient), 1 ether, "20/invalid pull balance"); - assertEq(mERC20.balanceOf(address(owrERC20_OR)), 0 ether, "1/invalid balance"); - assertEq(mERC20.balanceOf(principalRecipient), 0 ether, "2/invalid tranche 1 recipient balance"); - assertEq(mERC20.balanceOf(rewardRecipient), 0.5 ether, "3/invalid tranche 2 recipient balance - 1"); + assertEq(owrERC20_OR.distributedFunds(), 1.5 ether, "21/invalid distributed funds"); + assertEq(owrERC20_OR.fundsPendingWithdrawal(), 1 ether, "22/invalid funds pending"); - assertEq(owrERC20_OR.getPullBalance(principalRecipient), 0 ether, "4/invalid pull balance"); - assertEq(owrERC20_OR.getPullBalance(rewardRecipient), 0 ether, "5/invalid pull balance"); + /// 3 + address(mERC20).safeTransfer(address(owrERC20_OR), 32 ether); + assertEq(mERC20.balanceOf(address(owrERC20_OR)), 33 ether); - assertEq(owrERC20_OR.distributedFunds(), 0.5 ether, "6/invalid distributed funds"); - assertEq(owrERC20_OR.fundsPendingWithdrawal(), 0 ether, "7/invalid funds pending withdrawal"); + owrERC20_OR.distributeFunds(); - address(mERC20).safeTransfer(address(owrERC20_OR), 1 ether); - assertEq(mERC20.balanceOf(address(owrERC20_OR)), 1 ether, "8/invalid balance"); + assertEq(mERC20.balanceOf(address(owrERC20_OR)), 1 ether); + assertEq(mERC20.balanceOf(principalRecipient), 32 ether); + assertEq(mERC20.balanceOf(rewardRecipient), 0.5 ether); - owrERC20_OR.distributeFundsPull(); + assertEq(owrERC20_OR.getPullBalance(principalRecipient), 0 ether); + assertEq(owrERC20_OR.getPullBalance(rewardRecipient), 1 ether); - assertEq(mERC20.balanceOf(address(owrERC20_OR)), 1 ether, "9/invalid balance"); - assertEq(mERC20.balanceOf(principalRecipient), 0 ether, "10/invalid recipeint balance"); - assertEq(mERC20.balanceOf(rewardRecipient), 0.5 ether, "11/invalid recipient balance"); + assertEq(owrERC20_OR.distributedFunds(), 33.5 ether); + assertEq(owrERC20_OR.fundsPendingWithdrawal(), 1 ether); - assertEq(owrERC20_OR.getPullBalance(principalRecipient), 0, "12/invalid recipient pull balance"); - assertEq(owrERC20_OR.getPullBalance(rewardRecipient), 1 ether, "13/invalid recipient pull balance"); + owrERC20_OR.withdraw(rewardRecipient); - assertEq(owrERC20_OR.distributedFunds(), 1.5 ether, "14/invalid distributed funds balance"); - assertEq(owrERC20_OR.fundsPendingWithdrawal(), 1 ether, "15/invalid funds pending balance"); + assertEq(mERC20.balanceOf(address(owrERC20_OR)), 0 ether); + assertEq(mERC20.balanceOf(principalRecipient), 32 ether); + assertEq(mERC20.balanceOf(rewardRecipient), 1.5 ether); - owrERC20_OR.distributeFundsPull(); + assertEq(owrERC20_OR.getPullBalance(principalRecipient), 0 ether); + assertEq(owrERC20_OR.getPullBalance(rewardRecipient), 0 ether); - assertEq(mERC20.balanceOf(address(owrERC20_OR)), 1 ether, "16/invalid balance"); - assertEq(mERC20.balanceOf(principalRecipient), 0 ether, "17/invalid recipient balance"); - assertEq(mERC20.balanceOf(rewardRecipient), 0.5 ether, "18/invalid recipient balance"); + assertEq(owrERC20_OR.distributedFunds(), 33.5 ether); + assertEq(owrERC20_OR.fundsPendingWithdrawal(), 0 ether); + } - assertEq(owrERC20_OR.getPullBalance(principalRecipient), 0 ether, "19/invalid pull balance"); - assertEq(owrERC20_OR.getPullBalance(rewardRecipient), 1 ether, "20/invalid pull balance"); + function testFuzzCan_distributeDepositsToRecipients( + uint256 _recipientsSeed, + uint256 _thresholdsSeed, + uint8 _numDeposits, + uint256 _ethAmount, + uint256 _erc20Amount + ) public { + _ethAmount = uint256(bound(_ethAmount, 0.01 ether, 34 ether)); + _erc20Amount = uint256(bound(_erc20Amount, 0.01 ether, 34 ether)); + vm.assume(_numDeposits > 0); + (address _principalRecipient, address _rewardRecipient, uint256 _trancheThreshold) = + generateTranches(_recipientsSeed, _thresholdsSeed); - assertEq(owrERC20_OR.distributedFunds(), 1.5 ether, "21/invalid distributed funds"); - assertEq(owrERC20_OR.fundsPendingWithdrawal(), 1 ether, "22/invalid funds pending"); + owrETH = owrFactory.createOWRecipient( + ETH_ADDRESS, recoveryAddress, _principalRecipient, _rewardRecipient, _trancheThreshold + ); - /// 3 - address(mERC20).safeTransfer(address(owrERC20_OR), 32 ether); - assertEq(mERC20.balanceOf(address(owrERC20_OR)), 33 ether); + owrERC20 = owrFactory.createOWRecipient( + address(mERC20), recoveryAddress, _principalRecipient, _rewardRecipient, _trancheThreshold + ); - owrERC20_OR.distributeFunds(); + /// test eth + for (uint256 i = 0; i < _numDeposits; i++) { + address(owrETH).safeTransferETH(_ethAmount); + } + owrETH.distributeFunds(); - assertEq(mERC20.balanceOf(address(owrERC20_OR)), 1 ether); - assertEq(mERC20.balanceOf(principalRecipient), 32 ether); - assertEq(mERC20.balanceOf(rewardRecipient), 0.5 ether); + uint256 _totalETHAmount = uint256(_numDeposits) * uint256(_ethAmount); - assertEq(owrERC20_OR.getPullBalance(principalRecipient), 0 ether); - assertEq(owrERC20_OR.getPullBalance(rewardRecipient), 1 ether); + assertEq(address(owrETH).balance, 0 ether, "invalid balance"); + assertEq(owrETH.distributedFunds(), _totalETHAmount, "undistributed funds"); + assertEq(owrETH.fundsPendingWithdrawal(), 0 ether, "funds pending withdraw"); - assertEq(owrERC20_OR.distributedFunds(), 33.5 ether); - assertEq(owrERC20_OR.fundsPendingWithdrawal(), 1 ether); + if (BALANCE_CLASSIFICATION_THRESHOLD > _totalETHAmount) { + // then all of the deposit should be classified as reward + assertEq(_principalRecipient.balance, 0, "should not classify reward as principal"); - owrERC20_OR.withdraw(rewardRecipient); + assertEq(_rewardRecipient.balance, _totalETHAmount, "invalid amount"); + } - assertEq(mERC20.balanceOf(address(owrERC20_OR)), 0 ether); - assertEq(mERC20.balanceOf(principalRecipient), 32 ether); - assertEq(mERC20.balanceOf(rewardRecipient), 1.5 ether); + if (_ethAmount > BALANCE_CLASSIFICATION_THRESHOLD) { + // then all of reward classified as principal + // but check if _totalETHAmount > first threshold + if (_totalETHAmount > _trancheThreshold) { + // there is reward + assertEq(_principalRecipient.balance, _trancheThreshold, "invalid amount"); - assertEq(owrERC20_OR.getPullBalance(principalRecipient), 0 ether); - assertEq(owrERC20_OR.getPullBalance(rewardRecipient), 0 ether); + assertEq( + _rewardRecipient.balance, _totalETHAmount - _trancheThreshold, "should not classify principal as reward" + ); + } else { + // eelse no rewards + assertEq(_principalRecipient.balance, _totalETHAmount, "invalid amount"); - assertEq(owrERC20_OR.distributedFunds(), 33.5 ether); - assertEq(owrERC20_OR.fundsPendingWithdrawal(), 0 ether); + assertEq(_rewardRecipient.balance, 0, "should not classify principal as reward"); + } } - function testFuzzCan_distributeDepositsToRecipients( - uint256 _recipientsSeed, - uint256 _thresholdsSeed, - uint8 _numDeposits, - uint256 _ethAmount, - uint256 _erc20Amount - ) public { - _ethAmount = uint256(bound(_ethAmount, 0.01 ether, 34 ether)); - _erc20Amount = uint256(bound(_erc20Amount, 0.01 ether, 34 ether)); - vm.assume(_numDeposits > 0); - ( - address _principalRecipient, - address _rewardRecipient, - uint256 _trancheThreshold - ) = generateTranches(_recipientsSeed, _thresholdsSeed); - - owrETH = owrFactory.createOWRecipient( - ETH_ADDRESS, - recoveryAddress, - _principalRecipient, - _rewardRecipient, - _trancheThreshold - ); - - owrERC20 = owrFactory.createOWRecipient( - address(mERC20), - recoveryAddress, - _principalRecipient, - _rewardRecipient, - _trancheThreshold - ); + // test erc20 - /// test eth - for (uint256 i = 0; i < _numDeposits; i++) { - address(owrETH).safeTransferETH(_ethAmount); - } - owrETH.distributeFunds(); - - - uint256 _totalETHAmount = uint256(_numDeposits) * uint256(_ethAmount); - - assertEq(address(owrETH).balance, 0 ether, "invalid balance"); - assertEq(owrETH.distributedFunds(), _totalETHAmount, "undistributed funds"); - assertEq(owrETH.fundsPendingWithdrawal(), 0 ether, "funds pending withdraw"); - - if (BALANCE_CLASSIFICATION_THRESHOLD > _totalETHAmount) { - // then all of the deposit should be classified as reward - assertEq( - _principalRecipient.balance, - 0, - "should not classify reward as principal" - ); - - assertEq( - _rewardRecipient.balance, - _totalETHAmount, - "invalid amount" - ); - } - - if (_ethAmount > BALANCE_CLASSIFICATION_THRESHOLD) { - // then all of reward classified as principal - // but check if _totalETHAmount > first threshold - if (_totalETHAmount > _trancheThreshold) { - // there is reward - assertEq( - _principalRecipient.balance, - _trancheThreshold, - "invalid amount" - ); - - assertEq( - _rewardRecipient.balance, - _totalETHAmount - _trancheThreshold, - "should not classify principal as reward" - ); - } else { - // eelse no rewards - assertEq( - _principalRecipient.balance, - _totalETHAmount, - "invalid amount" - ); - - assertEq( - _rewardRecipient.balance, - 0, - "should not classify principal as reward" - ); - } - } - - // test erc20 - - for (uint256 i = 0; i < _numDeposits; i++) { - address(mERC20).safeTransfer(address(owrERC20), _erc20Amount); - owrERC20.distributeFunds(); - } - - uint256 _totalERC20Amount = uint256(_numDeposits) * uint256(_erc20Amount); - - assertEq(mERC20.balanceOf(address(owrERC20)), 0 ether, "invalid erc20 balance"); - assertEq(owrERC20.distributedFunds(), _totalERC20Amount, "incorrect distributed funds"); - assertEq(owrERC20.fundsPendingWithdrawal(), 0 ether, "invalid funds pending withdrawal"); - - if (BALANCE_CLASSIFICATION_THRESHOLD > _totalERC20Amount) { - // then all of the deposit should be classified as reward - assertEq( - mERC20.balanceOf(_principalRecipient), - 0, - "should not classify reward as principal" - ); - - assertEq( - mERC20.balanceOf(_rewardRecipient), - _totalERC20Amount, - "invalid amount reward classification" - ); - } - - if (_erc20Amount > BALANCE_CLASSIFICATION_THRESHOLD) { - // then all of reward classified as principal - // but check if _totalERC20Amount > first threshold - if (_totalERC20Amount > _trancheThreshold) { - // there is reward - assertEq( - mERC20.balanceOf(_principalRecipient), - _trancheThreshold, - "invalid amount principal classification" - ); - - assertEq( - mERC20.balanceOf(_rewardRecipient), - _totalERC20Amount - _trancheThreshold, - "should not classify principal as reward" - ); - } else { - // eelse no rewards - assertEq( - mERC20.balanceOf(_principalRecipient), - _totalERC20Amount, - "invalid amount" - ); - - assertEq( - mERC20.balanceOf(_rewardRecipient), - 0, - "should not classify principal as reward" - ); - } - } + for (uint256 i = 0; i < _numDeposits; i++) { + address(mERC20).safeTransfer(address(owrERC20), _erc20Amount); + owrERC20.distributeFunds(); } - function testFuzzCan_distributePullDepositsToRecipients( - uint256 _recipientsSeed, - uint256 _thresholdsSeed, - uint8 _numDeposits, - uint256 _ethAmount, - uint256 _erc20Amount - ) public { - _ethAmount = uint256(bound(_ethAmount, 0.01 ether, 40 ether)); - _erc20Amount = uint256(bound(_erc20Amount, 0.01 ether, 40 ether)); - vm.assume(_numDeposits > 0); - - ( - address _principalRecipient, - address _rewardRecipient, - uint256 _trancheThreshold - ) = generateTranches(_recipientsSeed, _thresholdsSeed); - - owrETH = owrFactory.createOWRecipient( - ETH_ADDRESS, - recoveryAddress, - _principalRecipient, - _rewardRecipient, - _trancheThreshold - ); - owrERC20 = owrFactory.createOWRecipient( - address(mERC20), - recoveryAddress, - _principalRecipient, - _rewardRecipient, - _trancheThreshold - ); + uint256 _totalERC20Amount = uint256(_numDeposits) * uint256(_erc20Amount); - /// test eth + assertEq(mERC20.balanceOf(address(owrERC20)), 0 ether, "invalid erc20 balance"); + assertEq(owrERC20.distributedFunds(), _totalERC20Amount, "incorrect distributed funds"); + assertEq(owrERC20.fundsPendingWithdrawal(), 0 ether, "invalid funds pending withdrawal"); - for (uint256 i = 0; i < _numDeposits; i++) { - address(owrETH).safeTransferETH(_ethAmount); - owrETH.distributeFundsPull(); - } - uint256 _totalETHAmount = uint256(_numDeposits) * uint256(_ethAmount); + if (BALANCE_CLASSIFICATION_THRESHOLD > _totalERC20Amount) { + // then all of the deposit should be classified as reward + assertEq(mERC20.balanceOf(_principalRecipient), 0, "should not classify reward as principal"); - assertEq(address(owrETH).balance, _totalETHAmount); - assertEq(owrETH.distributedFunds(), _totalETHAmount); - assertEq(owrETH.fundsPendingWithdrawal(), _totalETHAmount); + assertEq(mERC20.balanceOf(_rewardRecipient), _totalERC20Amount, "invalid amount reward classification"); + } - uint256 principal = owrETH.getPullBalance(_principalRecipient); - assertEq( - owrETH.getPullBalance(_principalRecipient), - (_ethAmount >= BALANCE_CLASSIFICATION_THRESHOLD) - ? _trancheThreshold > _totalETHAmount ? _totalETHAmount : _trancheThreshold - : 0 - , - "5/invalid recipient balance" - ); + if (_erc20Amount > BALANCE_CLASSIFICATION_THRESHOLD) { + // then all of reward classified as principal + // but check if _totalERC20Amount > first threshold + if (_totalERC20Amount > _trancheThreshold) { + // there is reward + assertEq(mERC20.balanceOf(_principalRecipient), _trancheThreshold, "invalid amount principal classification"); - uint256 reward = owrETH.getPullBalance(_rewardRecipient); assertEq( - owrETH.getPullBalance(_rewardRecipient), - (_ethAmount >= BALANCE_CLASSIFICATION_THRESHOLD) - ? _totalETHAmount > _trancheThreshold ? (_totalETHAmount - _trancheThreshold) : 0 - : _totalETHAmount - , - "6/invalid recipient balance" + mERC20.balanceOf(_rewardRecipient), + _totalERC20Amount - _trancheThreshold, + "should not classify principal as reward" ); + } else { + // eelse no rewards + assertEq(mERC20.balanceOf(_principalRecipient), _totalERC20Amount, "invalid amount"); - - owrETH.withdraw(_principalRecipient); - owrETH.withdraw(_rewardRecipient); - + assertEq(mERC20.balanceOf(_rewardRecipient), 0, "should not classify principal as reward"); + } + } + } + + function testFuzzCan_distributePullDepositsToRecipients( + uint256 _recipientsSeed, + uint256 _thresholdsSeed, + uint8 _numDeposits, + uint256 _ethAmount, + uint256 _erc20Amount + ) public { + _ethAmount = uint256(bound(_ethAmount, 0.01 ether, 40 ether)); + _erc20Amount = uint256(bound(_erc20Amount, 0.01 ether, 40 ether)); + vm.assume(_numDeposits > 0); + + (address _principalRecipient, address _rewardRecipient, uint256 _trancheThreshold) = + generateTranches(_recipientsSeed, _thresholdsSeed); + + owrETH = owrFactory.createOWRecipient( + ETH_ADDRESS, recoveryAddress, _principalRecipient, _rewardRecipient, _trancheThreshold + ); + owrERC20 = owrFactory.createOWRecipient( + address(mERC20), recoveryAddress, _principalRecipient, _rewardRecipient, _trancheThreshold + ); - assertEq(address(owrETH).balance, 0); - assertEq(owrETH.distributedFunds(), _totalETHAmount); - assertEq(owrETH.fundsPendingWithdrawal(), 0); + /// test eth - assertEq(_principalRecipient.balance, principal, "10/invalid principal balance"); - assertEq(_rewardRecipient.balance, reward, "11/invalid reward balance"); + for (uint256 i = 0; i < _numDeposits; i++) { + address(owrETH).safeTransferETH(_ethAmount); + owrETH.distributeFundsPull(); + } + uint256 _totalETHAmount = uint256(_numDeposits) * uint256(_ethAmount); + + assertEq(address(owrETH).balance, _totalETHAmount); + assertEq(owrETH.distributedFunds(), _totalETHAmount); + assertEq(owrETH.fundsPendingWithdrawal(), _totalETHAmount); + + uint256 principal = owrETH.getPullBalance(_principalRecipient); + assertEq( + owrETH.getPullBalance(_principalRecipient), + (_ethAmount >= BALANCE_CLASSIFICATION_THRESHOLD) + ? _trancheThreshold > _totalETHAmount ? _totalETHAmount : _trancheThreshold + : 0, + "5/invalid recipient balance" + ); - /// test erc20 + uint256 reward = owrETH.getPullBalance(_rewardRecipient); + assertEq( + owrETH.getPullBalance(_rewardRecipient), + (_ethAmount >= BALANCE_CLASSIFICATION_THRESHOLD) + ? _totalETHAmount > _trancheThreshold ? (_totalETHAmount - _trancheThreshold) : 0 + : _totalETHAmount, + "6/invalid recipient balance" + ); - for (uint256 i = 0; i < _numDeposits; i++) { - address(mERC20).safeTransfer(address(owrERC20), _erc20Amount); - owrERC20.distributeFundsPull(); - } - uint256 _totalERC20Amount = - uint256(_numDeposits) * uint256(_erc20Amount); + owrETH.withdraw(_principalRecipient); + owrETH.withdraw(_rewardRecipient); - assertEq(mERC20.balanceOf(address(owrERC20)), _totalERC20Amount); - assertEq(owrERC20.distributedFunds(), _totalERC20Amount); - assertEq(owrERC20.fundsPendingWithdrawal(), _totalERC20Amount); + assertEq(address(owrETH).balance, 0); + assertEq(owrETH.distributedFunds(), _totalETHAmount); + assertEq(owrETH.fundsPendingWithdrawal(), 0); - principal = owrERC20.getPullBalance(_principalRecipient); - assertEq( - owrERC20.getPullBalance(_principalRecipient), - (_erc20Amount >= BALANCE_CLASSIFICATION_THRESHOLD) - ? _trancheThreshold > _totalERC20Amount ? _totalERC20Amount : _trancheThreshold - : 0 - , - "16/invalid recipient balance" - ); + assertEq(_principalRecipient.balance, principal, "10/invalid principal balance"); + assertEq(_rewardRecipient.balance, reward, "11/invalid reward balance"); - reward = owrERC20.getPullBalance(_rewardRecipient); - assertEq( - owrERC20.getPullBalance(_rewardRecipient), - (_erc20Amount >= BALANCE_CLASSIFICATION_THRESHOLD) - ? _totalERC20Amount > _trancheThreshold ? (_totalERC20Amount - _trancheThreshold) : 0 - : _totalERC20Amount - , - "17/invalid recipient balance" - ); + /// test erc20 - owrERC20.withdraw(_principalRecipient); - owrERC20.withdraw(_rewardRecipient); + for (uint256 i = 0; i < _numDeposits; i++) { + address(mERC20).safeTransfer(address(owrERC20), _erc20Amount); + owrERC20.distributeFundsPull(); + } + uint256 _totalERC20Amount = uint256(_numDeposits) * uint256(_erc20Amount); + + assertEq(mERC20.balanceOf(address(owrERC20)), _totalERC20Amount); + assertEq(owrERC20.distributedFunds(), _totalERC20Amount); + assertEq(owrERC20.fundsPendingWithdrawal(), _totalERC20Amount); + + principal = owrERC20.getPullBalance(_principalRecipient); + assertEq( + owrERC20.getPullBalance(_principalRecipient), + (_erc20Amount >= BALANCE_CLASSIFICATION_THRESHOLD) + ? _trancheThreshold > _totalERC20Amount ? _totalERC20Amount : _trancheThreshold + : 0, + "16/invalid recipient balance" + ); + reward = owrERC20.getPullBalance(_rewardRecipient); + assertEq( + owrERC20.getPullBalance(_rewardRecipient), + (_erc20Amount >= BALANCE_CLASSIFICATION_THRESHOLD) + ? _totalERC20Amount > _trancheThreshold ? (_totalERC20Amount - _trancheThreshold) : 0 + : _totalERC20Amount, + "17/invalid recipient balance" + ); - assertEq(mERC20.balanceOf(address(owrERC20)), 0, "18/invalid balance"); - assertEq(owrERC20.distributedFunds(), _totalERC20Amount, "19/invalid balance"); - assertEq(owrERC20.fundsPendingWithdrawal(), 0, "20/invalid funds pending"); + owrERC20.withdraw(_principalRecipient); + owrERC20.withdraw(_rewardRecipient); - assertEq(mERC20.balanceOf(_principalRecipient), principal, "21/invalid principal balance"); - assertEq(mERC20.balanceOf(_rewardRecipient), reward, "22/invalid reward balance"); - } + assertEq(mERC20.balanceOf(address(owrERC20)), 0, "18/invalid balance"); + assertEq(owrERC20.distributedFunds(), _totalERC20Amount, "19/invalid balance"); + assertEq(owrERC20.fundsPendingWithdrawal(), 0, "20/invalid funds pending"); -} \ No newline at end of file + assertEq(mERC20.balanceOf(_principalRecipient), principal, "21/invalid principal balance"); + assertEq(mERC20.balanceOf(_rewardRecipient), reward, "22/invalid reward balance"); + } +} diff --git a/src/test/waterfall/OptimisticWithdrawalRecipientFactory.t.sol b/src/test/waterfall/OptimisticWithdrawalRecipientFactory.t.sol index 75ab164..8bd63c2 100644 --- a/src/test/waterfall/OptimisticWithdrawalRecipientFactory.t.sol +++ b/src/test/waterfall/OptimisticWithdrawalRecipientFactory.t.sol @@ -2,288 +2,177 @@ pragma solidity 0.8.17; import "forge-std/Test.sol"; -import { OptimisticWithdrawalRecipient } from "src/owr/OptimisticWithdrawalRecipient.sol"; -import { OptimisticWithdrawalRecipientFactory } from "src/owr/OptimisticWithdrawalRecipientFactory.sol"; +import {OptimisticWithdrawalRecipient} from "src/owr/OptimisticWithdrawalRecipient.sol"; +import {OptimisticWithdrawalRecipientFactory} from "src/owr/OptimisticWithdrawalRecipientFactory.sol"; import {MockERC20} from "../utils/mocks/MockERC20.sol"; import {OWRTestHelper} from "./OWRTestHelper.t.sol"; contract OptimisticWithdrawalRecipientFactoryTest is OWRTestHelper, Test { + event CreateOWRecipient( + address indexed owr, + address token, + address recoveryAddress, + address principalRecipient, + address rewardRecipient, + uint256 threshold + ); + + OptimisticWithdrawalRecipientFactory owrFactoryModule; + MockERC20 mERC20; + + address public recoveryAddress; + address public principalRecipient; + address public rewardRecipient; + uint256 public threshold; + + function setUp() public { + mERC20 = new MockERC20("Test Token", "TOK", 18); + mERC20.mint(type(uint256).max); + + owrFactoryModule = new OptimisticWithdrawalRecipientFactory(); + + recoveryAddress = makeAddr("recoveryAddress"); + (principalRecipient, rewardRecipient) = generateTrancheRecipients(10); + threshold = ETH_STAKE; + } + + function testCan_createOWRecipient() public { + owrFactoryModule.createOWRecipient(ETH_ADDRESS, recoveryAddress, principalRecipient, rewardRecipient, threshold); + + owrFactoryModule.createOWRecipient(address(mERC20), recoveryAddress, principalRecipient, rewardRecipient, threshold); + + recoveryAddress = address(0); + owrFactoryModule.createOWRecipient(ETH_ADDRESS, recoveryAddress, principalRecipient, rewardRecipient, threshold); + + owrFactoryModule.createOWRecipient(address(mERC20), recoveryAddress, principalRecipient, rewardRecipient, threshold); + } + + 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 + ); + owrFactoryModule.createOWRecipient(ETH_ADDRESS, recoveryAddress, principalRecipient, rewardRecipient, threshold); + + // don't check deploy address + vm.expectEmit(false, true, true, true); + emit CreateOWRecipient( + address(0xdead), address(mERC20), recoveryAddress, principalRecipient, rewardRecipient, threshold + ); + owrFactoryModule.createOWRecipient(address(mERC20), recoveryAddress, principalRecipient, rewardRecipient, threshold); + + recoveryAddress = address(0); + + // don't check deploy address + vm.expectEmit(false, true, true, true); + emit CreateOWRecipient( + address(0xdead), ETH_ADDRESS, recoveryAddress, principalRecipient, rewardRecipient, threshold + ); + owrFactoryModule.createOWRecipient(ETH_ADDRESS, recoveryAddress, principalRecipient, rewardRecipient, threshold); + + // don't check deploy address + vm.expectEmit(false, true, true, true); + emit CreateOWRecipient( + address(0xdead), address(mERC20), recoveryAddress, principalRecipient, rewardRecipient, threshold + ); + owrFactoryModule.createOWRecipient(address(mERC20), recoveryAddress, principalRecipient, rewardRecipient, threshold); + } + + function testCannot_createWithInvalidRecipients() public { + (principalRecipient, rewardRecipient, threshold) = generateTranches(1, 1); + // eth + vm.expectRevert(OptimisticWithdrawalRecipientFactory.Invalid__Recipients.selector); + owrFactoryModule.createOWRecipient(ETH_ADDRESS, recoveryAddress, address(0), rewardRecipient, threshold); + + vm.expectRevert(OptimisticWithdrawalRecipientFactory.Invalid__Recipients.selector); + owrFactoryModule.createOWRecipient(ETH_ADDRESS, recoveryAddress, address(0), address(0), threshold); + + vm.expectRevert(OptimisticWithdrawalRecipientFactory.Invalid__Recipients.selector); + owrFactoryModule.createOWRecipient(ETH_ADDRESS, recoveryAddress, principalRecipient, address(0), threshold); + + // erc20 + vm.expectRevert(OptimisticWithdrawalRecipientFactory.Invalid__Recipients.selector); + owrFactoryModule.createOWRecipient(address(mERC20), recoveryAddress, address(0), rewardRecipient, threshold); + + vm.expectRevert(OptimisticWithdrawalRecipientFactory.Invalid__Recipients.selector); + owrFactoryModule.createOWRecipient(address(mERC20), recoveryAddress, address(0), address(0), threshold); + + vm.expectRevert(OptimisticWithdrawalRecipientFactory.Invalid__Recipients.selector); + owrFactoryModule.createOWRecipient(address(mERC20), recoveryAddress, principalRecipient, address(0), threshold); + } + + function testCannot_createWithInvalidThreshold() public { + (principalRecipient, rewardRecipient) = generateTrancheRecipients(2); + threshold = 0; + + vm.expectRevert(OptimisticWithdrawalRecipientFactory.Invalid__ZeroThreshold.selector); + owrFactoryModule.createOWRecipient(ETH_ADDRESS, recoveryAddress, principalRecipient, rewardRecipient, threshold); + + vm.expectRevert( + abi.encodeWithSelector( + OptimisticWithdrawalRecipientFactory.Invalid__ThresholdTooLarge.selector, type(uint128).max + ) + ); + owrFactoryModule.createOWRecipient( + ETH_ADDRESS, recoveryAddress, principalRecipient, rewardRecipient, type(uint128).max + ); + } + + /// ----------------------------------------------------------------------- + /// Fuzzing Tests + /// ---------------------------------------------------------------------- + + function testFuzzCan_createOWRecipient(address _recoveryAddress, uint256 recipientsSeed, uint256 thresholdSeed) + public + { + recoveryAddress = _recoveryAddress; + + (principalRecipient, rewardRecipient, threshold) = generateTranches(recipientsSeed, thresholdSeed); + + vm.expectEmit(false, true, true, true); + emit CreateOWRecipient( + address(0xdead), ETH_ADDRESS, recoveryAddress, principalRecipient, rewardRecipient, threshold + ); + owrFactoryModule.createOWRecipient(ETH_ADDRESS, recoveryAddress, principalRecipient, rewardRecipient, threshold); + + vm.expectEmit(false, true, true, true); + emit CreateOWRecipient( + address(0xdead), address(mERC20), recoveryAddress, principalRecipient, rewardRecipient, threshold + ); + owrFactoryModule.createOWRecipient(address(mERC20), recoveryAddress, principalRecipient, rewardRecipient, threshold); + } + + function testFuzzCannot_CreateWithZeroThreshold(uint256 _receipientSeed) public { + threshold = 0; + (principalRecipient, rewardRecipient) = generateTrancheRecipients(_receipientSeed); + + // eth + vm.expectRevert(OptimisticWithdrawalRecipientFactory.Invalid__ZeroThreshold.selector); + owrFactoryModule.createOWRecipient(ETH_ADDRESS, recoveryAddress, principalRecipient, rewardRecipient, threshold); + + // erc20 + vm.expectRevert(OptimisticWithdrawalRecipientFactory.Invalid__ZeroThreshold.selector); + + owrFactoryModule.createOWRecipient(address(mERC20), recoveryAddress, principalRecipient, rewardRecipient, threshold); + } + + function testFuzzCannot_CreateWithLargeThreshold(uint256 _receipientSeed, uint256 _threshold) public { + vm.assume(_threshold > type(uint96).max); + + threshold = _threshold; + (principalRecipient, rewardRecipient) = generateTrancheRecipients(_receipientSeed); + + vm.expectRevert( + abi.encodeWithSelector(OptimisticWithdrawalRecipientFactory.Invalid__ThresholdTooLarge.selector, _threshold) + ); + owrFactoryModule.createOWRecipient(ETH_ADDRESS, recoveryAddress, principalRecipient, rewardRecipient, threshold); - event CreateOWRecipient( - address indexed owr, - address token, - address recoveryAddress, - address principalRecipient, - address rewardRecipient, - uint256 threshold + vm.expectRevert( + abi.encodeWithSelector(OptimisticWithdrawalRecipientFactory.Invalid__ThresholdTooLarge.selector, _threshold) ); - OptimisticWithdrawalRecipientFactory owrFactoryModule; - MockERC20 mERC20; - - address public recoveryAddress; - address public principalRecipient; - address public rewardRecipient; - uint256 public threshold; - - function setUp() public { - mERC20 = new MockERC20("Test Token", "TOK", 18); - mERC20.mint(type(uint256).max); - - owrFactoryModule = new OptimisticWithdrawalRecipientFactory(); - - recoveryAddress = makeAddr("recoveryAddress"); - (principalRecipient, rewardRecipient) = generateTrancheRecipients(10); - threshold = ETH_STAKE; - } - - function testCan_createOWRecipient() public { - owrFactoryModule.createOWRecipient( - ETH_ADDRESS, recoveryAddress, principalRecipient, rewardRecipient, threshold - ); - - owrFactoryModule.createOWRecipient( - address(mERC20), recoveryAddress, principalRecipient, rewardRecipient, threshold - ); - - recoveryAddress = address(0); - owrFactoryModule.createOWRecipient( - ETH_ADDRESS, recoveryAddress, principalRecipient, rewardRecipient, threshold - ); - - owrFactoryModule.createOWRecipient( - address(mERC20), recoveryAddress, principalRecipient, rewardRecipient, threshold - ); - } - - - 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 - ); - owrFactoryModule.createOWRecipient( - ETH_ADDRESS, recoveryAddress, principalRecipient, rewardRecipient, threshold - ); - - // don't check deploy address - vm.expectEmit(false, true, true, true); - emit CreateOWRecipient( - address(0xdead), - address(mERC20), - recoveryAddress, - principalRecipient, rewardRecipient, - threshold - ); - owrFactoryModule.createOWRecipient( - address(mERC20), recoveryAddress, principalRecipient, rewardRecipient, threshold - ); - - recoveryAddress = address(0); - - // don't check deploy address - vm.expectEmit(false, true, true, true); - emit CreateOWRecipient( - address(0xdead), - ETH_ADDRESS, - recoveryAddress, - principalRecipient, rewardRecipient, - threshold - ); - owrFactoryModule.createOWRecipient( - ETH_ADDRESS, recoveryAddress, principalRecipient, rewardRecipient, threshold - ); - - // don't check deploy address - vm.expectEmit(false, true, true, true); - emit CreateOWRecipient( - address(0xdead), - address(mERC20), - recoveryAddress, - principalRecipient, rewardRecipient, - threshold - ); - owrFactoryModule.createOWRecipient( - address(mERC20), recoveryAddress, principalRecipient, rewardRecipient, threshold - ); - } - - function testCannot_createWithInvalidRecipients() public { - (principalRecipient, rewardRecipient, threshold) = generateTranches(1, 1); - // eth - vm.expectRevert( - OptimisticWithdrawalRecipientFactory.Invalid__Recipients.selector - ); - owrFactoryModule.createOWRecipient( - ETH_ADDRESS, recoveryAddress, address(0), rewardRecipient, threshold - ); - - vm.expectRevert( - OptimisticWithdrawalRecipientFactory.Invalid__Recipients.selector - ); - owrFactoryModule.createOWRecipient( - ETH_ADDRESS, recoveryAddress, address(0), address(0), threshold - ); - - vm.expectRevert( - OptimisticWithdrawalRecipientFactory.Invalid__Recipients.selector - ); - owrFactoryModule.createOWRecipient( - ETH_ADDRESS, recoveryAddress, principalRecipient, address(0), threshold - ); - - // erc20 - vm.expectRevert( - OptimisticWithdrawalRecipientFactory.Invalid__Recipients.selector - ); - owrFactoryModule.createOWRecipient( - address(mERC20), recoveryAddress, address(0), rewardRecipient, threshold - ); - - vm.expectRevert( - OptimisticWithdrawalRecipientFactory.Invalid__Recipients.selector - ); - owrFactoryModule.createOWRecipient( - address(mERC20), recoveryAddress, address(0), address(0), threshold - ); - - vm.expectRevert( - OptimisticWithdrawalRecipientFactory.Invalid__Recipients.selector - ); - owrFactoryModule.createOWRecipient( - address(mERC20), recoveryAddress, principalRecipient, address(0), threshold - ); - } - - function testCannot_createWithInvalidThreshold() public { - (principalRecipient, rewardRecipient) = generateTrancheRecipients(2); - threshold = 0; - - vm.expectRevert( - OptimisticWithdrawalRecipientFactory - .Invalid__ZeroThreshold - .selector - ); - owrFactoryModule.createOWRecipient( - ETH_ADDRESS, recoveryAddress, principalRecipient, rewardRecipient, threshold - ); - - vm.expectRevert( - abi.encodeWithSelector(OptimisticWithdrawalRecipientFactory - .Invalid__ThresholdTooLarge - .selector, - type(uint128).max - ) - ); - owrFactoryModule.createOWRecipient( - ETH_ADDRESS, recoveryAddress, principalRecipient, rewardRecipient, type(uint128).max - ); - } - - - /// ----------------------------------------------------------------------- - /// Fuzzing Tests - /// ---------------------------------------------------------------------- - - function testFuzzCan_createOWRecipient( - address _recoveryAddress, - uint256 recipientsSeed, - uint256 thresholdSeed - ) public { - recoveryAddress = _recoveryAddress; - - (principalRecipient, rewardRecipient, threshold) = generateTranches(recipientsSeed, thresholdSeed); - - vm.expectEmit(false, true, true, true); - emit CreateOWRecipient( - address(0xdead), - ETH_ADDRESS, - recoveryAddress, - principalRecipient, rewardRecipient, - threshold - ); - owrFactoryModule.createOWRecipient( - ETH_ADDRESS, recoveryAddress, principalRecipient, rewardRecipient, threshold - ); - - vm.expectEmit(false, true, true, true); - emit CreateOWRecipient( - address(0xdead), - address(mERC20), - recoveryAddress, - principalRecipient, rewardRecipient, - threshold - ); - owrFactoryModule.createOWRecipient( - address(mERC20), recoveryAddress, principalRecipient, rewardRecipient, threshold - ); - } - - function testFuzzCannot_CreateWithZeroThreshold( - uint256 _receipientSeed - ) public { - threshold = 0; - (principalRecipient, rewardRecipient) = generateTrancheRecipients(_receipientSeed); - - // eth - vm.expectRevert( - OptimisticWithdrawalRecipientFactory.Invalid__ZeroThreshold.selector - ); - owrFactoryModule.createOWRecipient( - ETH_ADDRESS, recoveryAddress, principalRecipient, rewardRecipient, threshold - ); - - // erc20 - vm.expectRevert( - OptimisticWithdrawalRecipientFactory.Invalid__ZeroThreshold.selector - ); - - owrFactoryModule.createOWRecipient( - address(mERC20), recoveryAddress, principalRecipient, rewardRecipient, threshold - ); - } - - function testFuzzCannot_CreateWithLargeThreshold( - uint256 _receipientSeed, - uint256 _threshold - ) public { - vm.assume(_threshold > type(uint96).max); - - threshold = _threshold; - (principalRecipient, rewardRecipient) = generateTrancheRecipients(_receipientSeed); - - 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 - ); - - } - - -} \ No newline at end of file + owrFactoryModule.createOWRecipient(address(mERC20), recoveryAddress, principalRecipient, rewardRecipient, threshold); + } +} From 41c3761a6c44bfaaa0dbb8666ad8cb51e4dad40d Mon Sep 17 00:00:00 2001 From: samparsky <8148384+samparsky@users.noreply.github.com> Date: Thu, 24 Aug 2023 18:11:06 +0300 Subject: [PATCH 08/82] chore: additional formatting contracts --- src/lido/LidoSplitFactory.sol | 1 + src/owr/OptimisticWithdrawalRecipient.sol | 1 + src/owr/OptimisticWithdrawalRecipientFactory.sol | 1 + 3 files changed, 3 insertions(+) diff --git a/src/lido/LidoSplitFactory.sol b/src/lido/LidoSplitFactory.sol index 14f9d53..3e7a83d 100644 --- a/src/lido/LidoSplitFactory.sol +++ b/src/lido/LidoSplitFactory.sol @@ -5,6 +5,7 @@ import {LibClone} from "solady/utils/LibClone.sol"; import {ERC20} from "solmate/tokens/ERC20.sol"; import "./LidoSplit.sol"; + /// @title LidoSplitFactory /// @author Obol /// @notice A factory contract for cheaply deploying LidoSplit. diff --git a/src/owr/OptimisticWithdrawalRecipient.sol b/src/owr/OptimisticWithdrawalRecipient.sol index 1f818f1..65b6b5a 100644 --- a/src/owr/OptimisticWithdrawalRecipient.sol +++ b/src/owr/OptimisticWithdrawalRecipient.sol @@ -5,6 +5,7 @@ import {Clone} from "solady/utils/Clone.sol"; import {ERC20} from "solmate/tokens/ERC20.sol"; import {SafeTransferLib} from "solady/utils/SafeTransferLib.sol"; + /// @title OptimisticWithdrawalRecipient /// @author Obol /// @notice A maximally-composable contract that distributes payments diff --git a/src/owr/OptimisticWithdrawalRecipientFactory.sol b/src/owr/OptimisticWithdrawalRecipientFactory.sol index de26be4..934e903 100644 --- a/src/owr/OptimisticWithdrawalRecipientFactory.sol +++ b/src/owr/OptimisticWithdrawalRecipientFactory.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.17; import {OptimisticWithdrawalRecipient} from "./OptimisticWithdrawalRecipient.sol"; import {LibClone} from "solady/utils/LibClone.sol"; + /// @title OptimisticWithdrawalRecipientFactory /// @author Obol /// @notice A factory contract for cheaply deploying OptimisticWithdrawalRecipient. From 8f295ad55990734f22c1b50b385322238e0211b8 Mon Sep 17 00:00:00 2001 From: samparsky <8148384+samparsky@users.noreply.github.com> Date: Fri, 8 Sep 2023 13:07:59 +0100 Subject: [PATCH 09/82] chore: remove unused variables --- src/lido/LidoSplitFactory.sol | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/lido/LidoSplitFactory.sol b/src/lido/LidoSplitFactory.sol index 3e7a83d..6635232 100644 --- a/src/lido/LidoSplitFactory.sol +++ b/src/lido/LidoSplitFactory.sol @@ -55,9 +55,7 @@ contract LidoSplitFactory { function createSplit(address splitWallet) external returns (address lidoSplit) { if (splitWallet == address(0)) revert Invalid_Wallet(); - bytes memory data = abi.encodePacked(stETH, wstETH, splitWallet); - - lidoSplit = address(lidoSplitImpl).clone(data); + lidoSplit = address(lidoSplitImpl).clone(abi.encodePacked(stETH, wstETH, splitWallet)); emit CreateLidoSplit(lidoSplit); } From de2efdce9c4512388fa498ede352533737dd7c26 Mon Sep 17 00:00:00 2001 From: samparsky <8148384+samparsky@users.noreply.github.com> Date: Fri, 8 Sep 2023 13:08:23 +0100 Subject: [PATCH 10/82] chore: apply forge fmt --- src/lido/LidoSplitFactory.sol | 1 - src/owr/OptimisticWithdrawalRecipient.sol | 1 - src/owr/OptimisticWithdrawalRecipientFactory.sol | 1 - 3 files changed, 3 deletions(-) diff --git a/src/lido/LidoSplitFactory.sol b/src/lido/LidoSplitFactory.sol index 6635232..2b9f25a 100644 --- a/src/lido/LidoSplitFactory.sol +++ b/src/lido/LidoSplitFactory.sol @@ -5,7 +5,6 @@ import {LibClone} from "solady/utils/LibClone.sol"; import {ERC20} from "solmate/tokens/ERC20.sol"; import "./LidoSplit.sol"; - /// @title LidoSplitFactory /// @author Obol /// @notice A factory contract for cheaply deploying LidoSplit. diff --git a/src/owr/OptimisticWithdrawalRecipient.sol b/src/owr/OptimisticWithdrawalRecipient.sol index 65b6b5a..1f818f1 100644 --- a/src/owr/OptimisticWithdrawalRecipient.sol +++ b/src/owr/OptimisticWithdrawalRecipient.sol @@ -5,7 +5,6 @@ import {Clone} from "solady/utils/Clone.sol"; import {ERC20} from "solmate/tokens/ERC20.sol"; import {SafeTransferLib} from "solady/utils/SafeTransferLib.sol"; - /// @title OptimisticWithdrawalRecipient /// @author Obol /// @notice A maximally-composable contract that distributes payments diff --git a/src/owr/OptimisticWithdrawalRecipientFactory.sol b/src/owr/OptimisticWithdrawalRecipientFactory.sol index 934e903..de26be4 100644 --- a/src/owr/OptimisticWithdrawalRecipientFactory.sol +++ b/src/owr/OptimisticWithdrawalRecipientFactory.sol @@ -4,7 +4,6 @@ pragma solidity ^0.8.17; import {OptimisticWithdrawalRecipient} from "./OptimisticWithdrawalRecipient.sol"; import {LibClone} from "solady/utils/LibClone.sol"; - /// @title OptimisticWithdrawalRecipientFactory /// @author Obol /// @notice A factory contract for cheaply deploying OptimisticWithdrawalRecipient. From 93129084041f03fa96dbd8ff8c7a5a0403ae1e79 Mon Sep 17 00:00:00 2001 From: samparsky <8148384+samparsky@users.noreply.github.com> Date: Thu, 24 Aug 2023 17:29:55 +0300 Subject: [PATCH 11/82] chore: add imsc implementation and test --- src/controllers/ImmutableSplitController.sol | 142 +++++++++++ .../ImmutableSplitControllerFactory.sol | 223 ++++++++++++++++++ src/interfaces/ISplitMain.sol | 12 + src/test/controllers/IMSC.t.sol | 74 ++++++ src/test/controllers/IMSCFactory.t.sol | 63 +++++ 5 files changed, 514 insertions(+) create mode 100644 src/controllers/ImmutableSplitController.sol create mode 100644 src/controllers/ImmutableSplitControllerFactory.sol create mode 100644 src/test/controllers/IMSC.t.sol create mode 100644 src/test/controllers/IMSCFactory.t.sol diff --git a/src/controllers/ImmutableSplitController.sol b/src/controllers/ImmutableSplitController.sol new file mode 100644 index 0000000..5298e93 --- /dev/null +++ b/src/controllers/ImmutableSplitController.sol @@ -0,0 +1,142 @@ +// SPDX-License-Identifier: MIT +pragma solidity =0.8.17; +import {ISplitMain} from "../interfaces/ISplitMain.sol"; +import {Clone} from "solady/utils/Clone.sol"; + + +/// @author Obol +/// @dev Deploys a contract that can update a split should be called once as the +/// configuration is defined at deployment and cannot change +contract ImmutableSplitController is Clone { + + /// ----------------------------------------------------------------------- + /// errors + /// ----------------------------------------------------------------------- + + /// @notice IMSC already initialized + error Initialized(); + + /// ----------------------------------------------------------------------- + /// storage + /// ----------------------------------------------------------------------- + + /// ----------------------------------------------------------------------- + /// storage - constants + /// ----------------------------------------------------------------------- + uint256 internal constant ADDRESS_BITS = 160; + uint256 internal constant ONE_WORD = 32; + + /// ----------------------------------------------------------------------- + /// storage - cwia offsets + /// ----------------------------------------------------------------------- + + // splitMain (address, 20 bytes) + // 0; first item + uint256 internal constant SPLIT_MAIN_OFFSET = 0; + // // split (address, 20 bytes) + // // 1; second item + // uint256 internal constant SPLIT_ADDRESS_OFFSET = 20; + // distributorFee (uint32, 4 bytes) + // 2; third item + uint256 internal constant DISTRIBUTOR_FEE_OFFSET = 20; + // recipeints size (uint8, 1 byte ) + // 3; fourth item + uint256 internal constant RECIPIENTS_SIZE_OFFSET = 24; + // recipients data () + // 4; fifth item + uint256 internal constant RECIPIENTS_OFFSET = 25; + + /// ----------------------------------------------------------------------- + /// storage - mutable + /// ----------------------------------------------------------------------- + address public split; + + + constructor() {} + + function init(address _split) external { + if (_split != address(0)) revert Initialized(); + + split = _split; + } + + /// Updates split with the hardcoded configuration + /// @dev Updates split with stored split configuration + function updateSplit() external payable { + // @TODO accept control + address _splitMain = splitMain(); + address _split = split; + uint32 _distributorFee = uint32(distributorFee()); + + ( + address[] memory accounts, + uint32[] memory percentAllocations + ) = getNewSplitConfiguration(); + + ISplitMain(_splitMain).updateSplit( + _split, + accounts, + percentAllocations, + _distributorFee + ); + } + + /// Address of SplitMain + /// @dev equivalent to address public immutable splitMain; + function splitMain() public pure returns (address) { + return _getArgAddress(SPLIT_MAIN_OFFSET); + } + + // /// Address of split to update + // /// @dev equivalent to address public immutable split; + // function split() public pure returns (address) { + // return _getArgAddress(SPLIT_ADDRESS_OFFSET); + // } + + /// Fee charged by distributor + /// @dev equivalent to address public immutable distributorFee; + function distributorFee() public pure returns(uint256) { + return _getArgUint32(DISTRIBUTOR_FEE_OFFSET); + } + + // Returns unpacked recipients + /// @return accounts Addresses to receive payments + /// @return percentAllocations Percentage share for split accounts + function getNewSplitConfiguration() public pure returns ( + address[] memory accounts, + uint32[] memory percentAllocations + ) { + // fetch the size first + // then parse the data gradually + uint256 size = _recipientsSize(); + accounts = new address[](size); + percentAllocations = new uint32[](size); + + uint i; + for (i; i < size;) { + uint256 recipient = _getRecipient(i); + accounts[i] = address(uint160(recipient)); + percentAllocations[i] = uint32(recipient >> ADDRESS_BITS); + unchecked { + i++; + } + } + + } + + /// Number of recipeints + /// @dev equivalent to address internal immutable _recipientsSize; + function _recipientsSize() internal pure returns (uint256) { + return _getArgUint8(RECIPIENTS_SIZE_OFFSET); + } + + /// Gets recipient i + /// @dev emulates to uint256[] internal immutable recipient; + function _getRecipient(uint256 i) internal pure returns (uint256) { + unchecked { + // shouldn't overflow + 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 new file mode 100644 index 0000000..49f846b --- /dev/null +++ b/src/controllers/ImmutableSplitControllerFactory.sol @@ -0,0 +1,223 @@ +// SPDX-License-Identifier: MIT +pragma solidity =0.8.17; +import {ISplitMain} from "../interfaces/ISplitMain.sol"; +import {LibClone} from "solady/utils/LibClone.sol"; +import {ImmutableSplitController} from "./ImmutableSplitController.sol"; + + +/// @author Obol +/// @dev Deploys ImmutableSplitController cheaply using cwia clones +contract ImmutableSplitControllerFactory { + + 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); + + /// ----------------------------------------------------------------------- + /// libraries + /// ----------------------------------------------------------------------- + + using LibClone for address; + + /// ----------------------------------------------------------------------- + /// events + /// ----------------------------------------------------------------------- + + /// Emitted after a new IMSC is deployed + /// @param controller Address of newly created IMSC clone + /// @param split Address of split + /// @param accounts Addresses of + /// @param percentAllocations Addresses to recover non-waterfall tokens to + /// @param distributorFee Amount of + event CreateIMSC( + address indexed controller, + address indexed split, + address[] accounts, + uint32[] percentAllocations, + uint256 distributorFee + ); + + + /// ----------------------------------------------------------------------- + /// storage + /// ----------------------------------------------------------------------- + + /// ----------------------------------------------------------------------- + /// storage - constants & immutables + /// ----------------------------------------------------------------------- + uint256 internal constant ADDRESS_BITS = 160; + /// @notice constant to scale uints into percentages (1e6 == 100%) + uint256 public constant PERCENTAGE_SCALE = 1e6; + /// @notice maximum distributor fee; 1e5 = 10% * PERCENTAGE_SCALE + uint256 internal constant MAX_DISTRIBUTOR_FEE = 1e5; + + /// @dev splitMain address + address public immutable splitMain; + + /// @dev Implementation of ImmutableSplitController + ImmutableSplitController public immutable controller; + + /// ----------------------------------------------------------------------- + /// modifiers + /// ----------------------------------------------------------------------- + modifier validSplit( + address[] memory accounts, + uint32[] memory percentAllocations, + uint32 distributorFee + ) { + 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); + _; + } + + + /// Creates Factory + /// @dev initializes the factory + /// @param splitMain_ Address of splitMain + constructor(address splitMain_) { + splitMain = splitMain_; + controller = new ImmutableSplitController(); + } + + /// Deploys a new immutable controller + /// @dev Create a new immutable split controller + /// @param split Address of the split to create a controller for + /// @param accounts Ordered, unique list of addresses with ownership in the split + /// @param percentAllocations Percent allocations associated with each address + /// @param distributorFee Distributor fee share + /// @param deploymentSalt salt to use for deterministic deploy + function createController( + address split, + address[] calldata accounts, + uint32[] calldata percentAllocations, + uint32 distributorFee, + bytes32 deploymentSalt + ) + external validSplit(accounts, percentAllocations, distributorFee) + returns (ImmutableSplitController newController) + { + // ensure it's valid split + + bytes memory data = _packSplitControllerData( + accounts, + percentAllocations, + distributorFee + ); + + newController = ImmutableSplitController( + address(controller).cloneDeterministic(data, deploymentSalt) + ); + + // initialize with split address + newController.init(split); + + emit CreateIMSC( + address(controller), + split, + accounts, + percentAllocations, + distributorFee + ); + } + + /// @notice Predicts the address for an immutable split controller created with + /// recipients `accounts` with ownerships `percentAllocations` + /// and a keeper fee for splitting of `distributorFee` + /// @param accounts Ordered, unique list of addresses with ownership in the split + /// @param percentAllocations Percent allocations associated with each address + /// @param distributorFee Keeper fee paid by split to cover gas costs of distribution + /// @param deploymentSalt Salt to use to deploy + /// @return splitController Predicted address of such a split controller + function predictSplitControllerAddress( + address[] calldata accounts, + uint32[] calldata percentAllocations, + uint32 distributorFee, + bytes32 deploymentSalt + ) external view returns (address splitController) { + bytes memory data = _packSplitControllerData( + accounts, + percentAllocations, + distributorFee + ); + + splitController = address(controller).predictDeterministicAddress( + data, + deploymentSalt, + address(this) + ); + } + + /// @dev Packs split controller data + /// @param accounts Ordered, unique list of addresses with ownership in the split + /// @param percentAllocations Percent allocations associated with each address + /// @param distributorFee Keeper fee paid by split to cover gas costs of distribution + function _packSplitControllerData( + address[] calldata accounts, + uint32[] calldata percentAllocations, + uint32 distributorFee + ) internal view returns (bytes memory data) { + uint256 recipientsSize = accounts.length; + uint256[] memory recipients = new uint[](recipientsSize); + + uint i; + for(i; i < recipientsSize; ) { + recipients[i] = (uint256(percentAllocations[i]) << ADDRESS_BITS) | + uint256(uint160(accounts[i])); + + unchecked { + i++; + } + } + + data = abi.encodePacked( + splitMain, distributorFee, uint8(recipientsSize), recipients + ); + } + +} \ No newline at end of file diff --git a/src/interfaces/ISplitMain.sol b/src/interfaces/ISplitMain.sol index 1fe2e31..3e974c2 100644 --- a/src/interfaces/ISplitMain.sol +++ b/src/interfaces/ISplitMain.sol @@ -78,4 +78,16 @@ interface ISplitMain { /// @param withdrawETH Withdraw all ETH if nonzero /// @param tokens Addresses of ERC20s to withdraw function withdraw(address account, uint256 withdrawETH, ERC20[] calldata tokens) external; + + /// @notice Updates an existing split with recipients `accounts` with ownerships `percentAllocations` and a keeper fee for splitting of `distributorFee` + /// @param split Address of mutable split to update + /// @param accounts Ordered, unique list of addresses with ownership in the split + /// @param percentAllocations Percent allocations associated with each address + /// @param distributorFee Keeper fee paid by split to cover gas costs of distribution + function updateSplit( + address split, + address[] calldata accounts, + uint32[] calldata percentAllocations, + uint32 distributorFee + ) external; } diff --git a/src/test/controllers/IMSC.t.sol b/src/test/controllers/IMSC.t.sol new file mode 100644 index 0000000..c9cac65 --- /dev/null +++ b/src/test/controllers/IMSC.t.sol @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: MIT +pragma solidity =0.8.17; +import "forge-std/Test.sol"; +import { + ImmutableSplitControllerFactory, + ImmutableSplitController +} from "src/controllers/ImmutableSplitControllerFactory.sol"; +import {ISplitMain} from "src/interfaces/ISplitMain.sol"; + + +contract IMSC is Test { + + address internal SPLIT_MAIN_GOERLI = 0x2ed6c4B5dA6378c7897AC67Ba9e43102Feb694EE; + + ImmutableSplitControllerFactory public factory; + ImmutableSplitController public cntrlImpl; + + ImmutableSplitController public controller; + + address[] accounts; + uint32[] percentAllocations; + + address[] controllerAccounts; + uint32[] controllerPercentAllocations; + + function setUp() public { + uint256 goerliBlock = 8_529_931; + vm.createSelectFork(getChain("goerli").rpcUrl, goerliBlock); + + factory = new ImmutableSplitControllerFactory(SPLIT_MAIN_GOERLI); + cntrlImpl = factory.controller(); + + accounts = new address[](2); + accounts[0] = makeAddr("accounts0"); + accounts[1] = makeAddr("accounts1"); + + percentAllocations = new uint32[](2); + percentAllocations[0] = 400_000; + percentAllocations[1] = 600_000; + + controllerAccounts = new address[](3); + controllerAccounts[0] = makeAddr("accounts0"); + controllerAccounts[1] = makeAddr("accounts1"); + controllerAccounts[2] = makeAddr("accounts2"); + + controllerPercentAllocations = new uint32[](3); + controllerPercentAllocations[0] = 400_000; + controllerPercentAllocations[1] = 300_000; + controllerPercentAllocations[2] = 300_000; + + bytes32 deploymentSalt = keccak256(abi.encodePacked(uint256(64))); + + // predict controller address + address controller = factory.predictSplitControllerAddress( + split, + controllerAccounts, + controllerPercentAllocations, + 0, + keccak256(abi.encodePacked(uint256(64))) + ); + + address split = ISplitMain(SPLIT_MAIN_GOERLI).createSplit( + accounts, + percentAllocations, + 0, + controller + ); + + // deploy controller + + + } + +} \ No newline at end of file diff --git a/src/test/controllers/IMSCFactory.t.sol b/src/test/controllers/IMSCFactory.t.sol new file mode 100644 index 0000000..d8f48f0 --- /dev/null +++ b/src/test/controllers/IMSCFactory.t.sol @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: MIT +pragma solidity =0.8.17; +import "forge-std/Test.sol"; +import { + ImmutableSplitControllerFactory, + ImmutableSplitController +} from "src/controllers/ImmutableSplitControllerFactory.sol"; +import {ISplitMain} from "src/interfaces/ISplitMain.sol"; + + +contract IMSCFactory is Test { + + address internal SPLIT_MAIN_GOERLI = 0x2ed6c4B5dA6378c7897AC67Ba9e43102Feb694EE; + + ImmutableSplitControllerFactory public factory; + ImmutableSplitController public cntrlImpl; + + address[] accounts; + uint32[] percentAllocations; + + function setUp() public { + uint256 goerliBlock = 8_529_931; + vm.createSelectFork(getChain("goerli").rpcUrl, goerliBlock); + + factory = new ImmutableSplitControllerFactory(SPLIT_MAIN_GOERLI); + cntrlImpl = factory.controller(); + + accounts = new address[](2); + accounts[0] = makeAddr("accounts0"); + accounts[1] = makeAddr("accounts1"); + + percentAllocations = new uint32[](2); + percentAllocations[0] = 400_000; + percentAllocations[1] = 600_000; + } + + function test_createController() public { + address split = ISplitMain(SPLIT_MAIN_GOERLI).createSplit( + accounts, + percentAllocations, + 0, + address(0) + ); + + ImmutableSplitController controller = factory.createController( + split, + accounts, + percentAllocations, + 0, + keccak256(abi.encodePacked(uint256(12))) + ); + + address predictedAddress = factory.predictSplitControllerAddress( + accounts, + percentAllocations, + 0, + keccak256(abi.encodePacked(uint256(12))) + ); + + assertEq(address(controller), predictedAddress, "predicted_address_invalid"); + } + +} \ No newline at end of file From 3d4fda96761d88d979f50ce937b9c49baaaea0a4 Mon Sep 17 00:00:00 2001 From: samparsky <8148384+samparsky@users.noreply.github.com> Date: Tue, 29 Aug 2023 02:59:31 -0700 Subject: [PATCH 12/82] feat: udpate imsc implementation and tests --- src/controllers/ImmutableSplitController.sol | 21 +- .../ImmutableSplitControllerFactory.sol | 19 +- src/interfaces/ISplitMain.sol | 4 + src/interfaces/ISplitMainV2.sol | 2 + src/test/controllers/IMSC.t.sol | 190 +++++++++++++++++- src/test/controllers/IMSCFactory.t.sol | 149 +++++++++++++- 6 files changed, 356 insertions(+), 29 deletions(-) diff --git a/src/controllers/ImmutableSplitController.sol b/src/controllers/ImmutableSplitController.sol index 5298e93..9c48ec6 100644 --- a/src/controllers/ImmutableSplitController.sol +++ b/src/controllers/ImmutableSplitController.sol @@ -9,10 +9,6 @@ import {Clone} from "solady/utils/Clone.sol"; /// configuration is defined at deployment and cannot change contract ImmutableSplitController is Clone { - /// ----------------------------------------------------------------------- - /// errors - /// ----------------------------------------------------------------------- - /// @notice IMSC already initialized error Initialized(); @@ -54,30 +50,25 @@ contract ImmutableSplitController is Clone { constructor() {} - function init(address _split) external { - if (_split != address(0)) revert Initialized(); + function init(address splitAddress) external { + if (split != address(0)) revert Initialized(); - split = _split; + split = splitAddress; } /// Updates split with the hardcoded configuration /// @dev Updates split with stored split configuration function updateSplit() external payable { - // @TODO accept control - address _splitMain = splitMain(); - address _split = split; - uint32 _distributorFee = uint32(distributorFee()); - ( address[] memory accounts, uint32[] memory percentAllocations ) = getNewSplitConfiguration(); - ISplitMain(_splitMain).updateSplit( - _split, + ISplitMain(splitMain()).updateSplit( + split, accounts, percentAllocations, - _distributorFee + uint32(distributorFee()) ); } diff --git a/src/controllers/ImmutableSplitControllerFactory.sol b/src/controllers/ImmutableSplitControllerFactory.sol index 49f846b..79b384c 100644 --- a/src/controllers/ImmutableSplitControllerFactory.sol +++ b/src/controllers/ImmutableSplitControllerFactory.sol @@ -14,8 +14,8 @@ contract ImmutableSplitControllerFactory { /// @param accountsLength Length of accounts array /// @param allocationsLength Length of percentAllocations array error InvalidSplit__AccountsAndAllocationsMismatch( - uint256 accountsLength, - uint256 allocationsLength + uint256 accountsLength, + uint256 allocationsLength ); /// @notice Invalid percentAllocations sum `allocationsSum` must equal `PERCENTAGE_SCALE` /// @param allocationsSum Sum of percentAllocations array @@ -219,5 +219,20 @@ contract ImmutableSplitControllerFactory { splitMain, distributorFee, uint8(recipientsSize), recipients ); } + + /// @notice Sums array of uint32s + /// @param numbers Array of uint32s to sum + /// @return sum Sum of `numbers`. + function _getSum(uint32[] memory numbers) internal pure returns (uint32 sum) { + // overflow should be impossible in for-loop index + uint256 numbersLength = numbers.length; + for (uint256 i = 0; i < numbersLength; ) { + sum += numbers[i]; + unchecked { + // overflow should be impossible in for-loop index + ++i; + } + } + } } \ No newline at end of file diff --git a/src/interfaces/ISplitMain.sol b/src/interfaces/ISplitMain.sol index 3e974c2..c4b1a36 100644 --- a/src/interfaces/ISplitMain.sol +++ b/src/interfaces/ISplitMain.sol @@ -90,4 +90,8 @@ interface ISplitMain { uint32[] calldata percentAllocations, uint32 distributorFee ) external; + + + function getHash(address split) external view returns (bytes32); + } diff --git a/src/interfaces/ISplitMainV2.sol b/src/interfaces/ISplitMainV2.sol index da11571..460914c 100644 --- a/src/interfaces/ISplitMainV2.sol +++ b/src/interfaces/ISplitMainV2.sol @@ -104,6 +104,8 @@ interface ISplitMainV2 { /// @param tokens Addresses of ERC20s to withdraw function withdraw(address account, uint256 withdrawETH, ERC20[] calldata tokens) external; + + /** * EVENTS */ diff --git a/src/test/controllers/IMSC.t.sol b/src/test/controllers/IMSC.t.sol index c9cac65..624d1bb 100644 --- a/src/test/controllers/IMSC.t.sol +++ b/src/test/controllers/IMSC.t.sol @@ -9,8 +9,10 @@ import {ISplitMain} from "src/interfaces/ISplitMain.sol"; contract IMSC is Test { + error Initialized(); address internal SPLIT_MAIN_GOERLI = 0x2ed6c4B5dA6378c7897AC67Ba9e43102Feb694EE; + uint256 public constant PERCENTAGE_SCALE = 1e6; ImmutableSplitControllerFactory public factory; ImmutableSplitController public cntrlImpl; @@ -23,6 +25,8 @@ contract IMSC is Test { address[] controllerAccounts; uint32[] controllerPercentAllocations; + address split; + function setUp() public { uint256 goerliBlock = 8_529_931; vm.createSelectFork(getChain("goerli").rpcUrl, goerliBlock); @@ -41,7 +45,7 @@ contract IMSC is Test { controllerAccounts = new address[](3); controllerAccounts[0] = makeAddr("accounts0"); controllerAccounts[1] = makeAddr("accounts1"); - controllerAccounts[2] = makeAddr("accounts2"); + controllerAccounts[2] = makeAddr("accounts3"); controllerPercentAllocations = new uint32[](3); controllerPercentAllocations[0] = 400_000; @@ -51,24 +55,198 @@ contract IMSC is Test { bytes32 deploymentSalt = keccak256(abi.encodePacked(uint256(64))); // predict controller address - address controller = factory.predictSplitControllerAddress( - split, + address predictedControllerAddress = factory.predictSplitControllerAddress( controllerAccounts, controllerPercentAllocations, 0, - keccak256(abi.encodePacked(uint256(64))) + deploymentSalt ); - address split = ISplitMain(SPLIT_MAIN_GOERLI).createSplit( + split = ISplitMain(SPLIT_MAIN_GOERLI).createSplit( accounts, percentAllocations, 0, - controller + predictedControllerAddress ); // deploy controller + controller = factory.createController( + split, + controllerAccounts, + controllerPercentAllocations, + 0, + deploymentSalt + ); + } + + + function testCannot_DoubleInitialiseIMSC() public { + vm.expectRevert( + Initialized.selector + ); + + controller.init(address(0x3)); + } + + function testCan_getSplitMain() public { + assertEq(controller.splitMain(), SPLIT_MAIN_GOERLI, "valid splitMain address"); + } + + function testCan_getDistributorFee() public { + assertEq(controller.distributorFee(), 0 , "invalid distributor fee"); + + uint32 maxDistributorFee = 1e5; + + ImmutableSplitController customController = factory.createController( + split, + controllerAccounts, + controllerPercentAllocations, + maxDistributorFee, + keccak256(abi.encodePacked(uint256(640))) + ); + + assertEq( + customController.distributorFee(), + maxDistributorFee, + "invalid distributor fee" + ); + } + + function testCan_getSplitConfiguration() public { + ( + address[] memory localAccounts, + uint32[] memory localPercentAllocations + ) = controller.getNewSplitConfiguration(); + + assertEq( + localAccounts, controllerAccounts, "invalid accounts" + ); + + assertEq( + localPercentAllocations.length, + controllerPercentAllocations.length, + "unequal length percent allocations" + ); + + for(uint i; i < localPercentAllocations.length; i++) { + assertEq( + uint256(localPercentAllocations[i]), + uint256(controllerPercentAllocations[i]), + "invalid percentAllocations" + ); + } + } + + function testCan_getSplit() public { + assertEq( + controller.split(), + split + ); + } + + function testCan_updateSplit() public { + controller.updateSplit(); + + assertEq( + ISplitMain(SPLIT_MAIN_GOERLI).getHash(split), + _hashSplit(controllerAccounts, controllerPercentAllocations, 0), + "invalid split hash" + ); + } + + function testFuzz_updateSplit( + uint256 splitSeed, + uint256 controllerSeed, + uint8 splitSize, + uint8 controllerSize + ) public { + vm.assume(splitSeed != controllerSeed); + vm.assume(splitSize > 1); + vm.assume(controllerSize > 1); + + address[] memory splitterAccts = _generateAddresses(splitSeed, splitSize); + address[] memory ctrllerAccounts = _generateAddresses(controllerSeed, controllerSize); + + uint32[] memory splitterPercentAlloc = _generatePercentAlloc(splitSize); + uint32[] memory ctrllerPercentAlloc = _generatePercentAlloc(controllerSize); + + bytes32 deploymentSalt = keccak256(abi.encodePacked(uint256(604))); + + // predict controller address + address predictedControllerAddress = factory.predictSplitControllerAddress( + ctrllerAccounts, + ctrllerPercentAlloc, + 0, + deploymentSalt + ); + + // create split + address fuzzSplit = ISplitMain(SPLIT_MAIN_GOERLI).createSplit( + splitterAccts, + splitterPercentAlloc, + 0, + predictedControllerAddress + ); + + // create controller + controller = factory.createController( + fuzzSplit, + ctrllerAccounts, + ctrllerPercentAlloc, + 0, + deploymentSalt + ); + + // get current split hash + bytes32 currentSplitHash = ISplitMain(SPLIT_MAIN_GOERLI).getHash(fuzzSplit); + // update split + controller.updateSplit(); + + bytes32 newSplitHash = ISplitMain(SPLIT_MAIN_GOERLI).getHash(fuzzSplit); + + bytes32 calculatedSplitHash = _hashSplit( + ctrllerAccounts, + ctrllerPercentAlloc, + 0 + ); + + assertTrue(currentSplitHash != newSplitHash, "update split hash"); + assertEq(calculatedSplitHash, newSplitHash, "split hash equal"); + } + + function _hashSplit( + address[] memory accts, + uint32[] memory percentAlloc, + uint32 distributorFee + ) internal pure returns (bytes32) { + return keccak256(abi.encodePacked(accts, percentAlloc, distributorFee)); + } + + + function _generateAddresses( + uint256 _seed, + uint256 size + ) internal pure returns (address[] memory accts) { + accts = new address[](size); + uint160 seed = uint160(uint256(keccak256(abi.encodePacked(_seed)))); + for(uint160 i ; i < size; i++) { + accts[i] = address(seed); + seed += 1; + } + } + + function _generatePercentAlloc( + uint256 size + ) internal pure returns (uint32[] memory alloc) { + alloc = new uint32[](size); + for (uint i; i < size; i++) { + alloc[i] = uint32(PERCENTAGE_SCALE / size); + } + if (PERCENTAGE_SCALE % size != 0) { + alloc[size - 1] += uint32(PERCENTAGE_SCALE % size); + } } } \ No newline at end of file diff --git a/src/test/controllers/IMSCFactory.t.sol b/src/test/controllers/IMSCFactory.t.sol index d8f48f0..6fdf927 100644 --- a/src/test/controllers/IMSCFactory.t.sol +++ b/src/test/controllers/IMSCFactory.t.sol @@ -10,7 +10,19 @@ import {ISplitMain} from "src/interfaces/ISplitMain.sol"; contract IMSCFactory is Test { + error InvalidSplit__TooFewAccounts(uint256 accountsLength); + error InvalidSplit__AccountsAndAllocationsMismatch( + uint256 accountsLength, + uint256 allocationsLength + ); + error InvalidSplit__InvalidAllocationsSum(uint32 allocationsSum); + error InvalidSplit__AccountsOutOfOrder(uint256 index); + error InvalidSplit__AllocationMustBePositive(uint256 index); + error InvalidSplit__InvalidDistributorFee(uint32 distributorFee); + + address internal SPLIT_MAIN_GOERLI = 0x2ed6c4B5dA6378c7897AC67Ba9e43102Feb694EE; + uint32 public constant SPLIT_MAIN_PERCENTAGE_SCALE = 1e6; ImmutableSplitControllerFactory public factory; ImmutableSplitController public cntrlImpl; @@ -33,28 +45,153 @@ contract IMSCFactory is Test { percentAllocations[0] = 400_000; percentAllocations[1] = 600_000; } + + function test_RevertIfAccountSizeIsOne() public { + address[] memory newAccounts = new address[](1); + newAccounts[0] = makeAddr("testRevertIfAccountSizeIsOne"); - function test_createController() public { - address split = ISplitMain(SPLIT_MAIN_GOERLI).createSplit( + vm.expectRevert( + abi.encodeWithSelector(InvalidSplit__TooFewAccounts.selector, newAccounts.length) + ); + + factory.createController( + address(1), + newAccounts, + percentAllocations, + 0, + keccak256(abi.encodePacked(uint256(12))) + ); + } + + function test_RevertIfAccountAndAllocationMismatch() public { + uint32[] memory newPercentAllocations = new uint32[](3); + newPercentAllocations[0] = 200_000; + newPercentAllocations[1] = 200_000; + newPercentAllocations[2] = 600_000; + + vm.expectRevert( + abi.encodeWithSelector( + InvalidSplit__AccountsAndAllocationsMismatch.selector, + accounts.length, + newPercentAllocations.length + ) + ); + + factory.createController( + address(1), accounts, + newPercentAllocations, + 0, + keccak256(abi.encodePacked(uint256(12))) + ); + } + + + function test_RevertIfAccountOutOfOrder() public { + address[] memory newAccounts = new address[](2); + newAccounts[0] = address(0x4); + newAccounts[1] = address(0x1); + + vm.expectRevert( + abi.encodeWithSelector( + InvalidSplit__AccountsOutOfOrder.selector, + 0 + ) + ); + + factory.createController( + address(1), + newAccounts, percentAllocations, 0, - address(0) + keccak256(abi.encodePacked(uint256(12))) ); + } - ImmutableSplitController controller = factory.createController( - split, + function test_RevertIfZeroPercentAllocation() public { + uint32[] memory newPercentAllocations = new uint32[](2); + newPercentAllocations[0] = SPLIT_MAIN_PERCENTAGE_SCALE; + newPercentAllocations[1] = 0; + + vm.expectRevert( + abi.encodeWithSelector( + InvalidSplit__AllocationMustBePositive.selector, + 1 + ) + ); + + factory.createController( + address(1), + accounts, + newPercentAllocations, + 0, + keccak256(abi.encodePacked(uint256(12))) + ); + } + + function test_RevertIfInvalidDistributorFee() public { + uint32 invalidDistributorFee = 1e6; + + vm.expectRevert( + abi.encodeWithSelector( + InvalidSplit__InvalidDistributorFee.selector, + invalidDistributorFee + ) + ); + + factory.createController( + address(1), accounts, percentAllocations, + invalidDistributorFee, + keccak256(abi.encodePacked(uint256(12))) + ); + } + + function test_RevertIfInvalidAllocationSum() public { + uint32[] memory newPercentAllocations = new uint32[](2); + newPercentAllocations[0] = SPLIT_MAIN_PERCENTAGE_SCALE; + newPercentAllocations[1] = 1; + + vm.expectRevert( + abi.encodeWithSelector( + InvalidSplit__InvalidAllocationsSum.selector, + SPLIT_MAIN_PERCENTAGE_SCALE + 1 + ) + ); + + factory.createController( + address(1), + accounts, + newPercentAllocations, 0, keccak256(abi.encodePacked(uint256(12))) ); + } + + function test_CanCreateController() public { + bytes32 deploymentSalt = keccak256(abi.encodePacked(uint256(1102))); address predictedAddress = factory.predictSplitControllerAddress( accounts, percentAllocations, 0, - keccak256(abi.encodePacked(uint256(12))) + deploymentSalt + ); + + address split = ISplitMain(SPLIT_MAIN_GOERLI).createSplit( + accounts, + percentAllocations, + 0, + predictedAddress + ); + + ImmutableSplitController controller = factory.createController( + split, + accounts, + percentAllocations, + 0, + deploymentSalt ); assertEq(address(controller), predictedAddress, "predicted_address_invalid"); From f825942acb531b5922504845f982134289b056ad Mon Sep 17 00:00:00 2001 From: samparsky <8148384+samparsky@users.noreply.github.com> Date: Tue, 29 Aug 2023 03:05:09 -0700 Subject: [PATCH 13/82] chore: remove commented code, update natspec --- src/controllers/ImmutableSplitController.sol | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/src/controllers/ImmutableSplitController.sol b/src/controllers/ImmutableSplitController.sol index 9c48ec6..e2dcd5e 100644 --- a/src/controllers/ImmutableSplitController.sol +++ b/src/controllers/ImmutableSplitController.sol @@ -29,22 +29,20 @@ contract ImmutableSplitController is Clone { // splitMain (address, 20 bytes) // 0; first item uint256 internal constant SPLIT_MAIN_OFFSET = 0; - // // split (address, 20 bytes) - // // 1; second item - // uint256 internal constant SPLIT_ADDRESS_OFFSET = 20; // distributorFee (uint32, 4 bytes) - // 2; third item + // 1; second item uint256 internal constant DISTRIBUTOR_FEE_OFFSET = 20; // recipeints size (uint8, 1 byte ) - // 3; fourth item + // 2; third item uint256 internal constant RECIPIENTS_SIZE_OFFSET = 24; // recipients data () - // 4; fifth item + // 4; fourth item uint256 internal constant RECIPIENTS_OFFSET = 25; /// ----------------------------------------------------------------------- /// storage - mutable /// ----------------------------------------------------------------------- + /// @dev Address of split to update address public split; @@ -78,12 +76,6 @@ contract ImmutableSplitController is Clone { return _getArgAddress(SPLIT_MAIN_OFFSET); } - // /// Address of split to update - // /// @dev equivalent to address public immutable split; - // function split() public pure returns (address) { - // return _getArgAddress(SPLIT_ADDRESS_OFFSET); - // } - /// Fee charged by distributor /// @dev equivalent to address public immutable distributorFee; function distributorFee() public pure returns(uint256) { From 6862e4e1756ab9a2753d831b9c6c06852fa1474b Mon Sep 17 00:00:00 2001 From: samparsky <8148384+samparsky@users.noreply.github.com> Date: Mon, 11 Sep 2023 11:10:11 +0100 Subject: [PATCH 14/82] chore: add owner access to update split function --- src/controllers/ImmutableSplitController.sol | 22 ++++++++-- .../ImmutableSplitControllerFactory.sol | 40 +++++++++++-------- 2 files changed, 42 insertions(+), 20 deletions(-) diff --git a/src/controllers/ImmutableSplitController.sol b/src/controllers/ImmutableSplitController.sol index e2dcd5e..7c58ab1 100644 --- a/src/controllers/ImmutableSplitController.sol +++ b/src/controllers/ImmutableSplitController.sol @@ -12,6 +12,9 @@ contract ImmutableSplitController is Clone { /// @notice IMSC already initialized error Initialized(); + /// @notice + error Unauthorized(); + /// ----------------------------------------------------------------------- /// storage /// ----------------------------------------------------------------------- @@ -32,12 +35,15 @@ contract ImmutableSplitController is Clone { // distributorFee (uint32, 4 bytes) // 1; second item uint256 internal constant DISTRIBUTOR_FEE_OFFSET = 20; - // recipeints size (uint8, 1 byte ) + // onwer (address, 20 bytes) // 2; third item - uint256 internal constant RECIPIENTS_SIZE_OFFSET = 24; + uint256 internal constant OWNER_OFFSET = 24; + // recipeints size (uint8, 1 byte ) + // 3; third item + uint256 internal constant RECIPIENTS_SIZE_OFFSET = 44; // recipients data () // 4; fourth item - uint256 internal constant RECIPIENTS_OFFSET = 25; + uint256 internal constant RECIPIENTS_OFFSET = 45; /// ----------------------------------------------------------------------- /// storage - mutable @@ -57,6 +63,10 @@ contract ImmutableSplitController is Clone { /// Updates split with the hardcoded configuration /// @dev Updates split with stored split configuration function updateSplit() external payable { + if (msg.sender != owner()) { + revert Unauthorized(); + } + ( address[] memory accounts, uint32[] memory percentAllocations @@ -82,6 +92,12 @@ contract ImmutableSplitController is Clone { return _getArgUint32(DISTRIBUTOR_FEE_OFFSET); } + /// Adress of owner + /// @dev equivalent to address public immutable owner; + function owner() public pure returns (address) { + return _getArgAddress(OWNER_OFFSET); + } + // Returns unpacked recipients /// @return accounts Addresses to receive payments /// @return percentAllocations Percentage share for split accounts diff --git a/src/controllers/ImmutableSplitControllerFactory.sol b/src/controllers/ImmutableSplitControllerFactory.sol index 79b384c..e3a053e 100644 --- a/src/controllers/ImmutableSplitControllerFactory.sol +++ b/src/controllers/ImmutableSplitControllerFactory.sol @@ -43,12 +43,14 @@ contract ImmutableSplitControllerFactory { /// Emitted after a new IMSC is deployed /// @param controller Address of newly created IMSC clone /// @param split Address of split + /// @param owner Adderss of the owner of the controller /// @param accounts Addresses of /// @param percentAllocations Addresses to recover non-waterfall tokens to /// @param distributorFee Amount of event CreateIMSC( address indexed controller, address indexed split, + address owner, address[] accounts, uint32[] percentAllocations, uint256 distributorFee @@ -128,12 +130,14 @@ contract ImmutableSplitControllerFactory { /// Deploys a new immutable controller /// @dev Create a new immutable split controller /// @param split Address of the split to create a controller for + /// @param owner Address that can call the updateSplit(..) function /// @param accounts Ordered, unique list of addresses with ownership in the split /// @param percentAllocations Percent allocations associated with each address /// @param distributorFee Distributor fee share /// @param deploymentSalt salt to use for deterministic deploy function createController( address split, + address owner, address[] calldata accounts, uint32[] calldata percentAllocations, uint32 distributorFee, @@ -142,16 +146,16 @@ contract ImmutableSplitControllerFactory { external validSplit(accounts, percentAllocations, distributorFee) returns (ImmutableSplitController newController) { - // ensure it's valid split - - bytes memory data = _packSplitControllerData( - accounts, - percentAllocations, - distributorFee - ); - newController = ImmutableSplitController( - address(controller).cloneDeterministic(data, deploymentSalt) + address(controller).cloneDeterministic( + _packSplitControllerData( + owner, + accounts, + percentAllocations, + distributorFee + ), + deploymentSalt + ) ); // initialize with split address @@ -160,6 +164,7 @@ contract ImmutableSplitControllerFactory { emit CreateIMSC( address(controller), split, + owner, accounts, percentAllocations, distributorFee @@ -175,19 +180,19 @@ contract ImmutableSplitControllerFactory { /// @param deploymentSalt Salt to use to deploy /// @return splitController Predicted address of such a split controller function predictSplitControllerAddress( + address owner, address[] calldata accounts, uint32[] calldata percentAllocations, uint32 distributorFee, bytes32 deploymentSalt ) external view returns (address splitController) { - bytes memory data = _packSplitControllerData( - accounts, - percentAllocations, - distributorFee - ); - splitController = address(controller).predictDeterministicAddress( - data, + _packSplitControllerData( + owner, + accounts, + percentAllocations, + distributorFee + ), deploymentSalt, address(this) ); @@ -198,6 +203,7 @@ contract ImmutableSplitControllerFactory { /// @param percentAllocations Percent allocations associated with each address /// @param distributorFee Keeper fee paid by split to cover gas costs of distribution function _packSplitControllerData( + address owner, address[] calldata accounts, uint32[] calldata percentAllocations, uint32 distributorFee @@ -216,7 +222,7 @@ contract ImmutableSplitControllerFactory { } data = abi.encodePacked( - splitMain, distributorFee, uint8(recipientsSize), recipients + splitMain, distributorFee, owner, uint8(recipientsSize), recipients ); } From 72bea10c812c6c2076f6f1cccac01eb594e60758 Mon Sep 17 00:00:00 2001 From: samparsky <8148384+samparsky@users.noreply.github.com> Date: Mon, 11 Sep 2023 11:30:13 +0100 Subject: [PATCH 15/82] chore: update tests to include owner --- .../ImmutableSplitControllerFactory.sol | 13 ++++++++++ src/test/controllers/IMSC.t.sol | 25 +++++++++++++++++++ src/test/controllers/IMSCFactory.t.sol | 12 +++++++++ 3 files changed, 50 insertions(+) diff --git a/src/controllers/ImmutableSplitControllerFactory.sol b/src/controllers/ImmutableSplitControllerFactory.sol index e3a053e..c2bb0c7 100644 --- a/src/controllers/ImmutableSplitControllerFactory.sol +++ b/src/controllers/ImmutableSplitControllerFactory.sol @@ -9,6 +9,11 @@ import {ImmutableSplitController} from "./ImmutableSplitController.sol"; /// @dev Deploys ImmutableSplitController cheaply using cwia clones contract ImmutableSplitControllerFactory { + /// @dev invalid owner address + error Invalid_Owner(); + /// @dev invalid split address + error InvalidSplit_Address(); + /// @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 @@ -146,6 +151,14 @@ contract ImmutableSplitControllerFactory { external validSplit(accounts, percentAllocations, distributorFee) returns (ImmutableSplitController newController) { + if (split == address(0)) { + revert Invalid_Owner(); + } + + if (owner == address(0)) { + revert InvalidSplit_Address(); + } + newController = ImmutableSplitController( address(controller).cloneDeterministic( _packSplitControllerData( diff --git a/src/test/controllers/IMSC.t.sol b/src/test/controllers/IMSC.t.sol index 624d1bb..45b44b6 100644 --- a/src/test/controllers/IMSC.t.sol +++ b/src/test/controllers/IMSC.t.sol @@ -10,6 +10,7 @@ import {ISplitMain} from "src/interfaces/ISplitMain.sol"; contract IMSC is Test { error Initialized(); + error Unauthorized(); address internal SPLIT_MAIN_GOERLI = 0x2ed6c4B5dA6378c7897AC67Ba9e43102Feb694EE; uint256 public constant PERCENTAGE_SCALE = 1e6; @@ -26,6 +27,7 @@ contract IMSC is Test { uint32[] controllerPercentAllocations; address split; + address owner; function setUp() public { uint256 goerliBlock = 8_529_931; @@ -38,6 +40,8 @@ contract IMSC is Test { accounts[0] = makeAddr("accounts0"); accounts[1] = makeAddr("accounts1"); + owner = makeAddr("accounts3"); + percentAllocations = new uint32[](2); percentAllocations[0] = 400_000; percentAllocations[1] = 600_000; @@ -56,6 +60,7 @@ contract IMSC is Test { // predict controller address address predictedControllerAddress = factory.predictSplitControllerAddress( + owner, controllerAccounts, controllerPercentAllocations, 0, @@ -72,6 +77,7 @@ contract IMSC is Test { // deploy controller controller = factory.createController( split, + owner, controllerAccounts, controllerPercentAllocations, 0, @@ -92,6 +98,13 @@ contract IMSC is Test { assertEq(controller.splitMain(), SPLIT_MAIN_GOERLI, "valid splitMain address"); } + function testCan_getOwner() public { + assertEq( + controller.owner(), + owner, + "valid controller owner" + ); + } function testCan_getDistributorFee() public { assertEq(controller.distributorFee(), 0 , "invalid distributor fee"); @@ -99,6 +112,7 @@ contract IMSC is Test { ImmutableSplitController customController = factory.createController( split, + owner, controllerAccounts, controllerPercentAllocations, maxDistributorFee, @@ -144,7 +158,13 @@ contract IMSC is Test { ); } + function testCannot_updateSplitIfNonOwner() public { + vm.expectRevert(Unauthorized.selector); + controller.updateSplit(); + } + function testCan_updateSplit() public { + vm.prank(owner); controller.updateSplit(); assertEq( @@ -155,11 +175,13 @@ contract IMSC is Test { } function testFuzz_updateSplit( + address ownerAddress, uint256 splitSeed, uint256 controllerSeed, uint8 splitSize, uint8 controllerSize ) public { + vm.assume (ownerAddress != address(0)); vm.assume(splitSeed != controllerSeed); vm.assume(splitSize > 1); vm.assume(controllerSize > 1); @@ -174,6 +196,7 @@ contract IMSC is Test { // predict controller address address predictedControllerAddress = factory.predictSplitControllerAddress( + ownerAddress, ctrllerAccounts, ctrllerPercentAlloc, 0, @@ -191,6 +214,7 @@ contract IMSC is Test { // create controller controller = factory.createController( fuzzSplit, + ownerAddress, ctrllerAccounts, ctrllerPercentAlloc, 0, @@ -200,6 +224,7 @@ contract IMSC is Test { // get current split hash bytes32 currentSplitHash = ISplitMain(SPLIT_MAIN_GOERLI).getHash(fuzzSplit); // update split + vm.prank(ownerAddress); controller.updateSplit(); bytes32 newSplitHash = ISplitMain(SPLIT_MAIN_GOERLI).getHash(fuzzSplit); diff --git a/src/test/controllers/IMSCFactory.t.sol b/src/test/controllers/IMSCFactory.t.sol index 6fdf927..22adfbf 100644 --- a/src/test/controllers/IMSCFactory.t.sol +++ b/src/test/controllers/IMSCFactory.t.sol @@ -27,6 +27,8 @@ contract IMSCFactory is Test { ImmutableSplitControllerFactory public factory; ImmutableSplitController public cntrlImpl; + address owner; + address[] accounts; uint32[] percentAllocations; @@ -44,6 +46,8 @@ contract IMSCFactory is Test { percentAllocations = new uint32[](2); percentAllocations[0] = 400_000; percentAllocations[1] = 600_000; + + owner = makeAddr("owner"); } function test_RevertIfAccountSizeIsOne() public { @@ -56,6 +60,7 @@ contract IMSCFactory is Test { factory.createController( address(1), + owner, newAccounts, percentAllocations, 0, @@ -79,6 +84,7 @@ contract IMSCFactory is Test { factory.createController( address(1), + owner, accounts, newPercentAllocations, 0, @@ -101,6 +107,7 @@ contract IMSCFactory is Test { factory.createController( address(1), + owner, newAccounts, percentAllocations, 0, @@ -122,6 +129,7 @@ contract IMSCFactory is Test { factory.createController( address(1), + owner, accounts, newPercentAllocations, 0, @@ -141,6 +149,7 @@ contract IMSCFactory is Test { factory.createController( address(1), + owner, accounts, percentAllocations, invalidDistributorFee, @@ -162,6 +171,7 @@ contract IMSCFactory is Test { factory.createController( address(1), + owner, accounts, newPercentAllocations, 0, @@ -173,6 +183,7 @@ contract IMSCFactory is Test { bytes32 deploymentSalt = keccak256(abi.encodePacked(uint256(1102))); address predictedAddress = factory.predictSplitControllerAddress( + owner, accounts, percentAllocations, 0, @@ -188,6 +199,7 @@ contract IMSCFactory is Test { ImmutableSplitController controller = factory.createController( split, + owner, accounts, percentAllocations, 0, From 63bead35eda8e73571a2504c56312749acf8910a Mon Sep 17 00:00:00 2001 From: samparsky <8148384+samparsky@users.noreply.github.com> Date: Mon, 11 Sep 2023 11:35:47 +0100 Subject: [PATCH 16/82] test: add revert owner,split address checks --- src/test/controllers/IMSCFactory.t.sol | 28 ++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/test/controllers/IMSCFactory.t.sol b/src/test/controllers/IMSCFactory.t.sol index 22adfbf..15280bb 100644 --- a/src/test/controllers/IMSCFactory.t.sol +++ b/src/test/controllers/IMSCFactory.t.sol @@ -10,6 +10,8 @@ import {ISplitMain} from "src/interfaces/ISplitMain.sol"; contract IMSCFactory is Test { + error Invalid_Owner(); + error InvalidSplit_Address(); error InvalidSplit__TooFewAccounts(uint256 accountsLength); error InvalidSplit__AccountsAndAllocationsMismatch( uint256 accountsLength, @@ -179,6 +181,32 @@ contract IMSCFactory is Test { ); } + function test_RevertIfInvalidOwner() public { + vm.expectRevert(Invalid_Owner.selector); + + factory.createController( + address(1), + address(0), + accounts, + percentAllocations, + 0, + keccak256(abi.encodePacked(uint256(123))) + ); + } + + function test_RevertIfInvalidSplitAddress() public { + vm.expectRevert(InvalidSplit_Address.selector); + + factory.createController( + address(0), + address(1), + accounts, + percentAllocations, + 0, + keccak256(abi.encodePacked(uint256(123))) + ); + } + function test_CanCreateController() public { bytes32 deploymentSalt = keccak256(abi.encodePacked(uint256(1102))); From dbeddc1a2d6828e7c4b23225e050919845bd86ad Mon Sep 17 00:00:00 2001 From: samparsky <8148384+samparsky@users.noreply.github.com> Date: Mon, 11 Sep 2023 11:48:13 +0100 Subject: [PATCH 17/82] chore: fix error imsc factory --- src/controllers/ImmutableSplitControllerFactory.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/controllers/ImmutableSplitControllerFactory.sol b/src/controllers/ImmutableSplitControllerFactory.sol index c2bb0c7..1a31a55 100644 --- a/src/controllers/ImmutableSplitControllerFactory.sol +++ b/src/controllers/ImmutableSplitControllerFactory.sol @@ -152,11 +152,11 @@ contract ImmutableSplitControllerFactory { returns (ImmutableSplitController newController) { if (split == address(0)) { - revert Invalid_Owner(); + revert InvalidSplit_Address(); } if (owner == address(0)) { - revert InvalidSplit_Address(); + revert Invalid_Owner(); } newController = ImmutableSplitController( From eaa578c883a724cabdabf7094aa2e196dbe99a47 Mon Sep 17 00:00:00 2001 From: samparsky <8148384+samparsky@users.noreply.github.com> Date: Mon, 11 Sep 2023 11:49:39 +0100 Subject: [PATCH 18/82] test: add owner address check to imsc fuzz test --- src/test/controllers/IMSC.t.sol | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/test/controllers/IMSC.t.sol b/src/test/controllers/IMSC.t.sol index 45b44b6..e489077 100644 --- a/src/test/controllers/IMSC.t.sol +++ b/src/test/controllers/IMSC.t.sol @@ -181,7 +181,7 @@ contract IMSC is Test { uint8 splitSize, uint8 controllerSize ) public { - vm.assume (ownerAddress != address(0)); + vm.assume(ownerAddress != address(0)); vm.assume(splitSeed != controllerSeed); vm.assume(splitSize > 1); vm.assume(controllerSize > 1); @@ -221,6 +221,8 @@ contract IMSC is Test { deploymentSalt ); + assertEq(controller.owner(), ownerAddress, "invalid owner address"); + // get current split hash bytes32 currentSplitHash = ISplitMain(SPLIT_MAIN_GOERLI).getHash(fuzzSplit); // update split From 168081de1a7081af69134f933954ea0281b5f8e1 Mon Sep 17 00:00:00 2001 From: samparsky <8148384+samparsky@users.noreply.github.com> Date: Wed, 13 Sep 2023 12:49:07 +0100 Subject: [PATCH 19/82] chore: add imsc deployment script --- script/IMSCFactory.s.sol | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 script/IMSCFactory.s.sol diff --git a/script/IMSCFactory.s.sol b/script/IMSCFactory.s.sol new file mode 100644 index 0000000..f005b1f --- /dev/null +++ b/script/IMSCFactory.s.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: MIT +pragma solidity =0.8.17; + +import "forge-std/Script.sol"; +import {ImmutableSplitControllerFactory} from "src/controllers/ImmutableSplitControllerFactory.sol"; + +contract IMSCFactoryScript is Script { + function run(address splitMain) external { + uint256 privKey = vm.envUint("PRIVATE_KEY"); + vm.startBroadcast(privKey); + + new ImmutableSplitControllerFactory{salt: keccak256("obol.imsc.v1")}(splitMain); + vm.stopBroadcast(); + } +} From c07ddd08f65ef7840a485b306d31d4eabfdef670 Mon Sep 17 00:00:00 2001 From: samparsky <8148384+samparsky@users.noreply.github.com> Date: Wed, 13 Sep 2023 13:31:40 +0100 Subject: [PATCH 20/82] chore: add imscfactory deployment script --- script/IMSCFactory.s.sol | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/script/IMSCFactory.s.sol b/script/IMSCFactory.s.sol index f005b1f..3c55b73 100644 --- a/script/IMSCFactory.s.sol +++ b/script/IMSCFactory.s.sol @@ -5,11 +5,14 @@ import "forge-std/Script.sol"; import {ImmutableSplitControllerFactory} from "src/controllers/ImmutableSplitControllerFactory.sol"; contract IMSCFactoryScript is Script { - function run(address splitMain) external { + function run(address) external { + address SPLIT_MAIN_GOERLI = 0x2ed6c4B5dA6378c7897AC67Ba9e43102Feb694EE; + uint256 privKey = vm.envUint("PRIVATE_KEY"); vm.startBroadcast(privKey); - new ImmutableSplitControllerFactory{salt: keccak256("obol.imsc.v1")}(splitMain); + new ImmutableSplitControllerFactory{salt: keccak256("obol.imsc.v1")}(SPLIT_MAIN_GOERLI); + vm.stopBroadcast(); } } From 6d214970e1f44e1f9dd606da8da6b6e1c4c17bc0 Mon Sep 17 00:00:00 2001 From: samparsky <8148384+samparsky@users.noreply.github.com> Date: Wed, 13 Sep 2023 13:34:41 +0100 Subject: [PATCH 21/82] chore: add deployment addresses to readme --- README.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 3ac36ca..433952e 100644 --- a/README.md +++ b/README.md @@ -69,9 +69,13 @@ This repo can be deployed with `forge create` or running the deployment scripts. #### Goerli -OptimisticWithdrawalRecipient: https://goerli.etherscan.io/address/0x7b4b2cbbdac4f9b70252503a411574f4ff960e68 +OptimisticWithdrawalRecipient: https://goerli.etherscan.io/address/0xBB3b6CC2882B73eeFb32284Ecc9035839fb2C908 -OptimisticWithdrawalRecipientFactory: https://goerli.etherscan.io/address/0xBB3b6CC2882B73eeFb32284Ecc9035839fb2C908 +OptimisticWithdrawalRecipientFactory: https://goerli.etherscan.io/address/0x7b4b2cbbdac4f9b70252503a411574f4ff960e68 + +ImmutableSplitControllerFactory: https://goerli.etherscan.io/address/0x64a2c4A50B1f46c3e2bF753CFe270ceB18b5e18f + +ImmutableSplitController: https://goerli.etherscan.io/address/0x009894cdA6cB6d99866ca8E04e8EDeabd625712F ### Versioning From f7a172a2d61f61e4fc6a3757769c0925521f3ee0 Mon Sep 17 00:00:00 2001 From: samparsky <8148384+samparsky@users.noreply.github.com> Date: Mon, 11 Sep 2023 12:00:31 +0100 Subject: [PATCH 22/82] chore: remove array createOWRecipient --- src/owr/OptimisticWithdrawalRecipientFactory.sol | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/owr/OptimisticWithdrawalRecipientFactory.sol b/src/owr/OptimisticWithdrawalRecipientFactory.sol index de26be4..b644f15 100644 --- a/src/owr/OptimisticWithdrawalRecipientFactory.sol +++ b/src/owr/OptimisticWithdrawalRecipientFactory.sol @@ -108,15 +108,12 @@ contract OptimisticWithdrawalRecipientFactory { if (threshold > type(uint96).max) revert Invalid__ThresholdTooLarge(threshold); /// effects - - // copy recipients & threshold into storage - uint256[] memory tranches = new uint256[](RECIPIENT_SIZE); - // tranches size == recipients array size - tranches[0] = (threshold << ADDRESS_BITS) | uint256(uint160(principalRecipient)); - tranches[1] = uint256(uint160(rewardRecipient)); + uint256 principalData = (threshold << ADDRESS_BITS) | uint256(uint160(principalRecipient)); + uint256 rewardData = uint256(uint160(rewardRecipient)); // would not exceed contract size limits - bytes memory data = abi.encodePacked(token, recoveryAddress, tranches); + // important to not reorder + bytes memory data = abi.encodePacked(token, recoveryAddress, principalData, rewardData); owr = OptimisticWithdrawalRecipient(address(owrImpl).clone(data)); emit CreateOWRecipient(address(owr), token, recoveryAddress, principalRecipient, rewardRecipient, threshold); From 4fa865013361b0cbd6499a78a390359ac571e8a5 Mon Sep 17 00:00:00 2001 From: samparsky <8148384+samparsky@users.noreply.github.com> Date: Mon, 11 Sep 2023 12:08:40 +0100 Subject: [PATCH 23/82] chore: rename threshold to amountOfStake --- src/owr/OptimisticWithdrawalRecipientFactory.sol | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/owr/OptimisticWithdrawalRecipientFactory.sol b/src/owr/OptimisticWithdrawalRecipientFactory.sol index b644f15..b646208 100644 --- a/src/owr/OptimisticWithdrawalRecipientFactory.sol +++ b/src/owr/OptimisticWithdrawalRecipientFactory.sol @@ -87,7 +87,7 @@ contract OptimisticWithdrawalRecipientFactory { /// tokens (or ether) that isn't the token of the OWRecipient contract /// @param principalRecipient Address to distribute principal payments to /// @param rewardRecipient Address to distribute reward payments to - /// @param threshold Absolute payment threshold for principal recipient + /// @param amountOfStake Absolute amount of stake to be paid to principal recipient (multiple of 32 ETH) /// (reward recipient has no threshold & receives all residual flows) /// it cannot be greater than uint96 /// @return owr Address of new OptimisticWithdrawalRecipient clone @@ -96,19 +96,19 @@ contract OptimisticWithdrawalRecipientFactory { address recoveryAddress, address principalRecipient, address rewardRecipient, - uint256 threshold + uint256 amountOfStake ) external returns (OptimisticWithdrawalRecipient owr) { /// checks // ensure doesn't have address(0) if (principalRecipient == address(0) || rewardRecipient == address(0)) revert Invalid__Recipients(); // ensure threshold isn't zero - if (threshold == 0) revert Invalid__ZeroThreshold(); + if (amountOfStake == 0) revert Invalid__ZeroThreshold(); // ensure threshold isn't too large - if (threshold > type(uint96).max) revert Invalid__ThresholdTooLarge(threshold); + if (amountOfStake > type(uint96).max) revert Invalid__ThresholdTooLarge(amountOfStake); /// effects - uint256 principalData = (threshold << ADDRESS_BITS) | uint256(uint160(principalRecipient)); + uint256 principalData = (amountOfStake << ADDRESS_BITS) | uint256(uint160(principalRecipient)); uint256 rewardData = uint256(uint160(rewardRecipient)); // would not exceed contract size limits @@ -116,6 +116,6 @@ contract OptimisticWithdrawalRecipientFactory { bytes memory data = abi.encodePacked(token, recoveryAddress, principalData, rewardData); owr = OptimisticWithdrawalRecipient(address(owrImpl).clone(data)); - emit CreateOWRecipient(address(owr), token, recoveryAddress, principalRecipient, rewardRecipient, threshold); + emit CreateOWRecipient(address(owr), token, recoveryAddress, principalRecipient, rewardRecipient, amountOfStake); } } From f08ee4d9357858c9bbfc256367fb377ae5f173c2 Mon Sep 17 00:00:00 2001 From: samparsky <8148384+samparsky@users.noreply.github.com> Date: Mon, 11 Sep 2023 12:09:38 +0100 Subject: [PATCH 24/82] chore: rename claimedTrancheFunds to claimedPrincipalFunds --- src/owr/OptimisticWithdrawalRecipient.sol | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/owr/OptimisticWithdrawalRecipient.sol b/src/owr/OptimisticWithdrawalRecipient.sol index 1f818f1..dc9e506 100644 --- a/src/owr/OptimisticWithdrawalRecipient.sol +++ b/src/owr/OptimisticWithdrawalRecipient.sol @@ -129,7 +129,7 @@ contract OptimisticWithdrawalRecipient is Clone { /// Amount of distributed OWRecipient token for first tranche (principal) /// @dev ERC20s with very large decimals may overflow & cause issues - uint256 public claimedFirstTrancheFunds; + uint256 public claimedPrincipalFunds; /// Mapping to account balances for pulling mapping(address => uint256) internal pullBalances; @@ -278,7 +278,7 @@ contract OptimisticWithdrawalRecipient is Clone { uint256 _startingDistributedFunds = uint256(distributedFunds); uint256 _endingDistributedFunds; uint256 _fundsToBeDistributed; - uint256 _claimedFirstTrancheFunds = uint256(claimedFirstTrancheFunds); + uint256 _claimedFirstTrancheFunds = uint256(claimedPrincipalFunds); uint256 _memoryFundsPendingWithdrawal = uint256(fundsPendingWithdrawal); unchecked { // shouldn't overflow @@ -325,7 +325,7 @@ contract OptimisticWithdrawalRecipient is Clone { // Write to storage distributedFunds = uint128(_endingDistributedFunds); // the principal value - claimedFirstTrancheFunds += _payouts[PRINCIPAL_RECIPIENT_INDEX]; + claimedPrincipalFunds += _payouts[PRINCIPAL_RECIPIENT_INDEX]; } /// interactions From 4ef1c85f1c43b7ea1f323a6cf03ee0dd48fefce7 Mon Sep 17 00:00:00 2001 From: samparsky <8148384+samparsky@users.noreply.github.com> Date: Mon, 11 Sep 2023 13:29:22 +0100 Subject: [PATCH 25/82] chore: update owr implementation --- src/owr/OptimisticWithdrawalRecipient.sol | 127 ++++++++++-------- .../OptimisticWithdrawalRecipientFactory.sol | 12 +- .../OptimisticWithdrawalRecipient.t.sol | 49 ++++--- 3 files changed, 100 insertions(+), 88 deletions(-) diff --git a/src/owr/OptimisticWithdrawalRecipient.sol b/src/owr/OptimisticWithdrawalRecipient.sol index dc9e506..807d72f 100644 --- a/src/owr/OptimisticWithdrawalRecipient.sol +++ b/src/owr/OptimisticWithdrawalRecipient.sol @@ -5,6 +5,7 @@ import {Clone} from "solady/utils/Clone.sol"; import {ERC20} from "solmate/tokens/ERC20.sol"; import {SafeTransferLib} from "solady/utils/SafeTransferLib.sol"; + /// @title OptimisticWithdrawalRecipient /// @author Obol /// @notice A maximally-composable contract that distributes payments @@ -43,9 +44,10 @@ contract OptimisticWithdrawalRecipient is Clone { event ReceiveETH(uint256 amount); /// Emitted after funds are distributed to recipients - /// @param payouts Amount of payout + /// @param principalPayout Amount of principal paid out + /// @param rewardPayout Amount of reward paid out /// @param pullFlowFlag Flag for pushing funds to recipients or storing for pulling - event DistributeFunds(uint256[] payouts, uint256 pullFlowFlag); + event DistributeFunds(uint256 principalPayout, uint256 rewardPayout, uint256 pullFlowFlag); /// Emitted after non-OWRecipient tokens are recovered to a recipient /// @param recoveryAddressToken Recovered token (cannot be OptimisticWithdrawalRecipient token) @@ -127,7 +129,8 @@ contract OptimisticWithdrawalRecipient is Clone { /// @dev ERC20s with very large decimals may overflow & cause issues uint128 public fundsPendingWithdrawal; - /// Amount of distributed OWRecipient token for first tranche (principal) + /// Amount of distributed OWRecipient token for principal + /// @dev Would be less than or equal to amount of stake /// @dev ERC20s with very large decimals may overflow & cause issues uint256 public claimedPrincipalFunds; @@ -184,20 +187,24 @@ contract OptimisticWithdrawalRecipient is Clone { address _recoveryAddress = recoveryAddress(); if (_recoveryAddress == address(0)) { // ensure txn recipient is a valid OWR recipient - (address[] memory recipients,) = getTranches(); - bool validRecipient = false; - uint256 _numTranches = TRANCHE_SIZE; - for (uint256 i; i < _numTranches;) { - if (recipients[i] == recipient) { - validRecipient = true; - break; - } - unchecked { - // shouldn't overflow - ++i; - } + (address principalRecipient, address rewardRecipient,) = getTranches(); + // bool validRecipient = false; + + if (recipient != principalRecipient && recipient != rewardRecipient) { + revert InvalidTokenRecovery_InvalidRecipient(); } - if (!validRecipient) revert InvalidTokenRecovery_InvalidRecipient(); + // uint256 _numTranches = TRANCHE_SIZE; + // for (uint256 i; i < _numTranches;) { + // if (recipients[i] == recipient) { + // validRecipient = true; + // break; + // } + // unchecked { + // // shouldn't overflow + // ++i; + // } + // } + // if (!validRecipient) } else if (recipient != _recoveryAddress) { revert InvalidTokenRecovery_InvalidRecipient(); } @@ -240,17 +247,15 @@ contract OptimisticWithdrawalRecipient is Clone { /// ----------------------------------------------------------------------- /// Return unpacked tranches - /// @return recipients Addresses to distribute payments to - /// @return threshold Absolute payment threshold for principal - function getTranches() public pure returns (address[] memory recipients, uint256 threshold) { - recipients = new address[](TRANCHE_SIZE); - + /// @return principalRecipient Addres of principal recipient + /// @return rewardRecipient Address of reward recipient + /// @return amountOfPrincipalStake Absolute payment threshold for principal + function getTranches() public pure returns (address principalRecipient, address rewardRecipient, uint256 amountOfPrincipalStake) { uint256 tranche = _getTranche(PRINCIPAL_RECIPIENT_INDEX); - recipients[0] = address(uint160(tranche)); - threshold = tranche >> ADDRESS_BITS; + principalRecipient = address(uint160(tranche)); + amountOfPrincipalStake = tranche >> ADDRESS_BITS; - // recipients has one more entry than thresholds - recipients[1] = address(uint160(_getTranche(REWARD_RECIPIENT_INDEX))); + rewardRecipient = address(uint160(_getTranche(REWARD_RECIPIENT_INDEX))); } /// Returns the balance for account `account` @@ -278,7 +283,7 @@ contract OptimisticWithdrawalRecipient is Clone { uint256 _startingDistributedFunds = uint256(distributedFunds); uint256 _endingDistributedFunds; uint256 _fundsToBeDistributed; - uint256 _claimedFirstTrancheFunds = uint256(claimedPrincipalFunds); + uint256 _claimedPrincipalFunds = uint256(claimedPrincipalFunds); uint256 _memoryFundsPendingWithdrawal = uint256(fundsPendingWithdrawal); unchecked { // shouldn't overflow @@ -291,32 +296,35 @@ contract OptimisticWithdrawalRecipient is Clone { _fundsToBeDistributed = _endingDistributedFunds - _startingDistributedFunds; } - (address[] memory recipients, uint256 threshold) = getTranches(); + (address principalRecipient, address rewardRecipient, uint256 amountOfPrincipalStake) = getTranches(); // determine which tranche is getting paid based on funds to be distributed // 0 = first tranche // 1 = second tranche // construct the payout arrays - uint256 _payoutsLength = TRANCHE_SIZE; - uint256[] memory _payouts = new uint256[](_payoutsLength); + // uint256 _payoutsLength = TRANCHE_SIZE; + // uint256[] memory _payouts = new uint256[](_payoutsLength); + + uint256 _principalPayout; + uint256 _rewardPayout; unchecked { - // _claimedFirstTrancheFunds should always be <= threshold - uint256 firstTrancheRemaining = threshold - _claimedFirstTrancheFunds; + // _claimedPrincipalFunds should always be <= amountOfPrincipalStake + uint256 principalStakeRemaining = amountOfPrincipalStake - _claimedPrincipalFunds; - if (_fundsToBeDistributed >= BALANCE_CLASSIFICATION_THRESHOLD && firstTrancheRemaining > 0) { - if (_fundsToBeDistributed > firstTrancheRemaining) { + if (_fundsToBeDistributed >= BALANCE_CLASSIFICATION_THRESHOLD && principalStakeRemaining > 0) { + if (_fundsToBeDistributed > principalStakeRemaining) { // this means there is reward part of the funds to be distributed - _payouts[PRINCIPAL_RECIPIENT_INDEX] = firstTrancheRemaining; + _principalPayout = principalStakeRemaining; // shouldn't underflow - _payouts[REWARD_RECIPIENT_INDEX] = _fundsToBeDistributed - firstTrancheRemaining; + _rewardPayout = _fundsToBeDistributed - principalStakeRemaining; } else { // this means there is no reward part of the funds to be distributed - _payouts[PRINCIPAL_RECIPIENT_INDEX] = _fundsToBeDistributed; + _principalPayout = _fundsToBeDistributed; } } else { - _payouts[REWARD_RECIPIENT_INDEX] = _fundsToBeDistributed; + _rewardPayout = _fundsToBeDistributed; } } @@ -325,7 +333,7 @@ contract OptimisticWithdrawalRecipient is Clone { // Write to storage distributedFunds = uint128(_endingDistributedFunds); // the principal value - claimedPrincipalFunds += _payouts[PRINCIPAL_RECIPIENT_INDEX]; + claimedPrincipalFunds += _principalPayout; } /// interactions @@ -333,28 +341,33 @@ contract OptimisticWithdrawalRecipient is Clone { // pay outs // earlier tranche recipients may try to re-enter but will cause fn to revert // when later external calls fail (bc balance is emptied early) - for (uint256 i; i < _payoutsLength;) { - if (_payouts[i] > 0) { - if (pullFlowFlag == PULL) { - pullBalances[recipients[i]] += _payouts[i]; - _memoryFundsPendingWithdrawal += _payouts[i]; - } else if (_token == ETH_ADDRESS) { - (recipients[i]).safeTransferETH(_payouts[i]); - } else { - _token.safeTransfer(recipients[i], _payouts[i]); - } - } - unchecked { - // shouldn't overflow - ++i; - } - } + + // pay out principal + _payout(_token, principalRecipient, _principalPayout, pullFlowFlag); + // pay out reward + _payout(_token, rewardRecipient, _rewardPayout, pullFlowFlag); if (pullFlowFlag == PULL) { - // Write to storage - fundsPendingWithdrawal = uint128(_memoryFundsPendingWithdrawal); + if (_principalPayout > 0 || _rewardPayout > 0) { + // Write to storage + fundsPendingWithdrawal = uint128(_memoryFundsPendingWithdrawal + _principalPayout + _rewardPayout); + } } - emit DistributeFunds(_payouts, pullFlowFlag); + emit DistributeFunds(_principalPayout, _rewardPayout, pullFlowFlag); + } + + function _payout(address payoutToken, address recipient, uint256 payoutAmount, uint256 pullFlowFlag) internal { + if (payoutAmount > 0) { + if (pullFlowFlag == PULL) { + // Write to Storage + pullBalances[recipient] += payoutAmount; + } else if(payoutToken == ETH_ADDRESS) { + recipient.safeTransferETH(payoutAmount); + } else { + payoutToken.safeTransfer(recipient, payoutAmount); + } + } } + } diff --git a/src/owr/OptimisticWithdrawalRecipientFactory.sol b/src/owr/OptimisticWithdrawalRecipientFactory.sol index b646208..2dd9fd9 100644 --- a/src/owr/OptimisticWithdrawalRecipientFactory.sol +++ b/src/owr/OptimisticWithdrawalRecipientFactory.sol @@ -87,7 +87,7 @@ contract OptimisticWithdrawalRecipientFactory { /// tokens (or ether) that isn't the token of the OWRecipient contract /// @param principalRecipient Address to distribute principal payments to /// @param rewardRecipient Address to distribute reward payments to - /// @param amountOfStake Absolute amount of stake to be paid to principal recipient (multiple of 32 ETH) + /// @param amountOfPrincipalStake Absolute amount of stake to be paid to principal recipient (multiple of 32 ETH) /// (reward recipient has no threshold & receives all residual flows) /// it cannot be greater than uint96 /// @return owr Address of new OptimisticWithdrawalRecipient clone @@ -96,19 +96,19 @@ contract OptimisticWithdrawalRecipientFactory { address recoveryAddress, address principalRecipient, address rewardRecipient, - uint256 amountOfStake + uint256 amountOfPrincipalStake ) external returns (OptimisticWithdrawalRecipient owr) { /// checks // ensure doesn't have address(0) if (principalRecipient == address(0) || rewardRecipient == address(0)) revert Invalid__Recipients(); // ensure threshold isn't zero - if (amountOfStake == 0) revert Invalid__ZeroThreshold(); + if (amountOfPrincipalStake == 0) revert Invalid__ZeroThreshold(); // ensure threshold isn't too large - if (amountOfStake > type(uint96).max) revert Invalid__ThresholdTooLarge(amountOfStake); + if (amountOfPrincipalStake > type(uint96).max) revert Invalid__ThresholdTooLarge(amountOfPrincipalStake); /// effects - uint256 principalData = (amountOfStake << ADDRESS_BITS) | uint256(uint160(principalRecipient)); + uint256 principalData = (amountOfPrincipalStake << ADDRESS_BITS) | uint256(uint160(principalRecipient)); uint256 rewardData = uint256(uint160(rewardRecipient)); // would not exceed contract size limits @@ -116,6 +116,6 @@ contract OptimisticWithdrawalRecipientFactory { bytes memory data = abi.encodePacked(token, recoveryAddress, principalData, rewardData); owr = OptimisticWithdrawalRecipient(address(owrImpl).clone(data)); - emit CreateOWRecipient(address(owr), token, recoveryAddress, principalRecipient, rewardRecipient, amountOfStake); + emit CreateOWRecipient(address(owr), token, recoveryAddress, principalRecipient, rewardRecipient, amountOfPrincipalStake); } } diff --git a/src/test/waterfall/OptimisticWithdrawalRecipient.t.sol b/src/test/waterfall/OptimisticWithdrawalRecipient.t.sol index 246ac7b..667f149 100644 --- a/src/test/waterfall/OptimisticWithdrawalRecipient.t.sol +++ b/src/test/waterfall/OptimisticWithdrawalRecipient.t.sol @@ -14,7 +14,7 @@ contract OptimisticWithdrawalRecipientTest is OWRTestHelper, Test { event ReceiveETH(uint256 amount); - event DistributeFunds(uint256[] payouts, uint256 pullFlowFlag); + event DistributeFunds(uint256 principalPayout, uint256 rewardPayout, uint256 pullFlowFlag); event RecoverNonOWRecipientFunds(address nonOWRToken, address recipient, uint256 amount); @@ -60,17 +60,17 @@ contract OptimisticWithdrawalRecipientTest is OWRTestHelper, Test { function testGetTranches() public { // eth - (address[] memory recipients, uint256 wtrancheThreshold) = owrETH.getTranches(); + (address _principalRecipient, address _rewardRecipient, uint256 wtrancheThreshold) = owrETH.getTranches(); - assertEq(recipients[0], principalRecipient, "invalid principal recipient"); - assertEq(recipients[1], rewardRecipient, "invalid reward recipient"); + assertEq(_principalRecipient, principalRecipient, "invalid principal recipient"); + assertEq( _rewardRecipient, rewardRecipient, "invalid reward recipient"); assertEq(wtrancheThreshold, ETH_STAKE, "invalid eth tranche threshold"); // erc20 - (recipients, wtrancheThreshold) = owrERC20.getTranches(); + (_principalRecipient, _rewardRecipient, wtrancheThreshold) = owrERC20.getTranches(); - assertEq(recipients[0], principalRecipient, "invalid erc20 principal recipient"); - assertEq(recipients[1], rewardRecipient, "invalid erc20 reward recipient"); + assertEq(_principalRecipient, principalRecipient, "invalid erc20 principal recipient"); + assertEq(_rewardRecipient, rewardRecipient, "invalid erc20 reward recipient"); assertEq(wtrancheThreshold, ETH_STAKE, "invalid erc20 tranche threshold"); } @@ -207,30 +207,30 @@ contract OptimisticWithdrawalRecipientTest is OWRTestHelper, Test { } function testCan_emitOnDistributeToNoRecipients() public { - uint256[] memory payouts = new uint256[](2); - payouts[0] = 0 ether; - payouts[1] = 0 ether; + uint256 principalPayout; + uint256 rewardPayout; vm.expectEmit(true, true, true, true); - emit DistributeFunds(payouts, 0); + emit DistributeFunds(principalPayout, rewardPayout, 0); owrETH.distributeFunds(); } function testCan_distributeToSecondRecipient() public { address(owrETH).safeTransferETH(1 ether); - uint256[] memory payouts = new uint256[](2); - payouts[1] = 1 ether; + // uint256[] memory payouts = new uint256[](2); + uint256 rewardPayout = 1 ether; + uint256 principalPayout; vm.expectEmit(true, true, true, true); - emit DistributeFunds(payouts, 0); + emit DistributeFunds(principalPayout, rewardPayout, 0); owrETH.distributeFunds(); assertEq(address(owrETH).balance, 0 ether); assertEq(rewardRecipient.balance, 1 ether); - payouts[1] = 0; + rewardPayout = 0; vm.expectEmit(true, true, true, true); - emit DistributeFunds(payouts, 0); + emit DistributeFunds(principalPayout, rewardPayout, 0); owrETH.distributeFunds(); assertEq(address(owrETH).balance, 0 ether); assertEq(principalRecipient.balance, 0 ether); @@ -238,16 +238,16 @@ contract OptimisticWithdrawalRecipientTest is OWRTestHelper, Test { address(mERC20).safeTransfer(address(owrERC20_OR), 1 ether); - payouts[1] = 1 ether; + rewardPayout = 1 ether; vm.expectEmit(true, true, true, true); - emit DistributeFunds(payouts, 0); + emit DistributeFunds(principalPayout, rewardPayout, 0); owrERC20_OR.distributeFunds(); assertEq(mERC20.balanceOf(address(owrERC20_OR)), 0 ether); assertEq(mERC20.balanceOf(rewardRecipient), 1 ether); - payouts[1] = 0; + rewardPayout = 0; vm.expectEmit(true, true, true, true); - emit DistributeFunds(payouts, 0); + emit DistributeFunds(principalPayout, rewardPayout, 0); owrERC20_OR.distributeFunds(); assertEq(mERC20.balanceOf(address(owrERC20_OR)), 0 ether); assertEq(principalRecipient.balance, 0 ether); @@ -279,12 +279,11 @@ contract OptimisticWithdrawalRecipientTest is OWRTestHelper, Test { function testCan_distributeToBothRecipients() public { address(owrETH).safeTransferETH(36 ether); - uint256[] memory payouts = new uint256[](2); - payouts[0] = 32 ether; - payouts[1] = 4 ether; + uint256 principalPayout = 32 ether; + uint256 rewardPayout = 4 ether; vm.expectEmit(true, true, true, true); - emit DistributeFunds(payouts, 0); + emit DistributeFunds(principalPayout, rewardPayout, 0); owrETH.distributeFunds(); assertEq(address(owrETH).balance, 0 ether); assertEq(principalRecipient.balance, 32 ether); @@ -293,7 +292,7 @@ contract OptimisticWithdrawalRecipientTest is OWRTestHelper, Test { address(mERC20).safeTransfer(address(owrERC20_OR), 36 ether); vm.expectEmit(true, true, true, true); - emit DistributeFunds(payouts, 0); + emit DistributeFunds(principalPayout, rewardPayout, 0); owrERC20_OR.distributeFunds(); assertEq(mERC20.balanceOf(address(owrERC20_OR)), 0 ether); assertEq(principalRecipient.balance, 32 ether); From 6764bb3d6cb25212f905c228fa88dd8771b27bf7 Mon Sep 17 00:00:00 2001 From: samparsky <8148384+samparsky@users.noreply.github.com> Date: Mon, 11 Sep 2023 13:29:35 +0100 Subject: [PATCH 26/82] chore: apply forge fmt --- src/owr/OptimisticWithdrawalRecipient.sol | 16 +++++++++------- src/owr/OptimisticWithdrawalRecipientFactory.sol | 4 +++- .../OptimisticWithdrawalRecipient.t.sol | 4 ++-- 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/src/owr/OptimisticWithdrawalRecipient.sol b/src/owr/OptimisticWithdrawalRecipient.sol index 807d72f..35b34c0 100644 --- a/src/owr/OptimisticWithdrawalRecipient.sol +++ b/src/owr/OptimisticWithdrawalRecipient.sol @@ -5,7 +5,6 @@ import {Clone} from "solady/utils/Clone.sol"; import {ERC20} from "solmate/tokens/ERC20.sol"; import {SafeTransferLib} from "solady/utils/SafeTransferLib.sol"; - /// @title OptimisticWithdrawalRecipient /// @author Obol /// @notice A maximally-composable contract that distributes payments @@ -129,7 +128,7 @@ contract OptimisticWithdrawalRecipient is Clone { /// @dev ERC20s with very large decimals may overflow & cause issues uint128 public fundsPendingWithdrawal; - /// Amount of distributed OWRecipient token for principal + /// Amount of distributed OWRecipient token for principal /// @dev Would be less than or equal to amount of stake /// @dev ERC20s with very large decimals may overflow & cause issues uint256 public claimedPrincipalFunds; @@ -204,7 +203,7 @@ contract OptimisticWithdrawalRecipient is Clone { // ++i; // } // } - // if (!validRecipient) + // if (!validRecipient) } else if (recipient != _recoveryAddress) { revert InvalidTokenRecovery_InvalidRecipient(); } @@ -250,7 +249,11 @@ contract OptimisticWithdrawalRecipient is Clone { /// @return principalRecipient Addres of principal recipient /// @return rewardRecipient Address of reward recipient /// @return amountOfPrincipalStake Absolute payment threshold for principal - function getTranches() public pure returns (address principalRecipient, address rewardRecipient, uint256 amountOfPrincipalStake) { + function getTranches() + public + pure + returns (address principalRecipient, address rewardRecipient, uint256 amountOfPrincipalStake) + { uint256 tranche = _getTranche(PRINCIPAL_RECIPIENT_INDEX); principalRecipient = address(uint160(tranche)); amountOfPrincipalStake = tranche >> ADDRESS_BITS; @@ -341,7 +344,7 @@ contract OptimisticWithdrawalRecipient is Clone { // pay outs // earlier tranche recipients may try to re-enter but will cause fn to revert // when later external calls fail (bc balance is emptied early) - + // pay out principal _payout(_token, principalRecipient, _principalPayout, pullFlowFlag); // pay out reward @@ -362,12 +365,11 @@ contract OptimisticWithdrawalRecipient is Clone { if (pullFlowFlag == PULL) { // Write to Storage pullBalances[recipient] += payoutAmount; - } else if(payoutToken == ETH_ADDRESS) { + } else if (payoutToken == ETH_ADDRESS) { recipient.safeTransferETH(payoutAmount); } else { payoutToken.safeTransfer(recipient, payoutAmount); } } } - } diff --git a/src/owr/OptimisticWithdrawalRecipientFactory.sol b/src/owr/OptimisticWithdrawalRecipientFactory.sol index 2dd9fd9..669e5db 100644 --- a/src/owr/OptimisticWithdrawalRecipientFactory.sol +++ b/src/owr/OptimisticWithdrawalRecipientFactory.sol @@ -116,6 +116,8 @@ contract OptimisticWithdrawalRecipientFactory { bytes memory data = abi.encodePacked(token, recoveryAddress, principalData, rewardData); owr = OptimisticWithdrawalRecipient(address(owrImpl).clone(data)); - emit CreateOWRecipient(address(owr), token, recoveryAddress, principalRecipient, rewardRecipient, amountOfPrincipalStake); + emit CreateOWRecipient( + address(owr), token, recoveryAddress, principalRecipient, rewardRecipient, amountOfPrincipalStake + ); } } diff --git a/src/test/waterfall/OptimisticWithdrawalRecipient.t.sol b/src/test/waterfall/OptimisticWithdrawalRecipient.t.sol index 667f149..1e60b83 100644 --- a/src/test/waterfall/OptimisticWithdrawalRecipient.t.sol +++ b/src/test/waterfall/OptimisticWithdrawalRecipient.t.sol @@ -63,7 +63,7 @@ contract OptimisticWithdrawalRecipientTest is OWRTestHelper, Test { (address _principalRecipient, address _rewardRecipient, uint256 wtrancheThreshold) = owrETH.getTranches(); assertEq(_principalRecipient, principalRecipient, "invalid principal recipient"); - assertEq( _rewardRecipient, rewardRecipient, "invalid reward recipient"); + assertEq(_rewardRecipient, rewardRecipient, "invalid reward recipient"); assertEq(wtrancheThreshold, ETH_STAKE, "invalid eth tranche threshold"); // erc20 @@ -220,7 +220,7 @@ contract OptimisticWithdrawalRecipientTest is OWRTestHelper, Test { // uint256[] memory payouts = new uint256[](2); uint256 rewardPayout = 1 ether; - uint256 principalPayout; + uint256 principalPayout; vm.expectEmit(true, true, true, true); emit DistributeFunds(principalPayout, rewardPayout, 0); From 864ce57327d16173ba9f9e301995b2c392b524c4 Mon Sep 17 00:00:00 2001 From: samparsky <8148384+samparsky@users.noreply.github.com> Date: Mon, 11 Sep 2023 13:30:26 +0100 Subject: [PATCH 27/82] chore: update comments --- src/owr/OptimisticWithdrawalRecipient.sol | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/owr/OptimisticWithdrawalRecipient.sol b/src/owr/OptimisticWithdrawalRecipient.sol index 35b34c0..3c7c62a 100644 --- a/src/owr/OptimisticWithdrawalRecipient.sol +++ b/src/owr/OptimisticWithdrawalRecipient.sol @@ -301,14 +301,7 @@ contract OptimisticWithdrawalRecipient is Clone { (address principalRecipient, address rewardRecipient, uint256 amountOfPrincipalStake) = getTranches(); - // determine which tranche is getting paid based on funds to be distributed - // 0 = first tranche - // 1 = second tranche - - // construct the payout arrays - // uint256 _payoutsLength = TRANCHE_SIZE; - // uint256[] memory _payouts = new uint256[](_payoutsLength); - + // determine which recipeint is getting paid based on funds to be distributed uint256 _principalPayout; uint256 _rewardPayout; From 906ac58ca686c03bb68e85d13ff6658b78cdf74c Mon Sep 17 00:00:00 2001 From: samparsky <8148384+samparsky@users.noreply.github.com> Date: Mon, 11 Sep 2023 13:31:01 +0100 Subject: [PATCH 28/82] chore: update formatting --- src/owr/OptimisticWithdrawalRecipient.sol | 1 + src/owr/OptimisticWithdrawalRecipientFactory.sol | 1 + 2 files changed, 2 insertions(+) diff --git a/src/owr/OptimisticWithdrawalRecipient.sol b/src/owr/OptimisticWithdrawalRecipient.sol index 3c7c62a..a3f7f1c 100644 --- a/src/owr/OptimisticWithdrawalRecipient.sol +++ b/src/owr/OptimisticWithdrawalRecipient.sol @@ -5,6 +5,7 @@ import {Clone} from "solady/utils/Clone.sol"; import {ERC20} from "solmate/tokens/ERC20.sol"; import {SafeTransferLib} from "solady/utils/SafeTransferLib.sol"; + /// @title OptimisticWithdrawalRecipient /// @author Obol /// @notice A maximally-composable contract that distributes payments diff --git a/src/owr/OptimisticWithdrawalRecipientFactory.sol b/src/owr/OptimisticWithdrawalRecipientFactory.sol index 669e5db..91782d7 100644 --- a/src/owr/OptimisticWithdrawalRecipientFactory.sol +++ b/src/owr/OptimisticWithdrawalRecipientFactory.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.17; import {OptimisticWithdrawalRecipient} from "./OptimisticWithdrawalRecipient.sol"; import {LibClone} from "solady/utils/LibClone.sol"; + /// @title OptimisticWithdrawalRecipientFactory /// @author Obol /// @notice A factory contract for cheaply deploying OptimisticWithdrawalRecipient. From b9e4a55864a9fcd9c53e5db2099b57b7b75d1d4a Mon Sep 17 00:00:00 2001 From: samparsky <8148384+samparsky@users.noreply.github.com> Date: Mon, 11 Sep 2023 13:38:23 +0100 Subject: [PATCH 29/82] chore: remove unused code --- src/owr/OptimisticWithdrawalRecipient.sol | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/src/owr/OptimisticWithdrawalRecipient.sol b/src/owr/OptimisticWithdrawalRecipient.sol index a3f7f1c..0036a42 100644 --- a/src/owr/OptimisticWithdrawalRecipient.sol +++ b/src/owr/OptimisticWithdrawalRecipient.sol @@ -188,23 +188,9 @@ contract OptimisticWithdrawalRecipient is Clone { if (_recoveryAddress == address(0)) { // ensure txn recipient is a valid OWR recipient (address principalRecipient, address rewardRecipient,) = getTranches(); - // bool validRecipient = false; - if (recipient != principalRecipient && recipient != rewardRecipient) { revert InvalidTokenRecovery_InvalidRecipient(); } - // uint256 _numTranches = TRANCHE_SIZE; - // for (uint256 i; i < _numTranches;) { - // if (recipients[i] == recipient) { - // validRecipient = true; - // break; - // } - // unchecked { - // // shouldn't overflow - // ++i; - // } - // } - // if (!validRecipient) } else if (recipient != _recoveryAddress) { revert InvalidTokenRecovery_InvalidRecipient(); } From 61e6a37ff199ba65d339f33e28121a42477eb659 Mon Sep 17 00:00:00 2001 From: samparsky <8148384+samparsky@users.noreply.github.com> Date: Tue, 12 Sep 2023 10:15:27 +0100 Subject: [PATCH 30/82] chore: apply forge fmt --- src/owr/OptimisticWithdrawalRecipient.sol | 1 - src/owr/OptimisticWithdrawalRecipientFactory.sol | 1 - 2 files changed, 2 deletions(-) diff --git a/src/owr/OptimisticWithdrawalRecipient.sol b/src/owr/OptimisticWithdrawalRecipient.sol index 0036a42..a48c76f 100644 --- a/src/owr/OptimisticWithdrawalRecipient.sol +++ b/src/owr/OptimisticWithdrawalRecipient.sol @@ -5,7 +5,6 @@ import {Clone} from "solady/utils/Clone.sol"; import {ERC20} from "solmate/tokens/ERC20.sol"; import {SafeTransferLib} from "solady/utils/SafeTransferLib.sol"; - /// @title OptimisticWithdrawalRecipient /// @author Obol /// @notice A maximally-composable contract that distributes payments diff --git a/src/owr/OptimisticWithdrawalRecipientFactory.sol b/src/owr/OptimisticWithdrawalRecipientFactory.sol index 91782d7..669e5db 100644 --- a/src/owr/OptimisticWithdrawalRecipientFactory.sol +++ b/src/owr/OptimisticWithdrawalRecipientFactory.sol @@ -4,7 +4,6 @@ pragma solidity ^0.8.17; import {OptimisticWithdrawalRecipient} from "./OptimisticWithdrawalRecipient.sol"; import {LibClone} from "solady/utils/LibClone.sol"; - /// @title OptimisticWithdrawalRecipientFactory /// @author Obol /// @notice A factory contract for cheaply deploying OptimisticWithdrawalRecipient. From 441da0df4f483ed7bacccc46bd8cb80d9ff4d8dc Mon Sep 17 00:00:00 2001 From: samparsky <8148384+samparsky@users.noreply.github.com> Date: Fri, 15 Sep 2023 11:14:24 +0100 Subject: [PATCH 31/82] test: rename waterfall folder => owr --- src/test/{waterfall => owr}/OWRReentrancy.sol | 0 src/test/{waterfall => owr}/OWRTestHelper.t.sol | 0 src/test/{waterfall => owr}/OptimisticWithdrawalRecipient.t.sol | 0 .../{waterfall => owr}/OptimisticWithdrawalRecipientFactory.t.sol | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename src/test/{waterfall => owr}/OWRReentrancy.sol (100%) rename src/test/{waterfall => owr}/OWRTestHelper.t.sol (100%) rename src/test/{waterfall => owr}/OptimisticWithdrawalRecipient.t.sol (100%) rename src/test/{waterfall => owr}/OptimisticWithdrawalRecipientFactory.t.sol (100%) diff --git a/src/test/waterfall/OWRReentrancy.sol b/src/test/owr/OWRReentrancy.sol similarity index 100% rename from src/test/waterfall/OWRReentrancy.sol rename to src/test/owr/OWRReentrancy.sol diff --git a/src/test/waterfall/OWRTestHelper.t.sol b/src/test/owr/OWRTestHelper.t.sol similarity index 100% rename from src/test/waterfall/OWRTestHelper.t.sol rename to src/test/owr/OWRTestHelper.t.sol diff --git a/src/test/waterfall/OptimisticWithdrawalRecipient.t.sol b/src/test/owr/OptimisticWithdrawalRecipient.t.sol similarity index 100% rename from src/test/waterfall/OptimisticWithdrawalRecipient.t.sol rename to src/test/owr/OptimisticWithdrawalRecipient.t.sol diff --git a/src/test/waterfall/OptimisticWithdrawalRecipientFactory.t.sol b/src/test/owr/OptimisticWithdrawalRecipientFactory.t.sol similarity index 100% rename from src/test/waterfall/OptimisticWithdrawalRecipientFactory.t.sol rename to src/test/owr/OptimisticWithdrawalRecipientFactory.t.sol From 8f94f0f34b49caedb13030686637d8426a5c66c9 Mon Sep 17 00:00:00 2001 From: samparsky <8148384+samparsky@users.noreply.github.com> Date: Fri, 15 Sep 2023 11:49:07 +0100 Subject: [PATCH 32/82] chore: remove unused vars --- src/owr/OptimisticWithdrawalRecipient.sol | 1 - src/owr/OptimisticWithdrawalRecipientFactory.sol | 1 - 2 files changed, 2 deletions(-) diff --git a/src/owr/OptimisticWithdrawalRecipient.sol b/src/owr/OptimisticWithdrawalRecipient.sol index a48c76f..8ae57de 100644 --- a/src/owr/OptimisticWithdrawalRecipient.sol +++ b/src/owr/OptimisticWithdrawalRecipient.sol @@ -74,7 +74,6 @@ contract OptimisticWithdrawalRecipient is Clone { uint256 internal constant ONE_WORD = 32; uint256 internal constant ADDRESS_BITS = 160; - uint256 internal constant TRANCHE_SIZE = 2; /// @dev threshold for pushing balance update as reward or principal uint256 internal constant BALANCE_CLASSIFICATION_THRESHOLD = 16 ether; diff --git a/src/owr/OptimisticWithdrawalRecipientFactory.sol b/src/owr/OptimisticWithdrawalRecipientFactory.sol index 669e5db..16e994d 100644 --- a/src/owr/OptimisticWithdrawalRecipientFactory.sol +++ b/src/owr/OptimisticWithdrawalRecipientFactory.sol @@ -58,7 +58,6 @@ contract OptimisticWithdrawalRecipientFactory { /// ----------------------------------------------------------------------- uint256 internal constant ADDRESS_BITS = 160; - uint256 internal constant RECIPIENT_SIZE = 2; /// OptimisticWithdrawalRecipient implementation address OptimisticWithdrawalRecipient public immutable owrImpl; From 34480026b5bf2b718f2b844052c393fc641881a8 Mon Sep 17 00:00:00 2001 From: samparsky <8148384+samparsky@users.noreply.github.com> Date: Fri, 15 Sep 2023 12:17:52 +0100 Subject: [PATCH 33/82] chore: upgrade solidity version 0.8.17 => 0.8.19 --- .vscode/settings.json | 2 +- foundry.toml | 2 +- script/OWRFactoryScript.s.sol | 2 +- src/interfaces/IENSReverseRegistrar.sol | 2 +- src/interfaces/ILiquidWaterfall.sol | 2 +- src/interfaces/ISplitFactory.sol | 2 +- src/interfaces/ISplitMain.sol | 2 +- src/interfaces/ISplitMainV2.sol | 2 +- src/interfaces/IWaterfallFactoryModule.sol | 2 +- src/interfaces/IWaterfallModule.sol | 2 +- src/lido/LidoSplit.sol | 4 ++-- src/lido/LidoSplitFactory.sol | 4 ++-- src/owr/OptimisticWithdrawalRecipient.sol | 6 +++--- src/owr/OptimisticWithdrawalRecipientFactory.sol | 2 +- src/test/lido/LIdoSplitFactory.t.sol | 2 +- src/test/lido/LidoSplit.t.sol | 2 +- src/test/lido/LidoSplitTestHelper.sol | 2 +- src/test/lido/integration/LidoSplitIntegrationTest.sol | 2 +- src/test/owr/OWRReentrancy.sol | 2 +- src/test/owr/OWRTestHelper.t.sol | 2 +- src/test/owr/OptimisticWithdrawalRecipient.t.sol | 2 +- src/test/owr/OptimisticWithdrawalRecipientFactory.t.sol | 2 +- src/test/utils/mocks/MockNFT.sol | 2 +- 23 files changed, 27 insertions(+), 27 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 2b8dfb7..7698eac 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,6 +1,6 @@ { "solidity.packageDefaultDependenciesContractsDirectory": "src", "solidity.packageDefaultDependenciesDirectory": "lib", - "solidity.compileUsingRemoteVersion": "v0.8.17+commit.8df45f5f", + "solidity.compileUsingRemoteVersion": "v0.8.19", "solidity.defaultCompiler": "remote" } \ No newline at end of file diff --git a/foundry.toml b/foundry.toml index d7e9037..84f844b 100644 --- a/foundry.toml +++ b/foundry.toml @@ -7,7 +7,7 @@ remappings = [ 'solmate/=lib/solmate/src/', 'splits-tests/=lib/splits-utils/test/', ] -solc_version = '0.8.17' +solc_version = '0.8.19' [rpc_endpoints] diff --git a/script/OWRFactoryScript.s.sol b/script/OWRFactoryScript.s.sol index 008988e..acb0c00 100644 --- a/script/OWRFactoryScript.s.sol +++ b/script/OWRFactoryScript.s.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity =0.8.17; +pragma solidity ^0.8.19; import "forge-std/Script.sol"; import {OptimisticWithdrawalRecipientFactory} from "src/owr/OptimisticWithdrawalRecipientFactory.sol"; diff --git a/src/interfaces/IENSReverseRegistrar.sol b/src/interfaces/IENSReverseRegistrar.sol index 46cfa2e..e0b432d 100644 --- a/src/interfaces/IENSReverseRegistrar.sol +++ b/src/interfaces/IENSReverseRegistrar.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.13; +pragma solidity ^0.8.19; interface IENSReverseRegistrar { function claim(address owner) external returns (bytes32); diff --git a/src/interfaces/ILiquidWaterfall.sol b/src/interfaces/ILiquidWaterfall.sol index 1e9c669..c6f0987 100644 --- a/src/interfaces/ILiquidWaterfall.sol +++ b/src/interfaces/ILiquidWaterfall.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity =0.8.17; +pragma solidity ^0.8.19; interface ILiquidWaterfall { function balanceOf(address owner, uint256 id) external returns (uint256); diff --git a/src/interfaces/ISplitFactory.sol b/src/interfaces/ISplitFactory.sol index 7630214..4879fe3 100644 --- a/src/interfaces/ISplitFactory.sol +++ b/src/interfaces/ISplitFactory.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity =0.8.17; +pragma solidity ^0.8.19; import {ERC20} from "solmate/tokens/ERC20.sol"; import {ISplitMainV2} from "./ISplitMainV2.sol"; diff --git a/src/interfaces/ISplitMain.sol b/src/interfaces/ISplitMain.sol index c4b1a36..fa89953 100644 --- a/src/interfaces/ISplitMain.sol +++ b/src/interfaces/ISplitMain.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity =0.8.17; +pragma solidity ^0.8.19; import {ERC20} from "solmate/tokens/ERC20.sol"; diff --git a/src/interfaces/ISplitMainV2.sol b/src/interfaces/ISplitMainV2.sol index 460914c..2f0a8e4 100644 --- a/src/interfaces/ISplitMainV2.sol +++ b/src/interfaces/ISplitMainV2.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity =0.8.17; +pragma solidity ^0.8.19; import {ERC20} from "solmate/tokens/ERC20.sol"; diff --git a/src/interfaces/IWaterfallFactoryModule.sol b/src/interfaces/IWaterfallFactoryModule.sol index b383a52..941cf36 100644 --- a/src/interfaces/IWaterfallFactoryModule.sol +++ b/src/interfaces/IWaterfallFactoryModule.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity =0.8.17; +pragma solidity ^0.8.19; interface IWaterfallFactoryModule { /// Create a new WaterfallModule clone diff --git a/src/interfaces/IWaterfallModule.sol b/src/interfaces/IWaterfallModule.sol index 551000f..3634ec0 100644 --- a/src/interfaces/IWaterfallModule.sol +++ b/src/interfaces/IWaterfallModule.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity =0.8.17; +pragma solidity ^0.8.19; interface IWaterfallModule { /// Waterfalls target token inside the contract to next-in-line recipients diff --git a/src/lido/LidoSplit.sol b/src/lido/LidoSplit.sol index 96e2b54..8156d3e 100644 --- a/src/lido/LidoSplit.sol +++ b/src/lido/LidoSplit.sol @@ -1,5 +1,5 @@ -// SPDX-License-Identifier: MIT -pragma solidity =0.8.17; +// 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"; diff --git a/src/lido/LidoSplitFactory.sol b/src/lido/LidoSplitFactory.sol index 2b9f25a..f942dc9 100644 --- a/src/lido/LidoSplitFactory.sol +++ b/src/lido/LidoSplitFactory.sol @@ -1,5 +1,5 @@ -// SPDX-License-Identifier: MIT -pragma solidity =0.8.17; +// 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"; diff --git a/src/owr/OptimisticWithdrawalRecipient.sol b/src/owr/OptimisticWithdrawalRecipient.sol index 8ae57de..5ff0074 100644 --- a/src/owr/OptimisticWithdrawalRecipient.sol +++ b/src/owr/OptimisticWithdrawalRecipient.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.8.17; +pragma solidity 0.8.19; import {Clone} from "solady/utils/Clone.sol"; import {ERC20} from "solmate/tokens/ERC20.sol"; @@ -287,8 +287,8 @@ contract OptimisticWithdrawalRecipient is Clone { (address principalRecipient, address rewardRecipient, uint256 amountOfPrincipalStake) = getTranches(); // determine which recipeint is getting paid based on funds to be distributed - uint256 _principalPayout; - uint256 _rewardPayout; + uint256 _principalPayout = 0; + uint256 _rewardPayout = 0; unchecked { // _claimedPrincipalFunds should always be <= amountOfPrincipalStake diff --git a/src/owr/OptimisticWithdrawalRecipientFactory.sol b/src/owr/OptimisticWithdrawalRecipientFactory.sol index 16e994d..7e9519c 100644 --- a/src/owr/OptimisticWithdrawalRecipientFactory.sol +++ b/src/owr/OptimisticWithdrawalRecipientFactory.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.8.17; +pragma solidity 0.8.19; import {OptimisticWithdrawalRecipient} from "./OptimisticWithdrawalRecipient.sol"; import {LibClone} from "solady/utils/LibClone.sol"; diff --git a/src/test/lido/LIdoSplitFactory.t.sol b/src/test/lido/LIdoSplitFactory.t.sol index b46749f..a42f44c 100644 --- a/src/test/lido/LIdoSplitFactory.t.sol +++ b/src/test/lido/LIdoSplitFactory.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.17; +pragma solidity ^0.8.19; import "forge-std/Test.sol"; import {LidoSplitFactory} from "src/lido/LidoSplitFactory.sol"; diff --git a/src/test/lido/LidoSplit.t.sol b/src/test/lido/LidoSplit.t.sol index 751d086..d39b4e6 100644 --- a/src/test/lido/LidoSplit.t.sol +++ b/src/test/lido/LidoSplit.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.17; +pragma solidity ^0.8.19; import "forge-std/Test.sol"; import {LidoSplitFactory, LidoSplit} from "src/lido/LidoSplitFactory.sol"; diff --git a/src/test/lido/LidoSplitTestHelper.sol b/src/test/lido/LidoSplitTestHelper.sol index 4db029b..c705688 100644 --- a/src/test/lido/LidoSplitTestHelper.sol +++ b/src/test/lido/LidoSplitTestHelper.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.17; +pragma solidity ^0.8.19; contract LidoSplitTestHelper { address internal STETH_MAINNET_ADDRESS = address(0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84); diff --git a/src/test/lido/integration/LidoSplitIntegrationTest.sol b/src/test/lido/integration/LidoSplitIntegrationTest.sol index 64c656b..e901135 100644 --- a/src/test/lido/integration/LidoSplitIntegrationTest.sol +++ b/src/test/lido/integration/LidoSplitIntegrationTest.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.17; +pragma solidity ^0.8.19; import "forge-std/Test.sol"; import {LidoSplitFactory, LidoSplit} from "src/lido/LidoSplitFactory.sol"; diff --git a/src/test/owr/OWRReentrancy.sol b/src/test/owr/OWRReentrancy.sol index 4fded65..a189387 100644 --- a/src/test/owr/OWRReentrancy.sol +++ b/src/test/owr/OWRReentrancy.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.8.17; +pragma solidity ^0.8.19; import "forge-std/Test.sol"; import {OptimisticWithdrawalRecipient} from "src/owr/OptimisticWithdrawalRecipient.sol"; diff --git a/src/test/owr/OWRTestHelper.t.sol b/src/test/owr/OWRTestHelper.t.sol index 2b66a09..787cac4 100644 --- a/src/test/owr/OWRTestHelper.t.sol +++ b/src/test/owr/OWRTestHelper.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.17; +pragma solidity ^0.8.17; contract OWRTestHelper { address internal constant ETH_ADDRESS = address(0); diff --git a/src/test/owr/OptimisticWithdrawalRecipient.t.sol b/src/test/owr/OptimisticWithdrawalRecipient.t.sol index 1e60b83..974fd5c 100644 --- a/src/test/owr/OptimisticWithdrawalRecipient.t.sol +++ b/src/test/owr/OptimisticWithdrawalRecipient.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.17; +pragma solidity ^0.8.19; import "forge-std/Test.sol"; import {OptimisticWithdrawalRecipient} from "src/owr/OptimisticWithdrawalRecipient.sol"; diff --git a/src/test/owr/OptimisticWithdrawalRecipientFactory.t.sol b/src/test/owr/OptimisticWithdrawalRecipientFactory.t.sol index 8bd63c2..fd02690 100644 --- a/src/test/owr/OptimisticWithdrawalRecipientFactory.t.sol +++ b/src/test/owr/OptimisticWithdrawalRecipientFactory.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.17; +pragma solidity ^0.8.19; import "forge-std/Test.sol"; import {OptimisticWithdrawalRecipient} from "src/owr/OptimisticWithdrawalRecipient.sol"; diff --git a/src/test/utils/mocks/MockNFT.sol b/src/test/utils/mocks/MockNFT.sol index f95c02a..4518ac2 100644 --- a/src/test/utils/mocks/MockNFT.sol +++ b/src/test/utils/mocks/MockNFT.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity =0.8.17; +pragma solidity ^0.8.19; import "solmate/tokens/ERC721.sol"; From fdc47587014fae4863fbfe25452b82e628e12a90 Mon Sep 17 00:00:00 2001 From: samparsky <8148384+samparsky@users.noreply.github.com> Date: Fri, 15 Sep 2023 15:18:10 +0100 Subject: [PATCH 34/82] chore: fix solidity pragma lidosplitfactory --- src/lido/LidoSplitFactory.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lido/LidoSplitFactory.sol b/src/lido/LidoSplitFactory.sol index f942dc9..135bcd3 100644 --- a/src/lido/LidoSplitFactory.sol +++ b/src/lido/LidoSplitFactory.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity =0.8.19; +pragma solidity 0.8.19; import {LibClone} from "solady/utils/LibClone.sol"; import {ERC20} from "solmate/tokens/ERC20.sol"; From 1aba4196d79716bdd8ab6f6e635a7fe01398fe3c Mon Sep 17 00:00:00 2001 From: samparsky <8148384+samparsky@users.noreply.github.com> Date: Fri, 15 Sep 2023 16:38:45 +0100 Subject: [PATCH 35/82] chore: apply forge fmt, 0.8.17 => 0.8.19 --- script/IMSCFactory.s.sol | 2 +- src/controllers/ImmutableSplitController.sol | 251 +++++----- .../ImmutableSplitControllerFactory.sol | 429 ++++++++--------- src/interfaces/ISplitMain.sol | 5 +- src/interfaces/ISplitMainV2.sol | 2 - src/test/controllers/IMSC.t.sol | 441 ++++++++---------- src/test/controllers/IMSCFactory.t.sol | 377 ++++++--------- 7 files changed, 645 insertions(+), 862 deletions(-) diff --git a/script/IMSCFactory.s.sol b/script/IMSCFactory.s.sol index 3c55b73..c5577ed 100644 --- a/script/IMSCFactory.s.sol +++ b/script/IMSCFactory.s.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity =0.8.17; +pragma solidity 0.8.19; import "forge-std/Script.sol"; import {ImmutableSplitControllerFactory} from "src/controllers/ImmutableSplitControllerFactory.sol"; diff --git a/src/controllers/ImmutableSplitController.sol b/src/controllers/ImmutableSplitController.sol index 7c58ab1..5765dd3 100644 --- a/src/controllers/ImmutableSplitController.sol +++ b/src/controllers/ImmutableSplitController.sol @@ -1,141 +1,128 @@ // SPDX-License-Identifier: MIT -pragma solidity =0.8.17; +pragma solidity 0.8.19; + import {ISplitMain} from "../interfaces/ISplitMain.sol"; import {Clone} from "solady/utils/Clone.sol"; - /// @author Obol -/// @dev Deploys a contract that can update a split should be called once as the +/// @dev Deploys a contract that can update a split should be called once as the /// configuration is defined at deployment and cannot change contract ImmutableSplitController is Clone { - - /// @notice IMSC already initialized - error Initialized(); - - /// @notice - error Unauthorized(); - - /// ----------------------------------------------------------------------- - /// storage - /// ----------------------------------------------------------------------- - - /// ----------------------------------------------------------------------- - /// storage - constants - /// ----------------------------------------------------------------------- - uint256 internal constant ADDRESS_BITS = 160; - uint256 internal constant ONE_WORD = 32; - - /// ----------------------------------------------------------------------- - /// storage - cwia offsets - /// ----------------------------------------------------------------------- - - // splitMain (address, 20 bytes) - // 0; first item - uint256 internal constant SPLIT_MAIN_OFFSET = 0; - // distributorFee (uint32, 4 bytes) - // 1; second item - uint256 internal constant DISTRIBUTOR_FEE_OFFSET = 20; - // onwer (address, 20 bytes) - // 2; third item - uint256 internal constant OWNER_OFFSET = 24; - // recipeints size (uint8, 1 byte ) - // 3; third item - uint256 internal constant RECIPIENTS_SIZE_OFFSET = 44; - // recipients data () - // 4; fourth item - uint256 internal constant RECIPIENTS_OFFSET = 45; - - /// ----------------------------------------------------------------------- - /// storage - mutable - /// ----------------------------------------------------------------------- - /// @dev Address of split to update - address public split; - - - constructor() {} - - function init(address splitAddress) external { - if (split != address(0)) revert Initialized(); - - split = splitAddress; + /// @notice IMSC already initialized + error Initialized(); + + /// @notice + error Unauthorized(); + + /// ----------------------------------------------------------------------- + /// storage + /// ----------------------------------------------------------------------- + + /// ----------------------------------------------------------------------- + /// storage - constants + /// ----------------------------------------------------------------------- + uint256 internal constant ADDRESS_BITS = 160; + uint256 internal constant ONE_WORD = 32; + + /// ----------------------------------------------------------------------- + /// storage - cwia offsets + /// ----------------------------------------------------------------------- + + // splitMain (address, 20 bytes) + // 0; first item + uint256 internal constant SPLIT_MAIN_OFFSET = 0; + // distributorFee (uint32, 4 bytes) + // 1; second item + uint256 internal constant DISTRIBUTOR_FEE_OFFSET = 20; + // onwer (address, 20 bytes) + // 2; third item + uint256 internal constant OWNER_OFFSET = 24; + // recipeints size (uint8, 1 byte ) + // 3; third item + uint256 internal constant RECIPIENTS_SIZE_OFFSET = 44; + // recipients data () + // 4; fourth item + uint256 internal constant RECIPIENTS_OFFSET = 45; + + /// ----------------------------------------------------------------------- + /// storage - mutable + /// ----------------------------------------------------------------------- + /// @dev Address of split to update + address public split; + + constructor() {} + + function init(address splitAddress) external { + if (split != address(0)) revert Initialized(); + + split = splitAddress; + } + + /// Updates split with the hardcoded configuration + /// @dev Updates split with stored split configuration + function updateSplit() external payable { + if (msg.sender != owner()) revert Unauthorized(); + + (address[] memory accounts, uint32[] memory percentAllocations) = getNewSplitConfiguration(); + + ISplitMain(splitMain()).updateSplit(split, accounts, percentAllocations, uint32(distributorFee())); + } + + /// Address of SplitMain + /// @dev equivalent to address public immutable splitMain; + function splitMain() public pure returns (address) { + return _getArgAddress(SPLIT_MAIN_OFFSET); + } + + /// Fee charged by distributor + /// @dev equivalent to address public immutable distributorFee; + function distributorFee() public pure returns (uint256) { + return _getArgUint32(DISTRIBUTOR_FEE_OFFSET); + } + + /// Adress of owner + /// @dev equivalent to address public immutable owner; + function owner() public pure returns (address) { + return _getArgAddress(OWNER_OFFSET); + } + + // Returns unpacked recipients + /// @return accounts Addresses to receive payments + /// @return percentAllocations Percentage share for split accounts + function getNewSplitConfiguration() + public + pure + returns (address[] memory accounts, uint32[] memory percentAllocations) + { + // fetch the size first + // then parse the data gradually + uint256 size = _recipientsSize(); + accounts = new address[](size); + percentAllocations = new uint32[](size); + + uint256 i; + for (i; i < size;) { + uint256 recipient = _getRecipient(i); + accounts[i] = address(uint160(recipient)); + percentAllocations[i] = uint32(recipient >> ADDRESS_BITS); + unchecked { + i++; + } } - - /// Updates split with the hardcoded configuration - /// @dev Updates split with stored split configuration - function updateSplit() external payable { - if (msg.sender != owner()) { - revert Unauthorized(); - } - - ( - address[] memory accounts, - uint32[] memory percentAllocations - ) = getNewSplitConfiguration(); - - ISplitMain(splitMain()).updateSplit( - split, - accounts, - percentAllocations, - uint32(distributorFee()) - ); + } + + /// Number of recipeints + /// @dev equivalent to address internal immutable _recipientsSize; + function _recipientsSize() internal pure returns (uint256) { + return _getArgUint8(RECIPIENTS_SIZE_OFFSET); + } + + /// Gets recipient i + /// @dev emulates to uint256[] internal immutable recipient; + function _getRecipient(uint256 i) internal pure returns (uint256) { + unchecked { + // shouldn't overflow + return _getArgUint256(RECIPIENTS_OFFSET + (i * ONE_WORD)); } - - /// Address of SplitMain - /// @dev equivalent to address public immutable splitMain; - function splitMain() public pure returns (address) { - return _getArgAddress(SPLIT_MAIN_OFFSET); - } - - /// Fee charged by distributor - /// @dev equivalent to address public immutable distributorFee; - function distributorFee() public pure returns(uint256) { - return _getArgUint32(DISTRIBUTOR_FEE_OFFSET); - } - - /// Adress of owner - /// @dev equivalent to address public immutable owner; - function owner() public pure returns (address) { - return _getArgAddress(OWNER_OFFSET); - } - - // Returns unpacked recipients - /// @return accounts Addresses to receive payments - /// @return percentAllocations Percentage share for split accounts - function getNewSplitConfiguration() public pure returns ( - address[] memory accounts, - uint32[] memory percentAllocations - ) { - // fetch the size first - // then parse the data gradually - uint256 size = _recipientsSize(); - accounts = new address[](size); - percentAllocations = new uint32[](size); - - uint i; - for (i; i < size;) { - uint256 recipient = _getRecipient(i); - accounts[i] = address(uint160(recipient)); - percentAllocations[i] = uint32(recipient >> ADDRESS_BITS); - unchecked { - i++; - } - } - - } - - /// Number of recipeints - /// @dev equivalent to address internal immutable _recipientsSize; - function _recipientsSize() internal pure returns (uint256) { - return _getArgUint8(RECIPIENTS_SIZE_OFFSET); - } - - /// Gets recipient i - /// @dev emulates to uint256[] internal immutable recipient; - function _getRecipient(uint256 i) internal pure returns (uint256) { - unchecked { - // shouldn't overflow - 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 1a31a55..f90da72 100644 --- a/src/controllers/ImmutableSplitControllerFactory.sol +++ b/src/controllers/ImmutableSplitControllerFactory.sol @@ -1,257 +1,210 @@ // SPDX-License-Identifier: MIT -pragma solidity =0.8.17; +pragma solidity 0.8.19; + import {ISplitMain} from "../interfaces/ISplitMain.sol"; import {LibClone} from "solady/utils/LibClone.sol"; import {ImmutableSplitController} from "./ImmutableSplitController.sol"; - /// @author Obol /// @dev Deploys ImmutableSplitController cheaply using cwia clones contract ImmutableSplitControllerFactory { - - /// @dev invalid owner address - error Invalid_Owner(); - /// @dev invalid split address - error InvalidSplit_Address(); - /// @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); - - /// ----------------------------------------------------------------------- - /// libraries - /// ----------------------------------------------------------------------- - - using LibClone for address; - - /// ----------------------------------------------------------------------- - /// events - /// ----------------------------------------------------------------------- - - /// Emitted after a new IMSC is deployed - /// @param controller Address of newly created IMSC clone - /// @param split Address of split - /// @param owner Adderss of the owner of the controller - /// @param accounts Addresses of - /// @param percentAllocations Addresses to recover non-waterfall tokens to - /// @param distributorFee Amount of - event CreateIMSC( - address indexed controller, - address indexed split, - address owner, - address[] accounts, - uint32[] percentAllocations, - uint256 distributorFee - ); - - - /// ----------------------------------------------------------------------- - /// storage - /// ----------------------------------------------------------------------- - - /// ----------------------------------------------------------------------- - /// storage - constants & immutables - /// ----------------------------------------------------------------------- - uint256 internal constant ADDRESS_BITS = 160; - /// @notice constant to scale uints into percentages (1e6 == 100%) - uint256 public constant PERCENTAGE_SCALE = 1e6; - /// @notice maximum distributor fee; 1e5 = 10% * PERCENTAGE_SCALE - uint256 internal constant MAX_DISTRIBUTOR_FEE = 1e5; - - /// @dev splitMain address - address public immutable splitMain; - - /// @dev Implementation of ImmutableSplitController - ImmutableSplitController public immutable controller; - - /// ----------------------------------------------------------------------- - /// modifiers - /// ----------------------------------------------------------------------- - modifier validSplit( - address[] memory accounts, - uint32[] memory percentAllocations, - uint32 distributorFee - ) { - 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); - _; + /// @dev invalid owner address + error Invalid_Owner(); + /// @dev invalid split address + error InvalidSplit_Address(); + /// @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); + + /// ----------------------------------------------------------------------- + /// libraries + /// ----------------------------------------------------------------------- + + using LibClone for address; + + /// ----------------------------------------------------------------------- + /// events + /// ----------------------------------------------------------------------- + + /// Emitted after a new IMSC is deployed + /// @param controller Address of newly created IMSC clone + /// @param split Address of split + /// @param owner Adderss of the owner of the controller + /// @param accounts Addresses of + /// @param percentAllocations Addresses to recover non-waterfall tokens to + /// @param distributorFee Amount of + event CreateIMSC( + address indexed controller, + address indexed split, + address owner, + address[] accounts, + uint32[] percentAllocations, + uint256 distributorFee + ); + + /// ----------------------------------------------------------------------- + /// storage + /// ----------------------------------------------------------------------- + + /// ----------------------------------------------------------------------- + /// storage - constants & immutables + /// ----------------------------------------------------------------------- + uint256 internal constant ADDRESS_BITS = 160; + /// @notice constant to scale uints into percentages (1e6 == 100%) + uint256 public constant PERCENTAGE_SCALE = 1e6; + /// @notice maximum distributor fee; 1e5 = 10% * PERCENTAGE_SCALE + uint256 internal constant MAX_DISTRIBUTOR_FEE = 1e5; + + /// @dev splitMain address + address public immutable splitMain; + + /// @dev Implementation of ImmutableSplitController + ImmutableSplitController public immutable controller; + + /// ----------------------------------------------------------------------- + /// modifiers + /// ----------------------------------------------------------------------- + modifier validSplit(address[] memory accounts, uint32[] memory percentAllocations, uint32 distributorFee) { + if (accounts.length < 2) revert InvalidSplit__TooFewAccounts(accounts.length); + + if (accounts.length != percentAllocations.length) { + revert InvalidSplit__AccountsAndAllocationsMismatch(accounts.length, percentAllocations.length); } - - /// Creates Factory - /// @dev initializes the factory - /// @param splitMain_ Address of splitMain - constructor(address splitMain_) { - splitMain = splitMain_; - controller = new ImmutableSplitController(); + // _getSum should overflow if any percentAllocation[i] < 0 + if (_getSum(percentAllocations) != PERCENTAGE_SCALE) { + revert InvalidSplit__InvalidAllocationsSum(_getSum(percentAllocations)); } - /// Deploys a new immutable controller - /// @dev Create a new immutable split controller - /// @param split Address of the split to create a controller for - /// @param owner Address that can call the updateSplit(..) function - /// @param accounts Ordered, unique list of addresses with ownership in the split - /// @param percentAllocations Percent allocations associated with each address - /// @param distributorFee Distributor fee share - /// @param deploymentSalt salt to use for deterministic deploy - function createController( - address split, - address owner, - address[] calldata accounts, - uint32[] calldata percentAllocations, - uint32 distributorFee, - bytes32 deploymentSalt - ) - external validSplit(accounts, percentAllocations, distributorFee) - returns (ImmutableSplitController newController) - { - if (split == address(0)) { - revert InvalidSplit_Address(); - } - - if (owner == address(0)) { - revert Invalid_Owner(); - } - - newController = ImmutableSplitController( - address(controller).cloneDeterministic( - _packSplitControllerData( - owner, - accounts, - percentAllocations, - distributorFee - ), - deploymentSalt - ) - ); - - // initialize with split address - newController.init(split); - - emit CreateIMSC( - address(controller), - split, - owner, - accounts, - percentAllocations, - distributorFee - ); - } - - /// @notice Predicts the address for an immutable split controller created with - /// recipients `accounts` with ownerships `percentAllocations` - /// and a keeper fee for splitting of `distributorFee` - /// @param accounts Ordered, unique list of addresses with ownership in the split - /// @param percentAllocations Percent allocations associated with each address - /// @param distributorFee Keeper fee paid by split to cover gas costs of distribution - /// @param deploymentSalt Salt to use to deploy - /// @return splitController Predicted address of such a split controller - function predictSplitControllerAddress( - address owner, - address[] calldata accounts, - uint32[] calldata percentAllocations, - uint32 distributorFee, - bytes32 deploymentSalt - ) external view returns (address splitController) { - splitController = address(controller).predictDeterministicAddress( - _packSplitControllerData( - owner, - accounts, - percentAllocations, - distributorFee - ), - deploymentSalt, - address(this) - ); + 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); } - /// @dev Packs split controller data - /// @param accounts Ordered, unique list of addresses with ownership in the split - /// @param percentAllocations Percent allocations associated with each address - /// @param distributorFee Keeper fee paid by split to cover gas costs of distribution - function _packSplitControllerData( - address owner, - address[] calldata accounts, - uint32[] calldata percentAllocations, - uint32 distributorFee - ) internal view returns (bytes memory data) { - uint256 recipientsSize = accounts.length; - uint256[] memory recipients = new uint[](recipientsSize); - - uint i; - for(i; i < recipientsSize; ) { - recipients[i] = (uint256(percentAllocations[i]) << ADDRESS_BITS) | - uint256(uint160(accounts[i])); + if (distributorFee > MAX_DISTRIBUTOR_FEE) revert InvalidSplit__InvalidDistributorFee(distributorFee); + _; + } + + /// Creates Factory + /// @dev initializes the factory + /// @param splitMain_ Address of splitMain + constructor(address splitMain_) { + splitMain = splitMain_; + controller = new ImmutableSplitController(); + } + + /// Deploys a new immutable controller + /// @dev Create a new immutable split controller + /// @param split Address of the split to create a controller for + /// @param owner Address that can call the updateSplit(..) function + /// @param accounts Ordered, unique list of addresses with ownership in the split + /// @param percentAllocations Percent allocations associated with each address + /// @param distributorFee Distributor fee share + /// @param deploymentSalt salt to use for deterministic deploy + function createController( + address split, + address owner, + address[] calldata accounts, + uint32[] calldata percentAllocations, + uint32 distributorFee, + bytes32 deploymentSalt + ) external validSplit(accounts, percentAllocations, distributorFee) returns (ImmutableSplitController newController) { + if (split == address(0)) revert InvalidSplit_Address(); + + if (owner == address(0)) revert Invalid_Owner(); + + newController = ImmutableSplitController( + address(controller).cloneDeterministic( + _packSplitControllerData(owner, accounts, percentAllocations, distributorFee), deploymentSalt + ) + ); - unchecked { - i++; - } - } - - data = abi.encodePacked( - splitMain, distributorFee, owner, uint8(recipientsSize), recipients - ); + // initialize with split address + newController.init(split); + + emit CreateIMSC(address(controller), split, owner, accounts, percentAllocations, distributorFee); + } + + /// @notice Predicts the address for an immutable split controller created with + /// recipients `accounts` with ownerships `percentAllocations` + /// and a keeper fee for splitting of `distributorFee` + /// @param accounts Ordered, unique list of addresses with ownership in the split + /// @param percentAllocations Percent allocations associated with each address + /// @param distributorFee Keeper fee paid by split to cover gas costs of distribution + /// @param deploymentSalt Salt to use to deploy + /// @return splitController Predicted address of such a split controller + function predictSplitControllerAddress( + address owner, + address[] calldata accounts, + uint32[] calldata percentAllocations, + uint32 distributorFee, + bytes32 deploymentSalt + ) external view returns (address splitController) { + splitController = address(controller).predictDeterministicAddress( + _packSplitControllerData(owner, accounts, percentAllocations, distributorFee), deploymentSalt, address(this) + ); + } + + /// @dev Packs split controller data + /// @param accounts Ordered, unique list of addresses with ownership in the split + /// @param percentAllocations Percent allocations associated with each address + /// @param distributorFee Keeper fee paid by split to cover gas costs of distribution + function _packSplitControllerData( + address owner, + address[] calldata accounts, + uint32[] calldata percentAllocations, + uint32 distributorFee + ) internal view returns (bytes memory data) { + uint256 recipientsSize = accounts.length; + uint256[] memory recipients = new uint[](recipientsSize); + + uint256 i; + for (i; i < recipientsSize;) { + recipients[i] = (uint256(percentAllocations[i]) << ADDRESS_BITS) | uint256(uint160(accounts[i])); + + unchecked { + i++; + } } - /// @notice Sums array of uint32s - /// @param numbers Array of uint32s to sum - /// @return sum Sum of `numbers`. - function _getSum(uint32[] memory numbers) internal pure returns (uint32 sum) { + data = abi.encodePacked(splitMain, distributorFee, owner, uint8(recipientsSize), recipients); + } + + /// @notice Sums array of uint32s + /// @param numbers Array of uint32s to sum + /// @return sum Sum of `numbers`. + function _getSum(uint32[] memory numbers) internal pure returns (uint32 sum) { + // overflow should be impossible in for-loop index + uint256 numbersLength = numbers.length; + for (uint256 i = 0; i < numbersLength;) { + sum += numbers[i]; + unchecked { // overflow should be impossible in for-loop index - uint256 numbersLength = numbers.length; - for (uint256 i = 0; i < numbersLength; ) { - sum += numbers[i]; - unchecked { - // overflow should be impossible in for-loop index - ++i; - } - } + ++i; + } } - -} \ No newline at end of file + } +} diff --git a/src/interfaces/ISplitMain.sol b/src/interfaces/ISplitMain.sol index fa89953..707b2e6 100644 --- a/src/interfaces/ISplitMain.sol +++ b/src/interfaces/ISplitMain.sol @@ -79,7 +79,8 @@ interface ISplitMain { /// @param tokens Addresses of ERC20s to withdraw function withdraw(address account, uint256 withdrawETH, ERC20[] calldata tokens) external; - /// @notice Updates an existing split with recipients `accounts` with ownerships `percentAllocations` and a keeper fee for splitting of `distributorFee` + /// @notice Updates an existing split with recipients `accounts` with ownerships `percentAllocations` and a keeper fee + /// for splitting of `distributorFee` /// @param split Address of mutable split to update /// @param accounts Ordered, unique list of addresses with ownership in the split /// @param percentAllocations Percent allocations associated with each address @@ -91,7 +92,5 @@ interface ISplitMain { uint32 distributorFee ) external; - function getHash(address split) external view returns (bytes32); - } diff --git a/src/interfaces/ISplitMainV2.sol b/src/interfaces/ISplitMainV2.sol index 2f0a8e4..ecad225 100644 --- a/src/interfaces/ISplitMainV2.sol +++ b/src/interfaces/ISplitMainV2.sol @@ -104,8 +104,6 @@ interface ISplitMainV2 { /// @param tokens Addresses of ERC20s to withdraw function withdraw(address account, uint256 withdrawETH, ERC20[] calldata tokens) external; - - /** * EVENTS */ diff --git a/src/test/controllers/IMSC.t.sol b/src/test/controllers/IMSC.t.sol index e489077..d44aaec 100644 --- a/src/test/controllers/IMSC.t.sol +++ b/src/test/controllers/IMSC.t.sol @@ -1,279 +1,210 @@ // SPDX-License-Identifier: MIT -pragma solidity =0.8.17; +pragma solidity ^0.8.19; + import "forge-std/Test.sol"; import { - ImmutableSplitControllerFactory, - ImmutableSplitController + ImmutableSplitControllerFactory, + ImmutableSplitController } from "src/controllers/ImmutableSplitControllerFactory.sol"; import {ISplitMain} from "src/interfaces/ISplitMain.sol"; - contract IMSC is Test { - error Initialized(); - error Unauthorized(); - - address internal SPLIT_MAIN_GOERLI = 0x2ed6c4B5dA6378c7897AC67Ba9e43102Feb694EE; - uint256 public constant PERCENTAGE_SCALE = 1e6; - - ImmutableSplitControllerFactory public factory; - ImmutableSplitController public cntrlImpl; - - ImmutableSplitController public controller; - - address[] accounts; - uint32[] percentAllocations; - - address[] controllerAccounts; - uint32[] controllerPercentAllocations; - - address split; - address owner; - - function setUp() public { - uint256 goerliBlock = 8_529_931; - vm.createSelectFork(getChain("goerli").rpcUrl, goerliBlock); - - factory = new ImmutableSplitControllerFactory(SPLIT_MAIN_GOERLI); - cntrlImpl = factory.controller(); - - accounts = new address[](2); - accounts[0] = makeAddr("accounts0"); - accounts[1] = makeAddr("accounts1"); - - owner = makeAddr("accounts3"); - - percentAllocations = new uint32[](2); - percentAllocations[0] = 400_000; - percentAllocations[1] = 600_000; - - controllerAccounts = new address[](3); - controllerAccounts[0] = makeAddr("accounts0"); - controllerAccounts[1] = makeAddr("accounts1"); - controllerAccounts[2] = makeAddr("accounts3"); - - controllerPercentAllocations = new uint32[](3); - controllerPercentAllocations[0] = 400_000; - controllerPercentAllocations[1] = 300_000; - controllerPercentAllocations[2] = 300_000; - - bytes32 deploymentSalt = keccak256(abi.encodePacked(uint256(64))); - - // predict controller address - address predictedControllerAddress = factory.predictSplitControllerAddress( - owner, - controllerAccounts, - controllerPercentAllocations, - 0, - deploymentSalt - ); - - split = ISplitMain(SPLIT_MAIN_GOERLI).createSplit( - accounts, - percentAllocations, - 0, - predictedControllerAddress - ); - - // deploy controller - controller = factory.createController( - split, - owner, - controllerAccounts, - controllerPercentAllocations, - 0, - deploymentSalt - ); - } + error Initialized(); + error Unauthorized(); + address internal SPLIT_MAIN_GOERLI = 0x2ed6c4B5dA6378c7897AC67Ba9e43102Feb694EE; + uint256 public constant PERCENTAGE_SCALE = 1e6; - function testCannot_DoubleInitialiseIMSC() public { - vm.expectRevert( - Initialized.selector - ); - - controller.init(address(0x3)); - } + ImmutableSplitControllerFactory public factory; + ImmutableSplitController public cntrlImpl; - function testCan_getSplitMain() public { - assertEq(controller.splitMain(), SPLIT_MAIN_GOERLI, "valid splitMain address"); - } + ImmutableSplitController public controller; - function testCan_getOwner() public { - assertEq( - controller.owner(), - owner, - "valid controller owner" - ); - } - function testCan_getDistributorFee() public { - assertEq(controller.distributorFee(), 0 , "invalid distributor fee"); - - uint32 maxDistributorFee = 1e5; - - ImmutableSplitController customController = factory.createController( - split, - owner, - controllerAccounts, - controllerPercentAllocations, - maxDistributorFee, - keccak256(abi.encodePacked(uint256(640))) - ); - - assertEq( - customController.distributorFee(), - maxDistributorFee, - "invalid distributor fee" - ); - } + address[] accounts; + uint32[] percentAllocations; - function testCan_getSplitConfiguration() public { - ( - address[] memory localAccounts, - uint32[] memory localPercentAllocations - ) = controller.getNewSplitConfiguration(); - - assertEq( - localAccounts, controllerAccounts, "invalid accounts" - ); - - assertEq( - localPercentAllocations.length, - controllerPercentAllocations.length, - "unequal length percent allocations" - ); - - for(uint i; i < localPercentAllocations.length; i++) { - assertEq( - uint256(localPercentAllocations[i]), - uint256(controllerPercentAllocations[i]), - "invalid percentAllocations" - ); - } - } + address[] controllerAccounts; + uint32[] controllerPercentAllocations; - function testCan_getSplit() public { - assertEq( - controller.split(), - split - ); - } + address split; + address owner; - function testCannot_updateSplitIfNonOwner() public { - vm.expectRevert(Unauthorized.selector); - controller.updateSplit(); - } + function setUp() public { + uint256 goerliBlock = 8_529_931; + vm.createSelectFork(getChain("goerli").rpcUrl, goerliBlock); - function testCan_updateSplit() public { - vm.prank(owner); - controller.updateSplit(); + factory = new ImmutableSplitControllerFactory(SPLIT_MAIN_GOERLI); + cntrlImpl = factory.controller(); - assertEq( - ISplitMain(SPLIT_MAIN_GOERLI).getHash(split), - _hashSplit(controllerAccounts, controllerPercentAllocations, 0), - "invalid split hash" - ); - } + accounts = new address[](2); + accounts[0] = makeAddr("accounts0"); + accounts[1] = makeAddr("accounts1"); - function testFuzz_updateSplit( - address ownerAddress, - uint256 splitSeed, - uint256 controllerSeed, - uint8 splitSize, - uint8 controllerSize - ) public { - vm.assume(ownerAddress != address(0)); - vm.assume(splitSeed != controllerSeed); - vm.assume(splitSize > 1); - vm.assume(controllerSize > 1); - - address[] memory splitterAccts = _generateAddresses(splitSeed, splitSize); - address[] memory ctrllerAccounts = _generateAddresses(controllerSeed, controllerSize); - - uint32[] memory splitterPercentAlloc = _generatePercentAlloc(splitSize); - uint32[] memory ctrllerPercentAlloc = _generatePercentAlloc(controllerSize); - - bytes32 deploymentSalt = keccak256(abi.encodePacked(uint256(604))); - - // predict controller address - address predictedControllerAddress = factory.predictSplitControllerAddress( - ownerAddress, - ctrllerAccounts, - ctrllerPercentAlloc, - 0, - deploymentSalt - ); - - // create split - address fuzzSplit = ISplitMain(SPLIT_MAIN_GOERLI).createSplit( - splitterAccts, - splitterPercentAlloc, - 0, - predictedControllerAddress - ); - - // create controller - controller = factory.createController( - fuzzSplit, - ownerAddress, - ctrllerAccounts, - ctrllerPercentAlloc, - 0, - deploymentSalt - ); - - assertEq(controller.owner(), ownerAddress, "invalid owner address"); - - // get current split hash - bytes32 currentSplitHash = ISplitMain(SPLIT_MAIN_GOERLI).getHash(fuzzSplit); - // update split - vm.prank(ownerAddress); - controller.updateSplit(); - - bytes32 newSplitHash = ISplitMain(SPLIT_MAIN_GOERLI).getHash(fuzzSplit); - - bytes32 calculatedSplitHash = _hashSplit( - ctrllerAccounts, - ctrllerPercentAlloc, - 0 - ); - - assertTrue(currentSplitHash != newSplitHash, "update split hash"); - assertEq(calculatedSplitHash, newSplitHash, "split hash equal"); - } + owner = makeAddr("accounts3"); - function _hashSplit( - address[] memory accts, - uint32[] memory percentAlloc, - uint32 distributorFee - ) internal pure returns (bytes32) { - return keccak256(abi.encodePacked(accts, percentAlloc, distributorFee)); - } + percentAllocations = new uint32[](2); + percentAllocations[0] = 400_000; + percentAllocations[1] = 600_000; - - function _generateAddresses( - uint256 _seed, - uint256 size - ) internal pure returns (address[] memory accts) { - accts = new address[](size); - uint160 seed = uint160(uint256(keccak256(abi.encodePacked(_seed)))); - for(uint160 i ; i < size; i++) { - accts[i] = address(seed); - seed += 1; - } - } + controllerAccounts = new address[](3); + controllerAccounts[0] = makeAddr("accounts0"); + controllerAccounts[1] = makeAddr("accounts1"); + controllerAccounts[2] = makeAddr("accounts3"); + + controllerPercentAllocations = new uint32[](3); + controllerPercentAllocations[0] = 400_000; + controllerPercentAllocations[1] = 300_000; + controllerPercentAllocations[2] = 300_000; + + bytes32 deploymentSalt = keccak256(abi.encodePacked(uint256(64))); - function _generatePercentAlloc( - uint256 size - ) internal pure returns (uint32[] memory alloc) { + // predict controller address + address predictedControllerAddress = + factory.predictSplitControllerAddress(owner, controllerAccounts, controllerPercentAllocations, 0, deploymentSalt); - alloc = new uint32[](size); - for (uint i; i < size; i++) { - alloc[i] = uint32(PERCENTAGE_SCALE / size); - } + split = ISplitMain(SPLIT_MAIN_GOERLI).createSplit(accounts, percentAllocations, 0, predictedControllerAddress); + + // deploy controller + controller = + factory.createController(split, owner, controllerAccounts, controllerPercentAllocations, 0, deploymentSalt); + } + + function testCannot_DoubleInitialiseIMSC() public { + vm.expectRevert(Initialized.selector); + + controller.init(address(0x3)); + } + + function testCan_getSplitMain() public { + assertEq(controller.splitMain(), SPLIT_MAIN_GOERLI, "valid splitMain address"); + } + + function testCan_getOwner() public { + assertEq(controller.owner(), owner, "valid controller owner"); + } + + function testCan_getDistributorFee() public { + assertEq(controller.distributorFee(), 0, "invalid distributor fee"); + + uint32 maxDistributorFee = 1e5; + + ImmutableSplitController customController = factory.createController( + split, + owner, + controllerAccounts, + controllerPercentAllocations, + maxDistributorFee, + keccak256(abi.encodePacked(uint256(640))) + ); + + assertEq(customController.distributorFee(), maxDistributorFee, "invalid distributor fee"); + } + + function testCan_getSplitConfiguration() public { + (address[] memory localAccounts, uint32[] memory localPercentAllocations) = controller.getNewSplitConfiguration(); + + assertEq(localAccounts, controllerAccounts, "invalid accounts"); + + assertEq(localPercentAllocations.length, controllerPercentAllocations.length, "unequal length percent allocations"); + + for (uint256 i; i < localPercentAllocations.length; i++) { + assertEq( + uint256(localPercentAllocations[i]), uint256(controllerPercentAllocations[i]), "invalid percentAllocations" + ); + } + } + + function testCan_getSplit() public { + assertEq(controller.split(), split); + } + + function testCannot_updateSplitIfNonOwner() public { + vm.expectRevert(Unauthorized.selector); + controller.updateSplit(); + } + + function testCan_updateSplit() public { + vm.prank(owner); + controller.updateSplit(); + + assertEq( + ISplitMain(SPLIT_MAIN_GOERLI).getHash(split), + _hashSplit(controllerAccounts, controllerPercentAllocations, 0), + "invalid split hash" + ); + } + + function testFuzz_updateSplit( + address ownerAddress, + uint256 splitSeed, + uint256 controllerSeed, + uint8 splitSize, + uint8 controllerSize + ) public { + vm.assume(ownerAddress != address(0)); + vm.assume(splitSeed != controllerSeed); + vm.assume(splitSize > 1); + vm.assume(controllerSize > 1); + + address[] memory splitterAccts = _generateAddresses(splitSeed, splitSize); + address[] memory ctrllerAccounts = _generateAddresses(controllerSeed, controllerSize); + + uint32[] memory splitterPercentAlloc = _generatePercentAlloc(splitSize); + uint32[] memory ctrllerPercentAlloc = _generatePercentAlloc(controllerSize); + + bytes32 deploymentSalt = keccak256(abi.encodePacked(uint256(604))); + + // predict controller address + address predictedControllerAddress = + factory.predictSplitControllerAddress(ownerAddress, ctrllerAccounts, ctrllerPercentAlloc, 0, deploymentSalt); + + // create split + address fuzzSplit = + ISplitMain(SPLIT_MAIN_GOERLI).createSplit(splitterAccts, splitterPercentAlloc, 0, predictedControllerAddress); + + // create controller + controller = + factory.createController(fuzzSplit, ownerAddress, ctrllerAccounts, ctrllerPercentAlloc, 0, deploymentSalt); + + assertEq(controller.owner(), ownerAddress, "invalid owner address"); + + // get current split hash + bytes32 currentSplitHash = ISplitMain(SPLIT_MAIN_GOERLI).getHash(fuzzSplit); + // update split + vm.prank(ownerAddress); + controller.updateSplit(); + + bytes32 newSplitHash = ISplitMain(SPLIT_MAIN_GOERLI).getHash(fuzzSplit); + + bytes32 calculatedSplitHash = _hashSplit(ctrllerAccounts, ctrllerPercentAlloc, 0); + + assertTrue(currentSplitHash != newSplitHash, "update split hash"); + assertEq(calculatedSplitHash, newSplitHash, "split hash equal"); + } + + function _hashSplit(address[] memory accts, uint32[] memory percentAlloc, uint32 distributorFee) + internal + pure + returns (bytes32) + { + return keccak256(abi.encodePacked(accts, percentAlloc, distributorFee)); + } + + function _generateAddresses(uint256 _seed, uint256 size) internal pure returns (address[] memory accts) { + accts = new address[](size); + uint160 seed = uint160(uint256(keccak256(abi.encodePacked(_seed)))); + for (uint160 i; i < size; i++) { + accts[i] = address(seed); + seed += 1; + } + } - if (PERCENTAGE_SCALE % size != 0) { - alloc[size - 1] += uint32(PERCENTAGE_SCALE % size); - } + function _generatePercentAlloc(uint256 size) internal pure returns (uint32[] memory alloc) { + alloc = new uint32[](size); + for (uint256 i; i < size; i++) { + alloc[i] = uint32(PERCENTAGE_SCALE / size); } -} \ No newline at end of file + if (PERCENTAGE_SCALE % size != 0) alloc[size - 1] += uint32(PERCENTAGE_SCALE % size); + } +} diff --git a/src/test/controllers/IMSCFactory.t.sol b/src/test/controllers/IMSCFactory.t.sol index 15280bb..8241c99 100644 --- a/src/test/controllers/IMSCFactory.t.sol +++ b/src/test/controllers/IMSCFactory.t.sol @@ -1,240 +1,155 @@ // SPDX-License-Identifier: MIT -pragma solidity =0.8.17; +pragma solidity ^0.8.19; + import "forge-std/Test.sol"; import { - ImmutableSplitControllerFactory, - ImmutableSplitController + ImmutableSplitControllerFactory, + ImmutableSplitController } from "src/controllers/ImmutableSplitControllerFactory.sol"; import {ISplitMain} from "src/interfaces/ISplitMain.sol"; - contract IMSCFactory is Test { + error Invalid_Owner(); + error InvalidSplit_Address(); + error InvalidSplit__TooFewAccounts(uint256 accountsLength); + error InvalidSplit__AccountsAndAllocationsMismatch(uint256 accountsLength, uint256 allocationsLength); + error InvalidSplit__InvalidAllocationsSum(uint32 allocationsSum); + error InvalidSplit__AccountsOutOfOrder(uint256 index); + error InvalidSplit__AllocationMustBePositive(uint256 index); + error InvalidSplit__InvalidDistributorFee(uint32 distributorFee); + + address internal SPLIT_MAIN_GOERLI = 0x2ed6c4B5dA6378c7897AC67Ba9e43102Feb694EE; + uint32 public constant SPLIT_MAIN_PERCENTAGE_SCALE = 1e6; + + ImmutableSplitControllerFactory public factory; + ImmutableSplitController public cntrlImpl; + + address owner; + + address[] accounts; + uint32[] percentAllocations; + + function setUp() public { + uint256 goerliBlock = 8_529_931; + vm.createSelectFork(getChain("goerli").rpcUrl, goerliBlock); + + factory = new ImmutableSplitControllerFactory(SPLIT_MAIN_GOERLI); + cntrlImpl = factory.controller(); + + accounts = new address[](2); + accounts[0] = makeAddr("accounts0"); + accounts[1] = makeAddr("accounts1"); + + percentAllocations = new uint32[](2); + percentAllocations[0] = 400_000; + percentAllocations[1] = 600_000; + + owner = makeAddr("owner"); + } + + function test_RevertIfAccountSizeIsOne() public { + address[] memory newAccounts = new address[](1); + newAccounts[0] = makeAddr("testRevertIfAccountSizeIsOne"); + + vm.expectRevert(abi.encodeWithSelector(InvalidSplit__TooFewAccounts.selector, newAccounts.length)); + + factory.createController( + address(1), owner, newAccounts, percentAllocations, 0, keccak256(abi.encodePacked(uint256(12))) + ); + } + + function test_RevertIfAccountAndAllocationMismatch() public { + uint32[] memory newPercentAllocations = new uint32[](3); + newPercentAllocations[0] = 200_000; + newPercentAllocations[1] = 200_000; + newPercentAllocations[2] = 600_000; + + vm.expectRevert( + abi.encodeWithSelector( + InvalidSplit__AccountsAndAllocationsMismatch.selector, accounts.length, newPercentAllocations.length + ) + ); + + factory.createController( + address(1), owner, accounts, newPercentAllocations, 0, keccak256(abi.encodePacked(uint256(12))) + ); + } + + function test_RevertIfAccountOutOfOrder() public { + address[] memory newAccounts = new address[](2); + newAccounts[0] = address(0x4); + newAccounts[1] = address(0x1); + + vm.expectRevert(abi.encodeWithSelector(InvalidSplit__AccountsOutOfOrder.selector, 0)); + + factory.createController( + address(1), owner, newAccounts, percentAllocations, 0, keccak256(abi.encodePacked(uint256(12))) + ); + } - error Invalid_Owner(); - error InvalidSplit_Address(); - error InvalidSplit__TooFewAccounts(uint256 accountsLength); - error InvalidSplit__AccountsAndAllocationsMismatch( - uint256 accountsLength, - uint256 allocationsLength + function test_RevertIfZeroPercentAllocation() public { + uint32[] memory newPercentAllocations = new uint32[](2); + newPercentAllocations[0] = SPLIT_MAIN_PERCENTAGE_SCALE; + newPercentAllocations[1] = 0; + + vm.expectRevert(abi.encodeWithSelector(InvalidSplit__AllocationMustBePositive.selector, 1)); + + factory.createController( + address(1), owner, accounts, newPercentAllocations, 0, keccak256(abi.encodePacked(uint256(12))) + ); + } + + function test_RevertIfInvalidDistributorFee() public { + uint32 invalidDistributorFee = 1e6; + + vm.expectRevert(abi.encodeWithSelector(InvalidSplit__InvalidDistributorFee.selector, invalidDistributorFee)); + + factory.createController( + address(1), owner, accounts, percentAllocations, invalidDistributorFee, keccak256(abi.encodePacked(uint256(12))) + ); + } + + function test_RevertIfInvalidAllocationSum() public { + uint32[] memory newPercentAllocations = new uint32[](2); + newPercentAllocations[0] = SPLIT_MAIN_PERCENTAGE_SCALE; + newPercentAllocations[1] = 1; + + vm.expectRevert( + abi.encodeWithSelector(InvalidSplit__InvalidAllocationsSum.selector, SPLIT_MAIN_PERCENTAGE_SCALE + 1) + ); + + factory.createController( + address(1), owner, accounts, newPercentAllocations, 0, keccak256(abi.encodePacked(uint256(12))) ); - error InvalidSplit__InvalidAllocationsSum(uint32 allocationsSum); - error InvalidSplit__AccountsOutOfOrder(uint256 index); - error InvalidSplit__AllocationMustBePositive(uint256 index); - error InvalidSplit__InvalidDistributorFee(uint32 distributorFee); - - - address internal SPLIT_MAIN_GOERLI = 0x2ed6c4B5dA6378c7897AC67Ba9e43102Feb694EE; - uint32 public constant SPLIT_MAIN_PERCENTAGE_SCALE = 1e6; - - ImmutableSplitControllerFactory public factory; - ImmutableSplitController public cntrlImpl; - - address owner; - - address[] accounts; - uint32[] percentAllocations; - - function setUp() public { - uint256 goerliBlock = 8_529_931; - vm.createSelectFork(getChain("goerli").rpcUrl, goerliBlock); - - factory = new ImmutableSplitControllerFactory(SPLIT_MAIN_GOERLI); - cntrlImpl = factory.controller(); - - accounts = new address[](2); - accounts[0] = makeAddr("accounts0"); - accounts[1] = makeAddr("accounts1"); - - percentAllocations = new uint32[](2); - percentAllocations[0] = 400_000; - percentAllocations[1] = 600_000; - - owner = makeAddr("owner"); - } - - function test_RevertIfAccountSizeIsOne() public { - address[] memory newAccounts = new address[](1); - newAccounts[0] = makeAddr("testRevertIfAccountSizeIsOne"); - - vm.expectRevert( - abi.encodeWithSelector(InvalidSplit__TooFewAccounts.selector, newAccounts.length) - ); - - factory.createController( - address(1), - owner, - newAccounts, - percentAllocations, - 0, - keccak256(abi.encodePacked(uint256(12))) - ); - } - - function test_RevertIfAccountAndAllocationMismatch() public { - uint32[] memory newPercentAllocations = new uint32[](3); - newPercentAllocations[0] = 200_000; - newPercentAllocations[1] = 200_000; - newPercentAllocations[2] = 600_000; - - vm.expectRevert( - abi.encodeWithSelector( - InvalidSplit__AccountsAndAllocationsMismatch.selector, - accounts.length, - newPercentAllocations.length - ) - ); - - factory.createController( - address(1), - owner, - accounts, - newPercentAllocations, - 0, - keccak256(abi.encodePacked(uint256(12))) - ); - } - - - function test_RevertIfAccountOutOfOrder() public { - address[] memory newAccounts = new address[](2); - newAccounts[0] = address(0x4); - newAccounts[1] = address(0x1); - - vm.expectRevert( - abi.encodeWithSelector( - InvalidSplit__AccountsOutOfOrder.selector, - 0 - ) - ); - - factory.createController( - address(1), - owner, - newAccounts, - percentAllocations, - 0, - keccak256(abi.encodePacked(uint256(12))) - ); - } - - function test_RevertIfZeroPercentAllocation() public { - uint32[] memory newPercentAllocations = new uint32[](2); - newPercentAllocations[0] = SPLIT_MAIN_PERCENTAGE_SCALE; - newPercentAllocations[1] = 0; - - vm.expectRevert( - abi.encodeWithSelector( - InvalidSplit__AllocationMustBePositive.selector, - 1 - ) - ); - - factory.createController( - address(1), - owner, - accounts, - newPercentAllocations, - 0, - keccak256(abi.encodePacked(uint256(12))) - ); - } - - function test_RevertIfInvalidDistributorFee() public { - uint32 invalidDistributorFee = 1e6; - - vm.expectRevert( - abi.encodeWithSelector( - InvalidSplit__InvalidDistributorFee.selector, - invalidDistributorFee - ) - ); - - factory.createController( - address(1), - owner, - accounts, - percentAllocations, - invalidDistributorFee, - keccak256(abi.encodePacked(uint256(12))) - ); - } - - function test_RevertIfInvalidAllocationSum() public { - uint32[] memory newPercentAllocations = new uint32[](2); - newPercentAllocations[0] = SPLIT_MAIN_PERCENTAGE_SCALE; - newPercentAllocations[1] = 1; - - vm.expectRevert( - abi.encodeWithSelector( - InvalidSplit__InvalidAllocationsSum.selector, - SPLIT_MAIN_PERCENTAGE_SCALE + 1 - ) - ); - - factory.createController( - address(1), - owner, - accounts, - newPercentAllocations, - 0, - keccak256(abi.encodePacked(uint256(12))) - ); - } - - function test_RevertIfInvalidOwner() public { - vm.expectRevert(Invalid_Owner.selector); - - factory.createController( - address(1), - address(0), - accounts, - percentAllocations, - 0, - keccak256(abi.encodePacked(uint256(123))) - ); - } - - function test_RevertIfInvalidSplitAddress() public { - vm.expectRevert(InvalidSplit_Address.selector); - - factory.createController( - address(0), - address(1), - accounts, - percentAllocations, - 0, - keccak256(abi.encodePacked(uint256(123))) - ); - } - - function test_CanCreateController() public { - bytes32 deploymentSalt = keccak256(abi.encodePacked(uint256(1102))); - - address predictedAddress = factory.predictSplitControllerAddress( - owner, - accounts, - percentAllocations, - 0, - deploymentSalt - ); - - address split = ISplitMain(SPLIT_MAIN_GOERLI).createSplit( - accounts, - percentAllocations, - 0, - predictedAddress - ); - - ImmutableSplitController controller = factory.createController( - split, - owner, - accounts, - percentAllocations, - 0, - deploymentSalt - ); - - assertEq(address(controller), predictedAddress, "predicted_address_invalid"); - } - -} \ No newline at end of file + } + + function test_RevertIfInvalidOwner() public { + vm.expectRevert(Invalid_Owner.selector); + + factory.createController( + address(1), address(0), accounts, percentAllocations, 0, keccak256(abi.encodePacked(uint256(123))) + ); + } + + function test_RevertIfInvalidSplitAddress() public { + vm.expectRevert(InvalidSplit_Address.selector); + + factory.createController( + address(0), address(1), accounts, percentAllocations, 0, keccak256(abi.encodePacked(uint256(123))) + ); + } + + function test_CanCreateController() public { + bytes32 deploymentSalt = keccak256(abi.encodePacked(uint256(1102))); + + address predictedAddress = + factory.predictSplitControllerAddress(owner, accounts, percentAllocations, 0, deploymentSalt); + + address split = ISplitMain(SPLIT_MAIN_GOERLI).createSplit(accounts, percentAllocations, 0, predictedAddress); + + ImmutableSplitController controller = + factory.createController(split, owner, accounts, percentAllocations, 0, deploymentSalt); + + assertEq(address(controller), predictedAddress, "predicted_address_invalid"); + } +} From b2c2a9b827492fab3abe5f6db574f92d39d651fc Mon Sep 17 00:00:00 2001 From: samparsky <8148384+samparsky@users.noreply.github.com> Date: Fri, 15 Sep 2023 17:37:03 +0100 Subject: [PATCH 36/82] chore: add zero address check to imsc constructor arg --- src/controllers/ImmutableSplitController.sol | 4 ++-- src/controllers/ImmutableSplitControllerFactory.sol | 11 +++++++++-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/controllers/ImmutableSplitController.sol b/src/controllers/ImmutableSplitController.sol index 5765dd3..eb14129 100644 --- a/src/controllers/ImmutableSplitController.sol +++ b/src/controllers/ImmutableSplitController.sol @@ -100,8 +100,8 @@ contract ImmutableSplitController is Clone { accounts = new address[](size); percentAllocations = new uint32[](size); - uint256 i; - for (i; i < size;) { + uint256 i = 0; + for (; i < size;) { uint256 recipient = _getRecipient(i); accounts[i] = address(uint160(recipient)); percentAllocations[i] = uint32(recipient >> ADDRESS_BITS); diff --git a/src/controllers/ImmutableSplitControllerFactory.sol b/src/controllers/ImmutableSplitControllerFactory.sol index f90da72..f95df70 100644 --- a/src/controllers/ImmutableSplitControllerFactory.sol +++ b/src/controllers/ImmutableSplitControllerFactory.sol @@ -8,6 +8,8 @@ import {ImmutableSplitController} from "./ImmutableSplitController.sol"; /// @author Obol /// @dev Deploys ImmutableSplitController cheaply using cwia clones contract ImmutableSplitControllerFactory { + /// @dev invalid address + error Invalid_Address(); /// @dev invalid owner address error Invalid_Owner(); /// @dev invalid split address @@ -112,8 +114,13 @@ contract ImmutableSplitControllerFactory { /// @dev initializes the factory /// @param splitMain_ Address of splitMain constructor(address splitMain_) { + if (splitMain_ == address(0)) revert Invalid_Address(); + splitMain = splitMain_; controller = new ImmutableSplitController(); + // this is to prevent the initialization of the + // implementation contract by external actors + controller.init(address(1)); } /// Deploys a new immutable controller @@ -181,8 +188,8 @@ contract ImmutableSplitControllerFactory { uint256 recipientsSize = accounts.length; uint256[] memory recipients = new uint[](recipientsSize); - uint256 i; - for (i; i < recipientsSize;) { + uint256 i = 0; + for (; i < recipientsSize;) { recipients[i] = (uint256(percentAllocations[i]) << ADDRESS_BITS) | uint256(uint160(accounts[i])); unchecked { From b08baa2672e0be09f4269a5a21a8327aabe07aff Mon Sep 17 00:00:00 2001 From: samparsky <8148384+samparsky@users.noreply.github.com> Date: Fri, 15 Sep 2023 17:40:33 +0100 Subject: [PATCH 37/82] test: add zero address test --- src/test/controllers/IMSCFactory.t.sol | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/test/controllers/IMSCFactory.t.sol b/src/test/controllers/IMSCFactory.t.sol index 8241c99..7c78e05 100644 --- a/src/test/controllers/IMSCFactory.t.sol +++ b/src/test/controllers/IMSCFactory.t.sol @@ -9,6 +9,7 @@ import { import {ISplitMain} from "src/interfaces/ISplitMain.sol"; contract IMSCFactory is Test { + error Invalid_Address(); error Invalid_Owner(); error InvalidSplit_Address(); error InvalidSplit__TooFewAccounts(uint256 accountsLength); @@ -47,6 +48,11 @@ contract IMSCFactory is Test { owner = makeAddr("owner"); } + function test_RevertIfSplitMainIsInvalid() public { + vm.expectRevert(Invalid_Address.selector); + new ImmutableSplitControllerFactory(address(0)); + } + function test_RevertIfAccountSizeIsOne() public { address[] memory newAccounts = new address[](1); newAccounts[0] = makeAddr("testRevertIfAccountSizeIsOne"); From 50ce277919723c80b96f6353fa8d1f8facda6e0e Mon Sep 17 00:00:00 2001 From: samparsky <8148384+samparsky@users.noreply.github.com> Date: Fri, 15 Sep 2023 18:01:05 +0100 Subject: [PATCH 38/82] chore: apply forge fmt --- .../ImmutableSplitControllerFactory.sol | 39 +++++++---- src/interfaces/ISplitMain.sol | 69 ++++++++++++------- src/interfaces/ISplitMainV2.sol | 45 ++++++++---- src/lido/LidoSplitFactory.sol | 3 +- src/owr/OptimisticWithdrawalRecipient.sol | 21 ++++-- .../OptimisticWithdrawalRecipientFactory.sol | 9 ++- 6 files changed, 124 insertions(+), 62 deletions(-) diff --git a/src/controllers/ImmutableSplitControllerFactory.sol b/src/controllers/ImmutableSplitControllerFactory.sol index f95df70..80f90eb 100644 --- a/src/controllers/ImmutableSplitControllerFactory.sol +++ b/src/controllers/ImmutableSplitControllerFactory.sol @@ -16,11 +16,13 @@ contract ImmutableSplitControllerFactory { error InvalidSplit_Address(); /// @dev invalid split accounts configuration error InvalidSplit__TooFewAccounts(uint256 accountsLength); - /// @notice Array lengths of accounts & percentAllocations don't match (`accountsLength` != `allocationsLength`) + /// @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` + /// @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` @@ -29,7 +31,8 @@ contract ImmutableSplitControllerFactory { /// @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) + /// @notice Invalid distributorFee `distributorFee` cannot be greater than + /// 10% (1e5) /// @param distributorFee Invalid distributorFee amount error InvalidSplit__InvalidDistributorFee(uint32 distributorFee); @@ -102,7 +105,8 @@ contract ImmutableSplitControllerFactory { 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 + // overflow should be impossible in array access math with validated + // equal array lengths if (percentAllocations[loopLength] == uint32(0)) revert InvalidSplit__AllocationMustBePositive(loopLength); } @@ -127,8 +131,10 @@ contract ImmutableSplitControllerFactory { /// @dev Create a new immutable split controller /// @param split Address of the split to create a controller for /// @param owner Address that can call the updateSplit(..) function - /// @param accounts Ordered, unique list of addresses with ownership in the split - /// @param percentAllocations Percent allocations associated with each address + /// @param accounts Ordered, unique list of addresses with ownership in the + /// split + /// @param percentAllocations Percent allocations associated with each + /// address /// @param distributorFee Distributor fee share /// @param deploymentSalt salt to use for deterministic deploy function createController( @@ -155,12 +161,16 @@ contract ImmutableSplitControllerFactory { emit CreateIMSC(address(controller), split, owner, accounts, percentAllocations, distributorFee); } - /// @notice Predicts the address for an immutable split controller created with + /// @notice Predicts the address for an immutable split controller created + /// with /// recipients `accounts` with ownerships `percentAllocations` /// and a keeper fee for splitting of `distributorFee` - /// @param accounts Ordered, unique list of addresses with ownership in the split - /// @param percentAllocations Percent allocations associated with each address - /// @param distributorFee Keeper fee paid by split to cover gas costs of distribution + /// @param accounts Ordered, unique list of addresses with ownership in the + /// split + /// @param percentAllocations Percent allocations associated with each + /// address + /// @param distributorFee Keeper fee paid by split to cover gas costs of + /// distribution /// @param deploymentSalt Salt to use to deploy /// @return splitController Predicted address of such a split controller function predictSplitControllerAddress( @@ -176,9 +186,12 @@ contract ImmutableSplitControllerFactory { } /// @dev Packs split controller data - /// @param accounts Ordered, unique list of addresses with ownership in the split - /// @param percentAllocations Percent allocations associated with each address - /// @param distributorFee Keeper fee paid by split to cover gas costs of distribution + /// @param accounts Ordered, unique list of addresses with ownership in the + /// split + /// @param percentAllocations Percent allocations associated with each + /// address + /// @param distributorFee Keeper fee paid by split to cover gas costs of + /// distribution function _packSplitControllerData( address owner, address[] calldata accounts, diff --git a/src/interfaces/ISplitMain.sol b/src/interfaces/ISplitMain.sol index 707b2e6..062007c 100644 --- a/src/interfaces/ISplitMain.sol +++ b/src/interfaces/ISplitMain.sol @@ -11,11 +11,16 @@ struct SplitConfiguration { } interface ISplitMain { - /// @notice Creates a new split with recipients `accounts` with ownerships `percentAllocations`, a - /// keeper fee for splitting of `distributorFee` and the controlling address `controller` - /// @param accounts Ordered, unique list of addresses with ownership in the split - /// @param percentAllocations Percent allocations associated with each address - /// @param distributorFee Keeper fee paid by split to cover gas costs of distribution + /// @notice Creates a new split with recipients `accounts` with ownerships + /// `percentAllocations`, a + /// keeper fee for splitting of `distributorFee` and the controlling address + /// `controller` + /// @param accounts Ordered, unique list of addresses with ownership in the + /// split + /// @param percentAllocations Percent allocations associated with each + /// address + /// @param distributorFee Keeper fee paid by split to cover gas costs of + /// distribution /// @param controller Controlling address (0x0 if immutable) /// @return split Address of newly created split function createSplit( @@ -25,11 +30,16 @@ interface ISplitMain { address controller ) external returns (address); - /// @notice Predicts the address for an immutable split created with recipients `accounts` with - /// ownerships `percentAllocations` and a keeper fee for splitting of `distributorFee` - /// @param accounts Ordered, unique list of addresses with ownership in the split - /// @param percentAllocations Percent allocations associated with each address - /// @param distributorFee Keeper fee paid by split to cover gas costs of distribution + /// @notice Predicts the address for an immutable split created with + /// recipients `accounts` with + /// ownerships `percentAllocations` and a keeper fee for splitting of + /// `distributorFee` + /// @param accounts Ordered, unique list of addresses with ownership in the + /// split + /// @param percentAllocations Percent allocations associated with each + /// address + /// @param distributorFee Keeper fee paid by split to cover gas costs of + /// distribution /// @return split Predicted address of such an immutable split function predictImmutableSplitAddress( address[] calldata accounts, @@ -38,12 +48,16 @@ interface ISplitMain { ) external view returns (address split); /// @notice Distributes the ETH balance for split `split` - /// @dev `accounts`, `percentAllocations`, and `distributorFee` are verified by hashing + /// @dev `accounts`, `percentAllocations`, and `distributorFee` are verified + /// by hashing /// & comparing to the hash in storage associated with split `split` /// @param split Address of split to distribute balance for - /// @param accounts Ordered, unique list of addresses with ownership in the split - /// @param percentAllocations Percent allocations associated with each address - /// @param distributorFee Keeper fee paid by split to cover gas costs of distribution + /// @param accounts Ordered, unique list of addresses with ownership in the + /// split + /// @param percentAllocations Percent allocations associated with each + /// address + /// @param distributorFee Keeper fee paid by split to cover gas costs of + /// distribution /// @param distributorAddress Address to pay `distributorFee` to function distributeETH( address split, @@ -54,15 +68,20 @@ interface ISplitMain { ) external; /// @notice Distributes the ERC20 `token` balance for split `split` - /// @dev `accounts`, `percentAllocations`, and `distributorFee` are verified by hashing + /// @dev `accounts`, `percentAllocations`, and `distributorFee` are verified + /// by hashing /// & comparing to the hash in storage associated with split `split` /// @dev pernicious ERC20s may cause overflow in this function inside - /// _scaleAmountByPercentage, but results do not affect ETH & other ERC20 balances + /// _scaleAmountByPercentage, but results do not affect ETH & other ERC20 + /// balances /// @param split Address of split to distribute balance for /// @param token Address of ERC20 to distribute balance for - /// @param accounts Ordered, unique list of addresses with ownership in the split - /// @param percentAllocations Percent allocations associated with each address - /// @param distributorFee Keeper fee paid by split to cover gas costs of distribution + /// @param accounts Ordered, unique list of addresses with ownership in the + /// split + /// @param percentAllocations Percent allocations associated with each + /// address + /// @param distributorFee Keeper fee paid by split to cover gas costs of + /// distribution /// @param distributorAddress Address to pay `distributorFee` to function distributeERC20( address split, @@ -79,12 +98,16 @@ interface ISplitMain { /// @param tokens Addresses of ERC20s to withdraw function withdraw(address account, uint256 withdrawETH, ERC20[] calldata tokens) external; - /// @notice Updates an existing split with recipients `accounts` with ownerships `percentAllocations` and a keeper fee + /// @notice Updates an existing split with recipients `accounts` with + /// ownerships `percentAllocations` and a keeper fee /// for splitting of `distributorFee` /// @param split Address of mutable split to update - /// @param accounts Ordered, unique list of addresses with ownership in the split - /// @param percentAllocations Percent allocations associated with each address - /// @param distributorFee Keeper fee paid by split to cover gas costs of distribution + /// @param accounts Ordered, unique list of addresses with ownership in the + /// split + /// @param percentAllocations Percent allocations associated with each + /// address + /// @param distributorFee Keeper fee paid by split to cover gas costs of + /// distribution function updateSplit( address split, address[] calldata accounts, diff --git a/src/interfaces/ISplitMainV2.sol b/src/interfaces/ISplitMainV2.sol index ecad225..6723920 100644 --- a/src/interfaces/ISplitMainV2.sol +++ b/src/interfaces/ISplitMainV2.sol @@ -12,12 +12,17 @@ struct SplitConfiguration { } interface ISplitMainV2 { - /// @notice Creates a new split with recipients `accounts` with ownerships `percentAllocations`, a - /// keeper fee for splitting of `distributorFee` and the controlling address `controller` - /// @param accounts Ordered, unique list of addresses with ownership in the split - /// @param percentAllocations Percent allocations associated with each address + /// @notice Creates a new split with recipients `accounts` with ownerships + /// `percentAllocations`, a + /// keeper fee for splitting of `distributorFee` and the controlling address + /// `controller` + /// @param accounts Ordered, unique list of addresses with ownership in the + /// split + /// @param percentAllocations Percent allocations associated with each + /// address /// @param controller Controlling address (0x0 if immutable) - /// @param distributorFee Keeper fee paid by split to cover gas costs of distribution + /// @param distributorFee Keeper fee paid by split to cover gas costs of + /// distribution /// @return split Address of newly created split function createSplit( address splitWalletImplementation, @@ -28,11 +33,16 @@ interface ISplitMainV2 { uint32 distributorFee ) external returns (address); - /// @notice Predicts the address for an immutable split created with recipients `accounts` with - /// ownerships `percentAllocations` and a keeper fee for splitting of `distributorFee` - /// @param accounts Ordered, unique list of addresses with ownership in the split - /// @param percentAllocations Percent allocations associated with each address - /// @param distributorFee Keeper fee paid by split to cover gas costs of distribution + /// @notice Predicts the address for an immutable split created with + /// recipients `accounts` with + /// ownerships `percentAllocations` and a keeper fee for splitting of + /// `distributorFee` + /// @param accounts Ordered, unique list of addresses with ownership in the + /// split + /// @param percentAllocations Percent allocations associated with each + /// address + /// @param distributorFee Keeper fee paid by split to cover gas costs of + /// distribution /// @return split Predicted address of such an immutable split function predictImmutableSplitAddress( address splitWalletImplementation, @@ -57,12 +67,16 @@ interface ISplitMainV2 { function makeSplitImmutable(address split) external; /// @notice Distributes the ETH balance for split `split` - /// @dev `accounts`, `percentAllocations`, and `distributorFee` are verified by hashing + /// @dev `accounts`, `percentAllocations`, and `distributorFee` are verified + /// by hashing /// & comparing to the hash in storage associated with split `split` /// @param split Address of split to distribute balance for - /// @param accounts Ordered, unique list of addresses with ownership in the split - /// @param percentAllocations Percent allocations associated with each address - /// @param distributorFee Keeper fee paid by split to cover gas costs of distribution + /// @param accounts Ordered, unique list of addresses with ownership in the + /// split + /// @param percentAllocations Percent allocations associated with each + /// address + /// @param distributorFee Keeper fee paid by split to cover gas costs of + /// distribution /// @param distributorAddress Address to pay `distributorFee` to function distributeETH( address split, @@ -123,7 +137,8 @@ interface ISplitMainV2 { /** * @notice emitted after each initiated split control transfer * @param split Address of the split control transfer was initiated for - * @param newPotentialController Address of the split's new potential controller + * @param newPotentialController Address of the split's new potential + * controller */ event InitiateControlTransfer(address indexed split, address indexed newPotentialController); diff --git a/src/lido/LidoSplitFactory.sol b/src/lido/LidoSplitFactory.sol index 135bcd3..a31310e 100644 --- a/src/lido/LidoSplitFactory.sol +++ b/src/lido/LidoSplitFactory.sol @@ -48,7 +48,8 @@ contract LidoSplitFactory { lidoSplitImpl = new LidoSplit(); } - /// Creates a wrapper for splitWallet that transforms stETH token into wstETH + /// 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) { diff --git a/src/owr/OptimisticWithdrawalRecipient.sol b/src/owr/OptimisticWithdrawalRecipient.sol index 5ff0074..7fda447 100644 --- a/src/owr/OptimisticWithdrawalRecipient.sol +++ b/src/owr/OptimisticWithdrawalRecipient.sol @@ -45,11 +45,13 @@ contract OptimisticWithdrawalRecipient is Clone { /// Emitted after funds are distributed to recipients /// @param principalPayout Amount of principal paid out /// @param rewardPayout Amount of reward paid out - /// @param pullFlowFlag Flag for pushing funds to recipients or storing for pulling + /// @param pullFlowFlag Flag for pushing funds to recipients or storing for + /// pulling event DistributeFunds(uint256 principalPayout, uint256 rewardPayout, uint256 pullFlowFlag); /// Emitted after non-OWRecipient tokens are recovered to a recipient - /// @param recoveryAddressToken Recovered token (cannot be OptimisticWithdrawalRecipient token) + /// @param recoveryAddressToken Recovered token (cannot be + /// OptimisticWithdrawalRecipient token) /// @param recipient Address receiving recovered token /// @param amount Amount of recovered token event RecoverNonOWRecipientFunds(address recoveryAddressToken, address recipient, uint256 amount); @@ -91,7 +93,8 @@ contract OptimisticWithdrawalRecipient is Clone { uint256 internal constant TOKEN_OFFSET = 0; // 20 = token_offset (0) + token_size (address, 20 bytes) uint256 internal constant RECOVERY_ADDRESS_OFFSET = 20; - // 40 = recoveryAddress_offset (20) + recoveryAddress_size (address, 20 bytes) + // 40 = recoveryAddress_offset (20) + recoveryAddress_size (address, 20 + // bytes) uint256 internal constant TRANCHES_OFFSET = 40; /// Address of ERC20 to distribute (0x0 used for ETH) @@ -286,7 +289,8 @@ contract OptimisticWithdrawalRecipient is Clone { (address principalRecipient, address rewardRecipient, uint256 amountOfPrincipalStake) = getTranches(); - // determine which recipeint is getting paid based on funds to be distributed + // determine which recipeint is getting paid based on funds to be + // distributed uint256 _principalPayout = 0; uint256 _rewardPayout = 0; @@ -296,12 +300,14 @@ contract OptimisticWithdrawalRecipient is Clone { if (_fundsToBeDistributed >= BALANCE_CLASSIFICATION_THRESHOLD && principalStakeRemaining > 0) { if (_fundsToBeDistributed > principalStakeRemaining) { - // this means there is reward part of the funds to be distributed + // this means there is reward part of the funds to be + // distributed _principalPayout = principalStakeRemaining; // shouldn't underflow _rewardPayout = _fundsToBeDistributed - principalStakeRemaining; } else { - // this means there is no reward part of the funds to be distributed + // this means there is no reward part of the funds to be + // distributed _principalPayout = _fundsToBeDistributed; } } else { @@ -320,7 +326,8 @@ contract OptimisticWithdrawalRecipient is Clone { /// interactions // pay outs - // earlier tranche recipients may try to re-enter but will cause fn to revert + // earlier tranche recipients may try to re-enter but will cause fn to + // revert // when later external calls fail (bc balance is emptied early) // pay out principal diff --git a/src/owr/OptimisticWithdrawalRecipientFactory.sol b/src/owr/OptimisticWithdrawalRecipientFactory.sol index 7e9519c..7cf9c2d 100644 --- a/src/owr/OptimisticWithdrawalRecipientFactory.sol +++ b/src/owr/OptimisticWithdrawalRecipientFactory.sol @@ -6,7 +6,8 @@ import {LibClone} from "solady/utils/LibClone.sol"; /// @title OptimisticWithdrawalRecipientFactory /// @author Obol -/// @notice A factory contract for cheaply deploying OptimisticWithdrawalRecipient. +/// @notice A factory contract for cheaply deploying +/// OptimisticWithdrawalRecipient. /// @dev This contract uses token = address(0) to refer to ETH. contract OptimisticWithdrawalRecipientFactory { /// ----------------------------------------------------------------------- @@ -82,11 +83,13 @@ contract OptimisticWithdrawalRecipientFactory { /// @param token Address of ERC20 to distribute (0x0 used for ETH) /// @param recoveryAddress Address to recover non-OWR tokens to /// If this address is 0x0, recovery of unrelated tokens can be completed by - /// either the principal or reward recipients. If this address is set, only this address can recover + /// either the principal or reward recipients. If this address is set, only + /// this address can recover /// tokens (or ether) that isn't the token of the OWRecipient contract /// @param principalRecipient Address to distribute principal payments to /// @param rewardRecipient Address to distribute reward payments to - /// @param amountOfPrincipalStake Absolute amount of stake to be paid to principal recipient (multiple of 32 ETH) + /// @param amountOfPrincipalStake Absolute amount of stake to be paid to + /// principal recipient (multiple of 32 ETH) /// (reward recipient has no threshold & receives all residual flows) /// it cannot be greater than uint96 /// @return owr Address of new OptimisticWithdrawalRecipient clone From 6f2beae0e22ef1f90e4f49280ed45a5985c7d246 Mon Sep 17 00:00:00 2001 From: samparsky <8148384+samparsky@users.noreply.github.com> Date: Mon, 25 Sep 2023 00:26:58 +0100 Subject: [PATCH 39/82] chore: add IMSC recipients array size check #73 --- src/controllers/ImmutableSplitControllerFactory.sol | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/controllers/ImmutableSplitControllerFactory.sol b/src/controllers/ImmutableSplitControllerFactory.sol index 80f90eb..98237af 100644 --- a/src/controllers/ImmutableSplitControllerFactory.sol +++ b/src/controllers/ImmutableSplitControllerFactory.sol @@ -35,6 +35,9 @@ contract ImmutableSplitControllerFactory { /// 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); /// ----------------------------------------------------------------------- /// libraries @@ -199,6 +202,8 @@ contract ImmutableSplitControllerFactory { uint32 distributorFee ) internal view returns (bytes memory data) { uint256 recipientsSize = accounts.length; + if (recipientsSize > type(uint8).max) revert InvalidSplit__TooManyAccounts(recipientsSize); + uint256[] memory recipients = new uint[](recipientsSize); uint256 i = 0; From 08b424d3eb253ac55f8212a5345cf0eb6493fef8 Mon Sep 17 00:00:00 2001 From: samparsky <8148384+samparsky@users.noreply.github.com> Date: Mon, 25 Sep 2023 00:59:32 +0100 Subject: [PATCH 40/82] chore: add split balance check #78 --- src/controllers/ImmutableSplitController.sol | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/controllers/ImmutableSplitController.sol b/src/controllers/ImmutableSplitController.sol index eb14129..543ce37 100644 --- a/src/controllers/ImmutableSplitController.sol +++ b/src/controllers/ImmutableSplitController.sol @@ -14,6 +14,10 @@ contract ImmutableSplitController is Clone { /// @notice error Unauthorized(); + /// @notice Revert if split balance is > 1 + /// @dev Prevent distribution of current balance + error Invalid_SplitBalance(); + /// ----------------------------------------------------------------------- /// storage /// ----------------------------------------------------------------------- @@ -65,6 +69,9 @@ contract ImmutableSplitController is Clone { (address[] memory accounts, uint32[] memory percentAllocations) = getNewSplitConfiguration(); + // prevent distribution of existing money + if (address(split).balance > 1) revert Invalid_SplitBalance(); + ISplitMain(splitMain()).updateSplit(split, accounts, percentAllocations, uint32(distributorFee())); } From b832f7551a35b1af1a087ecddba557bb9af5937b Mon Sep 17 00:00:00 2001 From: samparsky <8148384+samparsky@users.noreply.github.com> Date: Tue, 26 Sep 2023 10:33:29 +0100 Subject: [PATCH 41/82] test: fix IMSC and IMSCFactory test cases; add additional test case --- src/test/controllers/IMSC.t.sol | 8 +++++ src/test/controllers/IMSCFactory.t.sol | 43 ++++++++++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/src/test/controllers/IMSC.t.sol b/src/test/controllers/IMSC.t.sol index d44aaec..7d3cc43 100644 --- a/src/test/controllers/IMSC.t.sol +++ b/src/test/controllers/IMSC.t.sol @@ -11,6 +11,7 @@ import {ISplitMain} from "src/interfaces/ISplitMain.sol"; contract IMSC is Test { error Initialized(); error Unauthorized(); + error Invalid_SplitBalance(); address internal SPLIT_MAIN_GOERLI = 0x2ed6c4B5dA6378c7897AC67Ba9e43102Feb694EE; uint256 public constant PERCENTAGE_SCALE = 1e6; @@ -123,6 +124,13 @@ contract IMSC is Test { controller.updateSplit(); } + function testCannot_updateSplitIfBalanceGreaterThanOne() public { + deal(address(split), 1 ether); + vm.expectRevert(Invalid_SplitBalance.selector); + vm.prank(owner); + controller.updateSplit(); + } + function testCan_updateSplit() public { vm.prank(owner); controller.updateSplit(); diff --git a/src/test/controllers/IMSCFactory.t.sol b/src/test/controllers/IMSCFactory.t.sol index 7c78e05..6a25d45 100644 --- a/src/test/controllers/IMSCFactory.t.sol +++ b/src/test/controllers/IMSCFactory.t.sol @@ -21,6 +21,7 @@ contract IMSCFactory is Test { address internal SPLIT_MAIN_GOERLI = 0x2ed6c4B5dA6378c7897AC67Ba9e43102Feb694EE; uint32 public constant SPLIT_MAIN_PERCENTAGE_SCALE = 1e6; + uint256 public constant PERCENTAGE_SCALE = 1e6; ImmutableSplitControllerFactory public factory; ImmutableSplitController public cntrlImpl; @@ -145,6 +146,30 @@ contract IMSCFactory is Test { ); } + function test_RevertIfRecipeintSizeTooMany() public { + bytes32 deploymentSalt = keccak256(abi.encodePacked(uint256(1102))); + + uint256 size = 400; + address[] memory localAccounts = _generateAddresses(1, size); + uint32[] memory localAllocations = _generatePercentAlloc(size); + + vm.expectRevert( + abi.encodeWithSelector( + ImmutableSplitControllerFactory.InvalidSplit__TooManyAccounts.selector, + size + ) + ); + + factory.createController( + address(1), + owner, + localAccounts, + localAllocations, + 0, + deploymentSalt + ); + } + function test_CanCreateController() public { bytes32 deploymentSalt = keccak256(abi.encodePacked(uint256(1102))); @@ -158,4 +183,22 @@ contract IMSCFactory is Test { assertEq(address(controller), predictedAddress, "predicted_address_invalid"); } + + function _generateAddresses(uint256 _seed, uint256 size) internal pure returns (address[] memory accts) { + accts = new address[](size); + uint160 seed = uint160(uint256(keccak256(abi.encodePacked(_seed)))); + for (uint160 i; i < size; i++) { + accts[i] = address(seed); + seed += 1; + } + } + + function _generatePercentAlloc(uint256 size) internal pure returns (uint32[] memory alloc) { + alloc = new uint32[](size); + for (uint256 i; i < size; i++) { + alloc[i] = uint32(PERCENTAGE_SCALE / size); + } + + if (PERCENTAGE_SCALE % size != 0) alloc[size - 1] += uint32(PERCENTAGE_SCALE % size); + } } From 62d4219b9167fe5f7bbbe549d4794d893e308b58 Mon Sep 17 00:00:00 2001 From: samparsky <8148384+samparsky@users.noreply.github.com> Date: Tue, 26 Sep 2023 10:43:29 +0100 Subject: [PATCH 42/82] chore: move stETH and wstETH addresses to LidoSplit --- src/lido/LidoSplit.sol | 40 +++++++++++----------------- src/lido/LidoSplitFactory.sol | 12 ++------- src/test/lido/LIdoSplitFactory.t.sol | 6 ++--- src/test/lido/LidoSplit.t.sol | 8 ++---- 4 files changed, 23 insertions(+), 43 deletions(-) diff --git a/src/lido/LidoSplit.sol b/src/lido/LidoSplit.sol index 8156d3e..3c68bc5 100644 --- a/src/lido/LidoSplit.sol +++ b/src/lido/LidoSplit.sol @@ -24,17 +24,24 @@ contract LidoSplit is Clone { /// storage - cwia offsets /// ----------------------------------------------------------------------- - // stETH (address, 20 bytes), - // 0; first item - uint256 internal constant ST_ETH_ADDRESS_OFFSET = 0; - // wstETH (address, 20 bytees) - // 20 = st_eth_offset(0) + st_eth_address_size(address, 20 bytes) - uint256 internal constant WST_ETH_ADDRESS_OFFSET = 20; // splitWallet (adress, 20 bytes) - // 40 = wst_eth_offset(20) + wst_eth_size(address, 20 bytes) - uint256 internal constant SPLIT_WALLET_ADDRESS_OFFSET = 40; + // 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; - constructor() {} + constructor(ERC20 _stETH, ERC20 _wstETH) { + stETH = _stETH; + wstETH = _wstETH; + } /// Address of split wallet to send funds to to /// @dev equivalent to address public immutable splitWallet @@ -42,25 +49,10 @@ contract LidoSplit is Clone { return _getArgAddress(SPLIT_WALLET_ADDRESS_OFFSET); } - /// Address of stETH token - /// @dev equivalent to address public immutable stETHAddress - function stETHAddress() public pure returns (address) { - return _getArgAddress(ST_ETH_ADDRESS_OFFSET); - } - - /// Address of wstETH token - /// @dev equivalent to address public immutable wstETHAddress - function wstETHAddress() public pure returns (address) { - return _getArgAddress(WST_ETH_ADDRESS_OFFSET); - } - /// 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) { - ERC20 stETH = ERC20(stETHAddress()); - ERC20 wstETH = ERC20(wstETHAddress()); - // get current balance uint256 balance = stETH.balanceOf(address(this)); // approve the wstETH diff --git a/src/lido/LidoSplitFactory.sol b/src/lido/LidoSplitFactory.sol index a31310e..922827c 100644 --- a/src/lido/LidoSplitFactory.sol +++ b/src/lido/LidoSplitFactory.sol @@ -33,19 +33,11 @@ contract LidoSplitFactory { /// storage /// ----------------------------------------------------------------------- - /// @notice stETH token address - ERC20 public immutable stETH; - - /// @notice wstETH token address - ERC20 public immutable wstETH; - /// @dev lido split implementation LidoSplit public immutable lidoSplitImpl; constructor(ERC20 _stETH, ERC20 _wstETH) { - stETH = _stETH; - wstETH = _wstETH; - lidoSplitImpl = new LidoSplit(); + lidoSplitImpl = new LidoSplit(_stETH, _wstETH); } /// Creates a wrapper for splitWallet that transforms stETH token into @@ -55,7 +47,7 @@ contract LidoSplitFactory { function createSplit(address splitWallet) external returns (address lidoSplit) { if (splitWallet == address(0)) revert Invalid_Wallet(); - lidoSplit = address(lidoSplitImpl).clone(abi.encodePacked(stETH, wstETH, splitWallet)); + lidoSplit = address(lidoSplitImpl).clone(abi.encodePacked(splitWallet)); emit CreateLidoSplit(lidoSplit); } diff --git a/src/test/lido/LIdoSplitFactory.t.sol b/src/test/lido/LIdoSplitFactory.t.sol index a42f44c..9ffb7cb 100644 --- a/src/test/lido/LIdoSplitFactory.t.sol +++ b/src/test/lido/LIdoSplitFactory.t.sol @@ -18,9 +18,9 @@ contract LidoSplitFactoryTest is LidoSplitTestHelper, Test { vm.createSelectFork(getChain("mainnet").rpcUrl, mainnetBlock); lidoSplitFactory = new LidoSplitFactory( - ERC20(STETH_MAINNET_ADDRESS), - ERC20(WSTETH_MAINNET_ADDRESS) - ); + ERC20(STETH_MAINNET_ADDRESS), + ERC20(WSTETH_MAINNET_ADDRESS) + ); demoSplit = makeAddr("demoSplit"); } diff --git a/src/test/lido/LidoSplit.t.sol b/src/test/lido/LidoSplit.t.sol index d39b4e6..89c3d00 100644 --- a/src/test/lido/LidoSplit.t.sol +++ b/src/test/lido/LidoSplit.t.sol @@ -28,8 +28,8 @@ contract LidoSplitTest is LidoSplitTestHelper, Test { function test_CloneArgsIsCorrect() public { assertEq(lidoSplit.splitWallet(), demoSplit, "invalid address"); - assertEq(lidoSplit.stETHAddress(), STETH_MAINNET_ADDRESS, "invalid stETH address"); - assertEq(lidoSplit.wstETHAddress(), WSTETH_MAINNET_ADDRESS, "invalid wstETH address"); + assertEq(address(lidoSplit.stETH()), STETH_MAINNET_ADDRESS, "invalid stETH address"); + assertEq(address(lidoSplit.wstETH()), WSTETH_MAINNET_ADDRESS, "invalid wstETH address"); } function test_CanDistribute() public { @@ -46,10 +46,6 @@ contract LidoSplitTest is LidoSplitTestHelper, Test { uint256 afterBalance = ERC20(WSTETH_MAINNET_ADDRESS).balanceOf(demoSplit); - console.log("checking"); - console.log(afterBalance); - console.log(prevBalance); - assertGe(afterBalance, prevBalance, "after balance greater"); } } From 5057aebcef2133add23e45f3829b69c6787a8d0a Mon Sep 17 00:00:00 2001 From: samparsky <8148384+samparsky@users.noreply.github.com> Date: Tue, 26 Sep 2023 10:48:11 +0100 Subject: [PATCH 43/82] chore: add rescueETH function to LidoSplit --- src/lido/LidoSplit.sol | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/lido/LidoSplit.sol b/src/lido/LidoSplit.sol index 3c68bc5..4b86033 100644 --- a/src/lido/LidoSplit.sol +++ b/src/lido/LidoSplit.sol @@ -19,6 +19,7 @@ contract LidoSplit is Clone { /// libraries /// ----------------------------------------------------------------------- using SafeTransferLib for ERC20; + using SafeTransferLib for address; /// ----------------------------------------------------------------------- /// storage - cwia offsets @@ -62,4 +63,11 @@ contract LidoSplit is Clone { // transfer to split wallet ERC20(wstETH).safeTransfer(splitWallet(), amount); } + + /// @notice Rescue stuck ETH + /// @return balance Amount of ETH rescued + function rescueETH() external returns (uint256 balance) { + balance = address(this).balance; + splitWallet().safeTransferETH(balance); + } } From 82c2ac5b0fa09a9243d716d6e1eb1e3e38be3c59 Mon Sep 17 00:00:00 2001 From: samparsky <8148384+samparsky@users.noreply.github.com> Date: Tue, 26 Sep 2023 11:00:39 +0100 Subject: [PATCH 44/82] chore: change rescueETH to rescueFunds to enable rescue of tokens --- src/lido/LidoSplit.sol | 22 +++++++++++++++++++--- src/test/lido/LidoSplit.t.sol | 6 ++++++ 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/src/lido/LidoSplit.sol b/src/lido/LidoSplit.sol index 4b86033..3b40106 100644 --- a/src/lido/LidoSplit.sol +++ b/src/lido/LidoSplit.sol @@ -15,12 +15,17 @@ interface IwSTETH { /// stETH token to wstETH token because stETH is a rebasing token /// @dev Wraps stETH to wstETH and transfers to defined SplitWallet address contract LidoSplit is Clone { + + error Invalid_Address(); + /// ----------------------------------------------------------------------- /// libraries /// ----------------------------------------------------------------------- using SafeTransferLib for ERC20; using SafeTransferLib for address; + address internal constant ETH_ADDRESS = address(0); + /// ----------------------------------------------------------------------- /// storage - cwia offsets /// ----------------------------------------------------------------------- @@ -29,6 +34,7 @@ contract LidoSplit is Clone { // 0; first item uint256 internal constant SPLIT_WALLET_ADDRESS_OFFSET = 0; + /// ----------------------------------------------------------------------- /// storage /// ----------------------------------------------------------------------- @@ -65,9 +71,19 @@ contract LidoSplit is Clone { } /// @notice Rescue stuck ETH + /// Uses token == address(0) to represent ETH /// @return balance Amount of ETH rescued - function rescueETH() external returns (uint256 balance) { - balance = address(this).balance; - splitWallet().safeTransferETH(balance); + function rescueFunds(address token) external returns (uint256 balance) { + 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).transfer(splitWallet(), balance); + } } } diff --git a/src/test/lido/LidoSplit.t.sol b/src/test/lido/LidoSplit.t.sol index 89c3d00..ca7d83f 100644 --- a/src/test/lido/LidoSplit.t.sol +++ b/src/test/lido/LidoSplit.t.sol @@ -32,6 +32,12 @@ contract LidoSplitTest is LidoSplitTestHelper, Test { assertEq(address(lidoSplit.wstETH()), WSTETH_MAINNET_ADDRESS, "invalid wstETH address"); } + function test_CanRescueETH() public { + deal(lidoSplit.splitWallet(), 1 ether); + + + } + function test_CanDistribute() public { // we use a random account on Etherscan to credit the lidoSplit address // with 10 ether worth of stETH on mainnet From 8dadd7848315fbef7932f82c6e570b967babb692 Mon Sep 17 00:00:00 2001 From: samparsky <8148384+samparsky@users.noreply.github.com> Date: Tue, 26 Sep 2023 11:11:15 +0100 Subject: [PATCH 45/82] test: fix LidoSplit test suite; add rescueFunds test cases --- src/test/lido/LidoSplit.t.sol | 37 ++++++++++++++++++++++++++++++++--- 1 file changed, 34 insertions(+), 3 deletions(-) diff --git a/src/test/lido/LidoSplit.t.sol b/src/test/lido/LidoSplit.t.sol index ca7d83f..8bd504f 100644 --- a/src/test/lido/LidoSplit.t.sol +++ b/src/test/lido/LidoSplit.t.sol @@ -5,6 +5,8 @@ import "forge-std/Test.sol"; import {LidoSplitFactory, LidoSplit} from "src/lido/LidoSplitFactory.sol"; import {ERC20} from "solmate/tokens/ERC20.sol"; import {LidoSplitTestHelper} from "./LidoSplitTestHelper.sol"; +import { MockERC20 } from "src/test/utils/mocks/MockERC20.sol"; + contract LidoSplitTest is LidoSplitTestHelper, Test { LidoSplitFactory internal lidoSplitFactory; @@ -12,6 +14,8 @@ contract LidoSplitTest is LidoSplitTestHelper, Test { address demoSplit; + MockERC20 mERC20; + function setUp() public { uint256 mainnetBlock = 17_421_005; vm.createSelectFork(getChain("mainnet").rpcUrl, mainnetBlock); @@ -24,6 +28,9 @@ contract LidoSplitTest is LidoSplitTestHelper, Test { demoSplit = makeAddr("demoSplit"); lidoSplit = LidoSplit(lidoSplitFactory.createSplit(demoSplit)); + + mERC20 = new MockERC20("Test Token", "TOK", 18); + mERC20.mint(type(uint256).max); } function test_CloneArgsIsCorrect() public { @@ -32,10 +39,34 @@ contract LidoSplitTest is LidoSplitTestHelper, Test { assertEq(address(lidoSplit.wstETH()), WSTETH_MAINNET_ADDRESS, "invalid wstETH address"); } - function test_CanRescueETH() public { - deal(lidoSplit.splitWallet(), 1 ether); + function test_CanRescueFunds() public { + // rescue ETH + uint256 amountOfEther = 1 ether; + deal(address(lidoSplit), amountOfEther); + + 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"); + + // 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"); + } - + function testCannot_RescueLidoTokens() public { + vm.expectRevert( + LidoSplit.Invalid_Address.selector + ); + lidoSplit.rescueFunds(address(STETH_MAINNET_ADDRESS)); + + vm.expectRevert( + LidoSplit.Invalid_Address.selector + ); + lidoSplit.rescueFunds(address(WSTETH_MAINNET_ADDRESS)); } function test_CanDistribute() public { From 58fd2e4d76a7db3678dd8b2fbc007bde6464b2b4 Mon Sep 17 00:00:00 2001 From: samparsky <8148384+samparsky@users.noreply.github.com> Date: Wed, 27 Sep 2023 07:10:07 +0100 Subject: [PATCH 46/82] chore: enable rescueFunds for wstETH --- lib/solady | 2 +- src/lido/LidoSplit.sol | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/solady b/lib/solady index 2b33744..77809c1 160000 --- a/lib/solady +++ b/lib/solady @@ -1 +1 @@ -Subproject commit 2b33744067c2afa74a6ee364004e99035cae692e +Subproject commit 77809c18e010b914dde9518956a4ae7cb507d383 diff --git a/src/lido/LidoSplit.sol b/src/lido/LidoSplit.sol index 3b40106..ef69379 100644 --- a/src/lido/LidoSplit.sol +++ b/src/lido/LidoSplit.sol @@ -74,9 +74,7 @@ contract LidoSplit is Clone { /// Uses token == address(0) to represent ETH /// @return balance Amount of ETH rescued function rescueFunds(address token) external returns (uint256 balance) { - if (token == address(stETH) || token == address(wstETH)) { - revert Invalid_Address(); - } + if (token == address(stETH)) revert Invalid_Address(); if (token == ETH_ADDRESS) { balance = address(this).balance; From b27ba37010a7d185b3f54473d1ac50211da25f55 Mon Sep 17 00:00:00 2001 From: samparsky <8148384+samparsky@users.noreply.github.com> Date: Wed, 27 Sep 2023 07:10:47 +0100 Subject: [PATCH 47/82] test: fix testCannot_RescueLidoTokens test case --- src/test/lido/LidoSplit.t.sol | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/test/lido/LidoSplit.t.sol b/src/test/lido/LidoSplit.t.sol index 8bd504f..37a5d33 100644 --- a/src/test/lido/LidoSplit.t.sol +++ b/src/test/lido/LidoSplit.t.sol @@ -62,11 +62,6 @@ contract LidoSplitTest is LidoSplitTestHelper, Test { LidoSplit.Invalid_Address.selector ); lidoSplit.rescueFunds(address(STETH_MAINNET_ADDRESS)); - - vm.expectRevert( - LidoSplit.Invalid_Address.selector - ); - lidoSplit.rescueFunds(address(WSTETH_MAINNET_ADDRESS)); } function test_CanDistribute() public { From c1c4fb7b916d130e7f29a28d56c8abb88fab20eb Mon Sep 17 00:00:00 2001 From: samparsky <8148384+samparsky@users.noreply.github.com> Date: Sun, 24 Sep 2023 17:09:45 +0100 Subject: [PATCH 48/82] chore: remove token from OWRFactory --- src/owr/OptimisticWithdrawalRecipientFactory.sol | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/src/owr/OptimisticWithdrawalRecipientFactory.sol b/src/owr/OptimisticWithdrawalRecipientFactory.sol index 7cf9c2d..04b31a7 100644 --- a/src/owr/OptimisticWithdrawalRecipientFactory.sol +++ b/src/owr/OptimisticWithdrawalRecipientFactory.sol @@ -14,9 +14,6 @@ contract OptimisticWithdrawalRecipientFactory { /// errors /// ----------------------------------------------------------------------- - /// Invalid token - error Invalid_Token(); - /// Invalid number of recipients, must be 2 error Invalid__Recipients(); @@ -39,7 +36,6 @@ contract OptimisticWithdrawalRecipientFactory { /// Emitted after a new OptimisticWithdrawalRecipient module is deployed /// @param owr Address of newly created OptimisticWithdrawalRecipient clone - /// @param token Address of ERC20 to distribute (0x0 used for ETH) /// @param recoveryAddress Address to recover non-OWR tokens to /// @param principalRecipient Address to distribute principal payment to /// @param rewardRecipient Address to distribute reward payment to @@ -47,7 +43,6 @@ contract OptimisticWithdrawalRecipientFactory { /// (reward recipient has no threshold & receives all residual flows) event CreateOWRecipient( address indexed owr, - address token, address recoveryAddress, address principalRecipient, address rewardRecipient, @@ -80,7 +75,6 @@ contract OptimisticWithdrawalRecipientFactory { /// ----------------------------------------------------------------------- /// Create a new OptimisticWithdrawalRecipient clone - /// @param token Address of ERC20 to distribute (0x0 used for ETH) /// @param recoveryAddress Address to recover non-OWR tokens to /// If this address is 0x0, recovery of unrelated tokens can be completed by /// either the principal or reward recipients. If this address is set, only @@ -94,7 +88,6 @@ contract OptimisticWithdrawalRecipientFactory { /// it cannot be greater than uint96 /// @return owr Address of new OptimisticWithdrawalRecipient clone function createOWRecipient( - address token, address recoveryAddress, address principalRecipient, address rewardRecipient, @@ -115,11 +108,11 @@ contract OptimisticWithdrawalRecipientFactory { // would not exceed contract size limits // important to not reorder - bytes memory data = abi.encodePacked(token, recoveryAddress, principalData, rewardData); + bytes memory data = abi.encodePacked(recoveryAddress, principalData, rewardData); owr = OptimisticWithdrawalRecipient(address(owrImpl).clone(data)); emit CreateOWRecipient( - address(owr), token, recoveryAddress, principalRecipient, rewardRecipient, amountOfPrincipalStake + address(owr), recoveryAddress, principalRecipient, rewardRecipient, amountOfPrincipalStake ); } } From e2477377775cfc181ba1d30171ce977ff501f7f1 Mon Sep 17 00:00:00 2001 From: samparsky <8148384+samparsky@users.noreply.github.com> Date: Sun, 24 Sep 2023 17:21:57 +0100 Subject: [PATCH 49/82] chore: remove token from OWR implementation --- src/owr/OptimisticWithdrawalRecipient.sol | 62 ++++++------------- .../OptimisticWithdrawalRecipientFactory.sol | 2 +- 2 files changed, 19 insertions(+), 45 deletions(-) diff --git a/src/owr/OptimisticWithdrawalRecipient.sol b/src/owr/OptimisticWithdrawalRecipient.sol index 7fda447..02ffa69 100644 --- a/src/owr/OptimisticWithdrawalRecipient.sol +++ b/src/owr/OptimisticWithdrawalRecipient.sol @@ -9,10 +9,8 @@ import {SafeTransferLib} from "solady/utils/SafeTransferLib.sol"; /// @author Obol /// @notice A maximally-composable contract that distributes payments /// based on threshold to it's recipients -/// @dev Only one token can be distributed for a given deployment. There is a -/// recovery method for non-target tokens sent by accident. -/// Target ERC20s with very large decimals may overflow & cause issues. -/// This contract uses token = address(0) to refer to ETH. +/// @dev Only ETH can be distributed for a given deployment. There is a +/// recovery method for tokens sent by accident. contract OptimisticWithdrawalRecipient is Clone { /// ----------------------------------------------------------------------- /// libraries @@ -24,9 +22,6 @@ contract OptimisticWithdrawalRecipient is Clone { /// errors /// ----------------------------------------------------------------------- - /// Invalid token recovery; cannot recover the OWRecipient token - error InvalidTokenRecovery_OWRToken(); - /// Invalid token recovery recipient error InvalidTokenRecovery_InvalidRecipient(); @@ -49,9 +44,9 @@ contract OptimisticWithdrawalRecipient is Clone { /// pulling event DistributeFunds(uint256 principalPayout, uint256 rewardPayout, uint256 pullFlowFlag); - /// Emitted after non-OWRecipient tokens are recovered to a recipient + /// Emitted after tokens are recovered to a recipient /// @param recoveryAddressToken Recovered token (cannot be - /// OptimisticWithdrawalRecipient token) + /// ETH) /// @param recipient Address receiving recovered token /// @param amount Amount of recovered token event RecoverNonOWRecipientFunds(address recoveryAddressToken, address recipient, uint256 amount); @@ -86,22 +81,14 @@ contract OptimisticWithdrawalRecipient is Clone { /// storage - cwia offsets /// ----------------------------------------------------------------------- - // token (address, 20 bytes), recoveryAddress (address, 20 bytes), + // recoveryAddress (address, 20 bytes), // tranches (uint256[], numTranches * 32 bytes) // 0; first item - uint256 internal constant TOKEN_OFFSET = 0; - // 20 = token_offset (0) + token_size (address, 20 bytes) - uint256 internal constant RECOVERY_ADDRESS_OFFSET = 20; - // 40 = recoveryAddress_offset (20) + recoveryAddress_size (address, 20 + uint256 internal constant RECOVERY_ADDRESS_OFFSET = 0; + // 20 = recoveryAddress_offset (0) + recoveryAddress_size (address, 20 // bytes) - uint256 internal constant TRANCHES_OFFSET = 40; - - /// Address of ERC20 to distribute (0x0 used for ETH) - /// @dev equivalent to address public immutable token; - function token() public pure returns (address) { - return _getArgAddress(TOKEN_OFFSET); - } + uint256 internal constant TRANCHES_OFFSET = 20; /// Address to recover non-OWR tokens to /// @dev equivalent to address public immutable recoveryAddress; @@ -179,9 +166,6 @@ contract OptimisticWithdrawalRecipient is Clone { function recoverFunds(address nonOWRToken, address recipient) external payable { /// checks - // revert if caller tries to recover OWRecipient token - if (nonOWRToken == token()) revert InvalidTokenRecovery_OWRToken(); - // if recoveryAddress is set, recipient must match it // else, recipient must be one of the OWR recipients @@ -201,30 +185,22 @@ contract OptimisticWithdrawalRecipient is Clone { /// interactions // recover non-target token - uint256 amount; - if (nonOWRToken == ETH_ADDRESS) { - amount = address(this).balance; - recipient.safeTransferETH(amount); - } else { - amount = ERC20(nonOWRToken).balanceOf(address(this)); - nonOWRToken.safeTransfer(recipient, amount); - } - + uint256 amount = ERC20(nonOWRToken).balanceOf(address(this)); + nonOWRToken.safeTransfer(recipient, amount); + emit RecoverNonOWRecipientFunds(nonOWRToken, recipient, amount); } /// Withdraw token balance for account `account` /// @param account Address to withdraw on behalf of function withdraw(address account) external { - address _token = token(); uint256 tokenAmount = pullBalances[account]; unchecked { // shouldn't underflow; fundsPendingWithdrawal = sum(pullBalances) fundsPendingWithdrawal -= uint128(tokenAmount); } pullBalances[account] = 0; - if (_token == ETH_ADDRESS) account.safeTransferETH(tokenAmount); - else _token.safeTransfer(account, tokenAmount); + account.safeTransferETH(tokenAmount); emit Withdrawal(account, tokenAmount); } @@ -269,7 +245,7 @@ contract OptimisticWithdrawalRecipient is Clone { // load storage into memory // fetch the token we want to distribute - address _token = token(); + // address _token = token(); // the amount of funds distributed so far uint256 _startingDistributedFunds = uint256(distributedFunds); uint256 _endingDistributedFunds; @@ -283,7 +259,7 @@ contract OptimisticWithdrawalRecipient is Clone { - _memoryFundsPendingWithdrawal // recognizes 0x0 as ETH // shouldn't need to worry about re-entrancy from ERC20 view fn - + (_token == ETH_ADDRESS ? address(this).balance : ERC20(_token).balanceOf(address(this))); + + address(this).balance; _fundsToBeDistributed = _endingDistributedFunds - _startingDistributedFunds; } @@ -331,9 +307,9 @@ contract OptimisticWithdrawalRecipient is Clone { // when later external calls fail (bc balance is emptied early) // pay out principal - _payout(_token, principalRecipient, _principalPayout, pullFlowFlag); + _payout(principalRecipient, _principalPayout, pullFlowFlag); // pay out reward - _payout(_token, rewardRecipient, _rewardPayout, pullFlowFlag); + _payout(rewardRecipient, _rewardPayout, pullFlowFlag); if (pullFlowFlag == PULL) { if (_principalPayout > 0 || _rewardPayout > 0) { @@ -345,15 +321,13 @@ contract OptimisticWithdrawalRecipient is Clone { emit DistributeFunds(_principalPayout, _rewardPayout, pullFlowFlag); } - function _payout(address payoutToken, address recipient, uint256 payoutAmount, uint256 pullFlowFlag) internal { + function _payout(address recipient, uint256 payoutAmount, uint256 pullFlowFlag) internal { if (payoutAmount > 0) { if (pullFlowFlag == PULL) { // Write to Storage pullBalances[recipient] += payoutAmount; - } else if (payoutToken == ETH_ADDRESS) { - recipient.safeTransferETH(payoutAmount); } else { - payoutToken.safeTransfer(recipient, payoutAmount); + recipient.safeTransferETH(payoutAmount); } } } diff --git a/src/owr/OptimisticWithdrawalRecipientFactory.sol b/src/owr/OptimisticWithdrawalRecipientFactory.sol index 04b31a7..39bda87 100644 --- a/src/owr/OptimisticWithdrawalRecipientFactory.sol +++ b/src/owr/OptimisticWithdrawalRecipientFactory.sol @@ -75,7 +75,7 @@ contract OptimisticWithdrawalRecipientFactory { /// ----------------------------------------------------------------------- /// Create a new OptimisticWithdrawalRecipient clone - /// @param recoveryAddress Address to recover non-OWR tokens to + /// @param recoveryAddress Address to recover tokens to /// If this address is 0x0, recovery of unrelated tokens can be completed by /// either the principal or reward recipients. If this address is set, only /// this address can recover From 431dcc45cb21673c5645929ae656f1ae6a749060 Mon Sep 17 00:00:00 2001 From: samparsky <8148384+samparsky@users.noreply.github.com> Date: Sun, 24 Sep 2023 17:22:13 +0100 Subject: [PATCH 50/82] chore: apply forge fmt --- src/owr/OptimisticWithdrawalRecipient.sol | 2 +- src/owr/OptimisticWithdrawalRecipientFactory.sol | 10 ++-------- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/src/owr/OptimisticWithdrawalRecipient.sol b/src/owr/OptimisticWithdrawalRecipient.sol index 02ffa69..5e63f42 100644 --- a/src/owr/OptimisticWithdrawalRecipient.sol +++ b/src/owr/OptimisticWithdrawalRecipient.sol @@ -187,7 +187,7 @@ contract OptimisticWithdrawalRecipient is Clone { // recover non-target token uint256 amount = ERC20(nonOWRToken).balanceOf(address(this)); nonOWRToken.safeTransfer(recipient, amount); - + emit RecoverNonOWRecipientFunds(nonOWRToken, recipient, amount); } diff --git a/src/owr/OptimisticWithdrawalRecipientFactory.sol b/src/owr/OptimisticWithdrawalRecipientFactory.sol index 39bda87..a6331c3 100644 --- a/src/owr/OptimisticWithdrawalRecipientFactory.sol +++ b/src/owr/OptimisticWithdrawalRecipientFactory.sol @@ -42,11 +42,7 @@ contract OptimisticWithdrawalRecipientFactory { /// @param threshold Absolute payment threshold for OWR first recipient /// (reward recipient has no threshold & receives all residual flows) event CreateOWRecipient( - address indexed owr, - address recoveryAddress, - address principalRecipient, - address rewardRecipient, - uint256 threshold + address indexed owr, address recoveryAddress, address principalRecipient, address rewardRecipient, uint256 threshold ); /// ----------------------------------------------------------------------- @@ -111,8 +107,6 @@ contract OptimisticWithdrawalRecipientFactory { bytes memory data = abi.encodePacked(recoveryAddress, principalData, rewardData); owr = OptimisticWithdrawalRecipient(address(owrImpl).clone(data)); - emit CreateOWRecipient( - address(owr), recoveryAddress, principalRecipient, rewardRecipient, amountOfPrincipalStake - ); + emit CreateOWRecipient(address(owr), recoveryAddress, principalRecipient, rewardRecipient, amountOfPrincipalStake); } } From 7202e264ff9c2e3685ccbfde7ea0e4040c2078b6 Mon Sep 17 00:00:00 2001 From: samparsky <8148384+samparsky@users.noreply.github.com> Date: Sun, 24 Sep 2023 17:32:45 +0100 Subject: [PATCH 51/82] chore: remove distributedFunds variable from OWR - issue #80 --- src/owr/OptimisticWithdrawalRecipient.sol | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) diff --git a/src/owr/OptimisticWithdrawalRecipient.sol b/src/owr/OptimisticWithdrawalRecipient.sol index 5e63f42..b1fb6a5 100644 --- a/src/owr/OptimisticWithdrawalRecipient.sol +++ b/src/owr/OptimisticWithdrawalRecipient.sol @@ -111,7 +111,7 @@ contract OptimisticWithdrawalRecipient is Clone { /// Amount of distributed OWRecipient token /// @dev ERC20s with very large decimals may overflow & cause issues - uint128 public distributedFunds; + // uint128 public distributedFunds; /// Amount of active balance set aside for pulls /// @dev ERC20s with very large decimals may overflow & cause issues @@ -244,23 +244,12 @@ contract OptimisticWithdrawalRecipient is Clone { /// effects // load storage into memory - // fetch the token we want to distribute - // address _token = token(); - // the amount of funds distributed so far - uint256 _startingDistributedFunds = uint256(distributedFunds); - uint256 _endingDistributedFunds; + uint256 currentbalance = address(this).balance; uint256 _fundsToBeDistributed; uint256 _claimedPrincipalFunds = uint256(claimedPrincipalFunds); uint256 _memoryFundsPendingWithdrawal = uint256(fundsPendingWithdrawal); unchecked { - // shouldn't overflow - _endingDistributedFunds = _startingDistributedFunds - // fundsPendingWithdrawal is always <= _startingDistributedFunds - - _memoryFundsPendingWithdrawal - // recognizes 0x0 as ETH - // shouldn't need to worry about re-entrancy from ERC20 view fn - + address(this).balance; - _fundsToBeDistributed = _endingDistributedFunds - _startingDistributedFunds; + _fundsToBeDistributed = currentbalance - _memoryFundsPendingWithdrawal; } (address principalRecipient, address rewardRecipient, uint256 amountOfPrincipalStake) = getTranches(); @@ -292,9 +281,7 @@ contract OptimisticWithdrawalRecipient is Clone { } { - if (_endingDistributedFunds > type(uint128).max) revert InvalidDistribution_TooLarge(); // Write to storage - distributedFunds = uint128(_endingDistributedFunds); // the principal value claimedPrincipalFunds += _principalPayout; } From ce9ff7119dc6df2926dbc66e9ee5afd48cb8c980 Mon Sep 17 00:00:00 2001 From: samparsky <8148384+samparsky@users.noreply.github.com> Date: Sun, 24 Sep 2023 17:37:02 +0100 Subject: [PATCH 52/82] mend --- src/owr/OptimisticWithdrawalRecipient.sol | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/owr/OptimisticWithdrawalRecipient.sol b/src/owr/OptimisticWithdrawalRecipient.sol index b1fb6a5..450c870 100644 --- a/src/owr/OptimisticWithdrawalRecipient.sol +++ b/src/owr/OptimisticWithdrawalRecipient.sol @@ -109,10 +109,6 @@ contract OptimisticWithdrawalRecipient is Clone { /// storage - mutables /// ----------------------------------------------------------------------- - /// Amount of distributed OWRecipient token - /// @dev ERC20s with very large decimals may overflow & cause issues - // uint128 public distributedFunds; - /// Amount of active balance set aside for pulls /// @dev ERC20s with very large decimals may overflow & cause issues uint128 public fundsPendingWithdrawal; From 4332623ab0640f514d001a4955d428a8320d7ed5 Mon Sep 17 00:00:00 2001 From: samparsky <8148384+samparsky@users.noreply.github.com> Date: Sun, 24 Sep 2023 17:49:26 +0100 Subject: [PATCH 53/82] mend --- src/owr/OptimisticWithdrawalRecipient.sol | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/owr/OptimisticWithdrawalRecipient.sol b/src/owr/OptimisticWithdrawalRecipient.sol index 450c870..6d4a697 100644 --- a/src/owr/OptimisticWithdrawalRecipient.sol +++ b/src/owr/OptimisticWithdrawalRecipient.sol @@ -64,8 +64,6 @@ contract OptimisticWithdrawalRecipient is Clone { /// storage - constants /// ----------------------------------------------------------------------- - address internal constant ETH_ADDRESS = address(0); - uint256 internal constant PUSH = 0; uint256 internal constant PULL = 1; From 2b770d80cba13d1a0d239a83f62d33459de06336 Mon Sep 17 00:00:00 2001 From: samparsky <8148384+samparsky@users.noreply.github.com> Date: Sun, 24 Sep 2023 23:43:04 +0100 Subject: [PATCH 54/82] test: fix OWR and OWRFactory test cases --- src/test/owr/OWRTestHelper.t.sol | 2 +- .../owr/OptimisticWithdrawalRecipient.t.sol | 370 ++---------------- ...OptimisticWithdrawalRecipientFactory.t.sol | 80 +--- 3 files changed, 42 insertions(+), 410 deletions(-) diff --git a/src/test/owr/OWRTestHelper.t.sol b/src/test/owr/OWRTestHelper.t.sol index 787cac4..1451b2a 100644 --- a/src/test/owr/OWRTestHelper.t.sol +++ b/src/test/owr/OWRTestHelper.t.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.17; contract OWRTestHelper { - address internal constant ETH_ADDRESS = address(0); + // address internal constant ETH_ADDRESS = address(0); uint256 internal constant MAX_TRANCHE_SIZE = 2; diff --git a/src/test/owr/OptimisticWithdrawalRecipient.t.sol b/src/test/owr/OptimisticWithdrawalRecipient.t.sol index 974fd5c..c777e93 100644 --- a/src/test/owr/OptimisticWithdrawalRecipient.t.sol +++ b/src/test/owr/OptimisticWithdrawalRecipient.t.sol @@ -23,9 +23,9 @@ contract OptimisticWithdrawalRecipientTest is OWRTestHelper, Test { address internal recoveryAddress; OptimisticWithdrawalRecipient owrETH; - OptimisticWithdrawalRecipient owrERC20; + // OptimisticWithdrawalRecipient owrERC20; OptimisticWithdrawalRecipient owrETH_OR; - OptimisticWithdrawalRecipient owrERC20_OR; + // OptimisticWithdrawalRecipient owrERC20_OR; MockERC20 mERC20; address public principalRecipient; @@ -46,16 +46,10 @@ contract OptimisticWithdrawalRecipientTest is OWRTestHelper, Test { recoveryAddress = makeAddr("recoveryAddress"); owrETH = - owrFactory.createOWRecipient(ETH_ADDRESS, recoveryAddress, principalRecipient, rewardRecipient, trancheThreshold); - - owrERC20 = owrFactory.createOWRecipient( - address(mERC20), recoveryAddress, principalRecipient, rewardRecipient, trancheThreshold - ); + owrFactory.createOWRecipient(recoveryAddress, principalRecipient, rewardRecipient, trancheThreshold); owrETH_OR = - owrFactory.createOWRecipient(ETH_ADDRESS, address(0), principalRecipient, rewardRecipient, trancheThreshold); - owrERC20_OR = - owrFactory.createOWRecipient(address(mERC20), address(0), principalRecipient, rewardRecipient, trancheThreshold); + owrFactory.createOWRecipient(address(0), principalRecipient, rewardRecipient, trancheThreshold); } function testGetTranches() public { @@ -65,29 +59,16 @@ contract OptimisticWithdrawalRecipientTest is OWRTestHelper, Test { assertEq(_principalRecipient, principalRecipient, "invalid principal recipient"); assertEq(_rewardRecipient, rewardRecipient, "invalid reward recipient"); assertEq(wtrancheThreshold, ETH_STAKE, "invalid eth tranche threshold"); - - // erc20 - (_principalRecipient, _rewardRecipient, wtrancheThreshold) = owrERC20.getTranches(); - - assertEq(_principalRecipient, principalRecipient, "invalid erc20 principal recipient"); - assertEq(_rewardRecipient, rewardRecipient, "invalid erc20 reward recipient"); - assertEq(wtrancheThreshold, ETH_STAKE, "invalid erc20 tranche threshold"); } function testReceiveETH() public { address(owrETH).safeTransferETH(1 ether); assertEq(address(owrETH).balance, 1 ether); - - address(owrERC20).safeTransferETH(1 ether); - assertEq(address(owrERC20).balance, 1 ether); } function testReceiveTransfer() public { payable(address(owrETH)).transfer(1 ether); assertEq(address(owrETH).balance, 1 ether); - - payable(address(owrERC20)).transfer(1 ether); - assertEq(address(owrERC20).balance, 1 ether); } function testEmitOnReceiveETH() public { @@ -100,9 +81,6 @@ contract OptimisticWithdrawalRecipientTest is OWRTestHelper, Test { function testReceiveERC20() public { address(mERC20).safeTransfer(address(owrETH), 1 ether); assertEq(mERC20.balanceOf(address(owrETH)), 1 ether); - - address(mERC20).safeTransfer(address(owrERC20), 1 ether); - assertEq(mERC20.balanceOf(address(owrERC20)), 1 ether); } function testCan_recoverNonOWRFundsToRecipient() public { @@ -133,62 +111,23 @@ contract OptimisticWithdrawalRecipientTest is OWRTestHelper, Test { assertEq(address(owrETH_OR).balance, 1 ether); assertEq(mERC20.balanceOf(address(owrETH_OR)), 0 ether); assertEq(mERC20.balanceOf(rewardRecipient), 1 ether); - - address(owrERC20).safeTransferETH(1 ether); - address(mERC20).safeTransfer(address(owrERC20), 1 ether); - - vm.expectEmit(true, true, true, true); - emit RecoverNonOWRecipientFunds(ETH_ADDRESS, recoveryAddress, 1 ether); - owrERC20.recoverFunds(ETH_ADDRESS, recoveryAddress); - assertEq(mERC20.balanceOf(address(owrERC20)), 1 ether); - assertEq(address(owrERC20).balance, 0 ether); - assertEq(recoveryAddress.balance, 1 ether); - - address(owrERC20_OR).safeTransferETH(1 ether); - address(mERC20).safeTransfer(address(owrERC20_OR), 1 ether); - - vm.expectEmit(true, true, true, true); - emit RecoverNonOWRecipientFunds(ETH_ADDRESS, principalRecipient, 1 ether); - owrERC20_OR.recoverFunds(ETH_ADDRESS, principalRecipient); - assertEq(mERC20.balanceOf(address(owrERC20_OR)), 1 ether); - assertEq(address(owrERC20_OR).balance, 0 ether); - assertEq(principalRecipient.balance, 1 ether); - - address(owrERC20_OR).safeTransferETH(1 ether); - - owrERC20_OR.recoverFunds(ETH_ADDRESS, rewardRecipient); - assertEq(mERC20.balanceOf(address(owrERC20_OR)), 1 ether); - assertEq(address(owrERC20_OR).balance, 0 ether, "invalid erc20 balance"); - assertEq(rewardRecipient.balance, 1 ether, "invalid eth balance"); } function testCannot_recoverFundsToNonRecipient() public { vm.expectRevert(OptimisticWithdrawalRecipient.InvalidTokenRecovery_InvalidRecipient.selector); owrETH.recoverFunds(address(mERC20), address(1)); - vm.expectRevert(OptimisticWithdrawalRecipient.InvalidTokenRecovery_InvalidRecipient.selector); - owrERC20_OR.recoverFunds(ETH_ADDRESS, address(1)); - vm.expectRevert(OptimisticWithdrawalRecipient.InvalidTokenRecovery_InvalidRecipient.selector); owrETH_OR.recoverFunds(address(mERC20), address(2)); - - vm.expectRevert(OptimisticWithdrawalRecipient.InvalidTokenRecovery_InvalidRecipient.selector); - owrERC20_OR.recoverFunds(ETH_ADDRESS, address(2)); } - function testCannot_recoverOWRFunds() public { - vm.expectRevert(OptimisticWithdrawalRecipient.InvalidTokenRecovery_OWRToken.selector); - owrETH.recoverFunds(ETH_ADDRESS, recoveryAddress); - - vm.expectRevert(OptimisticWithdrawalRecipient.InvalidTokenRecovery_OWRToken.selector); - owrERC20_OR.recoverFunds(address(mERC20), recoveryAddress); + // function testCannot_recoverOWRFunds() public { + // vm.expectRevert(OptimisticWithdrawalRecipient.InvalidTokenRecovery_OWRToken.selector); + // owrETH.recoverFunds(address(mERC20), recoveryAddress); - vm.expectRevert(OptimisticWithdrawalRecipient.InvalidTokenRecovery_OWRToken.selector); - owrETH_OR.recoverFunds(ETH_ADDRESS, address(1)); - - vm.expectRevert(OptimisticWithdrawalRecipient.InvalidTokenRecovery_OWRToken.selector); - owrERC20_OR.recoverFunds(address(mERC20), address(1)); - } + // vm.expectRevert(OptimisticWithdrawalRecipient.InvalidTokenRecovery_OWRToken.selector); + // owrETH_OR.recoverFunds(address(mERC20), address(1)); + // } function testCan_OWRIsPayable() public { owrETH.distributeFunds{value: 2 ether}(); @@ -201,9 +140,6 @@ contract OptimisticWithdrawalRecipientTest is OWRTestHelper, Test { function testCan_distributeToNoRecipients() public { owrETH.distributeFunds(); assertEq(principalRecipient.balance, 0 ether); - - owrERC20_OR.distributeFunds(); - assertEq(mERC20.balanceOf(principalRecipient), 0 ether); } function testCan_emitOnDistributeToNoRecipients() public { @@ -218,7 +154,6 @@ contract OptimisticWithdrawalRecipientTest is OWRTestHelper, Test { function testCan_distributeToSecondRecipient() public { address(owrETH).safeTransferETH(1 ether); - // uint256[] memory payouts = new uint256[](2); uint256 rewardPayout = 1 ether; uint256 principalPayout; @@ -235,23 +170,6 @@ contract OptimisticWithdrawalRecipientTest is OWRTestHelper, Test { assertEq(address(owrETH).balance, 0 ether); assertEq(principalRecipient.balance, 0 ether); assertEq(rewardRecipient.balance, 1 ether); - - address(mERC20).safeTransfer(address(owrERC20_OR), 1 ether); - - rewardPayout = 1 ether; - vm.expectEmit(true, true, true, true); - emit DistributeFunds(principalPayout, rewardPayout, 0); - owrERC20_OR.distributeFunds(); - assertEq(mERC20.balanceOf(address(owrERC20_OR)), 0 ether); - assertEq(mERC20.balanceOf(rewardRecipient), 1 ether); - - rewardPayout = 0; - vm.expectEmit(true, true, true, true); - emit DistributeFunds(principalPayout, rewardPayout, 0); - owrERC20_OR.distributeFunds(); - assertEq(mERC20.balanceOf(address(owrERC20_OR)), 0 ether); - assertEq(principalRecipient.balance, 0 ether); - assertEq(rewardRecipient.balance, 1 ether); } function testCan_distributeMultipleDepositsToRewardRecipient() public { @@ -264,16 +182,6 @@ contract OptimisticWithdrawalRecipientTest is OWRTestHelper, Test { owrETH.distributeFunds(); assertEq(address(owrETH).balance, 0 ether); assertEq(rewardRecipient.balance, 1 ether); - - address(mERC20).safeTransfer(address(owrERC20_OR), 0.5 ether); - owrERC20_OR.distributeFunds(); - assertEq(mERC20.balanceOf(address(owrERC20_OR)), 0 ether); - assertEq(mERC20.balanceOf(rewardRecipient), 0.5 ether); - - address(mERC20).safeTransfer(address(owrERC20_OR), 0.5 ether); - owrERC20_OR.distributeFunds(); - assertEq(mERC20.balanceOf(address(owrERC20_OR)), 0 ether); - assertEq(mERC20.balanceOf(rewardRecipient), 1 ether); } function testCan_distributeToBothRecipients() public { @@ -288,15 +196,6 @@ contract OptimisticWithdrawalRecipientTest is OWRTestHelper, Test { assertEq(address(owrETH).balance, 0 ether); assertEq(principalRecipient.balance, 32 ether); assertEq(rewardRecipient.balance, 4 ether); - - address(mERC20).safeTransfer(address(owrERC20_OR), 36 ether); - - vm.expectEmit(true, true, true, true); - emit DistributeFunds(principalPayout, rewardPayout, 0); - owrERC20_OR.distributeFunds(); - assertEq(mERC20.balanceOf(address(owrERC20_OR)), 0 ether); - assertEq(principalRecipient.balance, 32 ether); - assertEq(rewardRecipient.balance, 4 ether); } function testCan_distributeMultipleDepositsToPrincipalRecipient() public { @@ -309,16 +208,6 @@ contract OptimisticWithdrawalRecipientTest is OWRTestHelper, Test { assertEq(address(owrETH).balance, 0 ether); assertEq(principalRecipient.balance, 32 ether); assertEq(rewardRecipient.balance, 0 ether); - - address(mERC20).safeTransfer(address(owrERC20_OR), 16 ether); - owrERC20_OR.distributeFunds(); - - address(mERC20).safeTransfer(address(owrERC20_OR), 16 ether); - owrERC20_OR.distributeFunds(); - - assertEq(mERC20.balanceOf(address(owrERC20_OR)), 0 ether); - assertEq(mERC20.balanceOf(principalRecipient), 32 ether); - assertEq(mERC20.balanceOf(rewardRecipient), 0); } function testCannot_distributeTooMuch() public { @@ -331,22 +220,12 @@ contract OptimisticWithdrawalRecipientTest is OWRTestHelper, Test { vm.expectRevert(OptimisticWithdrawalRecipient.InvalidDistribution_TooLarge.selector); owrETH.distributeFundsPull(); - - address(mERC20).safeTransfer(address(owrERC20_OR), type(uint128).max); - owrERC20_OR.distributeFunds(); - address(mERC20).safeTransfer(address(owrERC20_OR), 1); - - vm.expectRevert(OptimisticWithdrawalRecipient.InvalidDistribution_TooLarge.selector); - owrERC20_OR.distributeFunds(); - - vm.expectRevert(OptimisticWithdrawalRecipient.InvalidDistribution_TooLarge.selector); - owrERC20_OR.distributeFundsPull(); } function testCannot_reenterOWR() public { OWRReentrancy wr = new OWRReentrancy(); - owrETH = owrFactory.createOWRecipient(ETH_ADDRESS, recoveryAddress, address(wr), rewardRecipient, 1 ether); + owrETH = owrFactory.createOWRecipient(recoveryAddress, address(wr), rewardRecipient, 1 ether); address(owrETH).safeTransferETH(33 ether); vm.expectRevert(SafeTransferLib.ETHTransferFailed.selector); @@ -369,7 +248,7 @@ contract OptimisticWithdrawalRecipientTest is OWRTestHelper, Test { assertEq(owrETH.getPullBalance(principalRecipient), 32 ether); assertEq(owrETH.getPullBalance(rewardRecipient), 4 ether); - assertEq(owrETH.distributedFunds(), 36 ether); + // assertEq(owrETH.distributedFunds(), 36 ether); assertEq(owrETH.fundsPendingWithdrawal(), 36 ether); owrETH.withdraw(rewardRecipient); @@ -381,7 +260,7 @@ contract OptimisticWithdrawalRecipientTest is OWRTestHelper, Test { assertEq(owrETH.getPullBalance(principalRecipient), 32 ether); assertEq(owrETH.getPullBalance(rewardRecipient), 0); - assertEq(owrETH.distributedFunds(), 36 ether); + // assertEq(owrETH.distributedFunds(), 36 ether); assertEq(owrETH.fundsPendingWithdrawal(), 32 ether); owrETH.withdraw(principalRecipient); @@ -393,46 +272,8 @@ contract OptimisticWithdrawalRecipientTest is OWRTestHelper, Test { assertEq(owrETH.getPullBalance(principalRecipient), 0); assertEq(owrETH.getPullBalance(rewardRecipient), 0); - assertEq(owrETH.distributedFunds(), 36 ether); + // assertEq(owrETH.distributedFunds(), 36 ether); assertEq(owrETH.fundsPendingWithdrawal(), 0 ether); - - // test erc20 - address(mERC20).safeTransfer(address(owrERC20_OR), 36 ether); - owrERC20_OR.distributeFundsPull(); - - assertEq(mERC20.balanceOf(address(owrERC20_OR)), 36 ether); - assertEq(mERC20.balanceOf(principalRecipient), 0); - assertEq(mERC20.balanceOf(rewardRecipient), 0); - - assertEq(owrERC20_OR.getPullBalance(principalRecipient), 32 ether); - assertEq(owrERC20_OR.getPullBalance(rewardRecipient), 4 ether); - - assertEq(owrERC20_OR.distributedFunds(), 36 ether); - assertEq(owrERC20_OR.fundsPendingWithdrawal(), 36 ether); - - owrERC20_OR.withdraw(rewardRecipient); - - assertEq(mERC20.balanceOf(address(owrERC20_OR)), 32 ether); - assertEq(mERC20.balanceOf(principalRecipient), 0 ether); - assertEq(mERC20.balanceOf(rewardRecipient), 4 ether); - - assertEq(owrERC20_OR.getPullBalance(principalRecipient), 32 ether); - assertEq(owrERC20_OR.getPullBalance(rewardRecipient), 0 ether); - - assertEq(owrERC20_OR.distributedFunds(), 36 ether); - assertEq(owrERC20_OR.fundsPendingWithdrawal(), 32 ether); - - owrERC20_OR.withdraw(principalRecipient); - - assertEq(mERC20.balanceOf(address(owrERC20_OR)), 0 ether); - assertEq(mERC20.balanceOf(principalRecipient), 32 ether); - assertEq(mERC20.balanceOf(rewardRecipient), 4 ether); - - assertEq(owrERC20_OR.getPullBalance(principalRecipient), 0 ether); - assertEq(owrERC20_OR.getPullBalance(rewardRecipient), 0 ether); - - assertEq(owrERC20_OR.distributedFunds(), 36 ether); - assertEq(owrERC20_OR.fundsPendingWithdrawal(), 0 ether); } function testCan_distributePushAndPull() public { @@ -449,7 +290,7 @@ contract OptimisticWithdrawalRecipientTest is OWRTestHelper, Test { assertEq(owrETH.getPullBalance(principalRecipient), 0 ether); assertEq(owrETH.getPullBalance(rewardRecipient), 0 ether); - assertEq(owrETH.distributedFunds(), 0.5 ether); + // assertEq(owrETH.distributedFunds(), 0.5 ether); assertEq(owrETH.fundsPendingWithdrawal(), 0 ether); address(owrETH).safeTransferETH(1 ether); @@ -464,7 +305,7 @@ contract OptimisticWithdrawalRecipientTest is OWRTestHelper, Test { assertEq(owrETH.getPullBalance(principalRecipient), 0 ether); assertEq(owrETH.getPullBalance(rewardRecipient), 1 ether); - assertEq(owrETH.distributedFunds(), 1.5 ether); + // assertEq(owrETH.distributedFunds(), 1.5 ether); assertEq(owrETH.fundsPendingWithdrawal(), 1 ether); owrETH.distributeFunds(); @@ -476,7 +317,7 @@ contract OptimisticWithdrawalRecipientTest is OWRTestHelper, Test { assertEq(owrETH.getPullBalance(principalRecipient), 0); assertEq(owrETH.getPullBalance(rewardRecipient), 1 ether); - assertEq(owrETH.distributedFunds(), 1.5 ether); + // assertEq(owrETH.distributedFunds(), 1.5 ether); assertEq(owrETH.fundsPendingWithdrawal(), 1 ether); owrETH.distributeFundsPull(); @@ -488,7 +329,7 @@ contract OptimisticWithdrawalRecipientTest is OWRTestHelper, Test { assertEq(owrETH.getPullBalance(principalRecipient), 0); assertEq(owrETH.getPullBalance(rewardRecipient), 1 ether); - assertEq(owrETH.distributedFunds(), 1.5 ether); + // assertEq(owrETH.distributedFunds(), 1.5 ether); assertEq(owrETH.fundsPendingWithdrawal(), 1 ether); address(owrETH).safeTransferETH(1 ether); @@ -503,7 +344,7 @@ contract OptimisticWithdrawalRecipientTest is OWRTestHelper, Test { assertEq(owrETH.getPullBalance(principalRecipient), 0 ether); assertEq(owrETH.getPullBalance(rewardRecipient), 1 ether); - assertEq(owrETH.distributedFunds(), 2.5 ether); + // assertEq(owrETH.distributedFunds(), 2.5 ether); assertEq(owrETH.fundsPendingWithdrawal(), 1 ether); owrETH.withdraw(rewardRecipient); @@ -515,7 +356,7 @@ contract OptimisticWithdrawalRecipientTest is OWRTestHelper, Test { assertEq(owrETH.getPullBalance(principalRecipient), 0 ether); assertEq(owrETH.getPullBalance(rewardRecipient), 0 ether); - assertEq(owrETH.distributedFunds(), 2.5 ether); + // assertEq(owrETH.distributedFunds(), 2.5 ether); assertEq(owrETH.fundsPendingWithdrawal(), 0); address(owrETH).safeTransferETH(1 ether); @@ -528,80 +369,8 @@ contract OptimisticWithdrawalRecipientTest is OWRTestHelper, Test { assertEq(owrETH.getPullBalance(principalRecipient), 0 ether); assertEq(owrETH.getPullBalance(rewardRecipient), 0 ether); - assertEq(owrETH.distributedFunds(), 2.5 ether); + // assertEq(owrETH.distributedFunds(), 2.5 ether); assertEq(owrETH.fundsPendingWithdrawal(), 0 ether); - - // TEST ERC20 - - address(mERC20).safeTransfer(address(owrERC20_OR), 0.5 ether); - assertEq(mERC20.balanceOf(address(owrERC20_OR)), 0.5 ether); - - owrERC20_OR.distributeFunds(); - - assertEq(mERC20.balanceOf(address(owrERC20_OR)), 0 ether, "1/invalid balance"); - assertEq(mERC20.balanceOf(principalRecipient), 0 ether, "2/invalid tranche 1 recipient balance"); - assertEq(mERC20.balanceOf(rewardRecipient), 0.5 ether, "3/invalid tranche 2 recipient balance - 1"); - - assertEq(owrERC20_OR.getPullBalance(principalRecipient), 0 ether, "4/invalid pull balance"); - assertEq(owrERC20_OR.getPullBalance(rewardRecipient), 0 ether, "5/invalid pull balance"); - - assertEq(owrERC20_OR.distributedFunds(), 0.5 ether, "6/invalid distributed funds"); - assertEq(owrERC20_OR.fundsPendingWithdrawal(), 0 ether, "7/invalid funds pending withdrawal"); - - address(mERC20).safeTransfer(address(owrERC20_OR), 1 ether); - assertEq(mERC20.balanceOf(address(owrERC20_OR)), 1 ether, "8/invalid balance"); - - owrERC20_OR.distributeFundsPull(); - - assertEq(mERC20.balanceOf(address(owrERC20_OR)), 1 ether, "9/invalid balance"); - assertEq(mERC20.balanceOf(principalRecipient), 0 ether, "10/invalid recipeint balance"); - assertEq(mERC20.balanceOf(rewardRecipient), 0.5 ether, "11/invalid recipient balance"); - - assertEq(owrERC20_OR.getPullBalance(principalRecipient), 0, "12/invalid recipient pull balance"); - assertEq(owrERC20_OR.getPullBalance(rewardRecipient), 1 ether, "13/invalid recipient pull balance"); - - assertEq(owrERC20_OR.distributedFunds(), 1.5 ether, "14/invalid distributed funds balance"); - assertEq(owrERC20_OR.fundsPendingWithdrawal(), 1 ether, "15/invalid funds pending balance"); - - owrERC20_OR.distributeFundsPull(); - - assertEq(mERC20.balanceOf(address(owrERC20_OR)), 1 ether, "16/invalid balance"); - assertEq(mERC20.balanceOf(principalRecipient), 0 ether, "17/invalid recipient balance"); - assertEq(mERC20.balanceOf(rewardRecipient), 0.5 ether, "18/invalid recipient balance"); - - assertEq(owrERC20_OR.getPullBalance(principalRecipient), 0 ether, "19/invalid pull balance"); - assertEq(owrERC20_OR.getPullBalance(rewardRecipient), 1 ether, "20/invalid pull balance"); - - assertEq(owrERC20_OR.distributedFunds(), 1.5 ether, "21/invalid distributed funds"); - assertEq(owrERC20_OR.fundsPendingWithdrawal(), 1 ether, "22/invalid funds pending"); - - /// 3 - address(mERC20).safeTransfer(address(owrERC20_OR), 32 ether); - assertEq(mERC20.balanceOf(address(owrERC20_OR)), 33 ether); - - owrERC20_OR.distributeFunds(); - - assertEq(mERC20.balanceOf(address(owrERC20_OR)), 1 ether); - assertEq(mERC20.balanceOf(principalRecipient), 32 ether); - assertEq(mERC20.balanceOf(rewardRecipient), 0.5 ether); - - assertEq(owrERC20_OR.getPullBalance(principalRecipient), 0 ether); - assertEq(owrERC20_OR.getPullBalance(rewardRecipient), 1 ether); - - assertEq(owrERC20_OR.distributedFunds(), 33.5 ether); - assertEq(owrERC20_OR.fundsPendingWithdrawal(), 1 ether); - - owrERC20_OR.withdraw(rewardRecipient); - - assertEq(mERC20.balanceOf(address(owrERC20_OR)), 0 ether); - assertEq(mERC20.balanceOf(principalRecipient), 32 ether); - assertEq(mERC20.balanceOf(rewardRecipient), 1.5 ether); - - assertEq(owrERC20_OR.getPullBalance(principalRecipient), 0 ether); - assertEq(owrERC20_OR.getPullBalance(rewardRecipient), 0 ether); - - assertEq(owrERC20_OR.distributedFunds(), 33.5 ether); - assertEq(owrERC20_OR.fundsPendingWithdrawal(), 0 ether); } function testFuzzCan_distributeDepositsToRecipients( @@ -618,11 +387,7 @@ contract OptimisticWithdrawalRecipientTest is OWRTestHelper, Test { generateTranches(_recipientsSeed, _thresholdsSeed); owrETH = owrFactory.createOWRecipient( - ETH_ADDRESS, recoveryAddress, _principalRecipient, _rewardRecipient, _trancheThreshold - ); - - owrERC20 = owrFactory.createOWRecipient( - address(mERC20), recoveryAddress, _principalRecipient, _rewardRecipient, _trancheThreshold + recoveryAddress, _principalRecipient, _rewardRecipient, _trancheThreshold ); /// test eth @@ -634,7 +399,7 @@ contract OptimisticWithdrawalRecipientTest is OWRTestHelper, Test { uint256 _totalETHAmount = uint256(_numDeposits) * uint256(_ethAmount); assertEq(address(owrETH).balance, 0 ether, "invalid balance"); - assertEq(owrETH.distributedFunds(), _totalETHAmount, "undistributed funds"); + // assertEq(owrETH.distributedFunds(), _totalETHAmount, "undistributed funds"); assertEq(owrETH.fundsPendingWithdrawal(), 0 ether, "funds pending withdraw"); if (BALANCE_CLASSIFICATION_THRESHOLD > _totalETHAmount) { @@ -661,46 +426,6 @@ contract OptimisticWithdrawalRecipientTest is OWRTestHelper, Test { assertEq(_rewardRecipient.balance, 0, "should not classify principal as reward"); } } - - // test erc20 - - for (uint256 i = 0; i < _numDeposits; i++) { - address(mERC20).safeTransfer(address(owrERC20), _erc20Amount); - owrERC20.distributeFunds(); - } - - uint256 _totalERC20Amount = uint256(_numDeposits) * uint256(_erc20Amount); - - assertEq(mERC20.balanceOf(address(owrERC20)), 0 ether, "invalid erc20 balance"); - assertEq(owrERC20.distributedFunds(), _totalERC20Amount, "incorrect distributed funds"); - assertEq(owrERC20.fundsPendingWithdrawal(), 0 ether, "invalid funds pending withdrawal"); - - if (BALANCE_CLASSIFICATION_THRESHOLD > _totalERC20Amount) { - // then all of the deposit should be classified as reward - assertEq(mERC20.balanceOf(_principalRecipient), 0, "should not classify reward as principal"); - - assertEq(mERC20.balanceOf(_rewardRecipient), _totalERC20Amount, "invalid amount reward classification"); - } - - if (_erc20Amount > BALANCE_CLASSIFICATION_THRESHOLD) { - // then all of reward classified as principal - // but check if _totalERC20Amount > first threshold - if (_totalERC20Amount > _trancheThreshold) { - // there is reward - assertEq(mERC20.balanceOf(_principalRecipient), _trancheThreshold, "invalid amount principal classification"); - - assertEq( - mERC20.balanceOf(_rewardRecipient), - _totalERC20Amount - _trancheThreshold, - "should not classify principal as reward" - ); - } else { - // eelse no rewards - assertEq(mERC20.balanceOf(_principalRecipient), _totalERC20Amount, "invalid amount"); - - assertEq(mERC20.balanceOf(_rewardRecipient), 0, "should not classify principal as reward"); - } - } } function testFuzzCan_distributePullDepositsToRecipients( @@ -718,10 +443,7 @@ contract OptimisticWithdrawalRecipientTest is OWRTestHelper, Test { generateTranches(_recipientsSeed, _thresholdsSeed); owrETH = owrFactory.createOWRecipient( - ETH_ADDRESS, recoveryAddress, _principalRecipient, _rewardRecipient, _trancheThreshold - ); - owrERC20 = owrFactory.createOWRecipient( - address(mERC20), recoveryAddress, _principalRecipient, _rewardRecipient, _trancheThreshold + recoveryAddress, _principalRecipient, _rewardRecipient, _trancheThreshold ); /// test eth @@ -733,7 +455,7 @@ contract OptimisticWithdrawalRecipientTest is OWRTestHelper, Test { uint256 _totalETHAmount = uint256(_numDeposits) * uint256(_ethAmount); assertEq(address(owrETH).balance, _totalETHAmount); - assertEq(owrETH.distributedFunds(), _totalETHAmount); + // assertEq(owrETH.distributedFunds(), _totalETHAmount); assertEq(owrETH.fundsPendingWithdrawal(), _totalETHAmount); uint256 principal = owrETH.getPullBalance(_principalRecipient); @@ -758,50 +480,10 @@ contract OptimisticWithdrawalRecipientTest is OWRTestHelper, Test { owrETH.withdraw(_rewardRecipient); assertEq(address(owrETH).balance, 0); - assertEq(owrETH.distributedFunds(), _totalETHAmount); + // assertEq(owrETH.distributedFunds(), _totalETHAmount); assertEq(owrETH.fundsPendingWithdrawal(), 0); assertEq(_principalRecipient.balance, principal, "10/invalid principal balance"); assertEq(_rewardRecipient.balance, reward, "11/invalid reward balance"); - - /// test erc20 - - for (uint256 i = 0; i < _numDeposits; i++) { - address(mERC20).safeTransfer(address(owrERC20), _erc20Amount); - owrERC20.distributeFundsPull(); - } - uint256 _totalERC20Amount = uint256(_numDeposits) * uint256(_erc20Amount); - - assertEq(mERC20.balanceOf(address(owrERC20)), _totalERC20Amount); - assertEq(owrERC20.distributedFunds(), _totalERC20Amount); - assertEq(owrERC20.fundsPendingWithdrawal(), _totalERC20Amount); - - principal = owrERC20.getPullBalance(_principalRecipient); - assertEq( - owrERC20.getPullBalance(_principalRecipient), - (_erc20Amount >= BALANCE_CLASSIFICATION_THRESHOLD) - ? _trancheThreshold > _totalERC20Amount ? _totalERC20Amount : _trancheThreshold - : 0, - "16/invalid recipient balance" - ); - - reward = owrERC20.getPullBalance(_rewardRecipient); - assertEq( - owrERC20.getPullBalance(_rewardRecipient), - (_erc20Amount >= BALANCE_CLASSIFICATION_THRESHOLD) - ? _totalERC20Amount > _trancheThreshold ? (_totalERC20Amount - _trancheThreshold) : 0 - : _totalERC20Amount, - "17/invalid recipient balance" - ); - - owrERC20.withdraw(_principalRecipient); - owrERC20.withdraw(_rewardRecipient); - - assertEq(mERC20.balanceOf(address(owrERC20)), 0, "18/invalid balance"); - assertEq(owrERC20.distributedFunds(), _totalERC20Amount, "19/invalid balance"); - assertEq(owrERC20.fundsPendingWithdrawal(), 0, "20/invalid funds pending"); - - assertEq(mERC20.balanceOf(_principalRecipient), principal, "21/invalid principal balance"); - assertEq(mERC20.balanceOf(_rewardRecipient), reward, "22/invalid reward balance"); } } diff --git a/src/test/owr/OptimisticWithdrawalRecipientFactory.t.sol b/src/test/owr/OptimisticWithdrawalRecipientFactory.t.sol index fd02690..41f390c 100644 --- a/src/test/owr/OptimisticWithdrawalRecipientFactory.t.sol +++ b/src/test/owr/OptimisticWithdrawalRecipientFactory.t.sol @@ -10,7 +10,6 @@ import {OWRTestHelper} from "./OWRTestHelper.t.sol"; contract OptimisticWithdrawalRecipientFactoryTest is OWRTestHelper, Test { event CreateOWRecipient( address indexed owr, - address token, address recoveryAddress, address principalRecipient, address rewardRecipient, @@ -18,7 +17,6 @@ contract OptimisticWithdrawalRecipientFactoryTest is OWRTestHelper, Test { ); OptimisticWithdrawalRecipientFactory owrFactoryModule; - MockERC20 mERC20; address public recoveryAddress; address public principalRecipient; @@ -26,9 +24,6 @@ contract OptimisticWithdrawalRecipientFactoryTest is OWRTestHelper, Test { uint256 public threshold; function setUp() public { - mERC20 = new MockERC20("Test Token", "TOK", 18); - mERC20.mint(type(uint256).max); - owrFactoryModule = new OptimisticWithdrawalRecipientFactory(); recoveryAddress = makeAddr("recoveryAddress"); @@ -37,69 +32,41 @@ contract OptimisticWithdrawalRecipientFactoryTest is OWRTestHelper, Test { } function testCan_createOWRecipient() public { - owrFactoryModule.createOWRecipient(ETH_ADDRESS, recoveryAddress, principalRecipient, rewardRecipient, threshold); - - owrFactoryModule.createOWRecipient(address(mERC20), recoveryAddress, principalRecipient, rewardRecipient, threshold); + owrFactoryModule.createOWRecipient(recoveryAddress, principalRecipient, rewardRecipient, threshold); recoveryAddress = address(0); - owrFactoryModule.createOWRecipient(ETH_ADDRESS, recoveryAddress, principalRecipient, rewardRecipient, threshold); - - owrFactoryModule.createOWRecipient(address(mERC20), recoveryAddress, principalRecipient, rewardRecipient, threshold); + owrFactoryModule.createOWRecipient(recoveryAddress, principalRecipient, rewardRecipient, threshold); } 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 - ); - owrFactoryModule.createOWRecipient(ETH_ADDRESS, recoveryAddress, principalRecipient, rewardRecipient, threshold); - - // don't check deploy address - vm.expectEmit(false, true, true, true); - emit CreateOWRecipient( - address(0xdead), address(mERC20), recoveryAddress, principalRecipient, rewardRecipient, threshold + address(0xdead), recoveryAddress, principalRecipient, rewardRecipient, threshold ); - owrFactoryModule.createOWRecipient(address(mERC20), recoveryAddress, principalRecipient, rewardRecipient, threshold); + owrFactoryModule.createOWRecipient(recoveryAddress, principalRecipient, rewardRecipient, threshold); recoveryAddress = address(0); // don't check deploy address vm.expectEmit(false, true, true, true); emit CreateOWRecipient( - address(0xdead), ETH_ADDRESS, recoveryAddress, principalRecipient, rewardRecipient, threshold + address(0xdead), recoveryAddress, principalRecipient, rewardRecipient, threshold ); - owrFactoryModule.createOWRecipient(ETH_ADDRESS, recoveryAddress, principalRecipient, rewardRecipient, threshold); - - // don't check deploy address - vm.expectEmit(false, true, true, true); - emit CreateOWRecipient( - address(0xdead), address(mERC20), recoveryAddress, principalRecipient, rewardRecipient, threshold - ); - owrFactoryModule.createOWRecipient(address(mERC20), recoveryAddress, principalRecipient, rewardRecipient, threshold); + owrFactoryModule.createOWRecipient(recoveryAddress, principalRecipient, rewardRecipient, threshold); } function testCannot_createWithInvalidRecipients() public { (principalRecipient, rewardRecipient, threshold) = generateTranches(1, 1); // eth vm.expectRevert(OptimisticWithdrawalRecipientFactory.Invalid__Recipients.selector); - owrFactoryModule.createOWRecipient(ETH_ADDRESS, recoveryAddress, address(0), rewardRecipient, threshold); - - vm.expectRevert(OptimisticWithdrawalRecipientFactory.Invalid__Recipients.selector); - owrFactoryModule.createOWRecipient(ETH_ADDRESS, recoveryAddress, address(0), address(0), threshold); + owrFactoryModule.createOWRecipient(recoveryAddress, address(0), rewardRecipient, threshold); vm.expectRevert(OptimisticWithdrawalRecipientFactory.Invalid__Recipients.selector); - owrFactoryModule.createOWRecipient(ETH_ADDRESS, recoveryAddress, principalRecipient, address(0), threshold); + owrFactoryModule.createOWRecipient(recoveryAddress, address(0), address(0), threshold); - // erc20 vm.expectRevert(OptimisticWithdrawalRecipientFactory.Invalid__Recipients.selector); - owrFactoryModule.createOWRecipient(address(mERC20), recoveryAddress, address(0), rewardRecipient, threshold); - - vm.expectRevert(OptimisticWithdrawalRecipientFactory.Invalid__Recipients.selector); - owrFactoryModule.createOWRecipient(address(mERC20), recoveryAddress, address(0), address(0), threshold); - - vm.expectRevert(OptimisticWithdrawalRecipientFactory.Invalid__Recipients.selector); - owrFactoryModule.createOWRecipient(address(mERC20), recoveryAddress, principalRecipient, address(0), threshold); + owrFactoryModule.createOWRecipient( recoveryAddress, principalRecipient, address(0), threshold); } function testCannot_createWithInvalidThreshold() public { @@ -107,7 +74,7 @@ contract OptimisticWithdrawalRecipientFactoryTest is OWRTestHelper, Test { threshold = 0; vm.expectRevert(OptimisticWithdrawalRecipientFactory.Invalid__ZeroThreshold.selector); - owrFactoryModule.createOWRecipient(ETH_ADDRESS, recoveryAddress, principalRecipient, rewardRecipient, threshold); + owrFactoryModule.createOWRecipient( recoveryAddress, principalRecipient, rewardRecipient, threshold); vm.expectRevert( abi.encodeWithSelector( @@ -115,7 +82,7 @@ contract OptimisticWithdrawalRecipientFactoryTest is OWRTestHelper, Test { ) ); owrFactoryModule.createOWRecipient( - ETH_ADDRESS, recoveryAddress, principalRecipient, rewardRecipient, type(uint128).max + recoveryAddress, principalRecipient, rewardRecipient, type(uint128).max ); } @@ -132,15 +99,9 @@ contract OptimisticWithdrawalRecipientFactoryTest is OWRTestHelper, Test { vm.expectEmit(false, true, true, true); emit CreateOWRecipient( - address(0xdead), ETH_ADDRESS, recoveryAddress, principalRecipient, rewardRecipient, threshold - ); - owrFactoryModule.createOWRecipient(ETH_ADDRESS, recoveryAddress, principalRecipient, rewardRecipient, threshold); - - vm.expectEmit(false, true, true, true); - emit CreateOWRecipient( - address(0xdead), address(mERC20), recoveryAddress, principalRecipient, rewardRecipient, threshold + address(0xdead), recoveryAddress, principalRecipient, rewardRecipient, threshold ); - owrFactoryModule.createOWRecipient(address(mERC20), recoveryAddress, principalRecipient, rewardRecipient, threshold); + owrFactoryModule.createOWRecipient(recoveryAddress, principalRecipient, rewardRecipient, threshold); } function testFuzzCannot_CreateWithZeroThreshold(uint256 _receipientSeed) public { @@ -149,12 +110,7 @@ contract OptimisticWithdrawalRecipientFactoryTest is OWRTestHelper, Test { // eth vm.expectRevert(OptimisticWithdrawalRecipientFactory.Invalid__ZeroThreshold.selector); - owrFactoryModule.createOWRecipient(ETH_ADDRESS, recoveryAddress, principalRecipient, rewardRecipient, threshold); - - // erc20 - vm.expectRevert(OptimisticWithdrawalRecipientFactory.Invalid__ZeroThreshold.selector); - - owrFactoryModule.createOWRecipient(address(mERC20), recoveryAddress, principalRecipient, rewardRecipient, threshold); + owrFactoryModule.createOWRecipient(recoveryAddress, principalRecipient, rewardRecipient, threshold); } function testFuzzCannot_CreateWithLargeThreshold(uint256 _receipientSeed, uint256 _threshold) public { @@ -167,12 +123,6 @@ contract OptimisticWithdrawalRecipientFactoryTest is OWRTestHelper, Test { 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); + owrFactoryModule.createOWRecipient( recoveryAddress, principalRecipient, rewardRecipient, threshold); } } From c18bae509658169d25a6c87525ab5a36d3e421fd Mon Sep 17 00:00:00 2001 From: samparsky <8148384+samparsky@users.noreply.github.com> Date: Mon, 25 Sep 2023 00:00:10 +0100 Subject: [PATCH 55/82] mend --- .../owr/OptimisticWithdrawalRecipient.t.sol | 31 ++++++------------- 1 file changed, 9 insertions(+), 22 deletions(-) diff --git a/src/test/owr/OptimisticWithdrawalRecipient.t.sol b/src/test/owr/OptimisticWithdrawalRecipient.t.sol index c777e93..ba85595 100644 --- a/src/test/owr/OptimisticWithdrawalRecipient.t.sol +++ b/src/test/owr/OptimisticWithdrawalRecipient.t.sol @@ -23,9 +23,7 @@ contract OptimisticWithdrawalRecipientTest is OWRTestHelper, Test { address internal recoveryAddress; OptimisticWithdrawalRecipient owrETH; - // OptimisticWithdrawalRecipient owrERC20; OptimisticWithdrawalRecipient owrETH_OR; - // OptimisticWithdrawalRecipient owrERC20_OR; MockERC20 mERC20; address public principalRecipient; @@ -210,17 +208,17 @@ contract OptimisticWithdrawalRecipientTest is OWRTestHelper, Test { assertEq(rewardRecipient.balance, 0 ether); } - function testCannot_distributeTooMuch() public { - vm.deal(address(owrETH), type(uint128).max); - owrETH.distributeFunds(); - vm.deal(address(owrETH), 1); + // function testCannot_distributeTooMuch() public { + // vm.deal(address(owrETH), type(uint128).max); + // owrETH.distributeFunds(); + // vm.deal(address(owrETH), 1); - vm.expectRevert(OptimisticWithdrawalRecipient.InvalidDistribution_TooLarge.selector); - owrETH.distributeFunds(); + // vm.expectRevert(OptimisticWithdrawalRecipient.InvalidDistribution_TooLarge.selector); + // owrETH.distributeFunds(); - vm.expectRevert(OptimisticWithdrawalRecipient.InvalidDistribution_TooLarge.selector); - owrETH.distributeFundsPull(); - } + // vm.expectRevert(OptimisticWithdrawalRecipient.InvalidDistribution_TooLarge.selector); + // owrETH.distributeFundsPull(); + // } function testCannot_reenterOWR() public { OWRReentrancy wr = new OWRReentrancy(); @@ -248,7 +246,6 @@ contract OptimisticWithdrawalRecipientTest is OWRTestHelper, Test { assertEq(owrETH.getPullBalance(principalRecipient), 32 ether); assertEq(owrETH.getPullBalance(rewardRecipient), 4 ether); - // assertEq(owrETH.distributedFunds(), 36 ether); assertEq(owrETH.fundsPendingWithdrawal(), 36 ether); owrETH.withdraw(rewardRecipient); @@ -260,7 +257,6 @@ contract OptimisticWithdrawalRecipientTest is OWRTestHelper, Test { assertEq(owrETH.getPullBalance(principalRecipient), 32 ether); assertEq(owrETH.getPullBalance(rewardRecipient), 0); - // assertEq(owrETH.distributedFunds(), 36 ether); assertEq(owrETH.fundsPendingWithdrawal(), 32 ether); owrETH.withdraw(principalRecipient); @@ -272,7 +268,6 @@ contract OptimisticWithdrawalRecipientTest is OWRTestHelper, Test { assertEq(owrETH.getPullBalance(principalRecipient), 0); assertEq(owrETH.getPullBalance(rewardRecipient), 0); - // assertEq(owrETH.distributedFunds(), 36 ether); assertEq(owrETH.fundsPendingWithdrawal(), 0 ether); } @@ -290,7 +285,6 @@ contract OptimisticWithdrawalRecipientTest is OWRTestHelper, Test { assertEq(owrETH.getPullBalance(principalRecipient), 0 ether); assertEq(owrETH.getPullBalance(rewardRecipient), 0 ether); - // assertEq(owrETH.distributedFunds(), 0.5 ether); assertEq(owrETH.fundsPendingWithdrawal(), 0 ether); address(owrETH).safeTransferETH(1 ether); @@ -305,7 +299,6 @@ contract OptimisticWithdrawalRecipientTest is OWRTestHelper, Test { assertEq(owrETH.getPullBalance(principalRecipient), 0 ether); assertEq(owrETH.getPullBalance(rewardRecipient), 1 ether); - // assertEq(owrETH.distributedFunds(), 1.5 ether); assertEq(owrETH.fundsPendingWithdrawal(), 1 ether); owrETH.distributeFunds(); @@ -317,7 +310,6 @@ contract OptimisticWithdrawalRecipientTest is OWRTestHelper, Test { assertEq(owrETH.getPullBalance(principalRecipient), 0); assertEq(owrETH.getPullBalance(rewardRecipient), 1 ether); - // assertEq(owrETH.distributedFunds(), 1.5 ether); assertEq(owrETH.fundsPendingWithdrawal(), 1 ether); owrETH.distributeFundsPull(); @@ -329,7 +321,6 @@ contract OptimisticWithdrawalRecipientTest is OWRTestHelper, Test { assertEq(owrETH.getPullBalance(principalRecipient), 0); assertEq(owrETH.getPullBalance(rewardRecipient), 1 ether); - // assertEq(owrETH.distributedFunds(), 1.5 ether); assertEq(owrETH.fundsPendingWithdrawal(), 1 ether); address(owrETH).safeTransferETH(1 ether); @@ -344,7 +335,6 @@ contract OptimisticWithdrawalRecipientTest is OWRTestHelper, Test { assertEq(owrETH.getPullBalance(principalRecipient), 0 ether); assertEq(owrETH.getPullBalance(rewardRecipient), 1 ether); - // assertEq(owrETH.distributedFunds(), 2.5 ether); assertEq(owrETH.fundsPendingWithdrawal(), 1 ether); owrETH.withdraw(rewardRecipient); @@ -356,7 +346,6 @@ contract OptimisticWithdrawalRecipientTest is OWRTestHelper, Test { assertEq(owrETH.getPullBalance(principalRecipient), 0 ether); assertEq(owrETH.getPullBalance(rewardRecipient), 0 ether); - // assertEq(owrETH.distributedFunds(), 2.5 ether); assertEq(owrETH.fundsPendingWithdrawal(), 0); address(owrETH).safeTransferETH(1 ether); @@ -369,7 +358,6 @@ contract OptimisticWithdrawalRecipientTest is OWRTestHelper, Test { assertEq(owrETH.getPullBalance(principalRecipient), 0 ether); assertEq(owrETH.getPullBalance(rewardRecipient), 0 ether); - // assertEq(owrETH.distributedFunds(), 2.5 ether); assertEq(owrETH.fundsPendingWithdrawal(), 0 ether); } @@ -480,7 +468,6 @@ contract OptimisticWithdrawalRecipientTest is OWRTestHelper, Test { owrETH.withdraw(_rewardRecipient); assertEq(address(owrETH).balance, 0); - // assertEq(owrETH.distributedFunds(), _totalETHAmount); assertEq(owrETH.fundsPendingWithdrawal(), 0); assertEq(_principalRecipient.balance, principal, "10/invalid principal balance"); From 6e69d5d60c04649aafbf7012a0ebbd9030d146eb Mon Sep 17 00:00:00 2001 From: samparsky <8148384+samparsky@users.noreply.github.com> Date: Wed, 27 Sep 2023 06:54:08 +0100 Subject: [PATCH 56/82] chore: add _fundsToBeDistributed check; test: add OWR distribution too large check --- src/owr/OptimisticWithdrawalRecipient.sol | 1 + .../owr/OptimisticWithdrawalRecipient.t.sol | 26 +++++++------------ 2 files changed, 10 insertions(+), 17 deletions(-) diff --git a/src/owr/OptimisticWithdrawalRecipient.sol b/src/owr/OptimisticWithdrawalRecipient.sol index 6d4a697..f246cac 100644 --- a/src/owr/OptimisticWithdrawalRecipient.sol +++ b/src/owr/OptimisticWithdrawalRecipient.sol @@ -275,6 +275,7 @@ contract OptimisticWithdrawalRecipient is Clone { } { + if (_fundsToBeDistributed > type(uint128).max) revert InvalidDistribution_TooLarge(); // Write to storage // the principal value claimedPrincipalFunds += _principalPayout; diff --git a/src/test/owr/OptimisticWithdrawalRecipient.t.sol b/src/test/owr/OptimisticWithdrawalRecipient.t.sol index ba85595..9d51f5c 100644 --- a/src/test/owr/OptimisticWithdrawalRecipient.t.sol +++ b/src/test/owr/OptimisticWithdrawalRecipient.t.sol @@ -119,14 +119,6 @@ contract OptimisticWithdrawalRecipientTest is OWRTestHelper, Test { owrETH_OR.recoverFunds(address(mERC20), address(2)); } - // function testCannot_recoverOWRFunds() public { - // vm.expectRevert(OptimisticWithdrawalRecipient.InvalidTokenRecovery_OWRToken.selector); - // owrETH.recoverFunds(address(mERC20), recoveryAddress); - - // vm.expectRevert(OptimisticWithdrawalRecipient.InvalidTokenRecovery_OWRToken.selector); - // owrETH_OR.recoverFunds(address(mERC20), address(1)); - // } - function testCan_OWRIsPayable() public { owrETH.distributeFunds{value: 2 ether}(); @@ -208,17 +200,17 @@ contract OptimisticWithdrawalRecipientTest is OWRTestHelper, Test { assertEq(rewardRecipient.balance, 0 ether); } - // function testCannot_distributeTooMuch() public { - // vm.deal(address(owrETH), type(uint128).max); - // owrETH.distributeFunds(); - // vm.deal(address(owrETH), 1); + function testCannot_distributeTooMuch() public { + vm.deal(address(owrETH), type(uint128).max); + owrETH.distributeFunds(); + vm.deal(address(owrETH), 1); - // vm.expectRevert(OptimisticWithdrawalRecipient.InvalidDistribution_TooLarge.selector); - // owrETH.distributeFunds(); + vm.expectRevert(OptimisticWithdrawalRecipient.InvalidDistribution_TooLarge.selector); + owrETH.distributeFunds(); - // vm.expectRevert(OptimisticWithdrawalRecipient.InvalidDistribution_TooLarge.selector); - // owrETH.distributeFundsPull(); - // } + vm.expectRevert(OptimisticWithdrawalRecipient.InvalidDistribution_TooLarge.selector); + owrETH.distributeFundsPull(); + } function testCannot_reenterOWR() public { OWRReentrancy wr = new OWRReentrancy(); From 4a18a8d6eaca42cfb943b40b9624ecb96b67c8d0 Mon Sep 17 00:00:00 2001 From: samparsky <8148384+samparsky@users.noreply.github.com> Date: Wed, 27 Sep 2023 07:05:14 +0100 Subject: [PATCH 57/82] test: fix testCannot_distributeTooMuch test case --- src/test/owr/OptimisticWithdrawalRecipient.t.sol | 1 + 1 file changed, 1 insertion(+) diff --git a/src/test/owr/OptimisticWithdrawalRecipient.t.sol b/src/test/owr/OptimisticWithdrawalRecipient.t.sol index 9d51f5c..8422559 100644 --- a/src/test/owr/OptimisticWithdrawalRecipient.t.sol +++ b/src/test/owr/OptimisticWithdrawalRecipient.t.sol @@ -205,6 +205,7 @@ contract OptimisticWithdrawalRecipientTest is OWRTestHelper, Test { owrETH.distributeFunds(); vm.deal(address(owrETH), 1); + vm.deal(address(owrETH), type(uint136).max); vm.expectRevert(OptimisticWithdrawalRecipient.InvalidDistribution_TooLarge.selector); owrETH.distributeFunds(); From 9353f28254f26fe63619a10d9db502ca8034d050 Mon Sep 17 00:00:00 2001 From: samparsky <8148384+samparsky@users.noreply.github.com> Date: Thu, 28 Sep 2023 13:03:47 +0100 Subject: [PATCH 58/82] chore: add remapping, update solady module version --- .gitmodules | 2 +- foundry.toml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitmodules b/.gitmodules index a897cff..4a0b298 100644 --- a/.gitmodules +++ b/.gitmodules @@ -8,7 +8,7 @@ [submodule "lib/solady"] path = lib/solady url = https://github.com/vectorized/solady - branch = v0.0.92 + branch = v0.0.123 [submodule "lib/splits-utils"] path = lib/splits-utils url = https://github.com/0xSplits/splits-utils diff --git a/foundry.toml b/foundry.toml index 84f844b..48cd2d6 100644 --- a/foundry.toml +++ b/foundry.toml @@ -6,6 +6,7 @@ remappings = [ 'ds-test/=lib/ds-test/src/', 'solmate/=lib/solmate/src/', 'splits-tests/=lib/splits-utils/test/', + 'solady/=lib/solady/src/', ] solc_version = '0.8.19' From 2df88b628c96aee6a075a7fb4c7de64fa418ace6 Mon Sep 17 00:00:00 2001 From: samparsky <8148384+samparsky@users.noreply.github.com> Date: Thu, 28 Sep 2023 13:28:33 +0100 Subject: [PATCH 59/82] chore: update Lido.rescueFunds natspec; change transfer to safetransfer --- src/lido/LidoSplit.sol | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/lido/LidoSplit.sol b/src/lido/LidoSplit.sol index ef69379..6457e34 100644 --- a/src/lido/LidoSplit.sol +++ b/src/lido/LidoSplit.sol @@ -70,9 +70,9 @@ contract LidoSplit is Clone { ERC20(wstETH).safeTransfer(splitWallet(), amount); } - /// @notice Rescue stuck ETH + /// @notice Rescue stuck ETH and tokens /// Uses token == address(0) to represent ETH - /// @return balance Amount of ETH rescued + /// @return balance Amount of ETH or tokens rescued function rescueFunds(address token) external returns (uint256 balance) { if (token == address(stETH)) revert Invalid_Address(); @@ -81,7 +81,7 @@ contract LidoSplit is Clone { if (balance > 0) splitWallet().safeTransferETH(balance); } else { balance = ERC20(token).balanceOf(address(this)); - if (balance > 0) ERC20(token).transfer(splitWallet(), balance); + if (balance > 0) ERC20(token).safeTransfer(splitWallet(), balance); } } } From f6e984272e5a7163eb0b66f1fa1b9ccbe04828ae Mon Sep 17 00:00:00 2001 From: samparsky <8148384+samparsky@users.noreply.github.com> Date: Mon, 2 Oct 2023 10:09:24 +0100 Subject: [PATCH 60/82] chore: change claimedPrinicipalFunds to uint128 --- src/owr/OptimisticWithdrawalRecipient.sol | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/owr/OptimisticWithdrawalRecipient.sol b/src/owr/OptimisticWithdrawalRecipient.sol index f246cac..1b05b26 100644 --- a/src/owr/OptimisticWithdrawalRecipient.sol +++ b/src/owr/OptimisticWithdrawalRecipient.sol @@ -114,7 +114,7 @@ contract OptimisticWithdrawalRecipient is Clone { /// Amount of distributed OWRecipient token for principal /// @dev Would be less than or equal to amount of stake /// @dev ERC20s with very large decimals may overflow & cause issues - uint256 public claimedPrincipalFunds; + uint128 public claimedPrincipalFunds; /// Mapping to account balances for pulling mapping(address => uint256) internal pullBalances; @@ -278,7 +278,8 @@ contract OptimisticWithdrawalRecipient is Clone { if (_fundsToBeDistributed > type(uint128).max) revert InvalidDistribution_TooLarge(); // Write to storage // the principal value - claimedPrincipalFunds += _principalPayout; + // it cannot overflow + claimedPrincipalFunds += uint128(_principalPayout); } /// interactions From 49dce61ad75272d6efe0538f7b8383e52d9e0c94 Mon Sep 17 00:00:00 2001 From: samparsky <8148384+samparsky@users.noreply.github.com> Date: Mon, 2 Oct 2023 10:14:22 +0100 Subject: [PATCH 61/82] chore: add _principalPayout > 0 check --- src/owr/OptimisticWithdrawalRecipient.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/owr/OptimisticWithdrawalRecipient.sol b/src/owr/OptimisticWithdrawalRecipient.sol index 1b05b26..f1b414b 100644 --- a/src/owr/OptimisticWithdrawalRecipient.sol +++ b/src/owr/OptimisticWithdrawalRecipient.sol @@ -278,8 +278,8 @@ contract OptimisticWithdrawalRecipient is Clone { if (_fundsToBeDistributed > type(uint128).max) revert InvalidDistribution_TooLarge(); // Write to storage // the principal value - // it cannot overflow - claimedPrincipalFunds += uint128(_principalPayout); + // it cannot overflow because _principalPayout < _fundsToBeDistributed + if (_principalPayout > 0) claimedPrincipalFunds += uint128(_principalPayout); } /// interactions From 2f4f059bfd145f5f05d794948c918d65d222c3a9 Mon Sep 17 00:00:00 2001 From: samparsky <8148384+samparsky@users.noreply.github.com> Date: Mon, 2 Oct 2023 10:21:15 +0100 Subject: [PATCH 62/82] chore: remove unchecked block _fundsToBeDistributed --- src/owr/OptimisticWithdrawalRecipient.sol | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/owr/OptimisticWithdrawalRecipient.sol b/src/owr/OptimisticWithdrawalRecipient.sol index f1b414b..8ca9318 100644 --- a/src/owr/OptimisticWithdrawalRecipient.sol +++ b/src/owr/OptimisticWithdrawalRecipient.sol @@ -239,12 +239,9 @@ contract OptimisticWithdrawalRecipient is Clone { // load storage into memory uint256 currentbalance = address(this).balance; - uint256 _fundsToBeDistributed; uint256 _claimedPrincipalFunds = uint256(claimedPrincipalFunds); uint256 _memoryFundsPendingWithdrawal = uint256(fundsPendingWithdrawal); - unchecked { - _fundsToBeDistributed = currentbalance - _memoryFundsPendingWithdrawal; - } + uint256 _fundsToBeDistributed = currentbalance - _memoryFundsPendingWithdrawal; (address principalRecipient, address rewardRecipient, uint256 amountOfPrincipalStake) = getTranches(); From c28b4b70968473257a5ac8a0d776c17f9aa0f5b1 Mon Sep 17 00:00:00 2001 From: samparsky <8148384+samparsky@users.noreply.github.com> Date: Mon, 2 Oct 2023 16:41:10 +0100 Subject: [PATCH 63/82] chore: add reverse ens registrar to OWRFactory --- src/owr/OptimisticWithdrawalRecipientFactory.sol | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/owr/OptimisticWithdrawalRecipientFactory.sol b/src/owr/OptimisticWithdrawalRecipientFactory.sol index a6331c3..6d8c0f6 100644 --- a/src/owr/OptimisticWithdrawalRecipientFactory.sol +++ b/src/owr/OptimisticWithdrawalRecipientFactory.sol @@ -3,6 +3,7 @@ pragma solidity 0.8.19; import {OptimisticWithdrawalRecipient} from "./OptimisticWithdrawalRecipient.sol"; import {LibClone} from "solady/utils/LibClone.sol"; +import {IENSReverseRegistrar} from "../interfaces/IENSReverseRegistrar.sol"; /// @title OptimisticWithdrawalRecipientFactory /// @author Obol @@ -58,8 +59,14 @@ contract OptimisticWithdrawalRecipientFactory { /// constructor /// ----------------------------------------------------------------------- - constructor() { + constructor( + string memory _ensName, + address _ensReverseRegistrar, + address _ensOwner + ) { owrImpl = new OptimisticWithdrawalRecipient(); + IENSReverseRegistrar(_ensReverseRegistrar).setName(_ensName); + IENSReverseRegistrar(_ensReverseRegistrar).claim(_ensOwner); } /// ----------------------------------------------------------------------- From e83ef750d1f341275885cb42efa00ea55cb4df43 Mon Sep 17 00:00:00 2001 From: samparsky <8148384+samparsky@users.noreply.github.com> Date: Wed, 4 Oct 2023 15:52:11 +0100 Subject: [PATCH 64/82] test: fix OWRFactory test cases --- script/OWRFactoryScript.s.sol | 16 ++++++++++++++-- .../owr/OptimisticWithdrawalRecipient.t.sol | 19 ++++++++++++++++--- ...OptimisticWithdrawalRecipientFactory.t.sol | 16 ++++++++++++++-- 3 files changed, 44 insertions(+), 7 deletions(-) diff --git a/script/OWRFactoryScript.s.sol b/script/OWRFactoryScript.s.sol index acb0c00..b059623 100644 --- a/script/OWRFactoryScript.s.sol +++ b/script/OWRFactoryScript.s.sol @@ -5,10 +5,22 @@ import "forge-std/Script.sol"; import {OptimisticWithdrawalRecipientFactory} from "src/owr/OptimisticWithdrawalRecipientFactory.sol"; contract OWRFactoryScript is Script { - function run() 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")}(); + + new OptimisticWithdrawalRecipientFactory{salt: keccak256("obol.owrFactory.v1")}( + _name, + _ensReverseRegistrar, + _ensOwner + ); + vm.stopBroadcast(); } } diff --git a/src/test/owr/OptimisticWithdrawalRecipient.t.sol b/src/test/owr/OptimisticWithdrawalRecipient.t.sol index 8422559..bf003b7 100644 --- a/src/test/owr/OptimisticWithdrawalRecipient.t.sol +++ b/src/test/owr/OptimisticWithdrawalRecipient.t.sol @@ -8,16 +8,17 @@ import {MockERC20} from "../utils/mocks/MockERC20.sol"; import {SafeTransferLib} from "solady/utils/SafeTransferLib.sol"; import {OWRReentrancy} from "./OWRReentrancy.sol"; import {OWRTestHelper} from "./OWRTestHelper.t.sol"; +import {IENSReverseRegistrar} from "../../interfaces/IENSReverseRegistrar.sol"; contract OptimisticWithdrawalRecipientTest is OWRTestHelper, Test { using SafeTransferLib for address; event ReceiveETH(uint256 amount); - event DistributeFunds(uint256 principalPayout, uint256 rewardPayout, uint256 pullFlowFlag); - event RecoverNonOWRecipientFunds(address nonOWRToken, address recipient, uint256 amount); + address public ENS_REVERSE_REGISTRAR_GOERLI = 0x084b1c3C81545d370f3634392De611CaaBFf8148; + OptimisticWithdrawalRecipient public owrModule; OptimisticWithdrawalRecipientFactory public owrFactory; address internal recoveryAddress; @@ -31,7 +32,19 @@ contract OptimisticWithdrawalRecipientTest is OWRTestHelper, Test { uint256 internal trancheThreshold; function setUp() public { - owrFactory = new OptimisticWithdrawalRecipientFactory(); + owrFactory = new OptimisticWithdrawalRecipientFactory( + "demo.obol.eth", + ENS_REVERSE_REGISTRAR_GOERLI, + address(this) + ); + + vm.mockCall( + 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)) + ); + owrModule = owrFactory.owrImpl(); mERC20 = new MockERC20("demo", "DMT", 18); diff --git a/src/test/owr/OptimisticWithdrawalRecipientFactory.t.sol b/src/test/owr/OptimisticWithdrawalRecipientFactory.t.sol index 41f390c..ee19215 100644 --- a/src/test/owr/OptimisticWithdrawalRecipientFactory.t.sol +++ b/src/test/owr/OptimisticWithdrawalRecipientFactory.t.sol @@ -6,6 +6,7 @@ import {OptimisticWithdrawalRecipient} from "src/owr/OptimisticWithdrawalRecipie import {OptimisticWithdrawalRecipientFactory} from "src/owr/OptimisticWithdrawalRecipientFactory.sol"; import {MockERC20} from "../utils/mocks/MockERC20.sol"; import {OWRTestHelper} from "./OWRTestHelper.t.sol"; +import {IENSReverseRegistrar} from "../../interfaces/IENSReverseRegistrar.sol"; contract OptimisticWithdrawalRecipientFactoryTest is OWRTestHelper, Test { event CreateOWRecipient( @@ -16,6 +17,8 @@ contract OptimisticWithdrawalRecipientFactoryTest is OWRTestHelper, Test { uint256 threshold ); + address public ENS_REVERSE_REGISTRAR_GOERLI = 0x084b1c3C81545d370f3634392De611CaaBFf8148; + OptimisticWithdrawalRecipientFactory owrFactoryModule; address public recoveryAddress; @@ -24,8 +27,17 @@ contract OptimisticWithdrawalRecipientFactoryTest is OWRTestHelper, Test { uint256 public threshold; function setUp() public { - owrFactoryModule = new OptimisticWithdrawalRecipientFactory(); - + owrFactoryModule = new OptimisticWithdrawalRecipientFactory( + "demo.obol.eth", + ENS_REVERSE_REGISTRAR_GOERLI, + address(this) + ); + vm.mockCall( + 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)) + ); recoveryAddress = makeAddr("recoveryAddress"); (principalRecipient, rewardRecipient) = generateTrancheRecipients(10); threshold = ETH_STAKE; From 2f0b5b4e2ac749302455770c89399c703e88684d Mon Sep 17 00:00:00 2001 From: samparsky <8148384+samparsky@users.noreply.github.com> Date: Thu, 5 Oct 2023 10:29:36 +0100 Subject: [PATCH 65/82] test: mock Ensreverseregistrar calls --- src/test/owr/OptimisticWithdrawalRecipient.t.sol | 12 ++++++------ .../owr/OptimisticWithdrawalRecipientFactory.t.sol | 12 +++++++----- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/src/test/owr/OptimisticWithdrawalRecipient.t.sol b/src/test/owr/OptimisticWithdrawalRecipient.t.sol index bf003b7..cf1c5c1 100644 --- a/src/test/owr/OptimisticWithdrawalRecipient.t.sol +++ b/src/test/owr/OptimisticWithdrawalRecipient.t.sol @@ -32,18 +32,18 @@ contract OptimisticWithdrawalRecipientTest is OWRTestHelper, Test { uint256 internal trancheThreshold; function setUp() public { - owrFactory = new OptimisticWithdrawalRecipientFactory( - "demo.obol.eth", - ENS_REVERSE_REGISTRAR_GOERLI, - address(this) - ); - vm.mockCall( 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) + ); owrModule = owrFactory.owrImpl(); diff --git a/src/test/owr/OptimisticWithdrawalRecipientFactory.t.sol b/src/test/owr/OptimisticWithdrawalRecipientFactory.t.sol index ee19215..632dff0 100644 --- a/src/test/owr/OptimisticWithdrawalRecipientFactory.t.sol +++ b/src/test/owr/OptimisticWithdrawalRecipientFactory.t.sol @@ -27,17 +27,19 @@ contract OptimisticWithdrawalRecipientFactoryTest is OWRTestHelper, Test { uint256 public threshold; function setUp() public { - owrFactoryModule = new OptimisticWithdrawalRecipientFactory( - "demo.obol.eth", - ENS_REVERSE_REGISTRAR_GOERLI, - address(this) - ); vm.mockCall( 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) + ); + recoveryAddress = makeAddr("recoveryAddress"); (principalRecipient, rewardRecipient) = generateTrancheRecipients(10); threshold = ETH_STAKE; From 3539195524c8a69fcb50ca76b65282ebfa6c419c Mon Sep 17 00:00:00 2001 From: samparsky <8148384+samparsky@users.noreply.github.com> Date: Thu, 5 Oct 2023 10:57:55 +0100 Subject: [PATCH 66/82] chore: add feeReceipient to lidosplit --- src/lido/LidoSplit.sol | 28 +++++++++++++++++++++++++--- src/lido/LidoSplitFactory.sol | 4 ++-- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/src/lido/LidoSplit.sol b/src/lido/LidoSplit.sol index 6457e34..6146209 100644 --- a/src/lido/LidoSplit.sol +++ b/src/lido/LidoSplit.sol @@ -17,6 +17,8 @@ interface IwSTETH { contract LidoSplit is Clone { error Invalid_Address(); + error Invalid_FeeShare(uint256 fee); + error Invalid_FeeRecipient(); /// ----------------------------------------------------------------------- /// libraries @@ -25,6 +27,7 @@ contract LidoSplit is Clone { using SafeTransferLib for address; address internal constant ETH_ADDRESS = address(0); + uint256 internal constant PERCENTAGE_SCALE = 1e5; /// ----------------------------------------------------------------------- /// storage - cwia offsets @@ -45,9 +48,20 @@ contract LidoSplit is Clone { /// @notice wstETH token ERC20 public immutable wstETH; - constructor(ERC20 _stETH, ERC20 _wstETH) { + /// @notice fee address + address public immutable feeRecipient; + + /// @notice fee share + uint256 public immutable 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; } /// Address of split wallet to send funds to to @@ -66,8 +80,16 @@ contract LidoSplit is Clone { stETH.approve(address(wstETH), balance); // wrap into wseth amount = IwSTETH(address(wstETH)).wrap(balance); - // transfer to split wallet - ERC20(wstETH).safeTransfer(splitWallet(), amount); + if (feeShare > 0 ) { + uint256 fee = (amount * feeShare) / PERCENTAGE_SCALE; + // transfer to split wallet + 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 diff --git a/src/lido/LidoSplitFactory.sol b/src/lido/LidoSplitFactory.sol index 922827c..e64b3eb 100644 --- a/src/lido/LidoSplitFactory.sol +++ b/src/lido/LidoSplitFactory.sol @@ -36,8 +36,8 @@ contract LidoSplitFactory { /// @dev lido split implementation LidoSplit public immutable lidoSplitImpl; - constructor(ERC20 _stETH, ERC20 _wstETH) { - lidoSplitImpl = new LidoSplit(_stETH, _wstETH); + constructor(address _feeRecipient, uint256 _feeShare, ERC20 _stETH, ERC20 _wstETH) { + lidoSplitImpl = new LidoSplit(_feeRecipient, _feeShare, _stETH, _wstETH); } /// Creates a wrapper for splitWallet that transforms stETH token into From ba58cb82a6fe01148542f0c1ba6068a6f6a63905 Mon Sep 17 00:00:00 2001 From: samparsky <8148384+samparsky@users.noreply.github.com> Date: Mon, 9 Oct 2023 09:49:22 +0200 Subject: [PATCH 67/82] test: fix lido test cases --- src/test/lido/LIdoSplitFactory.t.sol | 19 +++++++++++++++++++ src/test/lido/LidoSplit.t.sol | 8 +++++--- .../integration/LidoSplitIntegrationTest.sol | 8 +++++--- 3 files changed, 29 insertions(+), 6 deletions(-) diff --git a/src/test/lido/LIdoSplitFactory.t.sol b/src/test/lido/LIdoSplitFactory.t.sol index 9ffb7cb..8172829 100644 --- a/src/test/lido/LIdoSplitFactory.t.sol +++ b/src/test/lido/LIdoSplitFactory.t.sol @@ -8,6 +8,7 @@ import {LidoSplitTestHelper} from "./LidoSplitTestHelper.sol"; contract LidoSplitFactoryTest is LidoSplitTestHelper, Test { LidoSplitFactory internal lidoSplitFactory; + LidoSplitFactory internal lidoSplitFactoryWithFee; address demoSplit; @@ -18,6 +19,15 @@ contract LidoSplitFactoryTest is LidoSplitTestHelper, Test { vm.createSelectFork(getChain("mainnet").rpcUrl, mainnetBlock); lidoSplitFactory = new LidoSplitFactory( + address(0), + 0, + ERC20(STETH_MAINNET_ADDRESS), + ERC20(WSTETH_MAINNET_ADDRESS) + ); + + lidoSplitFactoryWithFee = new LidoSplitFactory( + address(this), + 1e3, ERC20(STETH_MAINNET_ADDRESS), ERC20(WSTETH_MAINNET_ADDRESS) ); @@ -30,10 +40,19 @@ contract LidoSplitFactoryTest is LidoSplitTestHelper, Test { emit CreateLidoSplit(address(0x1)); lidoSplitFactory.createSplit(demoSplit); + + + vm.expectEmit(true, true, true, false, address(lidoSplitFactoryWithFee)); + emit CreateLidoSplit(address(0x1)); + + lidoSplitFactoryWithFee.createSplit(demoSplit); } function testCannot_CreateSplitInvalidAddress() public { vm.expectRevert(LidoSplitFactory.Invalid_Wallet.selector); lidoSplitFactory.createSplit(address(0)); + + vm.expectRevert(LidoSplitFactory.Invalid_Wallet.selector); + lidoSplitFactoryWithFee.createSplit(address(0)); } } diff --git a/src/test/lido/LidoSplit.t.sol b/src/test/lido/LidoSplit.t.sol index 37a5d33..1bd9a5c 100644 --- a/src/test/lido/LidoSplit.t.sol +++ b/src/test/lido/LidoSplit.t.sol @@ -21,9 +21,11 @@ contract LidoSplitTest is LidoSplitTestHelper, Test { vm.createSelectFork(getChain("mainnet").rpcUrl, mainnetBlock); lidoSplitFactory = new LidoSplitFactory( - ERC20(STETH_MAINNET_ADDRESS), - ERC20(WSTETH_MAINNET_ADDRESS) - ); + address(0), + 0, + ERC20(STETH_MAINNET_ADDRESS), + ERC20(WSTETH_MAINNET_ADDRESS) + ); demoSplit = makeAddr("demoSplit"); diff --git a/src/test/lido/integration/LidoSplitIntegrationTest.sol b/src/test/lido/integration/LidoSplitIntegrationTest.sol index e901135..45465a1 100644 --- a/src/test/lido/integration/LidoSplitIntegrationTest.sol +++ b/src/test/lido/integration/LidoSplitIntegrationTest.sol @@ -23,9 +23,11 @@ contract LidoSplitIntegrationTest is LidoSplitTestHelper, Test { vm.createSelectFork(getChain("mainnet").rpcUrl, mainnetBlock); lidoSplitFactory = new LidoSplitFactory( - ERC20(STETH_MAINNET_ADDRESS), - ERC20(WSTETH_MAINNET_ADDRESS) - ); + address(0), + 0, + ERC20(STETH_MAINNET_ADDRESS), + ERC20(WSTETH_MAINNET_ADDRESS) + ); accounts = new address[](2); accounts[0] = makeAddr("accounts0"); From 7eec2a8cda285a0c246855316a5193efca965346 Mon Sep 17 00:00:00 2001 From: samparsky <8148384+samparsky@users.noreply.github.com> Date: Mon, 9 Oct 2023 10:01:01 +0200 Subject: [PATCH 68/82] chore: add wstETH check to rescueFunds; use balanceOf wstETH to calculate balance --- src/lido/LidoSplit.sol | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/lido/LidoSplit.sol b/src/lido/LidoSplit.sol index 6146209..3b349d9 100644 --- a/src/lido/LidoSplit.sol +++ b/src/lido/LidoSplit.sol @@ -79,7 +79,12 @@ contract LidoSplit is Clone { // approve the wstETH stETH.approve(address(wstETH), balance); // wrap into wseth - amount = IwSTETH(address(wstETH)).wrap(balance); + // 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 @@ -96,7 +101,9 @@ contract LidoSplit is Clone { /// Uses token == address(0) to represent ETH /// @return balance Amount of ETH or tokens rescued function rescueFunds(address token) external returns (uint256 balance) { - if (token == address(stETH)) revert Invalid_Address(); + // 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; From 160fa7fc4783d92dbdf71dd9b19a2552282ad350 Mon Sep 17 00:00:00 2001 From: samparsky <8148384+samparsky@users.noreply.github.com> Date: Mon, 9 Oct 2023 11:37:19 +0200 Subject: [PATCH 69/82] test: add lido fee test cases --- src/lido/LidoSplit.sol | 6 +++ src/test/lido/LidoSplit.t.sol | 89 ++++++++++++++++++++++++++++++++++- 2 files changed, 93 insertions(+), 2 deletions(-) diff --git a/src/lido/LidoSplit.sol b/src/lido/LidoSplit.sol index 3b349d9..ea7422d 100644 --- a/src/lido/LidoSplit.sol +++ b/src/lido/LidoSplit.sol @@ -7,6 +7,7 @@ import {Clone} from "solady/utils/Clone.sol"; interface IwSTETH { function wrap(uint256 amount) external returns (uint256); + function getWstETHByStETH(uint256 _stETHAmount) external view returns (uint256); } /// @title LidoSplit @@ -54,6 +55,11 @@ contract LidoSplit is Clone { /// @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(); diff --git a/src/test/lido/LidoSplit.t.sol b/src/test/lido/LidoSplit.t.sol index 1bd9a5c..5603225 100644 --- a/src/test/lido/LidoSplit.t.sol +++ b/src/test/lido/LidoSplit.t.sol @@ -2,17 +2,26 @@ pragma solidity ^0.8.19; import "forge-std/Test.sol"; -import {LidoSplitFactory, LidoSplit} from "src/lido/LidoSplitFactory.sol"; +import {LidoSplitFactory, LidoSplit, IwSTETH} from "src/lido/LidoSplitFactory.sol"; import {ERC20} from "solmate/tokens/ERC20.sol"; import {LidoSplitTestHelper} from "./LidoSplitTestHelper.sol"; import { MockERC20 } from "src/test/utils/mocks/MockERC20.sol"; contract LidoSplitTest is LidoSplitTestHelper, Test { + + uint256 internal constant PERCENTAGE_SCALE = 1e5; + + LidoSplitFactory internal lidoSplitFactory; + LidoSplitFactory internal lidoSplitFactoryWithFee; + LidoSplit internal lidoSplit; + LidoSplit internal lidoSplitWithFee; address demoSplit; + address feeRecipient; + uint256 feeShare; MockERC20 mERC20; @@ -20,6 +29,9 @@ contract LidoSplitTest is LidoSplitTestHelper, Test { uint256 mainnetBlock = 17_421_005; vm.createSelectFork(getChain("mainnet").rpcUrl, mainnetBlock); + feeRecipient = makeAddr("feeRecipient"); + feeShare = 1e4; + lidoSplitFactory = new LidoSplitFactory( address(0), 0, @@ -27,18 +39,38 @@ contract LidoSplitTest is LidoSplitTestHelper, Test { ERC20(WSTETH_MAINNET_ADDRESS) ); + lidoSplitFactoryWithFee = new LidoSplitFactory( + feeRecipient, + feeShare, + ERC20(STETH_MAINNET_ADDRESS), + ERC20(WSTETH_MAINNET_ADDRESS) + ); + demoSplit = makeAddr("demoSplit"); lidoSplit = LidoSplit(lidoSplitFactory.createSplit(demoSplit)); + lidoSplitWithFee = LidoSplit(lidoSplitFactoryWithFee.createSplit(demoSplit)); mERC20 = new MockERC20("Test Token", "TOK", 18); mERC20.mint(type(uint256).max); } + function test_CannotCreateInvalidFeeRecipient() public { + + } + function test_CloneArgsIsCorrect() public { 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.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"); + assertEq(lidoSplitWithFee.feeShare(), feeShare, "invalid fee share /2"); } function test_CanRescueFunds() public { @@ -64,9 +96,14 @@ contract LidoSplitTest is LidoSplitTestHelper, Test { LidoSplit.Invalid_Address.selector ); lidoSplit.rescueFunds(address(STETH_MAINNET_ADDRESS)); + + vm.expectRevert( + LidoSplit.Invalid_Address.selector + ); + lidoSplit.rescueFunds(address(WSTETH_MAINNET_ADDRESS)); } - function test_CanDistribute() public { + function test_CanDistributeWithoutFee() public { // we use a random account on Etherscan to credit the lidoSplit address // with 10 ether worth of stETH on mainnet vm.prank(0x2bf3937b8BcccE4B65650F122Bb3f1976B937B2f); @@ -82,4 +119,52 @@ contract LidoSplitTest is LidoSplitTestHelper, Test { assertGe(afterBalance, prevBalance, "after balance greater"); } + + function test_CanDistributeWithFee() public { + // we use a random account on Etherscan to credit the lidoSplit address + // with 10 ether worth of stETH on mainnet + vm.prank(0x2bf3937b8BcccE4B65650F122Bb3f1976B937B2f); + uint256 amountToDistribute = 100 ether; + ERC20(STETH_MAINNET_ADDRESS).transfer(address(lidoSplitWithFee), amountToDistribute); + + uint256 prevBalance = ERC20(WSTETH_MAINNET_ADDRESS).balanceOf(demoSplit); + + uint256 balance = ERC20(STETH_MAINNET_ADDRESS).balanceOf(address(lidoSplitWithFee)); + + uint256 wstETHDistributed = IwSTETH(WSTETH_MAINNET_ADDRESS).getWstETHByStETH(balance); + + uint256 amount = lidoSplitWithFee.distribute(); + + assertTrue(amount > 0, "invalid amount"); + + uint256 afterBalance = ERC20(WSTETH_MAINNET_ADDRESS).balanceOf(demoSplit); + + assertGe(afterBalance, prevBalance, "after balance greater"); + + + uint256 expectedFee = (wstETHDistributed * feeShare) / PERCENTAGE_SCALE; + + assertEq( + ERC20(WSTETH_MAINNET_ADDRESS).balanceOf(feeRecipient), + expectedFee, + "invalid fee transferred" + ); + + assertEq( + ERC20(WSTETH_MAINNET_ADDRESS).balanceOf(demoSplit), + wstETHDistributed - expectedFee, + "invalid amount" + ); + } + + function testFuzz_CanDistributeWithFee( + address feeRecipient, + uint256 feeShare + ) public { + vm.assume(feeShare < PERCENTAGE_SCALE); + vm.assume(feeRecipient != address(0)); + + + + } } From 2d4f7a28023a2fca90eb0d7cce1aede249610847 Mon Sep 17 00:00:00 2001 From: samparsky <8148384+samparsky@users.noreply.github.com> Date: Mon, 9 Oct 2023 15:21:15 +0200 Subject: [PATCH 70/82] test: add fuzz fee share lidoSplit test case --- src/lido/LidoSplit.sol | 23 ++++---- src/test/lido/LIdoSplitFactory.t.sol | 1 - src/test/lido/LidoSplit.t.sol | 81 +++++++++++++++++----------- 3 files changed, 60 insertions(+), 45 deletions(-) diff --git a/src/lido/LidoSplit.sol b/src/lido/LidoSplit.sol index ea7422d..c2a2937 100644 --- a/src/lido/LidoSplit.sol +++ b/src/lido/LidoSplit.sol @@ -16,11 +16,10 @@ interface IwSTETH { /// stETH token to wstETH token because stETH is a rebasing token /// @dev Wraps stETH to wstETH and transfers to defined SplitWallet address contract LidoSplit is Clone { - error Invalid_Address(); error Invalid_FeeShare(uint256 fee); error Invalid_FeeRecipient(); - + /// ----------------------------------------------------------------------- /// libraries /// ----------------------------------------------------------------------- @@ -38,11 +37,10 @@ contract LidoSplit is Clone { // 0; first item uint256 internal constant SPLIT_WALLET_ADDRESS_OFFSET = 0; - /// ----------------------------------------------------------------------- /// storage /// ----------------------------------------------------------------------- - + /// @notice stETH token ERC20 public immutable stETH; @@ -52,7 +50,7 @@ contract LidoSplit is Clone { /// @notice fee address address public immutable feeRecipient; - /// @notice fee share + /// @notice fee share uint256 public immutable feeShare; /// @notice Constructor @@ -60,8 +58,8 @@ contract LidoSplit is Clone { /// @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); + 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; @@ -91,15 +89,16 @@ contract LidoSplit is Clone { // contract we would be able to rescue it amount = ERC20(wstETH).balanceOf(address(this)); - if (feeShare > 0 ) { + if (feeShare > 0) { uint256 fee = (amount * feeShare) / PERCENTAGE_SCALE; // transfer to split wallet - ERC20(wstETH).safeTransfer(splitWallet(), amount - fee); + // 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); + ERC20(wstETH).safeTransfer(splitWallet(), amount); } } @@ -109,8 +108,8 @@ contract LidoSplit is Clone { 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 == address(stETH) || token == address(wstETH)) revert Invalid_Address(); + if (token == ETH_ADDRESS) { balance = address(this).balance; if (balance > 0) splitWallet().safeTransferETH(balance); diff --git a/src/test/lido/LIdoSplitFactory.t.sol b/src/test/lido/LIdoSplitFactory.t.sol index 8172829..98a96f2 100644 --- a/src/test/lido/LIdoSplitFactory.t.sol +++ b/src/test/lido/LIdoSplitFactory.t.sol @@ -41,7 +41,6 @@ contract LidoSplitFactoryTest is LidoSplitTestHelper, Test { lidoSplitFactory.createSplit(demoSplit); - vm.expectEmit(true, true, true, false, address(lidoSplitFactoryWithFee)); emit CreateLidoSplit(address(0x1)); diff --git a/src/test/lido/LidoSplit.t.sol b/src/test/lido/LidoSplit.t.sol index 5603225..bf109b3 100644 --- a/src/test/lido/LidoSplit.t.sol +++ b/src/test/lido/LidoSplit.t.sol @@ -5,14 +5,11 @@ import "forge-std/Test.sol"; import {LidoSplitFactory, LidoSplit, IwSTETH} from "src/lido/LidoSplitFactory.sol"; import {ERC20} from "solmate/tokens/ERC20.sol"; import {LidoSplitTestHelper} from "./LidoSplitTestHelper.sol"; -import { MockERC20 } from "src/test/utils/mocks/MockERC20.sol"; - +import {MockERC20} from "src/test/utils/mocks/MockERC20.sol"; contract LidoSplitTest is LidoSplitTestHelper, Test { - uint256 internal constant PERCENTAGE_SCALE = 1e5; - LidoSplitFactory internal lidoSplitFactory; LidoSplitFactory internal lidoSplitFactoryWithFee; @@ -50,14 +47,12 @@ contract LidoSplitTest is LidoSplitTestHelper, Test { lidoSplit = LidoSplit(lidoSplitFactory.createSplit(demoSplit)); lidoSplitWithFee = LidoSplit(lidoSplitFactoryWithFee.createSplit(demoSplit)); - + mERC20 = new MockERC20("Test Token", "TOK", 18); mERC20.mint(type(uint256).max); } - function test_CannotCreateInvalidFeeRecipient() public { - - } + function test_CannotCreateInvalidFeeRecipient() public {} function test_CloneArgsIsCorrect() public { assertEq(lidoSplit.splitWallet(), demoSplit, "invalid address"); @@ -65,7 +60,7 @@ contract LidoSplitTest is LidoSplitTestHelper, Test { 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(address(lidoSplitWithFee.stETH()), STETH_MAINNET_ADDRESS, "invalid stETH address"); assertEq(address(lidoSplitWithFee.wstETH()), WSTETH_MAINNET_ADDRESS, "invalid wstETH address"); @@ -92,14 +87,10 @@ contract LidoSplitTest is LidoSplitTestHelper, Test { } function testCannot_RescueLidoTokens() public { - vm.expectRevert( - LidoSplit.Invalid_Address.selector - ); + vm.expectRevert(LidoSplit.Invalid_Address.selector); lidoSplit.rescueFunds(address(STETH_MAINNET_ADDRESS)); - vm.expectRevert( - LidoSplit.Invalid_Address.selector - ); + vm.expectRevert(LidoSplit.Invalid_Address.selector); lidoSplit.rescueFunds(address(WSTETH_MAINNET_ADDRESS)); } @@ -141,30 +132,56 @@ contract LidoSplitTest is LidoSplitTestHelper, Test { assertGe(afterBalance, prevBalance, "after balance greater"); - uint256 expectedFee = (wstETHDistributed * feeShare) / PERCENTAGE_SCALE; - - assertEq( - ERC20(WSTETH_MAINNET_ADDRESS).balanceOf(feeRecipient), - expectedFee, - "invalid fee transferred" - ); - assertEq( - ERC20(WSTETH_MAINNET_ADDRESS).balanceOf(demoSplit), - wstETHDistributed - expectedFee, - "invalid amount" - ); + assertEq(ERC20(WSTETH_MAINNET_ADDRESS).balanceOf(feeRecipient), expectedFee, "invalid fee transferred"); + + assertEq(ERC20(WSTETH_MAINNET_ADDRESS).balanceOf(demoSplit), wstETHDistributed - expectedFee, "invalid amount"); } function testFuzz_CanDistributeWithFee( - address feeRecipient, - uint256 feeShare + address anotherSplit, + uint256 amountToDistribute, + address fuzzFeeRecipient, + uint256 fuzzFeeShare ) public { - vm.assume(feeShare < PERCENTAGE_SCALE); - vm.assume(feeRecipient != address(0)); + vm.assume(anotherSplit != address(0)); + vm.assume(fuzzFeeShare > 0 && fuzzFeeShare < PERCENTAGE_SCALE); + vm.assume(fuzzFeeRecipient != address(0)); + vm.assume(amountToDistribute > 1 ether); + vm.assume(amountToDistribute < 10 ether); + + LidoSplitFactory fuzzFactorySplitWithFee = new LidoSplitFactory( + fuzzFeeRecipient, + fuzzFeeShare, + ERC20(STETH_MAINNET_ADDRESS), + ERC20(WSTETH_MAINNET_ADDRESS) + ); + + LidoSplit fuzzSplitWithFee = LidoSplit(fuzzFactorySplitWithFee.createSplit(anotherSplit)); + + vm.prank(0x2bf3937b8BcccE4B65650F122Bb3f1976B937B2f); + + ERC20(STETH_MAINNET_ADDRESS).transfer(address(fuzzSplitWithFee), amountToDistribute); + + uint256 prevBalance = ERC20(WSTETH_MAINNET_ADDRESS).balanceOf(anotherSplit); + + uint256 balance = ERC20(STETH_MAINNET_ADDRESS).balanceOf(address(fuzzSplitWithFee)); + + uint256 wstETHDistributed = IwSTETH(WSTETH_MAINNET_ADDRESS).getWstETHByStETH(balance); + + uint256 amount = fuzzSplitWithFee.distribute(); + + assertTrue(amount > 0, "invalid amount"); + + uint256 afterBalance = ERC20(WSTETH_MAINNET_ADDRESS).balanceOf(anotherSplit); + + assertGe(afterBalance, prevBalance, "after balance greater"); + + uint256 expectedFee = (wstETHDistributed * fuzzFeeShare) / PERCENTAGE_SCALE; - + assertEq(ERC20(WSTETH_MAINNET_ADDRESS).balanceOf(fuzzFeeRecipient), expectedFee, "invalid fee transferred"); + assertEq(ERC20(WSTETH_MAINNET_ADDRESS).balanceOf(anotherSplit), wstETHDistributed - expectedFee, "invalid amount"); } } From 85badd12575e4b4c9fb00940ce12dc572373b87a Mon Sep 17 00:00:00 2001 From: samparsky <8148384+samparsky@users.noreply.github.com> Date: Mon, 9 Oct 2023 15:24:04 +0200 Subject: [PATCH 71/82] chore: move IwSTETH to a separate file --- src/interfaces/IwsTETH.sol | 7 +++++++ src/lido/LidoSplit.sol | 6 +----- 2 files changed, 8 insertions(+), 5 deletions(-) create mode 100644 src/interfaces/IwsTETH.sol diff --git a/src/interfaces/IwsTETH.sol b/src/interfaces/IwsTETH.sol new file mode 100644 index 0000000..ec087bc --- /dev/null +++ b/src/interfaces/IwsTETH.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MIT +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/LidoSplit.sol b/src/lido/LidoSplit.sol index c2a2937..4691198 100644 --- a/src/lido/LidoSplit.sol +++ b/src/lido/LidoSplit.sol @@ -4,11 +4,7 @@ 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"; - -interface IwSTETH { - function wrap(uint256 amount) external returns (uint256); - function getWstETHByStETH(uint256 _stETHAmount) external view returns (uint256); -} +import {IwSTETH} from "../interfaces/IwSTETH.sol"; /// @title LidoSplit /// @author Obol From be22fe0566ac2dec5ce771cc062c9bf57116405e Mon Sep 17 00:00:00 2001 From: samparsky <8148384+samparsky@users.noreply.github.com> Date: Mon, 9 Oct 2023 15:31:17 +0200 Subject: [PATCH 72/82] chore: rename IwstETH.sol --- src/interfaces/IwsTETH.sol | 2 +- src/lido/LidoSplit.sol | 4 ++-- src/test/lido/LidoSplit.t.sol | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/interfaces/IwsTETH.sol b/src/interfaces/IwsTETH.sol index ec087bc..7918250 100644 --- a/src/interfaces/IwsTETH.sol +++ b/src/interfaces/IwsTETH.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.19; -interface IwSTETH { +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/LidoSplit.sol b/src/lido/LidoSplit.sol index 4691198..c4f0473 100644 --- a/src/lido/LidoSplit.sol +++ b/src/lido/LidoSplit.sol @@ -4,7 +4,7 @@ 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 {IwSTETH} from "../interfaces/IwSTETH.sol"; +import {IwstETH} from "../interfaces/IwstETH.sol"; /// @title LidoSplit /// @author Obol @@ -80,7 +80,7 @@ contract LidoSplit is Clone { stETH.approve(address(wstETH), balance); // wrap into wseth // we ignore the return value - IwSTETH(address(wstETH)).wrap(balance); + 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)); diff --git a/src/test/lido/LidoSplit.t.sol b/src/test/lido/LidoSplit.t.sol index bf109b3..ca9e2bd 100644 --- a/src/test/lido/LidoSplit.t.sol +++ b/src/test/lido/LidoSplit.t.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.19; import "forge-std/Test.sol"; -import {LidoSplitFactory, LidoSplit, IwSTETH} from "src/lido/LidoSplitFactory.sol"; +import {LidoSplitFactory, LidoSplit, IwstETH} from "src/lido/LidoSplitFactory.sol"; import {ERC20} from "solmate/tokens/ERC20.sol"; import {LidoSplitTestHelper} from "./LidoSplitTestHelper.sol"; import {MockERC20} from "src/test/utils/mocks/MockERC20.sol"; @@ -122,7 +122,7 @@ contract LidoSplitTest is LidoSplitTestHelper, Test { uint256 balance = ERC20(STETH_MAINNET_ADDRESS).balanceOf(address(lidoSplitWithFee)); - uint256 wstETHDistributed = IwSTETH(WSTETH_MAINNET_ADDRESS).getWstETHByStETH(balance); + uint256 wstETHDistributed = IwstETH(WSTETH_MAINNET_ADDRESS).getWstETHByStETH(balance); uint256 amount = lidoSplitWithFee.distribute(); @@ -168,7 +168,7 @@ contract LidoSplitTest is LidoSplitTestHelper, Test { uint256 balance = ERC20(STETH_MAINNET_ADDRESS).balanceOf(address(fuzzSplitWithFee)); - uint256 wstETHDistributed = IwSTETH(WSTETH_MAINNET_ADDRESS).getWstETHByStETH(balance); + uint256 wstETHDistributed = IwstETH(WSTETH_MAINNET_ADDRESS).getWstETHByStETH(balance); uint256 amount = fuzzSplitWithFee.distribute(); From e88212ab55559f3e4eea14008cbf90d84b02749c Mon Sep 17 00:00:00 2001 From: samparsky <8148384+samparsky@users.noreply.github.com> Date: Mon, 9 Oct 2023 15:35:06 +0200 Subject: [PATCH 73/82] chore: fix IwstETH.sol path --- src/lido/LidoSplit.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lido/LidoSplit.sol b/src/lido/LidoSplit.sol index c4f0473..f2c08ea 100644 --- a/src/lido/LidoSplit.sol +++ b/src/lido/LidoSplit.sol @@ -4,7 +4,7 @@ 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 {IwstETH} from "../interfaces/IwstETH.sol"; +import {IwstETH} from "src/interfaces/IwstETH.sol"; /// @title LidoSplit /// @author Obol From 0cadf7fe6277e58dab7a18715300969c86cf0829 Mon Sep 17 00:00:00 2001 From: samparsky <8148384+samparsky@users.noreply.github.com> Date: Mon, 9 Oct 2023 15:48:43 +0200 Subject: [PATCH 74/82] chore: fix IwstETH.sol path --- src/interfaces/{IwsTETH.sol => IwsETH.sol} | 0 src/lido/LidoSplit.sol | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename src/interfaces/{IwsTETH.sol => IwsETH.sol} (100%) diff --git a/src/interfaces/IwsTETH.sol b/src/interfaces/IwsETH.sol similarity index 100% rename from src/interfaces/IwsTETH.sol rename to src/interfaces/IwsETH.sol diff --git a/src/lido/LidoSplit.sol b/src/lido/LidoSplit.sol index f2c08ea..1fb9c77 100644 --- a/src/lido/LidoSplit.sol +++ b/src/lido/LidoSplit.sol @@ -4,7 +4,7 @@ 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 {IwstETH} from "src/interfaces/IwstETH.sol"; +import {IwstETH} from "src/interfaces/IwsETH.sol"; /// @title LidoSplit /// @author Obol From e5e100eba106f5e37400a48e5f584e5a93dc821c Mon Sep 17 00:00:00 2001 From: samparsky <8148384+samparsky@users.noreply.github.com> Date: Mon, 9 Oct 2023 15:49:19 +0200 Subject: [PATCH 75/82] chore: fix IwstETH.sol path --- src/interfaces/{IwsETH.sol => IwstETH.sol} | 0 src/lido/LidoSplit.sol | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename src/interfaces/{IwsETH.sol => IwstETH.sol} (100%) diff --git a/src/interfaces/IwsETH.sol b/src/interfaces/IwstETH.sol similarity index 100% rename from src/interfaces/IwsETH.sol rename to src/interfaces/IwstETH.sol diff --git a/src/lido/LidoSplit.sol b/src/lido/LidoSplit.sol index 1fb9c77..f2c08ea 100644 --- a/src/lido/LidoSplit.sol +++ b/src/lido/LidoSplit.sol @@ -4,7 +4,7 @@ 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 {IwstETH} from "src/interfaces/IwsETH.sol"; +import {IwstETH} from "src/interfaces/IwstETH.sol"; /// @title LidoSplit /// @author Obol From 4a7a713301124a8c3863df4ad591c2301b350131 Mon Sep 17 00:00:00 2001 From: samparsky <8148384+samparsky@users.noreply.github.com> Date: Mon, 9 Oct 2023 16:57:10 +0200 Subject: [PATCH 76/82] chore: rename lidosplit => obollidosplit --- docs/audit/LidoSplit.md | 6 +-- docs/audit/LidoSplitFactory.md | 8 +-- foundry.toml | 3 +- src/lido/{LidoSplit.sol => ObolLidoSplit.sol} | 4 +- ...itFactory.sol => ObolLidoSplitFactory.sol} | 16 +++--- ...ctory.t.sol => ObolLIdoSplitFactory.t.sol} | 24 ++++----- .../{LidoSplit.t.sol => ObolLidoSplit.t.sol} | 49 +++++++++++++------ ...Helper.sol => ObolLidoSplitTestHelper.sol} | 2 +- .../integration/LidoSplitIntegrationTest.sol | 14 +++--- 9 files changed, 72 insertions(+), 54 deletions(-) rename src/lido/{LidoSplit.sol => ObolLidoSplit.sol} (98%) rename src/lido/{LidoSplitFactory.sol => ObolLidoSplitFactory.sol} (81%) rename src/test/lido/{LIdoSplitFactory.t.sol => ObolLIdoSplitFactory.t.sol} (59%) rename src/test/lido/{LidoSplit.t.sol => ObolLidoSplit.t.sol} (77%) rename src/test/lido/{LidoSplitTestHelper.sol => ObolLidoSplitTestHelper.sol} (91%) diff --git a/docs/audit/LidoSplit.md b/docs/audit/LidoSplit.md index bf0ebe4..97505c1 100644 --- a/docs/audit/LidoSplit.md +++ b/docs/audit/LidoSplit.md @@ -48,7 +48,7 @@ Source Units in Scope: **`1`** (**100%**) | Type | File | Logic Contracts | Interfaces | Lines | nLines | nSLOC | Comment Lines | Complex. Score | Capabilities | | ---- | ------ | --------------- | ---------- | ----- | ------ | ----- | ------------- | -------------- | ------------ | -| ๐Ÿ“๐Ÿ” | src/lido/LidoSplit.sol | 1 | 1 | 72 | 64 | 29 | 31 | 34 | **** | +| ๐Ÿ“๐Ÿ” | src/lido/ObolLidoSplit.sol | 1 | 1 | 72 | 64 | 29 | 31 | 34 | **** | | ๐Ÿ“๐Ÿ” | **Totals** | **1** | **1** | **72** | **64** | **29** | **31** | **34** | **** | @@ -237,7 +237,7 @@ This section lists functions that are explicitly declared public or payable. Ple | File Name | SHA-1 Hash | |-------------|--------------| -| src/lido/LidoSplit.sol | a6d06d355c3e9abd9b6674b54a0b9b9960d3da33 | +| src/lido/ObolLidoSplit.sol | a6d06d355c3e9abd9b6674b54a0b9b9960d3da33 | Contracts Description Table @@ -250,7 +250,7 @@ This section lists functions that are explicitly declared public or payable. Ple | **IwSTETH** | Interface | ||| | โ”” | wrap | External โ—๏ธ | ๐Ÿ›‘ |NOโ—๏ธ | |||||| -| **LidoSplit** | Implementation | Clone ||| +| **ObolLidoSplit** | Implementation | Clone ||| | โ”” | | Public โ—๏ธ | ๐Ÿ›‘ |NOโ—๏ธ | | โ”” | splitWallet | Public โ—๏ธ | |NOโ—๏ธ | | โ”” | stETHAddress | Public โ—๏ธ | |NOโ—๏ธ | diff --git a/docs/audit/LidoSplitFactory.md b/docs/audit/LidoSplitFactory.md index 4749e3a..c518678 100644 --- a/docs/audit/LidoSplitFactory.md +++ b/docs/audit/LidoSplitFactory.md @@ -48,7 +48,7 @@ Source Units in Scope: **`1`** (**100%**) | Type | File | Logic Contracts | Interfaces | Lines | nLines | nSLOC | Comment Lines | Complex. Score | Capabilities | | ---- | ------ | --------------- | ---------- | ----- | ------ | ----- | ------------- | -------------- | ------------ | -| ๐Ÿ“ | src/lido/LidoSplitFactory.sol | 1 | **** | 73 | 73 | 31 | 25 | 24 | **๐ŸŒ€** | +| ๐Ÿ“ | src/lido/ObolLidoSplitFactory.sol | 1 | **** | 73 | 73 | 31 | 25 | 24 | **๐ŸŒ€** | | ๐Ÿ“ | **Totals** | **1** | **** | **73** | **73** | **31** | **25** | **24** | **๐ŸŒ€** | @@ -166,7 +166,7 @@ This section lists functions that are explicitly declared public or payable. Ple | ๐Ÿ“ค Transfers ETH | โšก Low-Level Calls | ๐Ÿ‘ฅ DelegateCall | ๐Ÿงฎ Uses Hash Functions | ๐Ÿ”– ECRecover | ๐ŸŒ€ New/Create/Create2 | | ---------------- | ----------------- | --------------- | ---------------------- | ------------ | --------------------- | -| **** | **** | **** | **** | **** | `yes`
โ†’ `NewContract:LidoSplit` | +| **** | **** | **** | **** | **** | `yes`
โ†’ `NewContract:ObolLidoSplit` | | โ™ป๏ธ TryCatch | ฮฃ Unchecked | | ---------- | ----------- | @@ -236,7 +236,7 @@ This section lists functions that are explicitly declared public or payable. Ple | File Name | SHA-1 Hash | |-------------|--------------| -| src/lido/LidoSplitFactory.sol | fbe7fc44155c90479b3d1c3f46886b2e67f0d5c0 | +| src/lido/ObolLidoSplitFactory.sol | fbe7fc44155c90479b3d1c3f46886b2e67f0d5c0 | Contracts Description Table @@ -246,7 +246,7 @@ This section lists functions that are explicitly declared public or payable. Ple |:----------:|:-------------------:|:----------------:|:----------------:|:---------------:| | โ”” | **Function Name** | **Visibility** | **Mutability** | **Modifiers** | |||||| -| **LidoSplitFactory** | Implementation | ||| +| **ObolLidoSplitFactory** | Implementation | ||| | โ”” | | Public โ—๏ธ | ๐Ÿ›‘ |NOโ—๏ธ | | โ”” | createSplit | External โ—๏ธ | ๐Ÿ›‘ |NOโ—๏ธ | diff --git a/foundry.toml b/foundry.toml index 48cd2d6..47860b3 100644 --- a/foundry.toml +++ b/foundry.toml @@ -9,6 +9,7 @@ remappings = [ 'solady/=lib/solady/src/', ] solc_version = '0.8.19' +gas_reports = ["*"] [rpc_endpoints] @@ -27,4 +28,4 @@ tab_width = 2 wrap_comments = true [fuzz] -runs = 1000 +runs = 10 diff --git a/src/lido/LidoSplit.sol b/src/lido/ObolLidoSplit.sol similarity index 98% rename from src/lido/LidoSplit.sol rename to src/lido/ObolLidoSplit.sol index f2c08ea..c27450b 100644 --- a/src/lido/LidoSplit.sol +++ b/src/lido/ObolLidoSplit.sol @@ -6,12 +6,12 @@ import {SafeTransferLib} from "solmate/utils/SafeTransferLib.sol"; import {Clone} from "solady/utils/Clone.sol"; import {IwstETH} from "src/interfaces/IwstETH.sol"; -/// @title LidoSplit +/// @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 LidoSplit is Clone { +contract ObolLidoSplit is Clone { error Invalid_Address(); error Invalid_FeeShare(uint256 fee); error Invalid_FeeRecipient(); diff --git a/src/lido/LidoSplitFactory.sol b/src/lido/ObolLidoSplitFactory.sol similarity index 81% rename from src/lido/LidoSplitFactory.sol rename to src/lido/ObolLidoSplitFactory.sol index e64b3eb..4d0514f 100644 --- a/src/lido/LidoSplitFactory.sol +++ b/src/lido/ObolLidoSplitFactory.sol @@ -3,13 +3,13 @@ pragma solidity 0.8.19; import {LibClone} from "solady/utils/LibClone.sol"; import {ERC20} from "solmate/tokens/ERC20.sol"; -import "./LidoSplit.sol"; +import "./ObolLidoSplit.sol"; -/// @title LidoSplitFactory +/// @title ObolLidoSplitFactory /// @author Obol -/// @notice A factory contract for cheaply deploying LidoSplit. +/// @notice A factory contract for cheaply deploying ObolLidoSplit. /// @dev The address returned should be used to as reward address for Lido -contract LidoSplitFactory { +contract ObolLidoSplitFactory { /// ----------------------------------------------------------------------- /// errors /// ----------------------------------------------------------------------- @@ -27,17 +27,17 @@ contract LidoSplitFactory { /// ----------------------------------------------------------------------- /// Emitted after lido split - event CreateLidoSplit(address split); + event CreateObolLidoSplit(address split); /// ----------------------------------------------------------------------- /// storage /// ----------------------------------------------------------------------- /// @dev lido split implementation - LidoSplit public immutable lidoSplitImpl; + ObolLidoSplit public immutable lidoSplitImpl; constructor(address _feeRecipient, uint256 _feeShare, ERC20 _stETH, ERC20 _wstETH) { - lidoSplitImpl = new LidoSplit(_feeRecipient, _feeShare, _stETH, _wstETH); + lidoSplitImpl = new ObolLidoSplit(_feeRecipient, _feeShare, _stETH, _wstETH); } /// Creates a wrapper for splitWallet that transforms stETH token into @@ -49,6 +49,6 @@ contract LidoSplitFactory { lidoSplit = address(lidoSplitImpl).clone(abi.encodePacked(splitWallet)); - emit CreateLidoSplit(lidoSplit); + emit CreateObolLidoSplit(lidoSplit); } } diff --git a/src/test/lido/LIdoSplitFactory.t.sol b/src/test/lido/ObolLIdoSplitFactory.t.sol similarity index 59% rename from src/test/lido/LIdoSplitFactory.t.sol rename to src/test/lido/ObolLIdoSplitFactory.t.sol index 98a96f2..fb86a1c 100644 --- a/src/test/lido/LIdoSplitFactory.t.sol +++ b/src/test/lido/ObolLIdoSplitFactory.t.sol @@ -2,30 +2,30 @@ pragma solidity ^0.8.19; import "forge-std/Test.sol"; -import {LidoSplitFactory} from "src/lido/LidoSplitFactory.sol"; +import {ObolLidoSplitFactory} from "src/lido/ObolLidoSplitFactory.sol"; import {ERC20} from "solmate/tokens/ERC20.sol"; -import {LidoSplitTestHelper} from "./LidoSplitTestHelper.sol"; +import {ObolLidoSplitTestHelper} from "./ObolLidoSplitTestHelper.sol"; -contract LidoSplitFactoryTest is LidoSplitTestHelper, Test { - LidoSplitFactory internal lidoSplitFactory; - LidoSplitFactory internal lidoSplitFactoryWithFee; +contract ObolLidoSplitFactoryTest is ObolLidoSplitTestHelper, Test { + ObolLidoSplitFactory internal lidoSplitFactory; + ObolLidoSplitFactory internal lidoSplitFactoryWithFee; address demoSplit; - event CreateLidoSplit(address split); + event CreateObolLidoSplit(address split); function setUp() public { uint256 mainnetBlock = 17_421_005; vm.createSelectFork(getChain("mainnet").rpcUrl, mainnetBlock); - lidoSplitFactory = new LidoSplitFactory( + lidoSplitFactory = new ObolLidoSplitFactory( address(0), 0, ERC20(STETH_MAINNET_ADDRESS), ERC20(WSTETH_MAINNET_ADDRESS) ); - lidoSplitFactoryWithFee = new LidoSplitFactory( + lidoSplitFactoryWithFee = new ObolLidoSplitFactory( address(this), 1e3, ERC20(STETH_MAINNET_ADDRESS), @@ -37,21 +37,21 @@ contract LidoSplitFactoryTest is LidoSplitTestHelper, Test { function testCan_CreateSplit() public { vm.expectEmit(true, true, true, false, address(lidoSplitFactory)); - emit CreateLidoSplit(address(0x1)); + emit CreateObolLidoSplit(address(0x1)); lidoSplitFactory.createSplit(demoSplit); vm.expectEmit(true, true, true, false, address(lidoSplitFactoryWithFee)); - emit CreateLidoSplit(address(0x1)); + emit CreateObolLidoSplit(address(0x1)); lidoSplitFactoryWithFee.createSplit(demoSplit); } function testCannot_CreateSplitInvalidAddress() public { - vm.expectRevert(LidoSplitFactory.Invalid_Wallet.selector); + vm.expectRevert(ObolLidoSplitFactory.Invalid_Wallet.selector); lidoSplitFactory.createSplit(address(0)); - vm.expectRevert(LidoSplitFactory.Invalid_Wallet.selector); + vm.expectRevert(ObolLidoSplitFactory.Invalid_Wallet.selector); lidoSplitFactoryWithFee.createSplit(address(0)); } } diff --git a/src/test/lido/LidoSplit.t.sol b/src/test/lido/ObolLidoSplit.t.sol similarity index 77% rename from src/test/lido/LidoSplit.t.sol rename to src/test/lido/ObolLidoSplit.t.sol index ca9e2bd..798a1e3 100644 --- a/src/test/lido/LidoSplit.t.sol +++ b/src/test/lido/ObolLidoSplit.t.sol @@ -2,19 +2,19 @@ pragma solidity ^0.8.19; import "forge-std/Test.sol"; -import {LidoSplitFactory, LidoSplit, IwstETH} from "src/lido/LidoSplitFactory.sol"; +import {ObolLidoSplitFactory, ObolLidoSplit, IwstETH} from "src/lido/ObolLidoSplitFactory.sol"; import {ERC20} from "solmate/tokens/ERC20.sol"; -import {LidoSplitTestHelper} from "./LidoSplitTestHelper.sol"; +import {ObolLidoSplitTestHelper} from "./ObolLidoSplitTestHelper.sol"; import {MockERC20} from "src/test/utils/mocks/MockERC20.sol"; -contract LidoSplitTest is LidoSplitTestHelper, Test { +contract ObolLidoSplitTest is ObolLidoSplitTestHelper, Test { uint256 internal constant PERCENTAGE_SCALE = 1e5; - LidoSplitFactory internal lidoSplitFactory; - LidoSplitFactory internal lidoSplitFactoryWithFee; + ObolLidoSplitFactory internal lidoSplitFactory; + ObolLidoSplitFactory internal lidoSplitFactoryWithFee; - LidoSplit internal lidoSplit; - LidoSplit internal lidoSplitWithFee; + ObolLidoSplit internal lidoSplit; + ObolLidoSplit internal lidoSplitWithFee; address demoSplit; address feeRecipient; @@ -29,14 +29,14 @@ contract LidoSplitTest is LidoSplitTestHelper, Test { feeRecipient = makeAddr("feeRecipient"); feeShare = 1e4; - lidoSplitFactory = new LidoSplitFactory( + lidoSplitFactory = new ObolLidoSplitFactory( address(0), 0, ERC20(STETH_MAINNET_ADDRESS), ERC20(WSTETH_MAINNET_ADDRESS) ); - lidoSplitFactoryWithFee = new LidoSplitFactory( + lidoSplitFactoryWithFee = new ObolLidoSplitFactory( feeRecipient, feeShare, ERC20(STETH_MAINNET_ADDRESS), @@ -45,14 +45,31 @@ contract LidoSplitTest is LidoSplitTestHelper, Test { demoSplit = makeAddr("demoSplit"); - lidoSplit = LidoSplit(lidoSplitFactory.createSplit(demoSplit)); - lidoSplitWithFee = LidoSplit(lidoSplitFactoryWithFee.createSplit(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 {} + function test_CannotCreateInvalidFeeRecipient() public { + 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(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(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.splitWallet(), demoSplit, "invalid address"); @@ -87,10 +104,10 @@ contract LidoSplitTest is LidoSplitTestHelper, Test { } function testCannot_RescueLidoTokens() public { - vm.expectRevert(LidoSplit.Invalid_Address.selector); + vm.expectRevert(ObolLidoSplit.Invalid_Address.selector); lidoSplit.rescueFunds(address(STETH_MAINNET_ADDRESS)); - vm.expectRevert(LidoSplit.Invalid_Address.selector); + vm.expectRevert(ObolLidoSplit.Invalid_Address.selector); lidoSplit.rescueFunds(address(WSTETH_MAINNET_ADDRESS)); } @@ -151,14 +168,14 @@ contract LidoSplitTest is LidoSplitTestHelper, Test { vm.assume(amountToDistribute > 1 ether); vm.assume(amountToDistribute < 10 ether); - LidoSplitFactory fuzzFactorySplitWithFee = new LidoSplitFactory( + ObolLidoSplitFactory fuzzFactorySplitWithFee = new ObolLidoSplitFactory( fuzzFeeRecipient, fuzzFeeShare, ERC20(STETH_MAINNET_ADDRESS), ERC20(WSTETH_MAINNET_ADDRESS) ); - LidoSplit fuzzSplitWithFee = LidoSplit(fuzzFactorySplitWithFee.createSplit(anotherSplit)); + ObolLidoSplit fuzzSplitWithFee = ObolLidoSplit(fuzzFactorySplitWithFee.createSplit(anotherSplit)); vm.prank(0x2bf3937b8BcccE4B65650F122Bb3f1976B937B2f); diff --git a/src/test/lido/LidoSplitTestHelper.sol b/src/test/lido/ObolLidoSplitTestHelper.sol similarity index 91% rename from src/test/lido/LidoSplitTestHelper.sol rename to src/test/lido/ObolLidoSplitTestHelper.sol index c705688..e75ffeb 100644 --- a/src/test/lido/LidoSplitTestHelper.sol +++ b/src/test/lido/ObolLidoSplitTestHelper.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.19; -contract LidoSplitTestHelper { +contract ObolLidoSplitTestHelper { address internal STETH_MAINNET_ADDRESS = address(0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84); address internal WSTETH_MAINNET_ADDRESS = address(0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0); address internal RANDOM_stETH_ACCOUNT_ADDRESS = address(0x2bf3937b8BcccE4B65650F122Bb3f1976B937B2f); diff --git a/src/test/lido/integration/LidoSplitIntegrationTest.sol b/src/test/lido/integration/LidoSplitIntegrationTest.sol index 45465a1..6c9e7e3 100644 --- a/src/test/lido/integration/LidoSplitIntegrationTest.sol +++ b/src/test/lido/integration/LidoSplitIntegrationTest.sol @@ -2,14 +2,14 @@ pragma solidity ^0.8.19; import "forge-std/Test.sol"; -import {LidoSplitFactory, LidoSplit} from "src/lido/LidoSplitFactory.sol"; +import {ObolLidoSplitFactory, ObolLidoSplit} from "src/lido/ObolLidoSplitFactory.sol"; import {ERC20} from "solmate/tokens/ERC20.sol"; -import {LidoSplitTestHelper} from "../LidoSplitTestHelper.sol"; +import {ObolLidoSplitTestHelper} from "../ObolLidoSplitTestHelper.sol"; import {ISplitMain} from "src/interfaces/ISplitMain.sol"; -contract LidoSplitIntegrationTest is LidoSplitTestHelper, Test { - LidoSplitFactory internal lidoSplitFactory; - LidoSplit internal lidoSplit; +contract ObolLidoSplitIntegrationTest is ObolLidoSplitTestHelper, Test { + ObolLidoSplitFactory internal lidoSplitFactory; + ObolLidoSplit internal lidoSplit; address splitter; @@ -22,7 +22,7 @@ contract LidoSplitIntegrationTest is LidoSplitTestHelper, Test { uint256 mainnetBlock = 17_421_005; vm.createSelectFork(getChain("mainnet").rpcUrl, mainnetBlock); - lidoSplitFactory = new LidoSplitFactory( + lidoSplitFactory = new ObolLidoSplitFactory( address(0), 0, ERC20(STETH_MAINNET_ADDRESS), @@ -39,7 +39,7 @@ contract LidoSplitIntegrationTest is LidoSplitTestHelper, Test { splitter = ISplitMain(SPLIT_MAIN_MAINNET).createSplit(accounts, percentAllocations, 0, address(0)); - lidoSplit = LidoSplit(lidoSplitFactory.createSplit(splitter)); + lidoSplit = ObolLidoSplit(lidoSplitFactory.createSplit(splitter)); } function test_CanDistribute() public { From 8e441a0c0f8b7fef1eb3821f3f51f421dca086b6 Mon Sep 17 00:00:00 2001 From: samparsky <8148384+samparsky@users.noreply.github.com> Date: Mon, 16 Oct 2023 10:13:02 +0300 Subject: [PATCH 77/82] chore: update obollidosplit audit docs --- docs/audit/{LidoSplit.md => ObolLidoSplit.md} | 25 ++++++++----------- ...plitFactory.md => ObolLidoSplitFactory.md} | 12 ++++----- 2 files changed, 17 insertions(+), 20 deletions(-) rename docs/audit/{LidoSplit.md => ObolLidoSplit.md} (92%) rename docs/audit/{LidoSplitFactory.md => ObolLidoSplitFactory.md} (94%) diff --git a/docs/audit/LidoSplit.md b/docs/audit/ObolLidoSplit.md similarity index 92% rename from docs/audit/LidoSplit.md rename to docs/audit/ObolLidoSplit.md index 97505c1..5eedf98 100644 --- a/docs/audit/LidoSplit.md +++ b/docs/audit/ObolLidoSplit.md @@ -48,8 +48,8 @@ Source Units in Scope: **`1`** (**100%**) | Type | File | Logic Contracts | Interfaces | Lines | nLines | nSLOC | Comment Lines | Complex. Score | Capabilities | | ---- | ------ | --------------- | ---------- | ----- | ------ | ----- | ------------- | -------------- | ------------ | -| ๐Ÿ“๐Ÿ” | src/lido/ObolLidoSplit.sol | 1 | 1 | 72 | 64 | 29 | 31 | 34 | **** | -| ๐Ÿ“๐Ÿ” | **Totals** | **1** | **1** | **72** | **64** | **29** | **31** | **34** | **** | +| ๐Ÿ“ | src/lido/ObolLidoSplit.sol | 1 | **** | 117 | 117 | 53 | 46 | 59 | **** | +| ๐Ÿ“ | **Totals** | **1** | **** | **117** | **117** | **53** | **46** | **59** | **** | Legend: [โž•] @@ -131,14 +131,14 @@ The analysis finished with **`0`** errors and **`0`** duplicate files. #### Inline Documentation -- **Comment-to-Source Ratio:** On average there are`1` code lines per comment (lower=better). +- **Comment-to-Source Ratio:** On average there are`1.15` code lines per comment (lower=better). - **ToDo's:** `0` #### Components | ๐Ÿ“Contracts | ๐Ÿ“šLibraries | ๐Ÿ”Interfaces | ๐ŸŽจAbstract | | ------------- | ----------- | ------------ | ---------- | -| 1 | 0 | 1 | 0 | +| 1 | 0 | 0 | 0 | #### Exposed Functions @@ -146,23 +146,23 @@ This section lists functions that are explicitly declared public or payable. Ple | ๐ŸŒPublic | ๐Ÿ’ฐPayable | | ---------- | --------- | -| 5 | 0 | +| 3 | 0 | | External | Internal | Private | Pure | View | | ---------- | -------- | ------- | ---- | ---- | -| 2 | 3 | 0 | 3 | 0 | +| 2 | 3 | 0 | 1 | 0 | #### StateVariables | Total | ๐ŸŒPublic | | ---------- | --------- | -| 3 | 0 | +| 7 | 4 | #### Capabilities | Solidity Versions observed | ๐Ÿงช Experimental Features | ๐Ÿ’ฐ Can Receive Funds | ๐Ÿ–ฅ Uses Assembly | ๐Ÿ’ฃ Has Destroyable Contracts | | -------------------------- | ------------------------ | -------------------- | ---------------- | ---------------------------- | -| `=0.8.17` | | **** | **** | **** | +| `0.8.19` | | **** | **** | **** | | ๐Ÿ“ค Transfers ETH | โšก Low-Level Calls | ๐Ÿ‘ฅ DelegateCall | ๐Ÿงฎ Uses Hash Functions | ๐Ÿ”– ECRecover | ๐ŸŒ€ New/Create/Create2 | | ---------------- | ----------------- | --------------- | ---------------------- | ------------ | --------------------- | @@ -179,6 +179,7 @@ This section lists functions that are explicitly declared public or payable. Ple | solady/utils/Clone.sol | 1 | | solmate/tokens/ERC20.sol | 1 | | solmate/utils/SafeTransferLib.sol | 1 | +| src/interfaces/IwstETH.sol | 1 | #### Totals @@ -237,7 +238,7 @@ This section lists functions that are explicitly declared public or payable. Ple | File Name | SHA-1 Hash | |-------------|--------------| -| src/lido/ObolLidoSplit.sol | a6d06d355c3e9abd9b6674b54a0b9b9960d3da33 | +| src/lido/ObolLidoSplit.sol | e60ac5c37593dd7b11dc04af62baa7b122e98ed5 | Contracts Description Table @@ -247,15 +248,11 @@ This section lists functions that are explicitly declared public or payable. Ple |:----------:|:-------------------:|:----------------:|:----------------:|:---------------:| | โ”” | **Function Name** | **Visibility** | **Mutability** | **Modifiers** | |||||| -| **IwSTETH** | Interface | ||| -| โ”” | wrap | External โ—๏ธ | ๐Ÿ›‘ |NOโ—๏ธ | -|||||| | **ObolLidoSplit** | Implementation | Clone ||| | โ”” | | Public โ—๏ธ | ๐Ÿ›‘ |NOโ—๏ธ | | โ”” | splitWallet | Public โ—๏ธ | |NOโ—๏ธ | -| โ”” | stETHAddress | Public โ—๏ธ | |NOโ—๏ธ | -| โ”” | wstETHAddress | Public โ—๏ธ | |NOโ—๏ธ | | โ”” | distribute | External โ—๏ธ | ๐Ÿ›‘ |NOโ—๏ธ | +| โ”” | rescueFunds | External โ—๏ธ | ๐Ÿ›‘ |NOโ—๏ธ | Legend diff --git a/docs/audit/LidoSplitFactory.md b/docs/audit/ObolLidoSplitFactory.md similarity index 94% rename from docs/audit/LidoSplitFactory.md rename to docs/audit/ObolLidoSplitFactory.md index c518678..f904fbf 100644 --- a/docs/audit/LidoSplitFactory.md +++ b/docs/audit/ObolLidoSplitFactory.md @@ -48,8 +48,8 @@ Source Units in Scope: **`1`** (**100%**) | Type | File | Logic Contracts | Interfaces | Lines | nLines | nSLOC | Comment Lines | Complex. Score | Capabilities | | ---- | ------ | --------------- | ---------- | ----- | ------ | ----- | ------------- | -------------- | ------------ | -| ๐Ÿ“ | src/lido/ObolLidoSplitFactory.sol | 1 | **** | 73 | 73 | 31 | 25 | 24 | **๐ŸŒ€** | -| ๐Ÿ“ | **Totals** | **1** | **** | **73** | **73** | **31** | **25** | **24** | **๐ŸŒ€** | +| ๐Ÿ“ | src/lido/ObolLidoSplitFactory.sol | 1 | **** | 54 | 54 | 18 | 24 | 22 | **๐ŸŒ€** | +| ๐Ÿ“ | **Totals** | **1** | **** | **54** | **54** | **18** | **24** | **22** | **๐ŸŒ€** | Legend: [โž•] @@ -131,7 +131,7 @@ The analysis finished with **`0`** errors and **`0`** duplicate files. #### Inline Documentation -- **Comment-to-Source Ratio:** On average there are`1.24` code lines per comment (lower=better). +- **Comment-to-Source Ratio:** On average there are`0.75` code lines per comment (lower=better). - **ToDo's:** `0` #### Components @@ -156,13 +156,13 @@ This section lists functions that are explicitly declared public or payable. Ple | Total | ๐ŸŒPublic | | ---------- | --------- | -| 3 | 3 | +| 1 | 1 | #### Capabilities | Solidity Versions observed | ๐Ÿงช Experimental Features | ๐Ÿ’ฐ Can Receive Funds | ๐Ÿ–ฅ Uses Assembly | ๐Ÿ’ฃ Has Destroyable Contracts | | -------------------------- | ------------------------ | -------------------- | ---------------- | ---------------------------- | -| `=0.8.17` | | **** | **** | **** | +| `0.8.19` | | **** | **** | **** | | ๐Ÿ“ค Transfers ETH | โšก Low-Level Calls | ๐Ÿ‘ฅ DelegateCall | ๐Ÿงฎ Uses Hash Functions | ๐Ÿ”– ECRecover | ๐ŸŒ€ New/Create/Create2 | | ---------------- | ----------------- | --------------- | ---------------------- | ------------ | --------------------- | @@ -236,7 +236,7 @@ This section lists functions that are explicitly declared public or payable. Ple | File Name | SHA-1 Hash | |-------------|--------------| -| src/lido/ObolLidoSplitFactory.sol | fbe7fc44155c90479b3d1c3f46886b2e67f0d5c0 | +| src/lido/ObolLidoSplitFactory.sol | 39e631fd6416d7ab96b78b1b26855fda259dff64 | Contracts Description Table From 45589b877ab025fb3f15f8bb3dc12ba59821a264 Mon Sep 17 00:00:00 2001 From: samparsky <8148384+samparsky@users.noreply.github.com> Date: Mon, 16 Oct 2023 10:57:06 +0300 Subject: [PATCH 78/82] chore: change IMSC license --- src/controllers/ImmutableSplitController.sol | 2 +- src/controllers/ImmutableSplitControllerFactory.sol | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/controllers/ImmutableSplitController.sol b/src/controllers/ImmutableSplitController.sol index 543ce37..0c7348a 100644 --- a/src/controllers/ImmutableSplitController.sol +++ b/src/controllers/ImmutableSplitController.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: GPL-3.0-or-later pragma solidity 0.8.19; import {ISplitMain} from "../interfaces/ISplitMain.sol"; diff --git a/src/controllers/ImmutableSplitControllerFactory.sol b/src/controllers/ImmutableSplitControllerFactory.sol index 98237af..4999872 100644 --- a/src/controllers/ImmutableSplitControllerFactory.sol +++ b/src/controllers/ImmutableSplitControllerFactory.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: GPL-3.0-or-later pragma solidity 0.8.19; import {ISplitMain} from "../interfaces/ISplitMain.sol"; From 864f81659327585fecd85a30d8e3595189b5c42a Mon Sep 17 00:00:00 2001 From: samparsky <8148384+samparsky@users.noreply.github.com> Date: Mon, 16 Oct 2023 10:58:13 +0300 Subject: [PATCH 79/82] chore: change obollido licenses --- src/test/lido/ObolLIdoSplitFactory.t.sol | 2 +- src/test/lido/ObolLidoSplit.t.sol | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/lido/ObolLIdoSplitFactory.t.sol b/src/test/lido/ObolLIdoSplitFactory.t.sol index fb86a1c..fb5d5cc 100644 --- a/src/test/lido/ObolLIdoSplitFactory.t.sol +++ b/src/test/lido/ObolLIdoSplitFactory.t.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: GPL-3.0-or-later pragma solidity ^0.8.19; import "forge-std/Test.sol"; diff --git a/src/test/lido/ObolLidoSplit.t.sol b/src/test/lido/ObolLidoSplit.t.sol index 798a1e3..4eba66f 100644 --- a/src/test/lido/ObolLidoSplit.t.sol +++ b/src/test/lido/ObolLidoSplit.t.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: GPL-3.0-or-later pragma solidity ^0.8.19; import "forge-std/Test.sol"; From ec6a5f6aa5f33f8d58ee21bdd41154b5afe865ce Mon Sep 17 00:00:00 2001 From: samparsky <8148384+samparsky@users.noreply.github.com> Date: Mon, 16 Oct 2023 11:25:09 +0300 Subject: [PATCH 80/82] chore: add fuzz fee recipient check to testFuzz_CanDistributeWithFee --- src/test/lido/ObolLidoSplit.t.sol | 1 + 1 file changed, 1 insertion(+) diff --git a/src/test/lido/ObolLidoSplit.t.sol b/src/test/lido/ObolLidoSplit.t.sol index 4eba66f..c9b98a0 100644 --- a/src/test/lido/ObolLidoSplit.t.sol +++ b/src/test/lido/ObolLidoSplit.t.sol @@ -163,6 +163,7 @@ contract ObolLidoSplitTest is ObolLidoSplitTestHelper, Test { 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); From 136283f8957a667cfa456b8edd40dae258ddc9d1 Mon Sep 17 00:00:00 2001 From: samparsky <8148384+samparsky@users.noreply.github.com> Date: Mon, 16 Oct 2023 11:26:21 +0300 Subject: [PATCH 81/82] chore: change fuzz runs to 100 --- foundry.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/foundry.toml b/foundry.toml index 47860b3..03b326f 100644 --- a/foundry.toml +++ b/foundry.toml @@ -28,4 +28,4 @@ tab_width = 2 wrap_comments = true [fuzz] -runs = 10 +runs = 100 From b8ebdfd17f6a9e411c14d7bb0dda9e74e85a1f96 Mon Sep 17 00:00:00 2001 From: samparsky <8148384+samparsky@users.noreply.github.com> Date: Mon, 16 Oct 2023 12:08:30 +0300 Subject: [PATCH 82/82] chore: change test files licenses to gpl --- src/test/controllers/IMSC.t.sol | 2 +- src/test/controllers/IMSCFactory.t.sol | 2 +- src/test/lido/ObolLidoSplitTestHelper.sol | 2 +- src/test/lido/integration/LidoSplitIntegrationTest.sol | 2 +- src/test/owr/OWRTestHelper.t.sol | 2 +- src/test/owr/OptimisticWithdrawalRecipient.t.sol | 2 +- src/test/owr/OptimisticWithdrawalRecipientFactory.t.sol | 2 +- src/test/utils/mocks/MockERC1155.sol | 2 +- src/test/utils/mocks/MockNFT.sol | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/test/controllers/IMSC.t.sol b/src/test/controllers/IMSC.t.sol index 7d3cc43..b1c9d95 100644 --- a/src/test/controllers/IMSC.t.sol +++ b/src/test/controllers/IMSC.t.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: GPL-3.0-or-later pragma solidity ^0.8.19; import "forge-std/Test.sol"; diff --git a/src/test/controllers/IMSCFactory.t.sol b/src/test/controllers/IMSCFactory.t.sol index 6a25d45..2079b24 100644 --- a/src/test/controllers/IMSCFactory.t.sol +++ b/src/test/controllers/IMSCFactory.t.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: GPL-3.0-or-later pragma solidity ^0.8.19; import "forge-std/Test.sol"; diff --git a/src/test/lido/ObolLidoSplitTestHelper.sol b/src/test/lido/ObolLidoSplitTestHelper.sol index e75ffeb..b2c8506 100644 --- a/src/test/lido/ObolLidoSplitTestHelper.sol +++ b/src/test/lido/ObolLidoSplitTestHelper.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: GPL-3.0-or-later pragma solidity ^0.8.19; contract ObolLidoSplitTestHelper { diff --git a/src/test/lido/integration/LidoSplitIntegrationTest.sol b/src/test/lido/integration/LidoSplitIntegrationTest.sol index 6c9e7e3..24049a8 100644 --- a/src/test/lido/integration/LidoSplitIntegrationTest.sol +++ b/src/test/lido/integration/LidoSplitIntegrationTest.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: GPL-3.0-or-later pragma solidity ^0.8.19; import "forge-std/Test.sol"; diff --git a/src/test/owr/OWRTestHelper.t.sol b/src/test/owr/OWRTestHelper.t.sol index 1451b2a..925b942 100644 --- a/src/test/owr/OWRTestHelper.t.sol +++ b/src/test/owr/OWRTestHelper.t.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: GPL-3.0-or-later pragma solidity ^0.8.17; contract OWRTestHelper { diff --git a/src/test/owr/OptimisticWithdrawalRecipient.t.sol b/src/test/owr/OptimisticWithdrawalRecipient.t.sol index cf1c5c1..aad4e64 100644 --- a/src/test/owr/OptimisticWithdrawalRecipient.t.sol +++ b/src/test/owr/OptimisticWithdrawalRecipient.t.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: GPL-3.0-or-later pragma solidity ^0.8.19; import "forge-std/Test.sol"; diff --git a/src/test/owr/OptimisticWithdrawalRecipientFactory.t.sol b/src/test/owr/OptimisticWithdrawalRecipientFactory.t.sol index 632dff0..321bb05 100644 --- a/src/test/owr/OptimisticWithdrawalRecipientFactory.t.sol +++ b/src/test/owr/OptimisticWithdrawalRecipientFactory.t.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: GPL-3.0-or-later pragma solidity ^0.8.19; import "forge-std/Test.sol"; diff --git a/src/test/utils/mocks/MockERC1155.sol b/src/test/utils/mocks/MockERC1155.sol index 7b9b55c..7176977 100644 --- a/src/test/utils/mocks/MockERC1155.sol +++ b/src/test/utils/mocks/MockERC1155.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: GPL-3.0-or-later pragma solidity ^0.8.17; import {ERC1155} from "solmate/tokens/ERC1155.sol"; diff --git a/src/test/utils/mocks/MockNFT.sol b/src/test/utils/mocks/MockNFT.sol index 4518ac2..53d3c6e 100644 --- a/src/test/utils/mocks/MockNFT.sol +++ b/src/test/utils/mocks/MockNFT.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: GPL-3.0-or-later pragma solidity ^0.8.19; import "solmate/tokens/ERC721.sol";