From 99eac4b74566cf4374b39b9b6508e48d45c62563 Mon Sep 17 00:00:00 2001 From: cosminobol Date: Thu, 18 Apr 2024 19:21:57 +0300 Subject: [PATCH 01/17] feat: added ObolERC1155Recipient --- src/interfaces/IERC1155Receiver.sol | 77 ++++++++ .../IOptimisticWithdrawalRecipient.sol | 6 + src/owr/ObolErc1155Recipient.sol | 181 ++++++++++++++++++ src/test/owr/ObolErc1155Recipient.t.sol | 122 ++++++++++++ src/test/owr/ObolErc1155RecipientMock.sol | 10 + 5 files changed, 396 insertions(+) create mode 100644 src/interfaces/IERC1155Receiver.sol create mode 100644 src/interfaces/IOptimisticWithdrawalRecipient.sol create mode 100644 src/owr/ObolErc1155Recipient.sol create mode 100644 src/test/owr/ObolErc1155Recipient.t.sol create mode 100644 src/test/owr/ObolErc1155RecipientMock.sol diff --git a/src/interfaces/IERC1155Receiver.sol b/src/interfaces/IERC1155Receiver.sol new file mode 100644 index 0000000..81adf1f --- /dev/null +++ b/src/interfaces/IERC1155Receiver.sol @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + + +/** + * @dev Interface of the ERC-165 standard, as defined in the + * https://eips.ethereum.org/EIPS/eip-165[ERC]. + * + * Implementers can declare support of contract interfaces, which can then be + * queried by others ({ERC165Checker}). + * + * For an implementation, see {ERC165}. + */ +interface IERC165 { + /** + * @dev Returns true if this contract implements the interface defined by + * `interfaceId`. See the corresponding + * https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[ERC section] + * to learn more about how these ids are created. + * + * This function call must use less than 30 000 gas. + */ + function supportsInterface(bytes4 interfaceId) external view returns (bool); +} + +/** + * @dev Interface that must be implemented by smart contracts in order to receive + * ERC-1155 token transfers. + */ +interface IERC1155Receiver is IERC165 { + /** + * @dev Handles the receipt of a single ERC-1155 token type. This function is + * called at the end of a `safeTransferFrom` after the balance has been updated. + * + * NOTE: To accept the transfer, this must return + * `bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)"))` + * (i.e. 0xf23a6e61, or its own function selector). + * + * @param operator The address which initiated the transfer (i.e. msg.sender) + * @param from The address which previously owned the token + * @param id The ID of the token being transferred + * @param value The amount of tokens being transferred + * @param data Additional data with no specified format + * @return `bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)"))` if transfer is allowed + */ + function onERC1155Received( + address operator, + address from, + uint256 id, + uint256 value, + bytes calldata data + ) external returns (bytes4); + + /** + * @dev Handles the receipt of a multiple ERC-1155 token types. This function + * is called at the end of a `safeBatchTransferFrom` after the balances have + * been updated. + * + * NOTE: To accept the transfer(s), this must return + * `bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)"))` + * (i.e. 0xbc197c81, or its own function selector). + * + * @param operator The address which initiated the batch transfer (i.e. msg.sender) + * @param from The address which previously owned the token + * @param ids An array containing ids of each token being transferred (order and length must match values array) + * @param values An array containing amounts of each token being transferred (order and length must match ids array) + * @param data Additional data with no specified format + * @return `bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)"))` if transfer is allowed + */ + function onERC1155BatchReceived( + address operator, + address from, + uint256[] calldata ids, + uint256[] calldata values, + bytes calldata data + ) external returns (bytes4); +} \ No newline at end of file diff --git a/src/interfaces/IOptimisticWithdrawalRecipient.sol b/src/interfaces/IOptimisticWithdrawalRecipient.sol new file mode 100644 index 0000000..698b6b6 --- /dev/null +++ b/src/interfaces/IOptimisticWithdrawalRecipient.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +interface IOptimisticWithdrawalRecipient { + function token() external view returns (address); +} \ No newline at end of file diff --git a/src/owr/ObolErc1155Recipient.sol b/src/owr/ObolErc1155Recipient.sol new file mode 100644 index 0000000..8e1a34b --- /dev/null +++ b/src/owr/ObolErc1155Recipient.sol @@ -0,0 +1,181 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.19; + +import {ERC1155} from "solady/tokens/ERC1155.sol"; +import {Ownable} from "solady/auth/Ownable.sol"; +import {LibString} from "solady/utils/LibString.sol"; +import {IOptimisticWithdrawalRecipient} from "../interfaces/IOptimisticWithdrawalRecipient.sol"; +import {ERC20} from "solmate/tokens/ERC20.sol"; +import {SafeTransferLib} from "solady/utils/SafeTransferLib.sol"; + +contract ObolErc1155Recipient is ERC1155, Ownable { + using SafeTransferLib for address; + + uint256 public lastId; + + struct OWRInfo { + address owr; + address withdrawalAddress; // split address + //TODO: do we need more info here ?! + } + + //TODO: how can we increase `claimable` here? + mapping (address owr => mapping (uint256 id => uint256 claimable)) public rewards; + mapping (uint256 => OWRInfo) public owrInfo; + + // mapping (address owr => uint256[] ids) public assignedTokens; + + string private _baseUri; + address private constant ETH_TOKEN_ADDRESS = address(0x0); + + error TokenNotTransferable(); + error TokenBatchMintLengthMismatch(); + error InvalidTokenAmount(); + error InvalidOwner(); + error InvalidOWR(); + error NothingToClaim(); + error ClaimFailed(); + + constructor(string memory baseUri_, address _owner) { + _initializeOwner(_owner); + + _baseUri = baseUri_; + + lastId = 1; + } + receive() external payable {} + + /// ----------------------------------------------------------------------- + /// functions - view & pure + /// ----------------------------------------------------------------------- + /// @dev Returns the Uniform Resource Identifier (URI) for token `id`. + function uri(uint256 id) public view override returns (string memory) { + return string(abi.encodePacked(_baseUri, LibString.toString(id))); + } + + /// @dev Returns the total amount of tokens stored by the contract. + function totalSupply() public view virtual returns (uint256) { + return lastId - 1; + } + + function isOwnerOf(uint256 id) public view returns (bool) { + return balanceOf(msg.sender, id) > 0; + } + + /// ----------------------------------------------------------------------- + /// functions - public & external + /// ----------------------------------------------------------------------- + /// @notice updates claimable rewards for `owr` and token `id` + /// @param owr the OptimisticWithdrawalRecipient contract + /// @param id the token id + /// @param amount the claimable amount to add + function onRewardsReceived(address owr, uint256 id, uint256 amount) external { + if (owr == address(0)) revert InvalidOWR(); + + if (msg.sender != owr) revert InvalidOwner(); + rewards[owr][id] += amount; + } + + /// @notice claims rewards to `OWRInfo.withdrawalAddress` + /// @dev callable by the owner + /// @param id the ERC1155 id + /// @return claimed the amount of rewards sent to `OWRInfo.withdrawalAddress` + function claim(uint256 id) external returns (uint256 claimed) { + claimed = _claim(id); + } + + /// @notice claims rewards to `OWRInfo.withdrawalAddress` from multiple token ids + /// @dev callable by the owner + /// @param ids the ERC1155 ids + /// @return claimed the amount of rewards sent to `OWRInfo.withdrawalAddress` per each id + function batchClaim(uint256[] calldata ids) external returns (uint256[] memory claimed) { + uint256 count = ids.length; + for (uint256 i; i < count; i ++) { + claimed[i] = _claim(ids[i]); + } + } + + + /// @notice mints a new token + /// @param to receiver address + /// @param amount the amount for `lastId` + /// @return mintedId id of the minted NFT + function mint(address to, uint256 amount, OWRInfo calldata info) external onlyOwner returns (uint256 mintedId) { + if (amount == 0) revert InvalidTokenAmount(); + _mint(to, lastId, amount, ""); + mintedId = _incrementId(); + owrInfo[mintedId] = info; + } + + /// @notice mints a batch of tokens + /// @param to receiver address + /// @param count batch length + /// @param amounts the amounts for each id + /// @param infos info per each id + /// @return mintedIds id list of the minted NFTs + function mintBatch( + address to, + uint256 count, + uint256[] calldata amounts, + OWRInfo[] calldata infos + ) external onlyOwner returns (uint256[] memory mintedIds) { + if (count != amounts.length) revert TokenBatchMintLengthMismatch(); + + mintedIds = new uint256[](count); + for (uint256 i;i < count; i++) { + if (amounts[i] == 0) revert InvalidTokenAmount(); + _mint(to, lastId, amounts[i], ""); + mintedIds[i] = _incrementId(); + owrInfo[mintedIds[i]] = infos[i]; + } + } + + /// @dev non-transferable + function safeTransferFrom( + address, + address, + uint256, + uint256, + bytes calldata + ) public pure override { + revert TokenNotTransferable(); + } + + /// @dev non-transferable + function safeBatchTransferFrom( + address, + address, + uint256[] calldata, + uint256[] calldata, + bytes calldata + ) public pure override { + revert TokenNotTransferable(); + } + + /// ----------------------------------------------------------------------- + /// functions - private + /// ----------------------------------------------------------------------- + function _incrementId() public returns (uint256 mintedId) { + mintedId = lastId; + lastId++; + } + + function _claim(uint256 id) private returns (uint256 claimed) { + if (!isOwnerOf(id)) revert InvalidOwner(); + + address _owr = owrInfo[id].owr; + if (_owr == address(0)) revert InvalidOWR(); + + claimed = rewards[_owr][id]; + if (claimed == 0) revert NothingToClaim(); + + address token = IOptimisticWithdrawalRecipient(_owr).token(); + if (token == address(0)) { + (bool sent,) = owrInfo[id].withdrawalAddress.call{value: claimed}(""); + if (!sent) revert ClaimFailed(); + } else { + token.safeTransfer(owrInfo[id].withdrawalAddress, claimed); + } + } + +} \ No newline at end of file diff --git a/src/test/owr/ObolErc1155Recipient.t.sol b/src/test/owr/ObolErc1155Recipient.t.sol new file mode 100644 index 0000000..e618688 --- /dev/null +++ b/src/test/owr/ObolErc1155Recipient.t.sol @@ -0,0 +1,122 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.19; + +import "forge-std/Test.sol"; +import {IERC1155Receiver} from "src/interfaces/IERC1155Receiver.sol"; +import {ObolErc1155Recipient} from "src/owr/ObolErc1155Recipient.sol"; +import {ObolErc1155RecipientMock} from "./ObolErc1155RecipientMock.sol"; +import {OptimisticWithdrawalRecipient} from "src/owr/OptimisticWithdrawalRecipient.sol"; +import {OptimisticWithdrawalRecipientFactory} from "src/owr/OptimisticWithdrawalRecipientFactory.sol"; +import {IENSReverseRegistrar} from "../../interfaces/IENSReverseRegistrar.sol"; +import {SafeTransferLib} from "solady/utils/SafeTransferLib.sol"; + +contract ObolErc1155RecipientTest is Test, IERC1155Receiver { + using SafeTransferLib for address; + + ObolErc1155RecipientMock recipient; + string constant BASE_URI = "https://github.com"; + uint256 internal constant ETH_STAKE = 32 ether; + address internal constant ETH_ADDRESS = address(0); + address internal constant ENS_REVERSE_REGISTRAR_GOERLI = 0x084b1c3C81545d370f3634392De611CaaBFf8148; + + + function setUp() public { + recipient = new ObolErc1155RecipientMock(BASE_URI, address(this)); + } + + function onERC1155Received( + address, + address, + uint256, + uint256, + bytes calldata + ) external pure override returns (bytes4) { + return this.onERC1155Received.selector; + } + + function onERC1155BatchReceived( + address, + address, + uint256[] calldata, + uint256[] calldata, + bytes calldata + ) external pure override returns (bytes4){ + return this.onERC1155Received.selector; + } + + function supportsInterface(bytes4) external pure override returns (bool) { + return true; + } + + function testInitialSupply() public { + assertEq(recipient.totalSupply(), 0); + } + + function testTransferFrom() public { + recipient.mint(address(this), 1, ObolErc1155Recipient.OWRInfo({owr: address(0), withdrawalAddress: address(0)})); + recipient.mint(address(this), 1, ObolErc1155Recipient.OWRInfo({owr: address(0), withdrawalAddress: address(0)})); + + vm.expectRevert(); + recipient.safeTransferFrom(address(this), address(this), 1, 0, ""); + + + uint256[] memory batchTokens = new uint256[](2); + batchTokens[0] = 1; + batchTokens[1] = 2; + uint256[] memory batchAmounts = new uint256[](2); + batchAmounts[0] = 0; + batchAmounts[0] = 1; + + vm.expectRevert(); + recipient.safeBatchTransferFrom(address(this), address(this), batchTokens, batchAmounts, ""); + } + + function testMint() public { + recipient.mint(address(this), 1, ObolErc1155Recipient.OWRInfo({owr: address(0), withdrawalAddress: address(0)})); + bool ownerOf1 = recipient.isOwnerOf(1); + assertEq(ownerOf1, true); + + + uint256[] memory amounts = new uint256[](2); + amounts[0] = 1; + amounts[1] = 1; + + ObolErc1155Recipient.OWRInfo[] memory infos = new ObolErc1155Recipient.OWRInfo[](2); + infos[0] = ObolErc1155Recipient.OWRInfo({owr: address(0), withdrawalAddress: address(0)}); + infos[1] = ObolErc1155Recipient.OWRInfo({owr: address(0), withdrawalAddress: address(0)}); + recipient.mintBatch(address(this), 2, amounts, infos); + bool ownerOf2 = recipient.isOwnerOf(2); + bool ownerOf3 = recipient.isOwnerOf(3); + assertEq(ownerOf2, true); + assertEq(ownerOf3, true); + } + + function testClaim() public { + address withdrawalAddress = makeAddr("withdrawalAddress"); + 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)) + ); + OptimisticWithdrawalRecipientFactory owrFactory = new OptimisticWithdrawalRecipientFactory("demo.obol.eth", ENS_REVERSE_REGISTRAR_GOERLI, address(this)); + + OptimisticWithdrawalRecipient owrETH = + owrFactory.createOWRecipient(ETH_ADDRESS, withdrawalAddress, withdrawalAddress, withdrawalAddress, ETH_STAKE); + + recipient.mint(address(this), 1, ObolErc1155Recipient.OWRInfo({owr: address(owrETH), withdrawalAddress: withdrawalAddress})); + + address(recipient).safeTransferETH(1 ether); + assertEq(address(recipient).balance, 1 ether); + + recipient.setRewards(1, address(owrETH), 1 ether); + assertEq(recipient.rewards(address(owrETH), 1), 1 ether); + + recipient.claim(1); + assertEq(withdrawalAddress.balance, 1 ether); + } +} \ No newline at end of file diff --git a/src/test/owr/ObolErc1155RecipientMock.sol b/src/test/owr/ObolErc1155RecipientMock.sol new file mode 100644 index 0000000..b020f32 --- /dev/null +++ b/src/test/owr/ObolErc1155RecipientMock.sol @@ -0,0 +1,10 @@ +import {ObolErc1155Recipient} from "src/owr/ObolErc1155Recipient.sol"; + +contract ObolErc1155RecipientMock is ObolErc1155Recipient { + constructor(string memory baseUri_, address _owner) ObolErc1155Recipient(baseUri_, _owner) { + } + + function setRewards(uint256 id, address owr, uint256 amount) external { + rewards[owr][id] += amount; + } +} \ No newline at end of file From 7077176e4434ca4f68ca886d151db4c48d3cab21 Mon Sep 17 00:00:00 2001 From: cosminobol Date: Tue, 23 Apr 2024 23:53:41 +0300 Subject: [PATCH 02/17] chore: push for ObolErc1155Recipient --- .../IOptimisticWithdrawalRecipient.sol | 2 + src/owr/ObolErc1155Recipient.sol | 45 ++++++++++++------- src/test/owr/ObolErc1155Recipient.t.sol | 33 ++++++++++++++ 3 files changed, 64 insertions(+), 16 deletions(-) diff --git a/src/interfaces/IOptimisticWithdrawalRecipient.sol b/src/interfaces/IOptimisticWithdrawalRecipient.sol index 698b6b6..c77cfe5 100644 --- a/src/interfaces/IOptimisticWithdrawalRecipient.sol +++ b/src/interfaces/IOptimisticWithdrawalRecipient.sol @@ -3,4 +3,6 @@ pragma solidity ^0.8.19; interface IOptimisticWithdrawalRecipient { function token() external view returns (address); + function distributeFunds() external payable; + function distributeFundsPull() external payable; } \ No newline at end of file diff --git a/src/owr/ObolErc1155Recipient.sol b/src/owr/ObolErc1155Recipient.sol index 8e1a34b..a7358b5 100644 --- a/src/owr/ObolErc1155Recipient.sol +++ b/src/owr/ObolErc1155Recipient.sol @@ -15,15 +15,12 @@ contract ObolErc1155Recipient is ERC1155, Ownable { struct OWRInfo { address owr; - address withdrawalAddress; // split address - //TODO: do we need more info here ?! + address withdrawalAddress; } - //TODO: how can we increase `claimable` here? - mapping (address owr => mapping (uint256 id => uint256 claimable)) public rewards; mapping (uint256 => OWRInfo) public owrInfo; - - // mapping (address owr => uint256[] ids) public assignedTokens; + mapping(address owr => uint256 id) public assignedToOWR; + mapping (address owr => mapping (uint256 id => uint256 claimable)) public rewards; string private _baseUri; address private constant ETH_TOKEN_ADDRESS = address(0x0); @@ -65,15 +62,20 @@ contract ObolErc1155Recipient is ERC1155, Ownable { /// ----------------------------------------------------------------------- /// functions - public & external /// ----------------------------------------------------------------------- - /// @notice updates claimable rewards for `owr` and token `id` - /// @param owr the OptimisticWithdrawalRecipient contract - /// @param id the token id - /// @param amount the claimable amount to add - function onRewardsReceived(address owr, uint256 id, uint256 amount) external { - if (owr == address(0)) revert InvalidOWR(); - - if (msg.sender != owr) revert InvalidOwner(); - rewards[owr][id] += amount; + + function receiveRewards(address owr) external onlyOwner { + uint256 _tokenId = assignedToOWR[owr]; + + // check if sender is owner of id + if (!isOwnerOf(_tokenId)) revert InvalidOwner(); + + // call .distribute() on OWR + uint256 balanceBefore = _getOWRTokenBalance(owr); + IOptimisticWithdrawalRecipient(owr).distributeFunds(); + uint256 balanceAfter = _getOWRTokenBalance(owr); + + // update rewards[owr][id] += received; + rewards[owr][_tokenId] += (balanceAfter - balanceBefore); } /// @notice claims rewards to `OWRInfo.withdrawalAddress` @@ -105,6 +107,7 @@ contract ObolErc1155Recipient is ERC1155, Ownable { _mint(to, lastId, amount, ""); mintedId = _incrementId(); owrInfo[mintedId] = info; + assignedToOWR[info.owr] = mintedId; } /// @notice mints a batch of tokens @@ -127,6 +130,7 @@ contract ObolErc1155Recipient is ERC1155, Ownable { _mint(to, lastId, amounts[i], ""); mintedIds[i] = _incrementId(); owrInfo[mintedIds[i]] = infos[i]; + assignedToOWR[infos[i].owr] = mintedIds[i]; } } @@ -170,7 +174,7 @@ contract ObolErc1155Recipient is ERC1155, Ownable { if (claimed == 0) revert NothingToClaim(); address token = IOptimisticWithdrawalRecipient(_owr).token(); - if (token == address(0)) { + if (token == ETH_TOKEN_ADDRESS) { (bool sent,) = owrInfo[id].withdrawalAddress.call{value: claimed}(""); if (!sent) revert ClaimFailed(); } else { @@ -178,4 +182,13 @@ contract ObolErc1155Recipient is ERC1155, Ownable { } } + function _getOWRTokenBalance(address owr) private view returns (uint256 balance) { + address token = IOptimisticWithdrawalRecipient(owr).token(); + if (token == ETH_TOKEN_ADDRESS) { + balance = address(this).balance; + } else { + balance = ERC20(token).balanceOf(address(this)); + } + } + } \ No newline at end of file diff --git a/src/test/owr/ObolErc1155Recipient.t.sol b/src/test/owr/ObolErc1155Recipient.t.sol index e618688..efc422a 100644 --- a/src/test/owr/ObolErc1155Recipient.t.sol +++ b/src/test/owr/ObolErc1155Recipient.t.sol @@ -119,4 +119,37 @@ contract ObolErc1155RecipientTest is Test, IERC1155Receiver { recipient.claim(1); assertEq(withdrawalAddress.balance, 1 ether); } + + function testReceiveRewards() public { + address withdrawalAddress = makeAddr("withdrawalAddress"); + 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)) + ); + OptimisticWithdrawalRecipientFactory owrFactory = new OptimisticWithdrawalRecipientFactory("demo.obol.eth", ENS_REVERSE_REGISTRAR_GOERLI, address(this)); + + OptimisticWithdrawalRecipient owrETH = + owrFactory.createOWRecipient(ETH_ADDRESS, withdrawalAddress, withdrawalAddress, address(recipient), ETH_STAKE); + + address(owrETH).safeTransferETH(1 ether); + + recipient.mint(address(this), 1, ObolErc1155Recipient.OWRInfo({owr: address(owrETH), withdrawalAddress: withdrawalAddress})); + bool ownerOf1 = recipient.isOwnerOf(1); + assertEq(ownerOf1, true); + + uint256 registeredRewards = recipient.rewards(address(owrETH), 1); + assertEq(registeredRewards, 0); + + recipient.receiveRewards(address(owrETH)); + assertEq(address(owrETH).balance, 0 ether); + + registeredRewards = recipient.rewards(address(owrETH), 1); + assertEq(registeredRewards, 1 ether); + } } \ No newline at end of file From 02f02ddaa41c4e77945dbba440a0816405587cf5 Mon Sep 17 00:00:00 2001 From: cosminobol Date: Tue, 30 Apr 2024 08:43:30 +0300 Subject: [PATCH 03/17] chore: allow transfer on erc1155 --- src/owr/ObolErc1155Recipient.sol | 63 +++++++++--------- src/test/owr/ObolErc1155ReceiverMock.sol | 30 +++++++++ src/test/owr/ObolErc1155Recipient.t.sol | 83 ++++++++++++++++++++---- 3 files changed, 133 insertions(+), 43 deletions(-) create mode 100644 src/test/owr/ObolErc1155ReceiverMock.sol diff --git a/src/owr/ObolErc1155Recipient.sol b/src/owr/ObolErc1155Recipient.sol index a7358b5..54de8ed 100644 --- a/src/owr/ObolErc1155Recipient.sol +++ b/src/owr/ObolErc1155Recipient.sol @@ -15,7 +15,7 @@ contract ObolErc1155Recipient is ERC1155, Ownable { struct OWRInfo { address owr; - address withdrawalAddress; + address rewardAddress; } mapping (uint256 => OWRInfo) public owrInfo; @@ -78,22 +78,22 @@ contract ObolErc1155Recipient is ERC1155, Ownable { rewards[owr][_tokenId] += (balanceAfter - balanceBefore); } - /// @notice claims rewards to `OWRInfo.withdrawalAddress` + /// @notice claims rewards to `OWRInfo.rewardAddress` /// @dev callable by the owner /// @param id the ERC1155 id - /// @return claimed the amount of rewards sent to `OWRInfo.withdrawalAddress` + /// @return claimed the amount of rewards sent to `OWRInfo.rewardAddress` function claim(uint256 id) external returns (uint256 claimed) { - claimed = _claim(id); + claimed = _claim(id, false); } - /// @notice claims rewards to `OWRInfo.withdrawalAddress` from multiple token ids + /// @notice claims rewards to `OWRInfo.rewardAddress` from multiple token ids /// @dev callable by the owner /// @param ids the ERC1155 ids - /// @return claimed the amount of rewards sent to `OWRInfo.withdrawalAddress` per each id + /// @return claimed the amount of rewards sent to `OWRInfo.rewardAddress` per each id function batchClaim(uint256[] calldata ids) external returns (uint256[] memory claimed) { uint256 count = ids.length; for (uint256 i; i < count; i ++) { - claimed[i] = _claim(ids[i]); + claimed[i] = _claim(ids[i], false); } } @@ -134,51 +134,52 @@ contract ObolErc1155Recipient is ERC1155, Ownable { } } - /// @dev non-transferable - function safeTransferFrom( - address, - address, - uint256, - uint256, - bytes calldata - ) public pure override { - revert TokenNotTransferable(); - } - - /// @dev non-transferable - function safeBatchTransferFrom( - address, - address, - uint256[] calldata, - uint256[] calldata, - bytes calldata - ) public pure override { - revert TokenNotTransferable(); + /// @dev Hook that is called before any token transfer. + /// Forces claim before a transfer happens + function _beforeTokenTransfer( + address from, + address to, + uint256[] memory ids, + uint256[] memory, + bytes memory + ) internal override { + // skip for mint or burn + if (from == address(0) || to == address(0)) return; + + // claim before transfer + uint256 length = ids.length; + for (uint i; i < length; i++) { + _claim(ids[i], true); //allow transfer even if `claimed == 0` + } } /// ----------------------------------------------------------------------- /// functions - private /// ----------------------------------------------------------------------- + function _useBeforeTokenTransfer() internal pure override returns (bool) { + return true; + } + function _incrementId() public returns (uint256 mintedId) { mintedId = lastId; lastId++; } - function _claim(uint256 id) private returns (uint256 claimed) { + function _claim(uint256 id, bool canSkipAmountCheck) private returns (uint256 claimed) { if (!isOwnerOf(id)) revert InvalidOwner(); address _owr = owrInfo[id].owr; if (_owr == address(0)) revert InvalidOWR(); claimed = rewards[_owr][id]; - if (claimed == 0) revert NothingToClaim(); + if (claimed == 0 && !canSkipAmountCheck) revert NothingToClaim(); address token = IOptimisticWithdrawalRecipient(_owr).token(); if (token == ETH_TOKEN_ADDRESS) { - (bool sent,) = owrInfo[id].withdrawalAddress.call{value: claimed}(""); + (bool sent,) = owrInfo[id].rewardAddress.call{value: claimed}(""); if (!sent) revert ClaimFailed(); } else { - token.safeTransfer(owrInfo[id].withdrawalAddress, claimed); + token.safeTransfer(owrInfo[id].rewardAddress, claimed); } } diff --git a/src/test/owr/ObolErc1155ReceiverMock.sol b/src/test/owr/ObolErc1155ReceiverMock.sol new file mode 100644 index 0000000..1d3554d --- /dev/null +++ b/src/test/owr/ObolErc1155ReceiverMock.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.19; + +import {IERC1155Receiver} from "src/interfaces/IERC1155Receiver.sol"; + +contract ObolErc1155ReceiverMock is IERC1155Receiver { + function onERC1155Received( + address, + address, + uint256, + uint256, + bytes calldata + ) external pure override returns (bytes4) { + return this.onERC1155Received.selector; + } + + function onERC1155BatchReceived( + address, + address, + uint256[] calldata, + uint256[] calldata, + bytes calldata + ) external pure override returns (bytes4){ + return this.onERC1155Received.selector; + } + + function supportsInterface(bytes4) external pure override returns (bool) { + return true; + } +} \ No newline at end of file diff --git a/src/test/owr/ObolErc1155Recipient.t.sol b/src/test/owr/ObolErc1155Recipient.t.sol index efc422a..b4f425d 100644 --- a/src/test/owr/ObolErc1155Recipient.t.sol +++ b/src/test/owr/ObolErc1155Recipient.t.sol @@ -9,6 +9,7 @@ import {OptimisticWithdrawalRecipient} from "src/owr/OptimisticWithdrawalRecipie import {OptimisticWithdrawalRecipientFactory} from "src/owr/OptimisticWithdrawalRecipientFactory.sol"; import {IENSReverseRegistrar} from "../../interfaces/IENSReverseRegistrar.sol"; import {SafeTransferLib} from "solady/utils/SafeTransferLib.sol"; +import {ObolErc1155ReceiverMock} from "./ObolErc1155ReceiverMock.sol"; contract ObolErc1155RecipientTest is Test, IERC1155Receiver { using SafeTransferLib for address; @@ -53,8 +54,8 @@ contract ObolErc1155RecipientTest is Test, IERC1155Receiver { } function testTransferFrom() public { - recipient.mint(address(this), 1, ObolErc1155Recipient.OWRInfo({owr: address(0), withdrawalAddress: address(0)})); - recipient.mint(address(this), 1, ObolErc1155Recipient.OWRInfo({owr: address(0), withdrawalAddress: address(0)})); + recipient.mint(address(this), 1, ObolErc1155Recipient.OWRInfo({owr: address(0), rewardAddress: address(0)})); + recipient.mint(address(this), 1, ObolErc1155Recipient.OWRInfo({owr: address(0), rewardAddress: address(0)})); vm.expectRevert(); recipient.safeTransferFrom(address(this), address(this), 1, 0, ""); @@ -72,7 +73,7 @@ contract ObolErc1155RecipientTest is Test, IERC1155Receiver { } function testMint() public { - recipient.mint(address(this), 1, ObolErc1155Recipient.OWRInfo({owr: address(0), withdrawalAddress: address(0)})); + recipient.mint(address(this), 1, ObolErc1155Recipient.OWRInfo({owr: address(0), rewardAddress: address(0)})); bool ownerOf1 = recipient.isOwnerOf(1); assertEq(ownerOf1, true); @@ -82,8 +83,8 @@ contract ObolErc1155RecipientTest is Test, IERC1155Receiver { amounts[1] = 1; ObolErc1155Recipient.OWRInfo[] memory infos = new ObolErc1155Recipient.OWRInfo[](2); - infos[0] = ObolErc1155Recipient.OWRInfo({owr: address(0), withdrawalAddress: address(0)}); - infos[1] = ObolErc1155Recipient.OWRInfo({owr: address(0), withdrawalAddress: address(0)}); + infos[0] = ObolErc1155Recipient.OWRInfo({owr: address(0), rewardAddress: address(0)}); + infos[1] = ObolErc1155Recipient.OWRInfo({owr: address(0), rewardAddress: address(0)}); recipient.mintBatch(address(this), 2, amounts, infos); bool ownerOf2 = recipient.isOwnerOf(2); bool ownerOf3 = recipient.isOwnerOf(3); @@ -92,7 +93,7 @@ contract ObolErc1155RecipientTest is Test, IERC1155Receiver { } function testClaim() public { - address withdrawalAddress = makeAddr("withdrawalAddress"); + address rewardAddress = makeAddr("rewardAddress"); vm.mockCall( ENS_REVERSE_REGISTRAR_GOERLI, abi.encodeWithSelector(IENSReverseRegistrar.setName.selector), @@ -106,9 +107,9 @@ contract ObolErc1155RecipientTest is Test, IERC1155Receiver { OptimisticWithdrawalRecipientFactory owrFactory = new OptimisticWithdrawalRecipientFactory("demo.obol.eth", ENS_REVERSE_REGISTRAR_GOERLI, address(this)); OptimisticWithdrawalRecipient owrETH = - owrFactory.createOWRecipient(ETH_ADDRESS, withdrawalAddress, withdrawalAddress, withdrawalAddress, ETH_STAKE); + owrFactory.createOWRecipient(ETH_ADDRESS, rewardAddress, rewardAddress, rewardAddress, ETH_STAKE); - recipient.mint(address(this), 1, ObolErc1155Recipient.OWRInfo({owr: address(owrETH), withdrawalAddress: withdrawalAddress})); + recipient.mint(address(this), 1, ObolErc1155Recipient.OWRInfo({owr: address(owrETH), rewardAddress: rewardAddress})); address(recipient).safeTransferETH(1 ether); assertEq(address(recipient).balance, 1 ether); @@ -117,11 +118,69 @@ contract ObolErc1155RecipientTest is Test, IERC1155Receiver { assertEq(recipient.rewards(address(owrETH), 1), 1 ether); recipient.claim(1); - assertEq(withdrawalAddress.balance, 1 ether); + assertEq(rewardAddress.balance, 1 ether); + } + + function testTransferWithRewards() public { + address rewardAddress = makeAddr("rewardAddress"); + address receiverAddress = address(new ObolErc1155ReceiverMock()); + + 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)) + ); + OptimisticWithdrawalRecipientFactory owrFactory = new OptimisticWithdrawalRecipientFactory("demo.obol.eth", ENS_REVERSE_REGISTRAR_GOERLI, address(this)); + + OptimisticWithdrawalRecipient owrETH = + owrFactory.createOWRecipient(ETH_ADDRESS, rewardAddress, rewardAddress, rewardAddress, ETH_STAKE); + + recipient.mint(address(this), 1, ObolErc1155Recipient.OWRInfo({owr: address(owrETH), rewardAddress: rewardAddress})); + + address(recipient).safeTransferETH(1 ether); + assertEq(address(recipient).balance, 1 ether); + + recipient.setRewards(1, address(owrETH), 1 ether); + assertEq(recipient.rewards(address(owrETH), 1), 1 ether); + + recipient.safeTransferFrom(address(this), receiverAddress, 1, 1, "0x"); + assertEq(rewardAddress.balance, 1 ether); + } + function testTransferWithoutRewards() public { + address rewardAddress = makeAddr("rewardAddress"); + address receiverAddress = address(new ObolErc1155ReceiverMock()); + + 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)) + ); + OptimisticWithdrawalRecipientFactory owrFactory = new OptimisticWithdrawalRecipientFactory("demo.obol.eth", ENS_REVERSE_REGISTRAR_GOERLI, address(this)); + + OptimisticWithdrawalRecipient owrETH = + owrFactory.createOWRecipient(ETH_ADDRESS, rewardAddress, rewardAddress, rewardAddress, ETH_STAKE); + + recipient.mint(address(this), 1, ObolErc1155Recipient.OWRInfo({owr: address(owrETH), rewardAddress: rewardAddress})); + + recipient.safeTransferFrom(address(this), receiverAddress, 1, 1, "0x"); + assertFalse(recipient.isOwnerOf(1)); + + vm.prank(receiverAddress); + assertTrue(recipient.isOwnerOf(1)); } function testReceiveRewards() public { - address withdrawalAddress = makeAddr("withdrawalAddress"); + address rewardAddress = makeAddr("rewardAddress"); vm.mockCall( ENS_REVERSE_REGISTRAR_GOERLI, abi.encodeWithSelector(IENSReverseRegistrar.setName.selector), @@ -135,11 +194,11 @@ contract ObolErc1155RecipientTest is Test, IERC1155Receiver { OptimisticWithdrawalRecipientFactory owrFactory = new OptimisticWithdrawalRecipientFactory("demo.obol.eth", ENS_REVERSE_REGISTRAR_GOERLI, address(this)); OptimisticWithdrawalRecipient owrETH = - owrFactory.createOWRecipient(ETH_ADDRESS, withdrawalAddress, withdrawalAddress, address(recipient), ETH_STAKE); + owrFactory.createOWRecipient(ETH_ADDRESS, rewardAddress, rewardAddress, address(recipient), ETH_STAKE); address(owrETH).safeTransferETH(1 ether); - recipient.mint(address(this), 1, ObolErc1155Recipient.OWRInfo({owr: address(owrETH), withdrawalAddress: withdrawalAddress})); + recipient.mint(address(this), 1, ObolErc1155Recipient.OWRInfo({owr: address(owrETH), rewardAddress: rewardAddress})); bool ownerOf1 = recipient.isOwnerOf(1); assertEq(ownerOf1, true); From f6334d68771c803b92adaa120144fdf4d3d08450 Mon Sep 17 00:00:00 2001 From: cosminobol Date: Wed, 8 May 2024 12:04:38 +0300 Subject: [PATCH 04/17] chore: deposit to ETH on mint --- src/interfaces/IDepositContract.sol | 25 ++ src/owr/ObolErc1155Recipient.sol | 402 +++++++++++---------- src/test/owr/DepositContractMock.sol | 25 ++ src/test/owr/ObolErc1155Recipient.t.sol | 403 +++++++++++----------- src/test/owr/ObolErc1155RecipientMock.sol | 5 +- 5 files changed, 481 insertions(+), 379 deletions(-) create mode 100644 src/interfaces/IDepositContract.sol create mode 100644 src/test/owr/DepositContractMock.sol diff --git a/src/interfaces/IDepositContract.sol b/src/interfaces/IDepositContract.sol new file mode 100644 index 0000000..17b0600 --- /dev/null +++ b/src/interfaces/IDepositContract.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +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; + + /// @notice Query the current deposit root hash. + /// @return The deposit root hash. + function get_deposit_root() external view returns (bytes32); + + /// @notice Query the current deposit count. + /// @return The deposit count encoded as a little endian 64-bit number. + function get_deposit_count() external view returns (bytes memory); +} \ No newline at end of file diff --git a/src/owr/ObolErc1155Recipient.sol b/src/owr/ObolErc1155Recipient.sol index 54de8ed..ebe9d8c 100644 --- a/src/owr/ObolErc1155Recipient.sol +++ b/src/owr/ObolErc1155Recipient.sol @@ -4,192 +4,238 @@ pragma solidity 0.8.19; import {ERC1155} from "solady/tokens/ERC1155.sol"; import {Ownable} from "solady/auth/Ownable.sol"; import {LibString} from "solady/utils/LibString.sol"; +import {IDepositContract} from "../interfaces/IDepositContract.sol"; import {IOptimisticWithdrawalRecipient} from "../interfaces/IOptimisticWithdrawalRecipient.sol"; import {ERC20} from "solmate/tokens/ERC20.sol"; import {SafeTransferLib} from "solady/utils/SafeTransferLib.sol"; contract ObolErc1155Recipient is ERC1155, Ownable { - using SafeTransferLib for address; - - uint256 public lastId; - - struct OWRInfo { - address owr; - address rewardAddress; - } - - mapping (uint256 => OWRInfo) public owrInfo; - mapping(address owr => uint256 id) public assignedToOWR; - mapping (address owr => mapping (uint256 id => uint256 claimable)) public rewards; - - string private _baseUri; - address private constant ETH_TOKEN_ADDRESS = address(0x0); - - error TokenNotTransferable(); - error TokenBatchMintLengthMismatch(); - error InvalidTokenAmount(); - error InvalidOwner(); - error InvalidOWR(); - error NothingToClaim(); - error ClaimFailed(); - - constructor(string memory baseUri_, address _owner) { - _initializeOwner(_owner); - - _baseUri = baseUri_; - - lastId = 1; - } - receive() external payable {} - - /// ----------------------------------------------------------------------- - /// functions - view & pure - /// ----------------------------------------------------------------------- - /// @dev Returns the Uniform Resource Identifier (URI) for token `id`. - function uri(uint256 id) public view override returns (string memory) { - return string(abi.encodePacked(_baseUri, LibString.toString(id))); - } - - /// @dev Returns the total amount of tokens stored by the contract. - function totalSupply() public view virtual returns (uint256) { - return lastId - 1; - } - - function isOwnerOf(uint256 id) public view returns (bool) { - return balanceOf(msg.sender, id) > 0; - } - - /// ----------------------------------------------------------------------- - /// functions - public & external - /// ----------------------------------------------------------------------- - - function receiveRewards(address owr) external onlyOwner { - uint256 _tokenId = assignedToOWR[owr]; - - // check if sender is owner of id - if (!isOwnerOf(_tokenId)) revert InvalidOwner(); - - // call .distribute() on OWR - uint256 balanceBefore = _getOWRTokenBalance(owr); - IOptimisticWithdrawalRecipient(owr).distributeFunds(); - uint256 balanceAfter = _getOWRTokenBalance(owr); - - // update rewards[owr][id] += received; - rewards[owr][_tokenId] += (balanceAfter - balanceBefore); + using SafeTransferLib for address; + + uint256 public lastId; + IDepositContract public ethDepositContract; + + struct OWRInfo { + address owr; + address rewardAddress; + } + + struct DepositInfo { + bytes pubkey; + bytes withdrawal_credentials; + bytes sig; + } + + mapping(uint256 => OWRInfo) public owrInfo; + mapping(address owr => uint256 id) public assignedToOWR; + mapping(address owr => mapping(uint256 id => uint256 claimable)) public rewards; + + string private _baseUri; + address private constant ETH_TOKEN_ADDRESS = address(0x0); + uint256 private constant ETH_DEPOSIT_AMOUNT = 32 ether; + + error TokenNotTransferable(); + error TokenBatchMintLengthMismatch(); + error InvalidTokenAmount(); + error InvalidOwner(); + error InvalidOWR(); + error NothingToClaim(); + error ClaimFailed(); + error InvalidDepositContract(); + + event DepositContractUpdated(address oldAddy, address newAddy); + + constructor(string memory baseUri_, address _owner, address _depositContract) { + if (_depositContract == address(0)) revert InvalidDepositContract(); + + lastId = 1; + _baseUri = baseUri_; + ethDepositContract = IDepositContract(_depositContract); + + _initializeOwner(_owner); + } + + receive() external payable {} + + /// ----------------------------------------------------------------------- + /// functions - view & pure + /// ----------------------------------------------------------------------- + /// @dev Returns the Uniform Resource Identifier (URI) for token `id`. + function uri(uint256 id) public view override returns (string memory) { + return string(abi.encodePacked(_baseUri, LibString.toString(id))); + } + + /// @dev Returns the total amount of tokens stored by the contract. + function totalSupply() public view virtual returns (uint256) { + return lastId - 1; + } + + function isOwnerOf(uint256 id) public view returns (bool) { + return balanceOf(msg.sender, id) > 0; + } + + /// ----------------------------------------------------------------------- + /// functions - public & external + /// ----------------------------------------------------------------------- + /// @notice sets the ETH DepositContract + /// @dev callable by the owner + /// @param depositContract the `DepositContract` address + function setDepositContract(address depositContract) external onlyOwner { + if (depositContract == address(0)) revert InvalidDepositContract(); + emit DepositContractUpdated(address(ethDepositContract), depositContract); + ethDepositContract = IDepositContract(depositContract); + } + + /// @notice triggers `OWR.distributeFunds` + /// @dev callable by the owner + /// @param owr the OWR address + function receiveRewards(address owr) external onlyOwner { + uint256 _tokenId = assignedToOWR[owr]; + + // check if sender is owner of id + if (!isOwnerOf(_tokenId)) revert InvalidOwner(); + + // call .distribute() on OWR + uint256 balanceBefore = _getOWRTokenBalance(owr); + IOptimisticWithdrawalRecipient(owr).distributeFunds(); + uint256 balanceAfter = _getOWRTokenBalance(owr); + + // update rewards[owr][id] += received; + rewards[owr][_tokenId] += (balanceAfter - balanceBefore); + } + + /// @notice claims rewards to `OWRInfo.rewardAddress` + /// @dev callable by the owner + /// @param id the ERC1155 id + /// @return claimed the amount of rewards sent to `OWRInfo.rewardAddress` + function claim(uint256 id) external returns (uint256 claimed) { + claimed = _claim(id, false); + } + + /// @notice claims rewards to `OWRInfo.rewardAddress` from multiple token ids + /// @dev callable by the owner + /// @param ids the ERC1155 ids + /// @return claimed the amount of rewards sent to `OWRInfo.rewardAddress` per each id + function batchClaim(uint256[] calldata ids) external returns (uint256[] memory claimed) { + uint256 count = ids.length; + for (uint256 i; i < count; i++) { + claimed[i] = _claim(ids[i], false); } - - /// @notice claims rewards to `OWRInfo.rewardAddress` - /// @dev callable by the owner - /// @param id the ERC1155 id - /// @return claimed the amount of rewards sent to `OWRInfo.rewardAddress` - function claim(uint256 id) external returns (uint256 claimed) { - claimed = _claim(id, false); - } - - /// @notice claims rewards to `OWRInfo.rewardAddress` from multiple token ids - /// @dev callable by the owner - /// @param ids the ERC1155 ids - /// @return claimed the amount of rewards sent to `OWRInfo.rewardAddress` per each id - function batchClaim(uint256[] calldata ids) external returns (uint256[] memory claimed) { - uint256 count = ids.length; - for (uint256 i; i < count; i ++) { - claimed[i] = _claim(ids[i], false); - } - } - + } + + /// @notice mints a new token + /// @param to receiver address + /// @param amount the amount for `lastId` + /// @return mintedId id of the minted NFT + function mint(address to, uint256 amount, OWRInfo calldata info, DepositInfo calldata depositInfo) + external + payable + onlyOwner + returns (uint256 mintedId) + { + // validation + if (amount == 0) revert InvalidTokenAmount(); + uint256 totalETH = ETH_DEPOSIT_AMOUNT * amount; + if (msg.value != totalETH) revert InvalidTokenAmount(); - /// @notice mints a new token - /// @param to receiver address - /// @param amount the amount for `lastId` - /// @return mintedId id of the minted NFT - function mint(address to, uint256 amount, OWRInfo calldata info) external onlyOwner returns (uint256 mintedId) { - if (amount == 0) revert InvalidTokenAmount(); - _mint(to, lastId, amount, ""); - mintedId = _incrementId(); - owrInfo[mintedId] = info; - assignedToOWR[info.owr] = mintedId; - } - - /// @notice mints a batch of tokens - /// @param to receiver address - /// @param count batch length - /// @param amounts the amounts for each id - /// @param infos info per each id - /// @return mintedIds id list of the minted NFTs - function mintBatch( - address to, - uint256 count, - uint256[] calldata amounts, - OWRInfo[] calldata infos - ) external onlyOwner returns (uint256[] memory mintedIds) { - if (count != amounts.length) revert TokenBatchMintLengthMismatch(); - - mintedIds = new uint256[](count); - for (uint256 i;i < count; i++) { - if (amounts[i] == 0) revert InvalidTokenAmount(); - _mint(to, lastId, amounts[i], ""); - mintedIds[i] = _incrementId(); - owrInfo[mintedIds[i]] = infos[i]; - assignedToOWR[infos[i].owr] = mintedIds[i]; - } - } - - /// @dev Hook that is called before any token transfer. - /// Forces claim before a transfer happens - function _beforeTokenTransfer( - address from, - address to, - uint256[] memory ids, - uint256[] memory, - bytes memory - ) internal override { - // skip for mint or burn - if (from == address(0) || to == address(0)) return; - - // claim before transfer - uint256 length = ids.length; - for (uint i; i < length; i++) { - _claim(ids[i], true); //allow transfer even if `claimed == 0` - } + // mint + _mint(to, lastId, amount, ""); + mintedId = _afterMint(info, depositInfo, totalETH); + } + + + /// @notice mints a batch of tokens + /// @param to receiver address + /// @param count batch length + /// @param amounts the amounts for each id + /// @param infos info per each id + /// @return mintedIds id list of the minted NFTs + function mintBatch(address to, uint256 count, uint256[] calldata amounts, OWRInfo[] calldata infos, DepositInfo calldata depositInfo) + external + payable + onlyOwner + returns (uint256[] memory mintedIds) + { + if (count != amounts.length) revert TokenBatchMintLengthMismatch(); + uint256 totalETH; + for (uint256 i; i < count; i++) { + totalETH += (ETH_DEPOSIT_AMOUNT * amounts[i]); } - - /// ----------------------------------------------------------------------- - /// functions - private - /// ----------------------------------------------------------------------- - function _useBeforeTokenTransfer() internal pure override returns (bool) { - return true; + if (totalETH != msg.value) revert InvalidTokenAmount(); + + mintedIds = new uint256[](count); + for (uint256 i; i < count; i++) { + if (amounts[i] == 0) revert InvalidTokenAmount(); + uint256 totalIndexETH = ETH_DEPOSIT_AMOUNT * amounts[i]; + _mint(to, lastId, amounts[i], ""); + mintedIds[i] = _afterMint(infos[i], depositInfo, totalIndexETH); } - - function _incrementId() public returns (uint256 mintedId) { - mintedId = lastId; - lastId++; + } + + /// @dev Hook that is called before any token transfer. + /// Forces claim before a transfer happens + function _beforeTokenTransfer(address from, address to, uint256[] memory ids, uint256[] memory, bytes memory) + internal + override + { + // skip for mint or burn + if (from == address(0) || to == address(0)) return; + + // claim before transfer + uint256 length = ids.length; + for (uint256 i; i < length; i++) { + _claim(ids[i], true); //allow transfer even if `claimed == 0` } - - function _claim(uint256 id, bool canSkipAmountCheck) private returns (uint256 claimed) { - if (!isOwnerOf(id)) revert InvalidOwner(); - - address _owr = owrInfo[id].owr; - if (_owr == address(0)) revert InvalidOWR(); - - claimed = rewards[_owr][id]; - if (claimed == 0 && !canSkipAmountCheck) revert NothingToClaim(); - - address token = IOptimisticWithdrawalRecipient(_owr).token(); - if (token == ETH_TOKEN_ADDRESS) { - (bool sent,) = owrInfo[id].rewardAddress.call{value: claimed}(""); - if (!sent) revert ClaimFailed(); - } else { - token.safeTransfer(owrInfo[id].rewardAddress, claimed); - } - } - - function _getOWRTokenBalance(address owr) private view returns (uint256 balance) { - address token = IOptimisticWithdrawalRecipient(owr).token(); - if (token == ETH_TOKEN_ADDRESS) { - balance = address(this).balance; - } else { - balance = ERC20(token).balanceOf(address(this)); - } + } + + /// ----------------------------------------------------------------------- + /// functions - private + /// ----------------------------------------------------------------------- + function _useBeforeTokenTransfer() internal pure override returns (bool) { + return true; + } + + function _incrementId() public returns (uint256 mintedId) { + mintedId = lastId; + lastId++; + } + + function _claim(uint256 id, bool canSkipAmountCheck) private returns (uint256 claimed) { + if (!isOwnerOf(id)) revert InvalidOwner(); + + address _owr = owrInfo[id].owr; + if (_owr == address(0)) revert InvalidOWR(); + + claimed = rewards[_owr][id]; + if (claimed == 0 && !canSkipAmountCheck) revert NothingToClaim(); + + address token = IOptimisticWithdrawalRecipient(_owr).token(); + if (token == ETH_TOKEN_ADDRESS) { + (bool sent,) = owrInfo[id].rewardAddress.call{value: claimed}(""); + if (!sent) revert ClaimFailed(); + } else { + token.safeTransfer(owrInfo[id].rewardAddress, claimed); } - -} \ No newline at end of file + } + + function _getOWRTokenBalance(address owr) private view returns (uint256 balance) { + address token = IOptimisticWithdrawalRecipient(owr).token(); + if (token == ETH_TOKEN_ADDRESS) balance = address(this).balance; + else balance = ERC20(token).balanceOf(address(this)); + } + + + function _afterMint(OWRInfo calldata info, DepositInfo calldata depositInfo, uint256 totalETH) private returns (uint256 mintedId) { + mintedId = _incrementId(); + owrInfo[mintedId] = info; + assignedToOWR[info.owr] = mintedId; + + // deposit to ETH `DepositContract` + ethDepositContract.deposit{value: totalETH}( + depositInfo.pubkey, + depositInfo.withdrawal_credentials, + depositInfo.sig, + ethDepositContract.get_deposit_root() + ); + } +} diff --git a/src/test/owr/DepositContractMock.sol b/src/test/owr/DepositContractMock.sol new file mode 100644 index 0000000..de53405 --- /dev/null +++ b/src/test/owr/DepositContractMock.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.19; + +contract DepositContractMock { + /// @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 {} + + /// @notice Query the current deposit root hash. + /// @return The deposit root hash. + function get_deposit_root() external pure returns (bytes32) {return bytes32(uint256(1));} + + /// @notice Query the current deposit count. + /// @return The deposit count encoded as a little endian 64-bit number. + function get_deposit_count() external pure returns (bytes memory) {return "0x";} +} \ No newline at end of file diff --git a/src/test/owr/ObolErc1155Recipient.t.sol b/src/test/owr/ObolErc1155Recipient.t.sol index b4f425d..e9ac3b9 100644 --- a/src/test/owr/ObolErc1155Recipient.t.sol +++ b/src/test/owr/ObolErc1155Recipient.t.sol @@ -10,205 +10,208 @@ import {OptimisticWithdrawalRecipientFactory} from "src/owr/OptimisticWithdrawal import {IENSReverseRegistrar} from "../../interfaces/IENSReverseRegistrar.sol"; import {SafeTransferLib} from "solady/utils/SafeTransferLib.sol"; import {ObolErc1155ReceiverMock} from "./ObolErc1155ReceiverMock.sol"; +import {DepositContractMock} from "./DepositContractMock.sol"; contract ObolErc1155RecipientTest is Test, IERC1155Receiver { - using SafeTransferLib for address; - - ObolErc1155RecipientMock recipient; - string constant BASE_URI = "https://github.com"; - uint256 internal constant ETH_STAKE = 32 ether; - address internal constant ETH_ADDRESS = address(0); - address internal constant ENS_REVERSE_REGISTRAR_GOERLI = 0x084b1c3C81545d370f3634392De611CaaBFf8148; - - - function setUp() public { - recipient = new ObolErc1155RecipientMock(BASE_URI, address(this)); - } - - function onERC1155Received( - address, - address, - uint256, - uint256, - bytes calldata - ) external pure override returns (bytes4) { - return this.onERC1155Received.selector; - } - - function onERC1155BatchReceived( - address, - address, - uint256[] calldata, - uint256[] calldata, - bytes calldata - ) external pure override returns (bytes4){ - return this.onERC1155Received.selector; - } - - function supportsInterface(bytes4) external pure override returns (bool) { - return true; - } - - function testInitialSupply() public { - assertEq(recipient.totalSupply(), 0); - } - - function testTransferFrom() public { - recipient.mint(address(this), 1, ObolErc1155Recipient.OWRInfo({owr: address(0), rewardAddress: address(0)})); - recipient.mint(address(this), 1, ObolErc1155Recipient.OWRInfo({owr: address(0), rewardAddress: address(0)})); - - vm.expectRevert(); - recipient.safeTransferFrom(address(this), address(this), 1, 0, ""); - - - uint256[] memory batchTokens = new uint256[](2); - batchTokens[0] = 1; - batchTokens[1] = 2; - uint256[] memory batchAmounts = new uint256[](2); - batchAmounts[0] = 0; - batchAmounts[0] = 1; - - vm.expectRevert(); - recipient.safeBatchTransferFrom(address(this), address(this), batchTokens, batchAmounts, ""); - } - - function testMint() public { - recipient.mint(address(this), 1, ObolErc1155Recipient.OWRInfo({owr: address(0), rewardAddress: address(0)})); - bool ownerOf1 = recipient.isOwnerOf(1); - assertEq(ownerOf1, true); - - - uint256[] memory amounts = new uint256[](2); - amounts[0] = 1; - amounts[1] = 1; - - ObolErc1155Recipient.OWRInfo[] memory infos = new ObolErc1155Recipient.OWRInfo[](2); - infos[0] = ObolErc1155Recipient.OWRInfo({owr: address(0), rewardAddress: address(0)}); - infos[1] = ObolErc1155Recipient.OWRInfo({owr: address(0), rewardAddress: address(0)}); - recipient.mintBatch(address(this), 2, amounts, infos); - bool ownerOf2 = recipient.isOwnerOf(2); - bool ownerOf3 = recipient.isOwnerOf(3); - assertEq(ownerOf2, true); - assertEq(ownerOf3, true); - } - - function testClaim() public { - address rewardAddress = makeAddr("rewardAddress"); - 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)) - ); - OptimisticWithdrawalRecipientFactory owrFactory = new OptimisticWithdrawalRecipientFactory("demo.obol.eth", ENS_REVERSE_REGISTRAR_GOERLI, address(this)); - - OptimisticWithdrawalRecipient owrETH = - owrFactory.createOWRecipient(ETH_ADDRESS, rewardAddress, rewardAddress, rewardAddress, ETH_STAKE); - - recipient.mint(address(this), 1, ObolErc1155Recipient.OWRInfo({owr: address(owrETH), rewardAddress: rewardAddress})); - - address(recipient).safeTransferETH(1 ether); - assertEq(address(recipient).balance, 1 ether); - - recipient.setRewards(1, address(owrETH), 1 ether); - assertEq(recipient.rewards(address(owrETH), 1), 1 ether); - - recipient.claim(1); - assertEq(rewardAddress.balance, 1 ether); - } - - function testTransferWithRewards() public { - address rewardAddress = makeAddr("rewardAddress"); - address receiverAddress = address(new ObolErc1155ReceiverMock()); - - 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)) - ); - OptimisticWithdrawalRecipientFactory owrFactory = new OptimisticWithdrawalRecipientFactory("demo.obol.eth", ENS_REVERSE_REGISTRAR_GOERLI, address(this)); - - OptimisticWithdrawalRecipient owrETH = - owrFactory.createOWRecipient(ETH_ADDRESS, rewardAddress, rewardAddress, rewardAddress, ETH_STAKE); - - recipient.mint(address(this), 1, ObolErc1155Recipient.OWRInfo({owr: address(owrETH), rewardAddress: rewardAddress})); - - address(recipient).safeTransferETH(1 ether); - assertEq(address(recipient).balance, 1 ether); - - recipient.setRewards(1, address(owrETH), 1 ether); - assertEq(recipient.rewards(address(owrETH), 1), 1 ether); - - recipient.safeTransferFrom(address(this), receiverAddress, 1, 1, "0x"); - assertEq(rewardAddress.balance, 1 ether); - } - function testTransferWithoutRewards() public { - address rewardAddress = makeAddr("rewardAddress"); - address receiverAddress = address(new ObolErc1155ReceiverMock()); - - 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)) - ); - OptimisticWithdrawalRecipientFactory owrFactory = new OptimisticWithdrawalRecipientFactory("demo.obol.eth", ENS_REVERSE_REGISTRAR_GOERLI, address(this)); - - OptimisticWithdrawalRecipient owrETH = - owrFactory.createOWRecipient(ETH_ADDRESS, rewardAddress, rewardAddress, rewardAddress, ETH_STAKE); - - recipient.mint(address(this), 1, ObolErc1155Recipient.OWRInfo({owr: address(owrETH), rewardAddress: rewardAddress})); - - recipient.safeTransferFrom(address(this), receiverAddress, 1, 1, "0x"); - assertFalse(recipient.isOwnerOf(1)); - - vm.prank(receiverAddress); - assertTrue(recipient.isOwnerOf(1)); - } - - function testReceiveRewards() public { - address rewardAddress = makeAddr("rewardAddress"); - 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)) - ); - OptimisticWithdrawalRecipientFactory owrFactory = new OptimisticWithdrawalRecipientFactory("demo.obol.eth", ENS_REVERSE_REGISTRAR_GOERLI, address(this)); - - OptimisticWithdrawalRecipient owrETH = - owrFactory.createOWRecipient(ETH_ADDRESS, rewardAddress, rewardAddress, address(recipient), ETH_STAKE); - - address(owrETH).safeTransferETH(1 ether); - - recipient.mint(address(this), 1, ObolErc1155Recipient.OWRInfo({owr: address(owrETH), rewardAddress: rewardAddress})); - bool ownerOf1 = recipient.isOwnerOf(1); - assertEq(ownerOf1, true); - - uint256 registeredRewards = recipient.rewards(address(owrETH), 1); - assertEq(registeredRewards, 0); - - recipient.receiveRewards(address(owrETH)); - assertEq(address(owrETH).balance, 0 ether); - - registeredRewards = recipient.rewards(address(owrETH), 1); - assertEq(registeredRewards, 1 ether); - } -} \ No newline at end of file + using SafeTransferLib for address; + + ObolErc1155RecipientMock recipient; + DepositContractMock depositContract; + string constant BASE_URI = "https://github.com"; + uint256 internal constant ETH_STAKE = 32 ether; + address internal constant ETH_ADDRESS = address(0); + address internal constant ENS_REVERSE_REGISTRAR_GOERLI = 0x084b1c3C81545d370f3634392De611CaaBFf8148; + + function setUp() public { + depositContract = new DepositContractMock(); + recipient = new ObolErc1155RecipientMock(BASE_URI, address(this), address(depositContract)); + } + + function onERC1155Received(address, address, uint256, uint256, bytes calldata) + external + pure + override + returns (bytes4) + { + return this.onERC1155Received.selector; + } + + function onERC1155BatchReceived(address, address, uint256[] calldata, uint256[] calldata, bytes calldata) + external + pure + override + returns (bytes4) + { + return this.onERC1155Received.selector; + } + + function supportsInterface(bytes4) external pure override returns (bool) { + return true; + } + + function testInitialSupply() public { + assertEq(recipient.totalSupply(), 0); + } + + function testTransferFrom() public { + recipient.mint(address(this), 1, ObolErc1155Recipient.OWRInfo({owr: address(0), rewardAddress: address(0)}), ObolErc1155Recipient.DepositInfo({pubkey: "0x", withdrawal_credentials: "0x", sig: "0x"})); + recipient.mint(address(this), 1, ObolErc1155Recipient.OWRInfo({owr: address(0), rewardAddress: address(0)}), ObolErc1155Recipient.DepositInfo({pubkey: "0x", withdrawal_credentials: "0x", sig: "0x"})); + + vm.expectRevert(); + recipient.safeTransferFrom(address(this), address(this), 1, 0, ""); + + uint256[] memory batchTokens = new uint256[](2); + batchTokens[0] = 1; + batchTokens[1] = 2; + uint256[] memory batchAmounts = new uint256[](2); + batchAmounts[0] = 0; + batchAmounts[0] = 1; + + vm.expectRevert(); + recipient.safeBatchTransferFrom(address(this), address(this), batchTokens, batchAmounts, ""); + } + + function testMint() public { + recipient.mint(address(this), 1, ObolErc1155Recipient.OWRInfo({owr: address(0), rewardAddress: address(0)}), ObolErc1155Recipient.DepositInfo({pubkey: "0x", withdrawal_credentials: "0x", sig: "0x"})); + bool ownerOf1 = recipient.isOwnerOf(1); + assertEq(ownerOf1, true); + + uint256[] memory amounts = new uint256[](2); + amounts[0] = 1; + amounts[1] = 1; + + ObolErc1155Recipient.OWRInfo[] memory infos = new ObolErc1155Recipient.OWRInfo[](2); + infos[0] = ObolErc1155Recipient.OWRInfo({owr: address(0), rewardAddress: address(0)}); + infos[1] = ObolErc1155Recipient.OWRInfo({owr: address(0), rewardAddress: address(0)}); + recipient.mintBatch(address(this), 2, amounts, infos, ObolErc1155Recipient.DepositInfo({pubkey: "0x", withdrawal_credentials: "0x", sig: "0x"})); + bool ownerOf2 = recipient.isOwnerOf(2); + bool ownerOf3 = recipient.isOwnerOf(3); + assertEq(ownerOf2, true); + assertEq(ownerOf3, true); + } + + function testClaim() public { + address rewardAddress = makeAddr("rewardAddress"); + 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)) + ); + OptimisticWithdrawalRecipientFactory owrFactory = + new OptimisticWithdrawalRecipientFactory("demo.obol.eth", ENS_REVERSE_REGISTRAR_GOERLI, address(this)); + + OptimisticWithdrawalRecipient owrETH = + owrFactory.createOWRecipient(ETH_ADDRESS, rewardAddress, rewardAddress, rewardAddress, ETH_STAKE); + + recipient.mint(address(this), 1, ObolErc1155Recipient.OWRInfo({owr: address(owrETH), rewardAddress: rewardAddress}), ObolErc1155Recipient.DepositInfo({pubkey: "0x", withdrawal_credentials: "0x", sig: "0x"})); + + address(recipient).safeTransferETH(1 ether); + assertEq(address(recipient).balance, 1 ether); + + recipient.setRewards(1, address(owrETH), 1 ether); + assertEq(recipient.rewards(address(owrETH), 1), 1 ether); + + recipient.claim(1); + assertEq(rewardAddress.balance, 1 ether); + } + + function testTransferWithRewards() public { + address rewardAddress = makeAddr("rewardAddress"); + address receiverAddress = address(new ObolErc1155ReceiverMock()); + + 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)) + ); + OptimisticWithdrawalRecipientFactory owrFactory = + new OptimisticWithdrawalRecipientFactory("demo.obol.eth", ENS_REVERSE_REGISTRAR_GOERLI, address(this)); + + OptimisticWithdrawalRecipient owrETH = + owrFactory.createOWRecipient(ETH_ADDRESS, rewardAddress, rewardAddress, rewardAddress, ETH_STAKE); + + recipient.mint(address(this), 1, ObolErc1155Recipient.OWRInfo({owr: address(owrETH), rewardAddress: rewardAddress}), ObolErc1155Recipient.DepositInfo({pubkey: "0x", withdrawal_credentials: "0x", sig: "0x"})); + + address(recipient).safeTransferETH(1 ether); + assertEq(address(recipient).balance, 1 ether); + + recipient.setRewards(1, address(owrETH), 1 ether); + assertEq(recipient.rewards(address(owrETH), 1), 1 ether); + + recipient.safeTransferFrom(address(this), receiverAddress, 1, 1, "0x"); + assertEq(rewardAddress.balance, 1 ether); + } + + function testTransferWithoutRewards() public { + address rewardAddress = makeAddr("rewardAddress"); + address receiverAddress = address(new ObolErc1155ReceiverMock()); + + 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)) + ); + OptimisticWithdrawalRecipientFactory owrFactory = + new OptimisticWithdrawalRecipientFactory("demo.obol.eth", ENS_REVERSE_REGISTRAR_GOERLI, address(this)); + + OptimisticWithdrawalRecipient owrETH = + owrFactory.createOWRecipient(ETH_ADDRESS, rewardAddress, rewardAddress, rewardAddress, ETH_STAKE); + + recipient.mint(address(this), 1, ObolErc1155Recipient.OWRInfo({owr: address(owrETH), rewardAddress: rewardAddress}), ObolErc1155Recipient.DepositInfo({pubkey: "0x", withdrawal_credentials: "0x", sig: "0x"})); + + recipient.safeTransferFrom(address(this), receiverAddress, 1, 1, "0x"); + assertFalse(recipient.isOwnerOf(1)); + + vm.prank(receiverAddress); + assertTrue(recipient.isOwnerOf(1)); + } + + function testReceiveRewards() public { + address rewardAddress = makeAddr("rewardAddress"); + 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)) + ); + OptimisticWithdrawalRecipientFactory owrFactory = + new OptimisticWithdrawalRecipientFactory("demo.obol.eth", ENS_REVERSE_REGISTRAR_GOERLI, address(this)); + + OptimisticWithdrawalRecipient owrETH = + owrFactory.createOWRecipient(ETH_ADDRESS, rewardAddress, rewardAddress, address(recipient), ETH_STAKE); + + address(owrETH).safeTransferETH(1 ether); + + recipient.mint(address(this), 1, ObolErc1155Recipient.OWRInfo({owr: address(owrETH), rewardAddress: rewardAddress}), ObolErc1155Recipient.DepositInfo({pubkey: "0x", withdrawal_credentials: "0x", sig: "0x"})); + bool ownerOf1 = recipient.isOwnerOf(1); + assertEq(ownerOf1, true); + + uint256 registeredRewards = recipient.rewards(address(owrETH), 1); + assertEq(registeredRewards, 0); + + recipient.receiveRewards(address(owrETH)); + assertEq(address(owrETH).balance, 0 ether); + + registeredRewards = recipient.rewards(address(owrETH), 1); + assertEq(registeredRewards, 1 ether); + } +} diff --git a/src/test/owr/ObolErc1155RecipientMock.sol b/src/test/owr/ObolErc1155RecipientMock.sol index b020f32..e106bf5 100644 --- a/src/test/owr/ObolErc1155RecipientMock.sol +++ b/src/test/owr/ObolErc1155RecipientMock.sol @@ -1,7 +1,10 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.19; + import {ObolErc1155Recipient} from "src/owr/ObolErc1155Recipient.sol"; contract ObolErc1155RecipientMock is ObolErc1155Recipient { - constructor(string memory baseUri_, address _owner) ObolErc1155Recipient(baseUri_, _owner) { + constructor(string memory baseUri_, address _owner, address _depositContract) ObolErc1155Recipient(baseUri_, _owner, _depositContract) { } function setRewards(uint256 id, address owr, uint256 amount) external { From 2fe1c6b75b49116bfd25e56c93b74425d7393661 Mon Sep 17 00:00:00 2001 From: cosminobol Date: Fri, 10 May 2024 09:06:29 +0300 Subject: [PATCH 05/17] chore: erc1155Recipient v2 --- src/owr/ObolErc1155Recipient.sol | 197 ++++++++++++++-------- src/test/owr/ObolErc1155Recipient.t.sol | 82 ++++++--- src/test/owr/ObolErc1155RecipientMock.sol | 4 +- 3 files changed, 189 insertions(+), 94 deletions(-) diff --git a/src/owr/ObolErc1155Recipient.sol b/src/owr/ObolErc1155Recipient.sol index ebe9d8c..17f0643 100644 --- a/src/owr/ObolErc1155Recipient.sol +++ b/src/owr/ObolErc1155Recipient.sol @@ -15,9 +15,11 @@ contract ObolErc1155Recipient is ERC1155, Ownable { uint256 public lastId; IDepositContract public ethDepositContract; - struct OWRInfo { + struct TokenInfo { address owr; + uint256 activeSupply; address rewardAddress; + uint256 claimable; } struct DepositInfo { @@ -26,22 +28,24 @@ contract ObolErc1155Recipient is ERC1155, Ownable { bytes sig; } - mapping(uint256 => OWRInfo) public owrInfo; - mapping(address owr => uint256 id) public assignedToOWR; - mapping(address owr => mapping(uint256 id => uint256 claimable)) public rewards; + mapping(uint256 id => TokenInfo) public tokenInfo; + mapping(address owr => uint256) public owrTokens; + + mapping(uint256 id => uint256) public totalSupply; + uint256 public totalSupplyAll; string private _baseUri; address private constant ETH_TOKEN_ADDRESS = address(0x0); uint256 private constant ETH_DEPOSIT_AMOUNT = 32 ether; - error TokenNotTransferable(); - error TokenBatchMintLengthMismatch(); + error LengthMismatch(); error InvalidTokenAmount(); error InvalidOwner(); error InvalidOWR(); error NothingToClaim(); error ClaimFailed(); error InvalidDepositContract(); + error InvalidLastSupply(); event DepositContractUpdated(address oldAddy, address newAddy); @@ -65,17 +69,13 @@ contract ObolErc1155Recipient is ERC1155, Ownable { return string(abi.encodePacked(_baseUri, LibString.toString(id))); } - /// @dev Returns the total amount of tokens stored by the contract. - function totalSupply() public view virtual returns (uint256) { - return lastId - 1; - } - + /// @dev Returns true if `msg.sender` is the owner of the contract function isOwnerOf(uint256 id) public view returns (bool) { return balanceOf(msg.sender, id) > 0; } /// ----------------------------------------------------------------------- - /// functions - public & external + /// functions - onlyOwner /// ----------------------------------------------------------------------- /// @notice sets the ETH DepositContract /// @dev callable by the owner @@ -86,11 +86,14 @@ contract ObolErc1155Recipient is ERC1155, Ownable { ethDepositContract = IDepositContract(depositContract); } + /// ----------------------------------------------------------------------- + /// functions - public & external + /// ----------------------------------------------------------------------- /// @notice triggers `OWR.distributeFunds` /// @dev callable by the owner /// @param owr the OWR address - function receiveRewards(address owr) external onlyOwner { - uint256 _tokenId = assignedToOWR[owr]; + function receiveRewards(address owr) external { + uint256 _tokenId = owrTokens[owr]; // check if sender is owner of id if (!isOwnerOf(_tokenId)) revert InvalidOwner(); @@ -100,76 +103,144 @@ contract ObolErc1155Recipient is ERC1155, Ownable { IOptimisticWithdrawalRecipient(owr).distributeFunds(); uint256 balanceAfter = _getOWRTokenBalance(owr); - // update rewards[owr][id] += received; - rewards[owr][_tokenId] += (balanceAfter - balanceBefore); + tokenInfo[_tokenId].claimable += (balanceAfter - balanceBefore); } - /// @notice claims rewards to `OWRInfo.rewardAddress` + /// @notice claims rewards to `TokenInfo.rewardAddress` /// @dev callable by the owner /// @param id the ERC1155 id - /// @return claimed the amount of rewards sent to `OWRInfo.rewardAddress` + /// @return claimed the amount of rewards sent to `TokenInfo.rewardAddress` function claim(uint256 id) external returns (uint256 claimed) { - claimed = _claim(id, false); + claimed = _claim(id); } - /// @notice claims rewards to `OWRInfo.rewardAddress` from multiple token ids + /// @notice claims rewards to `TokenInfo.rewardAddress` from multiple token ids /// @dev callable by the owner /// @param ids the ERC1155 ids - /// @return claimed the amount of rewards sent to `OWRInfo.rewardAddress` per each id + /// @return claimed the amount of rewards sent to `TokenInfo.rewardAddress` per each id function batchClaim(uint256[] calldata ids) external returns (uint256[] memory claimed) { uint256 count = ids.length; for (uint256 i; i < count; i++) { - claimed[i] = _claim(ids[i], false); + claimed[i] = _claim(ids[i]); } } /// @notice mints a new token + /// @dev supply can be increased later with `mintSupply` /// @param to receiver address /// @param amount the amount for `lastId` + /// @param owr OptimisticWithdrawalRecipient address + /// @param rewardAddress rewards receiver address /// @return mintedId id of the minted NFT - function mint(address to, uint256 amount, OWRInfo calldata info, DepositInfo calldata depositInfo) + function mint(address to, uint256 amount, address owr, address rewardAddress) external payable onlyOwner returns (uint256 mintedId) - { + { // validation if (amount == 0) revert InvalidTokenAmount(); - uint256 totalETH = ETH_DEPOSIT_AMOUNT * amount; - if (msg.value != totalETH) revert InvalidTokenAmount(); - - // mint - _mint(to, lastId, amount, ""); - mintedId = _afterMint(info, depositInfo, totalETH); - } + // mint + mintedId = _assignInfoAndExtractId(owr, rewardAddress); + _mint(to, mintedId, amount, ""); + + // increase total supply + totalSupply[mintedId] += amount; + totalSupplyAll += amount; + } /// @notice mints a batch of tokens + /// @dev supply can be increased later with `mintSupply` /// @param to receiver address /// @param count batch length /// @param amounts the amounts for each id - /// @param infos info per each id + /// @param owrs OptimisticWithdrawalRecipient addresses + /// @param rewardAddresses rewards receiver addresses /// @return mintedIds id list of the minted NFTs - function mintBatch(address to, uint256 count, uint256[] calldata amounts, OWRInfo[] calldata infos, DepositInfo calldata depositInfo) - external - payable - onlyOwner - returns (uint256[] memory mintedIds) - { - if (count != amounts.length) revert TokenBatchMintLengthMismatch(); - uint256 totalETH; + function mintBatch( + address to, + uint256 count, + uint256[] calldata amounts, + address[] calldata owrs, + address[] calldata rewardAddresses + ) external payable onlyOwner returns (uint256[] memory mintedIds) { + // validate + if (count != amounts.length) revert LengthMismatch(); + if (count != owrs.length) revert LengthMismatch(); + if (count != rewardAddresses.length) revert LengthMismatch(); + + // mint up to `count` + mintedIds = new uint256[](count); for (uint256 i; i < count; i++) { - totalETH += (ETH_DEPOSIT_AMOUNT * amounts[i]); + if (amounts[i] == 0) revert InvalidTokenAmount(); + mintedIds[i] = _assignInfoAndExtractId(owrs[i], rewardAddresses[i]); + _mint(to, mintedIds[i], amounts[i], ""); + + // increase total supply + totalSupply[mintedIds[i]] += amounts[i]; + totalSupplyAll += amounts[i]; } - if (totalETH != msg.value) revert InvalidTokenAmount(); + } - mintedIds = new uint256[](count); - for (uint256 i; i < count; i++) { - if (amounts[i] == 0) revert InvalidTokenAmount(); - uint256 totalIndexETH = ETH_DEPOSIT_AMOUNT * amounts[i]; - _mint(to, lastId, amounts[i], ""); - mintedIds[i] = _afterMint(infos[i], depositInfo, totalIndexETH); + /// @notice deposits ETH to `DepositContract` and activates part of supply + /// @param id token id + /// @param count amount of supply to activate + /// @param depositInfo deposit data needed for `DepositContract` + function depositForToken(uint256 id, uint256 count, DepositInfo[] calldata depositInfo) external payable { + // vaidate + if (!isOwnerOf(id)) revert InvalidOwner(); + + if (depositInfo.length != count) revert LengthMismatch(); + + uint256 crtActiveSupply = tokenInfo[id].activeSupply; + if (crtActiveSupply + count >= totalSupply[id]) revert InvalidLastSupply(); + + if (msg.value < count * ETH_DEPOSIT_AMOUNT) revert InvalidTokenAmount(); + + // deposit to ETH `DepositContract` + for (uint i; i < count; i++) { + ethDepositContract.deposit{value: ETH_DEPOSIT_AMOUNT}( + depositInfo[i].pubkey, depositInfo[i].withdrawal_credentials, depositInfo[i].sig, ethDepositContract.get_deposit_root() + ); } + + // activate supply + tokenInfo[id].activeSupply += count; + } + + /// @notice increases totalSupply for token id + /// @param id token id + /// @param amount newly added supply + function mintSupply(uint256 id, uint256 amount) external { + // validate + if (!isOwnerOf(id)) revert InvalidOwner(); + if (lastId < id) revert InvalidLastSupply(); + + // mint for existing id + _mint(msg.sender, id, amount, ""); + + // increase total supply + totalSupply[id] += amount; + totalSupplyAll += amount; + } + + /// @notice decreases totalSupply for token id + /// @param id token id + /// @param amount newly removed supply + function burn(uint256 id, uint256 amount) external { + // vaidate + if (!isOwnerOf(id)) revert InvalidOwner(); + if (amount == 0) revert InvalidTokenAmount(); + + _burn(msg.sender, id, amount); + + totalSupply[id] -= amount; + totalSupplyAll -= amount; + + // burn should be initiated on activeSupply withdrawal, but + // check just in case + tokenInfo[id].activeSupply =(tokenInfo[id].activeSupply > amount ? tokenInfo[id].activeSupply - amount: 0); } /// @dev Hook that is called before any token transfer. @@ -184,7 +255,7 @@ contract ObolErc1155Recipient is ERC1155, Ownable { // claim before transfer uint256 length = ids.length; for (uint256 i; i < length; i++) { - _claim(ids[i], true); //allow transfer even if `claimed == 0` + _claim(ids[i]); //allow transfer even if `claimed == 0` } } @@ -200,22 +271,21 @@ contract ObolErc1155Recipient is ERC1155, Ownable { lastId++; } - function _claim(uint256 id, bool canSkipAmountCheck) private returns (uint256 claimed) { + function _claim(uint256 id) private returns (uint256 claimed) { if (!isOwnerOf(id)) revert InvalidOwner(); - address _owr = owrInfo[id].owr; + address _owr = tokenInfo[id].owr; if (_owr == address(0)) revert InvalidOWR(); - - claimed = rewards[_owr][id]; - if (claimed == 0 && !canSkipAmountCheck) revert NothingToClaim(); + if (tokenInfo[id].claimable == 0) return 0; address token = IOptimisticWithdrawalRecipient(_owr).token(); if (token == ETH_TOKEN_ADDRESS) { - (bool sent,) = owrInfo[id].rewardAddress.call{value: claimed}(""); + (bool sent,) = tokenInfo[id].rewardAddress.call{value: tokenInfo[id].claimable}(""); if (!sent) revert ClaimFailed(); } else { - token.safeTransfer(owrInfo[id].rewardAddress, claimed); + token.safeTransfer(tokenInfo[id].rewardAddress, tokenInfo[id].claimable); } + tokenInfo[id].claimable = 0; } function _getOWRTokenBalance(address owr) private view returns (uint256 balance) { @@ -224,18 +294,11 @@ contract ObolErc1155Recipient is ERC1155, Ownable { else balance = ERC20(token).balanceOf(address(this)); } - - function _afterMint(OWRInfo calldata info, DepositInfo calldata depositInfo, uint256 totalETH) private returns (uint256 mintedId) { + function _assignInfoAndExtractId(address owr, address rewardAddress) private returns (uint256 mintedId) { mintedId = _incrementId(); - owrInfo[mintedId] = info; - assignedToOWR[info.owr] = mintedId; - // deposit to ETH `DepositContract` - ethDepositContract.deposit{value: totalETH}( - depositInfo.pubkey, - depositInfo.withdrawal_credentials, - depositInfo.sig, - ethDepositContract.get_deposit_root() - ); + TokenInfo memory info = TokenInfo({activeSupply: 0, claimable: 0, owr: owr, rewardAddress: rewardAddress}); + tokenInfo[mintedId] = info; + owrTokens[info.owr] = mintedId; } } diff --git a/src/test/owr/ObolErc1155Recipient.t.sol b/src/test/owr/ObolErc1155Recipient.t.sol index e9ac3b9..3999b7f 100644 --- a/src/test/owr/ObolErc1155Recipient.t.sol +++ b/src/test/owr/ObolErc1155Recipient.t.sol @@ -49,13 +49,13 @@ contract ObolErc1155RecipientTest is Test, IERC1155Receiver { return true; } - function testInitialSupply() public { - assertEq(recipient.totalSupply(), 0); + function testInitialSupply_owrErc1155() public { + assertEq(recipient.totalSupplyAll(), 0); } - function testTransferFrom() public { - recipient.mint(address(this), 1, ObolErc1155Recipient.OWRInfo({owr: address(0), rewardAddress: address(0)}), ObolErc1155Recipient.DepositInfo({pubkey: "0x", withdrawal_credentials: "0x", sig: "0x"})); - recipient.mint(address(this), 1, ObolErc1155Recipient.OWRInfo({owr: address(0), rewardAddress: address(0)}), ObolErc1155Recipient.DepositInfo({pubkey: "0x", withdrawal_credentials: "0x", sig: "0x"})); + function testTransferFrom_owrErc1155() public { + recipient.mint(address(this), 1, address(0), address(0)); + recipient.mint(address(this), 1, address(0), address(0)); vm.expectRevert(); recipient.safeTransferFrom(address(this), address(this), 1, 0, ""); @@ -71,8 +71,8 @@ contract ObolErc1155RecipientTest is Test, IERC1155Receiver { recipient.safeBatchTransferFrom(address(this), address(this), batchTokens, batchAmounts, ""); } - function testMint() public { - recipient.mint(address(this), 1, ObolErc1155Recipient.OWRInfo({owr: address(0), rewardAddress: address(0)}), ObolErc1155Recipient.DepositInfo({pubkey: "0x", withdrawal_credentials: "0x", sig: "0x"})); + function testMint_owrErc1155() public { + recipient.mint(address(this), 1, address(0), address(0)); bool ownerOf1 = recipient.isOwnerOf(1); assertEq(ownerOf1, true); @@ -80,17 +80,43 @@ contract ObolErc1155RecipientTest is Test, IERC1155Receiver { amounts[0] = 1; amounts[1] = 1; - ObolErc1155Recipient.OWRInfo[] memory infos = new ObolErc1155Recipient.OWRInfo[](2); - infos[0] = ObolErc1155Recipient.OWRInfo({owr: address(0), rewardAddress: address(0)}); - infos[1] = ObolErc1155Recipient.OWRInfo({owr: address(0), rewardAddress: address(0)}); - recipient.mintBatch(address(this), 2, amounts, infos, ObolErc1155Recipient.DepositInfo({pubkey: "0x", withdrawal_credentials: "0x", sig: "0x"})); + address[] memory owrs = new address[](2); + owrs[0] = address(0); + owrs[1] = address(0); + address[] memory rewardAddresses = new address[](2); + rewardAddresses[0] = address(0); + rewardAddresses[1] = address(0); + + recipient.mintBatch(address(this), 2, amounts, owrs, rewardAddresses); bool ownerOf2 = recipient.isOwnerOf(2); bool ownerOf3 = recipient.isOwnerOf(3); assertEq(ownerOf2, true); assertEq(ownerOf3, true); } - function testClaim() public { + function testMintSupply_owrErc1155() public { + recipient.mint(address(this), 1, address(0), address(0)); + bool ownerOf1 = recipient.isOwnerOf(1); + assertEq(ownerOf1, true); + + uint256 totalSupplyBefore = recipient.totalSupply(1); + recipient.mintSupply(1, 100); + uint256 totalSupplyAfter = recipient.totalSupply(1); + assertGt(totalSupplyAfter, totalSupplyBefore); + } + + function testBurn_owrErc1155() public { + recipient.mint(address(this), 100, address(0), address(0)); + bool ownerOf1 = recipient.isOwnerOf(1); + assertEq(ownerOf1, true); + + uint256 totalSupplyBefore = recipient.totalSupply(1); + recipient.burn(1, 50); + uint256 totalSupplyAfter = recipient.totalSupply(1); + assertLt(totalSupplyAfter, totalSupplyBefore); + } + + function testClaim_owrErc1155() public { address rewardAddress = makeAddr("rewardAddress"); vm.mockCall( ENS_REVERSE_REGISTRAR_GOERLI, @@ -108,19 +134,19 @@ contract ObolErc1155RecipientTest is Test, IERC1155Receiver { OptimisticWithdrawalRecipient owrETH = owrFactory.createOWRecipient(ETH_ADDRESS, rewardAddress, rewardAddress, rewardAddress, ETH_STAKE); - recipient.mint(address(this), 1, ObolErc1155Recipient.OWRInfo({owr: address(owrETH), rewardAddress: rewardAddress}), ObolErc1155Recipient.DepositInfo({pubkey: "0x", withdrawal_credentials: "0x", sig: "0x"})); + recipient.mint(address(this), 1, address(owrETH), rewardAddress); address(recipient).safeTransferETH(1 ether); assertEq(address(recipient).balance, 1 ether); - recipient.setRewards(1, address(owrETH), 1 ether); - assertEq(recipient.rewards(address(owrETH), 1), 1 ether); + recipient.setRewards(1, 1 ether); + assertEq(_getRewards(1), 1 ether); recipient.claim(1); assertEq(rewardAddress.balance, 1 ether); } - function testTransferWithRewards() public { + function testTransferWithRewards_owrErc1155() public { address rewardAddress = makeAddr("rewardAddress"); address receiverAddress = address(new ObolErc1155ReceiverMock()); @@ -140,19 +166,19 @@ contract ObolErc1155RecipientTest is Test, IERC1155Receiver { OptimisticWithdrawalRecipient owrETH = owrFactory.createOWRecipient(ETH_ADDRESS, rewardAddress, rewardAddress, rewardAddress, ETH_STAKE); - recipient.mint(address(this), 1, ObolErc1155Recipient.OWRInfo({owr: address(owrETH), rewardAddress: rewardAddress}), ObolErc1155Recipient.DepositInfo({pubkey: "0x", withdrawal_credentials: "0x", sig: "0x"})); + recipient.mint(address(this), 1, address(owrETH), rewardAddress); address(recipient).safeTransferETH(1 ether); assertEq(address(recipient).balance, 1 ether); - recipient.setRewards(1, address(owrETH), 1 ether); - assertEq(recipient.rewards(address(owrETH), 1), 1 ether); + recipient.setRewards(1, 1 ether); + assertEq(_getRewards(1), 1 ether); recipient.safeTransferFrom(address(this), receiverAddress, 1, 1, "0x"); assertEq(rewardAddress.balance, 1 ether); } - function testTransferWithoutRewards() public { + function testTransferWithoutRewards_owrErc1155() public { address rewardAddress = makeAddr("rewardAddress"); address receiverAddress = address(new ObolErc1155ReceiverMock()); @@ -172,7 +198,7 @@ contract ObolErc1155RecipientTest is Test, IERC1155Receiver { OptimisticWithdrawalRecipient owrETH = owrFactory.createOWRecipient(ETH_ADDRESS, rewardAddress, rewardAddress, rewardAddress, ETH_STAKE); - recipient.mint(address(this), 1, ObolErc1155Recipient.OWRInfo({owr: address(owrETH), rewardAddress: rewardAddress}), ObolErc1155Recipient.DepositInfo({pubkey: "0x", withdrawal_credentials: "0x", sig: "0x"})); + recipient.mint(address(this), 1, address(owrETH), rewardAddress); recipient.safeTransferFrom(address(this), receiverAddress, 1, 1, "0x"); assertFalse(recipient.isOwnerOf(1)); @@ -181,7 +207,7 @@ contract ObolErc1155RecipientTest is Test, IERC1155Receiver { assertTrue(recipient.isOwnerOf(1)); } - function testReceiveRewards() public { + function testReceiveRewards_owrErc1155() public { address rewardAddress = makeAddr("rewardAddress"); vm.mockCall( ENS_REVERSE_REGISTRAR_GOERLI, @@ -201,17 +227,23 @@ contract ObolErc1155RecipientTest is Test, IERC1155Receiver { address(owrETH).safeTransferETH(1 ether); - recipient.mint(address(this), 1, ObolErc1155Recipient.OWRInfo({owr: address(owrETH), rewardAddress: rewardAddress}), ObolErc1155Recipient.DepositInfo({pubkey: "0x", withdrawal_credentials: "0x", sig: "0x"})); + recipient.mint(address(this), 1, address(owrETH), rewardAddress); bool ownerOf1 = recipient.isOwnerOf(1); assertEq(ownerOf1, true); - uint256 registeredRewards = recipient.rewards(address(owrETH), 1); + uint256 registeredRewards = _getRewards(1); assertEq(registeredRewards, 0); recipient.receiveRewards(address(owrETH)); assertEq(address(owrETH).balance, 0 ether); - registeredRewards = recipient.rewards(address(owrETH), 1); + registeredRewards = _getRewards(1); assertEq(registeredRewards, 1 ether); } + + + function _getRewards(uint256 id) private view returns (uint256) { + (, , , uint256 claimable) = recipient.tokenInfo(id); + return claimable; + } } diff --git a/src/test/owr/ObolErc1155RecipientMock.sol b/src/test/owr/ObolErc1155RecipientMock.sol index e106bf5..5a7bf66 100644 --- a/src/test/owr/ObolErc1155RecipientMock.sol +++ b/src/test/owr/ObolErc1155RecipientMock.sol @@ -7,7 +7,7 @@ contract ObolErc1155RecipientMock is ObolErc1155Recipient { constructor(string memory baseUri_, address _owner, address _depositContract) ObolErc1155Recipient(baseUri_, _owner, _depositContract) { } - function setRewards(uint256 id, address owr, uint256 amount) external { - rewards[owr][id] += amount; + function setRewards(uint256 id, uint256 amount) external { + tokenInfo[id].claimable += amount; } } \ No newline at end of file From 7d00489fe81d16d18d461600346b85c78eb20137 Mon Sep 17 00:00:00 2001 From: cosminobol Date: Wed, 15 May 2024 22:36:32 +0300 Subject: [PATCH 06/17] patch: updated obolErc1155Recipient supplies --- src/owr/ObolErc1155Recipient.sol | 124 ++++++++++++++-------- src/test/owr/ObolErc1155Recipient.t.sol | 37 ++++--- src/test/owr/ObolErc1155RecipientMock.sol | 5 + 3 files changed, 104 insertions(+), 62 deletions(-) diff --git a/src/owr/ObolErc1155Recipient.sol b/src/owr/ObolErc1155Recipient.sol index 17f0643..d884917 100644 --- a/src/owr/ObolErc1155Recipient.sol +++ b/src/owr/ObolErc1155Recipient.sol @@ -8,8 +8,9 @@ import {IDepositContract} from "../interfaces/IDepositContract.sol"; import {IOptimisticWithdrawalRecipient} from "../interfaces/IOptimisticWithdrawalRecipient.sol"; import {ERC20} from "solmate/tokens/ERC20.sol"; import {SafeTransferLib} from "solady/utils/SafeTransferLib.sol"; +import {IERC165, IERC1155Receiver} from "src/interfaces/IERC1155Receiver.sol"; -contract ObolErc1155Recipient is ERC1155, Ownable { +contract ObolErc1155Recipient is ERC1155, Ownable, IERC1155Receiver { using SafeTransferLib for address; uint256 public lastId; @@ -17,9 +18,11 @@ contract ObolErc1155Recipient is ERC1155, Ownable { struct TokenInfo { address owr; - uint256 activeSupply; address rewardAddress; uint256 claimable; + + uint256 maxSupply; + address receiver; } struct DepositInfo { @@ -46,6 +49,7 @@ contract ObolErc1155Recipient is ERC1155, Ownable { error ClaimFailed(); error InvalidDepositContract(); error InvalidLastSupply(); + error TransferFailed(); event DepositContractUpdated(address oldAddy, address newAddy); @@ -74,6 +78,16 @@ contract ObolErc1155Recipient is ERC1155, Ownable { return balanceOf(msg.sender, id) > 0; } + /// @dev Returns true if `msg.sender` is the receiver of id + function isReceiverOf(uint256 id) public view returns (bool) { + return tokenInfo[id].receiver == msg.sender; + } + + /// @dev Returns max supply for id + function getMaxSupply(uint256 id) public view returns (uint256) { + return tokenInfo[id].maxSupply; + } + /// ----------------------------------------------------------------------- /// functions - onlyOwner /// ----------------------------------------------------------------------- @@ -90,13 +104,13 @@ contract ObolErc1155Recipient is ERC1155, Ownable { /// functions - public & external /// ----------------------------------------------------------------------- /// @notice triggers `OWR.distributeFunds` - /// @dev callable by the owner + /// @dev callable by the receiver /// @param owr the OWR address function receiveRewards(address owr) external { uint256 _tokenId = owrTokens[owr]; - // check if sender is owner of id - if (!isOwnerOf(_tokenId)) revert InvalidOwner(); + // check if sender is the receiver of id + if (!isReceiverOf(_tokenId)) revert InvalidOwner(); // call .distribute() on OWR uint256 balanceBefore = _getOWRTokenBalance(owr); @@ -126,60 +140,52 @@ contract ObolErc1155Recipient is ERC1155, Ownable { } /// @notice mints a new token - /// @dev supply can be increased later with `mintSupply` - /// @param to receiver address - /// @param amount the amount for `lastId` + /// @dev tokens are minted to address(this) and transferred when ETH is deposited to the DepositContract + /// @param to address registered as the tokens receiver + /// @param maxSupply the max allowed amount for `lastId` /// @param owr OptimisticWithdrawalRecipient address /// @param rewardAddress rewards receiver address /// @return mintedId id of the minted NFT - function mint(address to, uint256 amount, address owr, address rewardAddress) + function mint(address to, uint256 maxSupply, address owr, address rewardAddress) external payable onlyOwner returns (uint256 mintedId) { // validation - if (amount == 0) revert InvalidTokenAmount(); + if (maxSupply == 0) revert InvalidTokenAmount(); // mint - mintedId = _assignInfoAndExtractId(owr, rewardAddress); - _mint(to, mintedId, amount, ""); - - // increase total supply - totalSupply[mintedId] += amount; - totalSupplyAll += amount; + mintedId = _assignInfoAndExtractId(owr, rewardAddress, to, maxSupply); + _mint(address(this), mintedId, maxSupply, ""); } /// @notice mints a batch of tokens - /// @dev supply can be increased later with `mintSupply` + /// @dev tokens are minted to address(this) and transferred when ETH is deposited to the DepositContract /// @param to receiver address /// @param count batch length - /// @param amounts the amounts for each id + /// @param maxSupply the max allowed amounts for each id /// @param owrs OptimisticWithdrawalRecipient addresses /// @param rewardAddresses rewards receiver addresses /// @return mintedIds id list of the minted NFTs function mintBatch( address to, uint256 count, - uint256[] calldata amounts, + uint256[] calldata maxSupply, address[] calldata owrs, address[] calldata rewardAddresses ) external payable onlyOwner returns (uint256[] memory mintedIds) { // validate - if (count != amounts.length) revert LengthMismatch(); + if (count != maxSupply.length) revert LengthMismatch(); if (count != owrs.length) revert LengthMismatch(); if (count != rewardAddresses.length) revert LengthMismatch(); // mint up to `count` mintedIds = new uint256[](count); for (uint256 i; i < count; i++) { - if (amounts[i] == 0) revert InvalidTokenAmount(); - mintedIds[i] = _assignInfoAndExtractId(owrs[i], rewardAddresses[i]); - _mint(to, mintedIds[i], amounts[i], ""); - - // increase total supply - totalSupply[mintedIds[i]] += amounts[i]; - totalSupplyAll += amounts[i]; + if (maxSupply[i] == 0) revert InvalidTokenAmount(); + mintedIds[i] = _assignInfoAndExtractId(owrs[i], rewardAddresses[i], to, maxSupply[i]); + _mint(address(this), mintedIds[i], maxSupply[i], ""); } } @@ -189,12 +195,12 @@ contract ObolErc1155Recipient is ERC1155, Ownable { /// @param depositInfo deposit data needed for `DepositContract` function depositForToken(uint256 id, uint256 count, DepositInfo[] calldata depositInfo) external payable { // vaidate - if (!isOwnerOf(id)) revert InvalidOwner(); + if (!isReceiverOf(id)) revert InvalidOwner(); if (depositInfo.length != count) revert LengthMismatch(); - uint256 crtActiveSupply = tokenInfo[id].activeSupply; - if (crtActiveSupply + count >= totalSupply[id]) revert InvalidLastSupply(); + uint256 crtActiveSupply =totalSupply[id]; + if (crtActiveSupply + count >= getMaxSupply(id)) revert InvalidLastSupply(); if (msg.value < count * ETH_DEPOSIT_AMOUNT) revert InvalidTokenAmount(); @@ -204,33 +210,37 @@ contract ObolErc1155Recipient is ERC1155, Ownable { depositInfo[i].pubkey, depositInfo[i].withdrawal_credentials, depositInfo[i].sig, ethDepositContract.get_deposit_root() ); } - - // activate supply - tokenInfo[id].activeSupply += count; + + (bool success,) = address(this).call(abi.encodeWithSelector(this.safeTransferFrom.selector, tokenInfo[id].receiver, id, count, "0x")); + if (!success) revert TransferFailed(); + + // increase total supply + totalSupply[id] += count; + totalSupplyAll += count; } - /// @notice increases totalSupply for token id + /// @notice increases maxSupply for token id /// @param id token id /// @param amount newly added supply function mintSupply(uint256 id, uint256 amount) external { // validate - if (!isOwnerOf(id)) revert InvalidOwner(); + if (!isReceiverOf(id)) revert InvalidOwner(); if (lastId < id) revert InvalidLastSupply(); // mint for existing id - _mint(msg.sender, id, amount, ""); + _mint(address(this), id, amount, ""); - // increase total supply - totalSupply[id] += amount; - totalSupplyAll += amount; + // increase supply + tokenInfo[id].maxSupply += amount; } /// @notice decreases totalSupply for token id /// @param id token id /// @param amount newly removed supply + // TODO: refactor function burn(uint256 id, uint256 amount) external { // vaidate - if (!isOwnerOf(id)) revert InvalidOwner(); + if (!isReceiverOf(id)) revert InvalidOwner(); if (amount == 0) revert InvalidTokenAmount(); _burn(msg.sender, id, amount); @@ -240,7 +250,7 @@ contract ObolErc1155Recipient is ERC1155, Ownable { // burn should be initiated on activeSupply withdrawal, but // check just in case - tokenInfo[id].activeSupply =(tokenInfo[id].activeSupply > amount ? tokenInfo[id].activeSupply - amount: 0); + // tokenInfo[id].activeSupply =(tokenInfo[id].activeSupply > amount ? tokenInfo[id].activeSupply - amount: 0); } /// @dev Hook that is called before any token transfer. @@ -272,8 +282,6 @@ contract ObolErc1155Recipient is ERC1155, Ownable { } function _claim(uint256 id) private returns (uint256 claimed) { - if (!isOwnerOf(id)) revert InvalidOwner(); - address _owr = tokenInfo[id].owr; if (_owr == address(0)) revert InvalidOWR(); if (tokenInfo[id].claimable == 0) return 0; @@ -294,11 +302,37 @@ contract ObolErc1155Recipient is ERC1155, Ownable { else balance = ERC20(token).balanceOf(address(this)); } - function _assignInfoAndExtractId(address owr, address rewardAddress) private returns (uint256 mintedId) { + function _assignInfoAndExtractId(address owr, address rewardAddress, address receiver, uint256 maxSupply) private returns (uint256 mintedId) { mintedId = _incrementId(); - TokenInfo memory info = TokenInfo({activeSupply: 0, claimable: 0, owr: owr, rewardAddress: rewardAddress}); + TokenInfo memory info = TokenInfo({maxSupply: maxSupply, receiver: receiver, claimable: 0, owr: owr, rewardAddress: rewardAddress}); tokenInfo[mintedId] = info; owrTokens[info.owr] = mintedId; } + + /// ----------------------------------------------------------------------- + /// IERC1155Receiver + /// ----------------------------------------------------------------------- + function supportsInterface(bytes4 interfaceId) public view virtual override(ERC1155, IERC165) returns (bool) { + return interfaceId == type(IERC1155Receiver).interfaceId || super.supportsInterface(interfaceId); + } + function onERC1155Received( + address, + address, + uint256, + uint256, + bytes memory + ) public virtual override returns (bytes4) { + return this.onERC1155Received.selector; + } + + function onERC1155BatchReceived( + address, + address, + uint256[] memory, + uint256[] memory, + bytes memory + ) public virtual override returns (bytes4) { + return this.onERC1155BatchReceived.selector; + } } diff --git a/src/test/owr/ObolErc1155Recipient.t.sol b/src/test/owr/ObolErc1155Recipient.t.sol index 3999b7f..464e7f7 100644 --- a/src/test/owr/ObolErc1155Recipient.t.sol +++ b/src/test/owr/ObolErc1155Recipient.t.sol @@ -73,7 +73,7 @@ contract ObolErc1155RecipientTest is Test, IERC1155Receiver { function testMint_owrErc1155() public { recipient.mint(address(this), 1, address(0), address(0)); - bool ownerOf1 = recipient.isOwnerOf(1); + bool ownerOf1 = recipient.isReceiverOf(1); assertEq(ownerOf1, true); uint256[] memory amounts = new uint256[](2); @@ -88,33 +88,33 @@ contract ObolErc1155RecipientTest is Test, IERC1155Receiver { rewardAddresses[1] = address(0); recipient.mintBatch(address(this), 2, amounts, owrs, rewardAddresses); - bool ownerOf2 = recipient.isOwnerOf(2); - bool ownerOf3 = recipient.isOwnerOf(3); + bool ownerOf2 = recipient.isReceiverOf(2); + bool ownerOf3 = recipient.isReceiverOf(3); assertEq(ownerOf2, true); assertEq(ownerOf3, true); } function testMintSupply_owrErc1155() public { recipient.mint(address(this), 1, address(0), address(0)); - bool ownerOf1 = recipient.isOwnerOf(1); + bool ownerOf1 = recipient.isReceiverOf(1); assertEq(ownerOf1, true); - uint256 totalSupplyBefore = recipient.totalSupply(1); + uint256 maxSupplyBefore = recipient.getMaxSupply(1); recipient.mintSupply(1, 100); - uint256 totalSupplyAfter = recipient.totalSupply(1); - assertGt(totalSupplyAfter, totalSupplyBefore); + uint256 maxSupplyAfter = recipient.getMaxSupply(1); + assertGt(maxSupplyAfter, maxSupplyBefore); } - function testBurn_owrErc1155() public { - recipient.mint(address(this), 100, address(0), address(0)); - bool ownerOf1 = recipient.isOwnerOf(1); - assertEq(ownerOf1, true); + // function testBurn_owrErc1155() public { + // recipient.mint(address(this), 100, address(0), address(0)); + // bool ownerOf1 = recipient.isOwnerOf(1); + // assertEq(ownerOf1, true); - uint256 totalSupplyBefore = recipient.totalSupply(1); - recipient.burn(1, 50); - uint256 totalSupplyAfter = recipient.totalSupply(1); - assertLt(totalSupplyAfter, totalSupplyBefore); - } + // uint256 totalSupplyBefore = recipient.totalSupply(1); + // recipient.burn(1, 50); + // uint256 totalSupplyAfter = recipient.totalSupply(1); + // assertLt(totalSupplyAfter, totalSupplyBefore); + // } function testClaim_owrErc1155() public { address rewardAddress = makeAddr("rewardAddress"); @@ -167,6 +167,7 @@ contract ObolErc1155RecipientTest is Test, IERC1155Receiver { owrFactory.createOWRecipient(ETH_ADDRESS, rewardAddress, rewardAddress, rewardAddress, ETH_STAKE); recipient.mint(address(this), 1, address(owrETH), rewardAddress); + recipient.simulateReceiverMint(1, 1); address(recipient).safeTransferETH(1 ether); assertEq(address(recipient).balance, 1 ether); @@ -199,6 +200,7 @@ contract ObolErc1155RecipientTest is Test, IERC1155Receiver { owrFactory.createOWRecipient(ETH_ADDRESS, rewardAddress, rewardAddress, rewardAddress, ETH_STAKE); recipient.mint(address(this), 1, address(owrETH), rewardAddress); + recipient.simulateReceiverMint(1, 1); recipient.safeTransferFrom(address(this), receiverAddress, 1, 1, "0x"); assertFalse(recipient.isOwnerOf(1)); @@ -228,6 +230,7 @@ contract ObolErc1155RecipientTest is Test, IERC1155Receiver { address(owrETH).safeTransferETH(1 ether); recipient.mint(address(this), 1, address(owrETH), rewardAddress); + recipient.simulateReceiverMint(1, 1); bool ownerOf1 = recipient.isOwnerOf(1); assertEq(ownerOf1, true); @@ -243,7 +246,7 @@ contract ObolErc1155RecipientTest is Test, IERC1155Receiver { function _getRewards(uint256 id) private view returns (uint256) { - (, , , uint256 claimable) = recipient.tokenInfo(id); + (, , uint256 claimable , ,) = recipient.tokenInfo(id); return claimable; } } diff --git a/src/test/owr/ObolErc1155RecipientMock.sol b/src/test/owr/ObolErc1155RecipientMock.sol index 5a7bf66..b2701bc 100644 --- a/src/test/owr/ObolErc1155RecipientMock.sol +++ b/src/test/owr/ObolErc1155RecipientMock.sol @@ -10,4 +10,9 @@ contract ObolErc1155RecipientMock is ObolErc1155Recipient { function setRewards(uint256 id, uint256 amount) external { tokenInfo[id].claimable += amount; } + + function simulateReceiverMint(uint256 id, uint256 amount) external { + (bool success,) = address(this).call(abi.encodeWithSelector(this.safeTransferFrom.selector, address(this), tokenInfo[id].receiver, id, amount, "0x")); + if (!success) revert TransferFailed(); + } } \ No newline at end of file From 39e97531c62864d6c348d1393a7ca59db2bfc4ec Mon Sep 17 00:00:00 2001 From: cosminobol Date: Tue, 21 May 2024 17:52:43 +0300 Subject: [PATCH 07/17] latest erc1155 updates --- .../IOptimisticWithdrawalRecipient.sol | 1 + src/interfaces/ISplitWallet.sol | 6 + src/owr/ObolErc1155Recipient.sol | 300 ++++++++++++------ src/test/owr/ObolErc1155Recipient.t.sol | 149 +++++---- src/test/owr/ObolErc1155RecipientMock.sol | 9 +- 5 files changed, 314 insertions(+), 151 deletions(-) create mode 100644 src/interfaces/ISplitWallet.sol diff --git a/src/interfaces/IOptimisticWithdrawalRecipient.sol b/src/interfaces/IOptimisticWithdrawalRecipient.sol index c77cfe5..1485665 100644 --- a/src/interfaces/IOptimisticWithdrawalRecipient.sol +++ b/src/interfaces/IOptimisticWithdrawalRecipient.sol @@ -5,4 +5,5 @@ interface IOptimisticWithdrawalRecipient { function token() external view returns (address); function distributeFunds() external payable; function distributeFundsPull() external payable; + function getTranches() external view returns (address principalRecipient, address rewardRecipient, uint256 amountOfPrincipalStake); } \ No newline at end of file diff --git a/src/interfaces/ISplitWallet.sol b/src/interfaces/ISplitWallet.sol new file mode 100644 index 0000000..91981a2 --- /dev/null +++ b/src/interfaces/ISplitWallet.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +interface ISplitWallet { + function splitMain() external view returns (address); +} diff --git a/src/owr/ObolErc1155Recipient.sol b/src/owr/ObolErc1155Recipient.sol index ebe9d8c..4a0ef37 100644 --- a/src/owr/ObolErc1155Recipient.sol +++ b/src/owr/ObolErc1155Recipient.sol @@ -8,49 +8,60 @@ import {IDepositContract} from "../interfaces/IDepositContract.sol"; import {IOptimisticWithdrawalRecipient} from "../interfaces/IOptimisticWithdrawalRecipient.sol"; import {ERC20} from "solmate/tokens/ERC20.sol"; import {SafeTransferLib} from "solady/utils/SafeTransferLib.sol"; +import {IERC165, IERC1155Receiver} from "src/interfaces/IERC1155Receiver.sol"; +import {ISplitMain, SplitConfiguration} from "src/interfaces/ISplitMain.sol"; +import {ISplitWallet} from "src/interfaces/ISplitWallet.sol"; -contract ObolErc1155Recipient is ERC1155, Ownable { +/// @notice OWR principal recipient +/// @dev handles rewards and principal of OWR +contract ObolErc1155Recipient is ERC1155, Ownable, IERC1155Receiver { using SafeTransferLib for address; uint256 public lastId; - IDepositContract public ethDepositContract; - struct OWRInfo { + // BeaconChain deposit contract + IDepositContract public immutable depositContract; + + struct TokenInfo { address owr; address rewardAddress; + uint256 claimable; + + uint256 maxSupply; + address receiver; } struct DepositInfo { bytes pubkey; bytes withdrawal_credentials; bytes sig; + bytes32 root; } - mapping(uint256 => OWRInfo) public owrInfo; - mapping(address owr => uint256 id) public assignedToOWR; - mapping(address owr => mapping(uint256 id => uint256 claimable)) public rewards; + mapping(uint256 id => TokenInfo) public tokenInfo; + mapping(address owr => uint256) public owrTokens; + + mapping(uint256 id => uint256) public totalSupply; + uint256 public totalSupplyAll; string private _baseUri; address private constant ETH_TOKEN_ADDRESS = address(0x0); uint256 private constant ETH_DEPOSIT_AMOUNT = 32 ether; + uint256 private constant MIN_ETH_EXIT_AMOUNT = 16 ether; - error TokenNotTransferable(); - error TokenBatchMintLengthMismatch(); + error LengthMismatch(); error InvalidTokenAmount(); error InvalidOwner(); error InvalidOWR(); - error NothingToClaim(); error ClaimFailed(); - error InvalidDepositContract(); - - event DepositContractUpdated(address oldAddy, address newAddy); + error InvalidLastSupply(); + error TransferFailed(); + error InvalidBurnAmount(uint256 necessary, uint received); constructor(string memory baseUri_, address _owner, address _depositContract) { - if (_depositContract == address(0)) revert InvalidDepositContract(); - lastId = 1; _baseUri = baseUri_; - ethDepositContract = IDepositContract(_depositContract); + depositContract = IDepositContract(_depositContract); _initializeOwner(_owner); } @@ -65,111 +76,198 @@ contract ObolErc1155Recipient is ERC1155, Ownable { return string(abi.encodePacked(_baseUri, LibString.toString(id))); } - /// @dev Returns the total amount of tokens stored by the contract. - function totalSupply() public view virtual returns (uint256) { - return lastId - 1; - } - + /// @dev Returns true if `msg.sender` is the owner of the contract function isOwnerOf(uint256 id) public view returns (bool) { return balanceOf(msg.sender, id) > 0; } + /// @dev Returns true if `msg.sender` is the receiver of id + function isReceiverOf(uint256 id) public view returns (bool) { + return tokenInfo[id].receiver == msg.sender; + } + + /// @dev Returns max supply for id + function getMaxSupply(uint256 id) public view returns (uint256) { + return tokenInfo[id].maxSupply; + } + /// ----------------------------------------------------------------------- /// functions - public & external /// ----------------------------------------------------------------------- - /// @notice sets the ETH DepositContract - /// @dev callable by the owner - /// @param depositContract the `DepositContract` address - function setDepositContract(address depositContract) external onlyOwner { - if (depositContract == address(0)) revert InvalidDepositContract(); - emit DepositContractUpdated(address(ethDepositContract), depositContract); - ethDepositContract = IDepositContract(depositContract); - } - /// @notice triggers `OWR.distributeFunds` - /// @dev callable by the owner + /// @dev callable by the receiver /// @param owr the OWR address - function receiveRewards(address owr) external onlyOwner { - uint256 _tokenId = assignedToOWR[owr]; + function receiveRewards(address owr, SplitConfiguration calldata _splitConfig) external { + uint256 _tokenId = owrTokens[owr]; - // check if sender is owner of id - if (!isOwnerOf(_tokenId)) revert InvalidOwner(); + // check if sender is the receiver of id + if (!isReceiverOf(_tokenId)) revert InvalidOwner(); // call .distribute() on OWR uint256 balanceBefore = _getOWRTokenBalance(owr); IOptimisticWithdrawalRecipient(owr).distributeFunds(); + _distributeSplitsRewards(owr, _splitConfig); uint256 balanceAfter = _getOWRTokenBalance(owr); - // update rewards[owr][id] += received; - rewards[owr][_tokenId] += (balanceAfter - balanceBefore); + tokenInfo[_tokenId].claimable += (balanceAfter - balanceBefore); } - /// @notice claims rewards to `OWRInfo.rewardAddress` + function _distributeSplitsRewards(address owr, SplitConfiguration calldata _splitConfig) private { + (,address _split,) = IOptimisticWithdrawalRecipient(owr).getTranches(); + address _splitMain = ISplitWallet(_split).splitMain(); + address _token = IOptimisticWithdrawalRecipient(owr).token(); + + if (_token == address(0)) { + ISplitMain(_splitMain).distributeETH(_split, _splitConfig.accounts, _splitConfig.percentAllocations, _splitConfig.distributorFee, _splitConfig.controller); + ISplitMain(_splitMain).withdraw(address(this), 1, new ERC20[](0)); + } else { + ISplitMain(_splitMain).distributeERC20(_split, ERC20(_token), _splitConfig.accounts, _splitConfig.percentAllocations, _splitConfig.distributorFee, _splitConfig.controller); + + ERC20[] memory tokens = new ERC20[](1); + tokens[0] = ERC20(_token); + ISplitMain(_splitMain).withdraw(address(this), 0, tokens); + } + + } + + /// @notice claims rewards to `TokenInfo.rewardAddress` /// @dev callable by the owner /// @param id the ERC1155 id - /// @return claimed the amount of rewards sent to `OWRInfo.rewardAddress` + /// @return claimed the amount of rewards sent to `TokenInfo.rewardAddress` function claim(uint256 id) external returns (uint256 claimed) { - claimed = _claim(id, false); + claimed = _claim(id); } - /// @notice claims rewards to `OWRInfo.rewardAddress` from multiple token ids + /// @notice claims rewards to `TokenInfo.rewardAddress` from multiple token ids /// @dev callable by the owner /// @param ids the ERC1155 ids - /// @return claimed the amount of rewards sent to `OWRInfo.rewardAddress` per each id + /// @return claimed the amount of rewards sent to `TokenInfo.rewardAddress` per each id function batchClaim(uint256[] calldata ids) external returns (uint256[] memory claimed) { uint256 count = ids.length; for (uint256 i; i < count; i++) { - claimed[i] = _claim(ids[i], false); + claimed[i] = _claim(ids[i]); } } /// @notice mints a new token - /// @param to receiver address - /// @param amount the amount for `lastId` + /// @dev tokens are minted to address(this) and transferred when ETH is deposited to the DepositContract + /// @param to address registered as the tokens receiver + /// @param maxSupply the max allowed amount for `lastId` + /// @param owr OptimisticWithdrawalRecipient address + /// @param rewardAddress rewards receiver address /// @return mintedId id of the minted NFT - function mint(address to, uint256 amount, OWRInfo calldata info, DepositInfo calldata depositInfo) + function mint(address to, uint256 maxSupply, address owr, address rewardAddress) external payable onlyOwner returns (uint256 mintedId) - { + { // validation - if (amount == 0) revert InvalidTokenAmount(); - uint256 totalETH = ETH_DEPOSIT_AMOUNT * amount; - if (msg.value != totalETH) revert InvalidTokenAmount(); - - // mint - _mint(to, lastId, amount, ""); - mintedId = _afterMint(info, depositInfo, totalETH); - } + if (maxSupply == 0) revert InvalidTokenAmount(); + // mint + mintedId = _assignInfoAndExtractId(owr, rewardAddress, to, maxSupply); + _mint(address(this), mintedId, maxSupply, ""); + } /// @notice mints a batch of tokens + /// @dev tokens are minted to address(this) and transferred when ETH is deposited to the DepositContract /// @param to receiver address /// @param count batch length - /// @param amounts the amounts for each id - /// @param infos info per each id + /// @param maxSupply the max allowed amounts for each id + /// @param owrs OptimisticWithdrawalRecipient addresses + /// @param rewardAddresses rewards receiver addresses /// @return mintedIds id list of the minted NFTs - function mintBatch(address to, uint256 count, uint256[] calldata amounts, OWRInfo[] calldata infos, DepositInfo calldata depositInfo) - external - payable - onlyOwner - returns (uint256[] memory mintedIds) - { - if (count != amounts.length) revert TokenBatchMintLengthMismatch(); - uint256 totalETH; + function mintBatch( + address to, + uint256 count, + uint256[] calldata maxSupply, + address[] calldata owrs, + address[] calldata rewardAddresses + ) external payable onlyOwner returns (uint256[] memory mintedIds) { + // validate + if (count != maxSupply.length) revert LengthMismatch(); + if (count != owrs.length) revert LengthMismatch(); + if (count != rewardAddresses.length) revert LengthMismatch(); + + // mint up to `count` + mintedIds = new uint256[](count); for (uint256 i; i < count; i++) { - totalETH += (ETH_DEPOSIT_AMOUNT * amounts[i]); + if (maxSupply[i] == 0) revert InvalidTokenAmount(); + mintedIds[i] = _assignInfoAndExtractId(owrs[i], rewardAddresses[i], to, maxSupply[i]); + _mint(address(this), mintedIds[i], maxSupply[i], ""); } - if (totalETH != msg.value) revert InvalidTokenAmount(); + } - mintedIds = new uint256[](count); - for (uint256 i; i < count; i++) { - if (amounts[i] == 0) revert InvalidTokenAmount(); - uint256 totalIndexETH = ETH_DEPOSIT_AMOUNT * amounts[i]; - _mint(to, lastId, amounts[i], ""); - mintedIds[i] = _afterMint(infos[i], depositInfo, totalIndexETH); + /// @notice deposits ETH to `DepositContract` and activates part of supply + /// @param id token id + /// @param count amount of supply to activate + /// @param depositInfo deposit data needed for `DepositContract` + function depositForToken(uint256 id, uint256 count, DepositInfo[] calldata depositInfo) external payable { + // vaidate + if (!isReceiverOf(id)) revert InvalidOwner(); + + if (depositInfo.length != count) revert LengthMismatch(); + + uint256 crtActiveSupply =totalSupply[id]; + + if (crtActiveSupply + count > getMaxSupply(id)) revert InvalidLastSupply(); + + if (msg.value < count * ETH_DEPOSIT_AMOUNT) revert InvalidTokenAmount(); + + // deposit to ETH `DepositContract` + for (uint i; i < count; i++) { + depositContract.deposit{value: ETH_DEPOSIT_AMOUNT}( + depositInfo[i].pubkey, depositInfo[i].withdrawal_credentials, depositInfo[i].sig, depositInfo[i].root + ); } + + (bool success,) = address(this).call(abi.encodeWithSelector(this.safeTransferFrom.selector, tokenInfo[id].receiver, id, count, "0x")); + if (!success) revert TransferFailed(); + + // increase total supply + totalSupply[id] += count; + totalSupplyAll += count; + } + + /// @notice increases maxSupply for token id + /// @param id token id + /// @param amount newly added supply + function mintSupply(uint256 id, uint256 amount) external { + // validate + if (!isReceiverOf(id)) revert InvalidOwner(); + if (lastId < id) revert InvalidLastSupply(); + + // mint for existing id + _mint(address(this), id, amount, ""); + + // increase supply + tokenInfo[id].maxSupply += amount; + } + + /// @notice decreases totalSupply for token id + /// @param id token id + /// @param amount newly removed supply + function burn(uint256 id, uint256 amount) external { + // validate + if (!isReceiverOf(id)) revert InvalidOwner(); + if (amount == 0) revert InvalidTokenAmount(); + + uint256 minEthAmount = MIN_ETH_EXIT_AMOUNT * amount; + uint256 ethBalanceBefore = address(this).balance; + IOptimisticWithdrawalRecipient(tokenInfo[id].owr).distributeFunds(); + uint256 ethBalanceAfter = address(this).balance; + uint256 ethReceived = ethBalanceAfter - ethBalanceBefore; + + if(ethReceived < minEthAmount) revert InvalidBurnAmount(minEthAmount, ethReceived); + + _burn(msg.sender, id, amount); + + totalSupply[id] -= amount; + totalSupplyAll -= amount; + + (bool sent,) = tokenInfo[id].receiver.call{value: ethReceived}(""); + if (!sent) revert TransferFailed(); } /// @dev Hook that is called before any token transfer. @@ -184,7 +282,7 @@ contract ObolErc1155Recipient is ERC1155, Ownable { // claim before transfer uint256 length = ids.length; for (uint256 i; i < length; i++) { - _claim(ids[i], true); //allow transfer even if `claimed == 0` + _claim(ids[i]); //allow transfer even if `claimed == 0` } } @@ -200,22 +298,19 @@ contract ObolErc1155Recipient is ERC1155, Ownable { lastId++; } - function _claim(uint256 id, bool canSkipAmountCheck) private returns (uint256 claimed) { - if (!isOwnerOf(id)) revert InvalidOwner(); - - address _owr = owrInfo[id].owr; + function _claim(uint256 id) private returns (uint256 claimed) { + address _owr = tokenInfo[id].owr; if (_owr == address(0)) revert InvalidOWR(); - - claimed = rewards[_owr][id]; - if (claimed == 0 && !canSkipAmountCheck) revert NothingToClaim(); + if (tokenInfo[id].claimable == 0) return 0; address token = IOptimisticWithdrawalRecipient(_owr).token(); if (token == ETH_TOKEN_ADDRESS) { - (bool sent,) = owrInfo[id].rewardAddress.call{value: claimed}(""); + (bool sent,) = tokenInfo[id].rewardAddress.call{value: tokenInfo[id].claimable}(""); if (!sent) revert ClaimFailed(); } else { - token.safeTransfer(owrInfo[id].rewardAddress, claimed); + token.safeTransfer(tokenInfo[id].rewardAddress, tokenInfo[id].claimable); } + tokenInfo[id].claimable = 0; } function _getOWRTokenBalance(address owr) private view returns (uint256 balance) { @@ -224,18 +319,37 @@ contract ObolErc1155Recipient is ERC1155, Ownable { else balance = ERC20(token).balanceOf(address(this)); } - - function _afterMint(OWRInfo calldata info, DepositInfo calldata depositInfo, uint256 totalETH) private returns (uint256 mintedId) { + function _assignInfoAndExtractId(address owr, address rewardAddress, address receiver, uint256 maxSupply) private returns (uint256 mintedId) { mintedId = _incrementId(); - owrInfo[mintedId] = info; - assignedToOWR[info.owr] = mintedId; - // deposit to ETH `DepositContract` - ethDepositContract.deposit{value: totalETH}( - depositInfo.pubkey, - depositInfo.withdrawal_credentials, - depositInfo.sig, - ethDepositContract.get_deposit_root() - ); + TokenInfo memory info = TokenInfo({maxSupply: maxSupply, receiver: receiver, claimable: 0, owr: owr, rewardAddress: rewardAddress}); + tokenInfo[mintedId] = info; + owrTokens[info.owr] = mintedId; + } + + /// ----------------------------------------------------------------------- + /// IERC1155Receiver + /// ----------------------------------------------------------------------- + function supportsInterface(bytes4 interfaceId) public view virtual override(ERC1155, IERC165) returns (bool) { + return interfaceId == type(IERC1155Receiver).interfaceId || super.supportsInterface(interfaceId); + } + function onERC1155Received( + address, + address, + uint256, + uint256, + bytes memory + ) public virtual override returns (bytes4) { + return this.onERC1155Received.selector; + } + + function onERC1155BatchReceived( + address, + address, + uint256[] memory, + uint256[] memory, + bytes memory + ) public virtual override returns (bytes4) { + return this.onERC1155BatchReceived.selector; } } diff --git a/src/test/owr/ObolErc1155Recipient.t.sol b/src/test/owr/ObolErc1155Recipient.t.sol index e9ac3b9..7ffa9f9 100644 --- a/src/test/owr/ObolErc1155Recipient.t.sol +++ b/src/test/owr/ObolErc1155Recipient.t.sol @@ -11,6 +11,8 @@ import {IENSReverseRegistrar} from "../../interfaces/IENSReverseRegistrar.sol"; import {SafeTransferLib} from "solady/utils/SafeTransferLib.sol"; import {ObolErc1155ReceiverMock} from "./ObolErc1155ReceiverMock.sol"; import {DepositContractMock} from "./DepositContractMock.sol"; +import {ISplitMain} from "src/interfaces/ISplitMain.sol"; + contract ObolErc1155RecipientTest is Test, IERC1155Receiver { using SafeTransferLib for address; @@ -49,13 +51,13 @@ contract ObolErc1155RecipientTest is Test, IERC1155Receiver { return true; } - function testInitialSupply() public { - assertEq(recipient.totalSupply(), 0); + function testInitialSupply_owrErc1155() public { + assertEq(recipient.totalSupplyAll(), 0); } - function testTransferFrom() public { - recipient.mint(address(this), 1, ObolErc1155Recipient.OWRInfo({owr: address(0), rewardAddress: address(0)}), ObolErc1155Recipient.DepositInfo({pubkey: "0x", withdrawal_credentials: "0x", sig: "0x"})); - recipient.mint(address(this), 1, ObolErc1155Recipient.OWRInfo({owr: address(0), rewardAddress: address(0)}), ObolErc1155Recipient.DepositInfo({pubkey: "0x", withdrawal_credentials: "0x", sig: "0x"})); + function testTransferFrom_owrErc1155() public { + recipient.mint(address(this), 1, address(0), address(0)); + recipient.mint(address(this), 1, address(0), address(0)); vm.expectRevert(); recipient.safeTransferFrom(address(this), address(this), 1, 0, ""); @@ -71,26 +73,52 @@ contract ObolErc1155RecipientTest is Test, IERC1155Receiver { recipient.safeBatchTransferFrom(address(this), address(this), batchTokens, batchAmounts, ""); } - function testMint() public { - recipient.mint(address(this), 1, ObolErc1155Recipient.OWRInfo({owr: address(0), rewardAddress: address(0)}), ObolErc1155Recipient.DepositInfo({pubkey: "0x", withdrawal_credentials: "0x", sig: "0x"})); - bool ownerOf1 = recipient.isOwnerOf(1); + function testMint_owrErc1155() public { + recipient.mint(address(this), 1, address(0), address(0)); + bool ownerOf1 = recipient.isReceiverOf(1); assertEq(ownerOf1, true); uint256[] memory amounts = new uint256[](2); amounts[0] = 1; amounts[1] = 1; - ObolErc1155Recipient.OWRInfo[] memory infos = new ObolErc1155Recipient.OWRInfo[](2); - infos[0] = ObolErc1155Recipient.OWRInfo({owr: address(0), rewardAddress: address(0)}); - infos[1] = ObolErc1155Recipient.OWRInfo({owr: address(0), rewardAddress: address(0)}); - recipient.mintBatch(address(this), 2, amounts, infos, ObolErc1155Recipient.DepositInfo({pubkey: "0x", withdrawal_credentials: "0x", sig: "0x"})); - bool ownerOf2 = recipient.isOwnerOf(2); - bool ownerOf3 = recipient.isOwnerOf(3); + address[] memory owrs = new address[](2); + owrs[0] = address(0); + owrs[1] = address(0); + address[] memory rewardAddresses = new address[](2); + rewardAddresses[0] = address(0); + rewardAddresses[1] = address(0); + + recipient.mintBatch(address(this), 2, amounts, owrs, rewardAddresses); + bool ownerOf2 = recipient.isReceiverOf(2); + bool ownerOf3 = recipient.isReceiverOf(3); assertEq(ownerOf2, true); assertEq(ownerOf3, true); } - function testClaim() public { + function testMintSupply_owrErc1155() public { + recipient.mint(address(this), 1, address(0), address(0)); + bool ownerOf1 = recipient.isReceiverOf(1); + assertEq(ownerOf1, true); + + uint256 maxSupplyBefore = recipient.getMaxSupply(1); + recipient.mintSupply(1, 100); + uint256 maxSupplyAfter = recipient.getMaxSupply(1); + assertGt(maxSupplyAfter, maxSupplyBefore); + } + + // function testBurn_owrErc1155() public { + // recipient.mint(address(this), 100, address(0), address(0)); + // bool ownerOf1 = recipient.isOwnerOf(1); + // assertEq(ownerOf1, true); + + // uint256 totalSupplyBefore = recipient.totalSupply(1); + // recipient.burn(1, 50); + // uint256 totalSupplyAfter = recipient.totalSupply(1); + // assertLt(totalSupplyAfter, totalSupplyBefore); + // } + + function testClaim_owrErc1155() public { address rewardAddress = makeAddr("rewardAddress"); vm.mockCall( ENS_REVERSE_REGISTRAR_GOERLI, @@ -108,19 +136,19 @@ contract ObolErc1155RecipientTest is Test, IERC1155Receiver { OptimisticWithdrawalRecipient owrETH = owrFactory.createOWRecipient(ETH_ADDRESS, rewardAddress, rewardAddress, rewardAddress, ETH_STAKE); - recipient.mint(address(this), 1, ObolErc1155Recipient.OWRInfo({owr: address(owrETH), rewardAddress: rewardAddress}), ObolErc1155Recipient.DepositInfo({pubkey: "0x", withdrawal_credentials: "0x", sig: "0x"})); + recipient.mint(address(this), 1, address(owrETH), rewardAddress); address(recipient).safeTransferETH(1 ether); assertEq(address(recipient).balance, 1 ether); - recipient.setRewards(1, address(owrETH), 1 ether); - assertEq(recipient.rewards(address(owrETH), 1), 1 ether); + recipient.setRewards(1, 1 ether); + assertEq(_getRewards(1), 1 ether); recipient.claim(1); assertEq(rewardAddress.balance, 1 ether); } - function testTransferWithRewards() public { + function testTransferWithRewards_owrErc1155() public { address rewardAddress = makeAddr("rewardAddress"); address receiverAddress = address(new ObolErc1155ReceiverMock()); @@ -140,19 +168,20 @@ contract ObolErc1155RecipientTest is Test, IERC1155Receiver { OptimisticWithdrawalRecipient owrETH = owrFactory.createOWRecipient(ETH_ADDRESS, rewardAddress, rewardAddress, rewardAddress, ETH_STAKE); - recipient.mint(address(this), 1, ObolErc1155Recipient.OWRInfo({owr: address(owrETH), rewardAddress: rewardAddress}), ObolErc1155Recipient.DepositInfo({pubkey: "0x", withdrawal_credentials: "0x", sig: "0x"})); + recipient.mint(address(this), 1, address(owrETH), rewardAddress); + recipient.simulateReceiverMint(1, 1); address(recipient).safeTransferETH(1 ether); assertEq(address(recipient).balance, 1 ether); - recipient.setRewards(1, address(owrETH), 1 ether); - assertEq(recipient.rewards(address(owrETH), 1), 1 ether); + recipient.setRewards(1, 1 ether); + assertEq(_getRewards(1), 1 ether); recipient.safeTransferFrom(address(this), receiverAddress, 1, 1, "0x"); assertEq(rewardAddress.balance, 1 ether); } - function testTransferWithoutRewards() public { + function testTransferWithoutRewards_owrErc1155() public { address rewardAddress = makeAddr("rewardAddress"); address receiverAddress = address(new ObolErc1155ReceiverMock()); @@ -172,7 +201,8 @@ contract ObolErc1155RecipientTest is Test, IERC1155Receiver { OptimisticWithdrawalRecipient owrETH = owrFactory.createOWRecipient(ETH_ADDRESS, rewardAddress, rewardAddress, rewardAddress, ETH_STAKE); - recipient.mint(address(this), 1, ObolErc1155Recipient.OWRInfo({owr: address(owrETH), rewardAddress: rewardAddress}), ObolErc1155Recipient.DepositInfo({pubkey: "0x", withdrawal_credentials: "0x", sig: "0x"})); + recipient.mint(address(this), 1, address(owrETH), rewardAddress); + recipient.simulateReceiverMint(1, 1); recipient.safeTransferFrom(address(this), receiverAddress, 1, 1, "0x"); assertFalse(recipient.isOwnerOf(1)); @@ -181,37 +211,44 @@ contract ObolErc1155RecipientTest is Test, IERC1155Receiver { assertTrue(recipient.isOwnerOf(1)); } - function testReceiveRewards() public { - address rewardAddress = makeAddr("rewardAddress"); - 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)) - ); - OptimisticWithdrawalRecipientFactory owrFactory = - new OptimisticWithdrawalRecipientFactory("demo.obol.eth", ENS_REVERSE_REGISTRAR_GOERLI, address(this)); - - OptimisticWithdrawalRecipient owrETH = - owrFactory.createOWRecipient(ETH_ADDRESS, rewardAddress, rewardAddress, address(recipient), ETH_STAKE); - - address(owrETH).safeTransferETH(1 ether); - - recipient.mint(address(this), 1, ObolErc1155Recipient.OWRInfo({owr: address(owrETH), rewardAddress: rewardAddress}), ObolErc1155Recipient.DepositInfo({pubkey: "0x", withdrawal_credentials: "0x", sig: "0x"})); - bool ownerOf1 = recipient.isOwnerOf(1); - assertEq(ownerOf1, true); - - uint256 registeredRewards = recipient.rewards(address(owrETH), 1); - assertEq(registeredRewards, 0); - - recipient.receiveRewards(address(owrETH)); - assertEq(address(owrETH).balance, 0 ether); - - registeredRewards = recipient.rewards(address(owrETH), 1); - assertEq(registeredRewards, 1 ether); + // function testReceiveRewards_owrErc1155() public { + // address rewardAddress = makeAddr("rewardAddress"); + // 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)) + // ); + // OptimisticWithdrawalRecipientFactory owrFactory = + // new OptimisticWithdrawalRecipientFactory("demo.obol.eth", ENS_REVERSE_REGISTRAR_GOERLI, address(this)); + + // OptimisticWithdrawalRecipient owrETH = + // owrFactory.createOWRecipient(ETH_ADDRESS, rewardAddress, rewardAddress, address(recipient), ETH_STAKE); + + // address(owrETH).safeTransferETH(1 ether); + + // recipient.mint(address(this), 1, address(owrETH), rewardAddress); + // recipient.simulateReceiverMint(1, 1); + // bool ownerOf1 = recipient.isOwnerOf(1); + // assertEq(ownerOf1, true); + + // uint256 registeredRewards = _getRewards(1); + // assertEq(registeredRewards, 0); + + // recipient.receiveRewards(address(owrETH)); + // assertEq(address(owrETH).balance, 0 ether); + + // registeredRewards = _getRewards(1); + // assertEq(registeredRewards, 1 ether); + // } + + + function _getRewards(uint256 id) private view returns (uint256) { + (, , uint256 claimable , ,) = recipient.tokenInfo(id); + return claimable; } } diff --git a/src/test/owr/ObolErc1155RecipientMock.sol b/src/test/owr/ObolErc1155RecipientMock.sol index e106bf5..b2701bc 100644 --- a/src/test/owr/ObolErc1155RecipientMock.sol +++ b/src/test/owr/ObolErc1155RecipientMock.sol @@ -7,7 +7,12 @@ contract ObolErc1155RecipientMock is ObolErc1155Recipient { constructor(string memory baseUri_, address _owner, address _depositContract) ObolErc1155Recipient(baseUri_, _owner, _depositContract) { } - function setRewards(uint256 id, address owr, uint256 amount) external { - rewards[owr][id] += amount; + function setRewards(uint256 id, uint256 amount) external { + tokenInfo[id].claimable += amount; + } + + function simulateReceiverMint(uint256 id, uint256 amount) external { + (bool success,) = address(this).call(abi.encodeWithSelector(this.safeTransferFrom.selector, address(this), tokenInfo[id].receiver, id, amount, "0x")); + if (!success) revert TransferFailed(); } } \ No newline at end of file From 97499d6e955867736b711af0d1bd0350a155fa16 Mon Sep 17 00:00:00 2001 From: cosminobol Date: Mon, 3 Jun 2024 15:57:10 +0300 Subject: [PATCH 08/17] patch: updated ObolErc1155Recipient with partition groups --- src/interfaces/IPullSplit.sol | 28 ++ src/interfaces/ISplitsWarehouse.sol | 14 + src/owr/ObolErc1155Recipient.sol | 333 +++++++----------- ...ck.sol => ObolErc1155ReceiverMock.sol.txt} | 0 ...t.t.sol => ObolErc1155Recipient.t.sol.txt} | 0 ...k.sol => ObolErc1155RecipientMock.sol.txt} | 0 6 files changed, 174 insertions(+), 201 deletions(-) create mode 100644 src/interfaces/IPullSplit.sol create mode 100644 src/interfaces/ISplitsWarehouse.sol rename src/test/owr/{ObolErc1155ReceiverMock.sol => ObolErc1155ReceiverMock.sol.txt} (100%) rename src/test/owr/{ObolErc1155Recipient.t.sol => ObolErc1155Recipient.t.sol.txt} (100%) rename src/test/owr/{ObolErc1155RecipientMock.sol => ObolErc1155RecipientMock.sol.txt} (100%) diff --git a/src/interfaces/IPullSplit.sol b/src/interfaces/IPullSplit.sol new file mode 100644 index 0000000..fb3bf47 --- /dev/null +++ b/src/interfaces/IPullSplit.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + + +interface IPullSplit { + struct PullSplitConfiguration { + address[] recipients; + uint256[] allocations; + uint256 totalAllocation; + uint16 distributionIncentive; + } + + function distribute( + PullSplitConfiguration calldata _split, + address _token, + address _distributor + ) external; + + function distribute( + PullSplitConfiguration calldata _split, + address _token, + uint256 _distributeAmount, + bool _performWarehouseTransfer, + address _distributor + ) external; + + function SPLITS_WAREHOUSE() external view returns (address); +} \ No newline at end of file diff --git a/src/interfaces/ISplitsWarehouse.sol b/src/interfaces/ISplitsWarehouse.sol new file mode 100644 index 0000000..d3e9c40 --- /dev/null +++ b/src/interfaces/ISplitsWarehouse.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +interface ISplitsWarehouse { + function NATIVE_TOKEN() external view returns (address); + + function deposit(address receiver, address token, uint256 amount) external payable; + + function batchDeposit(address[] calldata receivers, address token, uint256[] calldata amounts) external; + + function batchTransfer(address[] calldata receivers, address token, uint256[] calldata amounts) external; + + function withdraw(address owner, address token) external; +} \ No newline at end of file diff --git a/src/owr/ObolErc1155Recipient.sol b/src/owr/ObolErc1155Recipient.sol index c2eea99..43282c7 100644 --- a/src/owr/ObolErc1155Recipient.sol +++ b/src/owr/ObolErc1155Recipient.sol @@ -10,45 +10,53 @@ import {ERC20} from "solmate/tokens/ERC20.sol"; import {SafeTransferLib} from "solady/utils/SafeTransferLib.sol"; import {IERC165, IERC1155Receiver} from "src/interfaces/IERC1155Receiver.sol"; import {ISplitMain, SplitConfiguration} from "src/interfaces/ISplitMain.sol"; -import {ISplitWallet} from "src/interfaces/ISplitWallet.sol"; +import {IPullSplit} from "src/interfaces/IPullSplit.sol"; +import {ISplitsWarehouse} from "src/interfaces/ISplitsWarehouse.sol"; /// @notice OWR principal recipient /// @dev handles rewards and principal of OWR contract ObolErc1155Recipient is ERC1155, Ownable, IERC1155Receiver { using SafeTransferLib for address; - uint256 public lastId; - - // BeaconChain deposit contract - IDepositContract public immutable depositContract; - - struct TokenInfo { - address owr; - address rewardAddress; - uint256 claimable; - - uint256 maxSupply; - address receiver; - } - struct DepositInfo { bytes pubkey; bytes withdrawal_credentials; bytes sig; bytes32 root; } + struct Partition { + uint256 maxSupply; + address owr; + address operator; + } + + uint256 public lastId; + uint256 public partitionCount; + mapping(uint256 _partitionId => Partition) public partitions; + mapping(uint256 _partitionId => uint256[] _tokenIds) public partitionTokens; + mapping(address _owr => uint256 _partitionId) public owrsPartition; + + mapping(uint256 _tokenId => uint256 _partitionId) public tokensPartition; + mapping(uint256 _tokenId => address _owner) public ownerOf; - mapping(uint256 id => TokenInfo) public tokenInfo; - mapping(address owr => uint256) public owrTokens; + mapping(address _owner => uint256 _amount) public claimable; mapping(uint256 id => uint256) public totalSupply; uint256 public totalSupplyAll; + // BeaconChain deposit contract + IDepositContract public immutable depositContract; + string private _baseUri; address private constant ETH_TOKEN_ADDRESS = address(0x0); uint256 private constant ETH_DEPOSIT_AMOUNT = 32 ether; uint256 private constant MIN_ETH_EXIT_AMOUNT = 16 ether; + error PartitionSupplyReached(); + error DepositAmountNotValid(); + error PartitionNotValid(); + error OwrNotValid(); + error LengthMismatch(); error InvalidTokenAmount(); error InvalidOwner(); @@ -58,6 +66,9 @@ contract ObolErc1155Recipient is ERC1155, Ownable, IERC1155Receiver { error TransferFailed(); error InvalidBurnAmount(uint256 necessary, uint received); + event PartitionCreated(address indexed _owr, uint256 indexed _partitionId, uint256 indexed _maxSupply); + event Minted(uint256 indexed _partitionId, uint256 indexed _mintedId, address indexed _sender); + constructor(string memory baseUri_, address _owner, address _depositContract) { lastId = 1; _baseUri = baseUri_; @@ -77,199 +88,141 @@ contract ObolErc1155Recipient is ERC1155, Ownable, IERC1155Receiver { } /// @dev Returns true if `msg.sender` is the owner of the contract - function isOwnerOf(uint256 id) public view returns (bool) { - return balanceOf(msg.sender, id) > 0; - } - - /// @dev Returns true if `msg.sender` is the receiver of id - function isReceiverOf(uint256 id) public view returns (bool) { - return tokenInfo[id].receiver == msg.sender; + function isOwnerOf(uint256 _tokenId) public view returns (bool) { + return balanceOf(msg.sender, _tokenId) > 0; } /// @dev Returns max supply for id - function getMaxSupply(uint256 id) public view returns (uint256) { - return tokenInfo[id].maxSupply; + function getMaxSupply(uint256 _tokenId) public view returns (uint256) { + uint256 _partition = tokensPartition[_tokenId]; + return partitions[_partition].maxSupply; } /// ----------------------------------------------------------------------- /// functions - public & external /// ----------------------------------------------------------------------- - /// @notice triggers `OWR.distributeFunds` - /// @dev callable by the receiver - /// @param owr the OWR address - function receiveRewards(address owr, SplitConfiguration calldata _splitConfig) external { - uint256 _tokenId = owrTokens[owr]; - - // check if sender is the receiver of id - if (!isReceiverOf(_tokenId)) revert InvalidOwner(); - - // call .distribute() on OWR - uint256 balanceBefore = _getOWRTokenBalance(owr); - IOptimisticWithdrawalRecipient(owr).distributeFunds(); - _distributeSplitsRewards(owr, _splitConfig); - uint256 balanceAfter = _getOWRTokenBalance(owr); - - tokenInfo[_tokenId].claimable += (balanceAfter - balanceBefore); - } - - function _distributeSplitsRewards(address owr, SplitConfiguration calldata _splitConfig) private { - (,address _split,) = IOptimisticWithdrawalRecipient(owr).getTranches(); - address _splitMain = ISplitWallet(_split).splitMain(); - address _token = IOptimisticWithdrawalRecipient(owr).token(); - - if (_token == address(0)) { - ISplitMain(_splitMain).distributeETH(_split, _splitConfig.accounts, _splitConfig.percentAllocations, _splitConfig.distributorFee, _splitConfig.controller); - ISplitMain(_splitMain).withdraw(address(this), 1, new ERC20[](0)); - } else { - ISplitMain(_splitMain).distributeERC20(_split, ERC20(_token), _splitConfig.accounts, _splitConfig.percentAllocations, _splitConfig.distributorFee, _splitConfig.controller); - ERC20[] memory tokens = new ERC20[](1); - tokens[0] = ERC20(_token); - ISplitMain(_splitMain).withdraw(address(this), 0, tokens); - } - - } - - /// @notice claims rewards to `TokenInfo.rewardAddress` - /// @dev callable by the owner - /// @param id the ERC1155 id - /// @return claimed the amount of rewards sent to `TokenInfo.rewardAddress` - function claim(uint256 id) external returns (uint256 claimed) { - claimed = _claim(id); - } - - /// @notice claims rewards to `TokenInfo.rewardAddress` from multiple token ids - /// @dev callable by the owner - /// @param ids the ERC1155 ids - /// @return claimed the amount of rewards sent to `TokenInfo.rewardAddress` per each id - function batchClaim(uint256[] calldata ids) external returns (uint256[] memory claimed) { - uint256 count = ids.length; - for (uint256 i; i < count; i++) { - claimed[i] = _claim(ids[i]); - } + /// @notice creates a new partition + /// @dev TODO: TBD DOS risk + /// @param maxSupply the maximum number of unique tokens + /// @param owr the Optimistic Withdrawal Recipient address + function createPartition(uint256 maxSupply, address owr) external { + uint256 _id = partitionCount; + partitions[_id] = Partition({maxSupply: maxSupply, owr: owr, operator: msg.sender}); + owrsPartition[owr] = _id; + + partitionCount++; + emit PartitionCreated(owr, _id, maxSupply); } - /// @notice mints a new token - /// @dev tokens are minted to address(this) and transferred when ETH is deposited to the DepositContract - /// @param to address registered as the tokens receiver - /// @param maxSupply the max allowed amount for `lastId` - /// @param owr OptimisticWithdrawalRecipient address - /// @param rewardAddress rewards receiver address + /// @notice mints a new token and deposits to ETH deposit contract + /// @param _partitionId the partition to assign it to + /// @param depositInfo deposit data needed for `DepositContract` /// @return mintedId id of the minted NFT - function mint(address to, uint256 maxSupply, address owr, address rewardAddress) - external - payable - onlyOwner - returns (uint256 mintedId) - { + function mint(uint256 _partitionId, DepositInfo calldata depositInfo) external payable returns (uint256 mintedId) { // validation - if (maxSupply == 0) revert InvalidTokenAmount(); + if (partitions[_partitionId].owr == address(0)) revert PartitionNotValid(); + if (partitionTokens[_partitionId].length + 1 > partitions[_partitionId].maxSupply) revert PartitionSupplyReached(); + if (msg.value != ETH_DEPOSIT_AMOUNT) revert DepositAmountNotValid(); - // mint - mintedId = _assignInfoAndExtractId(owr, rewardAddress, to, maxSupply); - _mint(address(this), mintedId, maxSupply, ""); - } - - /// @notice mints a batch of tokens - /// @dev tokens are minted to address(this) and transferred when ETH is deposited to the DepositContract - /// @param to receiver address - /// @param count batch length - /// @param maxSupply the max allowed amounts for each id - /// @param owrs OptimisticWithdrawalRecipient addresses - /// @param rewardAddresses rewards receiver addresses - /// @return mintedIds id list of the minted NFTs - function mintBatch( - address to, - uint256 count, - uint256[] calldata maxSupply, - address[] calldata owrs, - address[] calldata rewardAddresses - ) external payable onlyOwner returns (uint256[] memory mintedIds) { - // validate - if (count != maxSupply.length) revert LengthMismatch(); - if (count != owrs.length) revert LengthMismatch(); - if (count != rewardAddresses.length) revert LengthMismatch(); - - // mint up to `count` - mintedIds = new uint256[](count); - for (uint256 i; i < count; i++) { - if (maxSupply[i] == 0) revert InvalidTokenAmount(); - mintedIds[i] = _assignInfoAndExtractId(owrs[i], rewardAddresses[i], to, maxSupply[i]); - _mint(address(this), mintedIds[i], maxSupply[i], ""); - } - } - - /// @notice deposits ETH to `DepositContract` and activates part of supply - /// @param id token id - /// @param count amount of supply to activate - /// @param depositInfo deposit data needed for `DepositContract` - function depositForToken(uint256 id, uint256 count, DepositInfo[] calldata depositInfo) external payable { - // vaidate - if (!isReceiverOf(id)) revert InvalidOwner(); - - if (depositInfo.length != count) revert LengthMismatch(); - - uint256 crtActiveSupply =totalSupply[id]; - - if (crtActiveSupply + count > getMaxSupply(id)) revert InvalidLastSupply(); - - if (msg.value < count * ETH_DEPOSIT_AMOUNT) revert InvalidTokenAmount(); - - // deposit to ETH `DepositContract` - for (uint i; i < count; i++) { - depositContract.deposit{value: ETH_DEPOSIT_AMOUNT}( - depositInfo[i].pubkey, depositInfo[i].withdrawal_credentials, depositInfo[i].sig, depositInfo[i].root - ); - } + // retrieve id + mintedId = _incrementId(); - (bool success,) = address(this).call(abi.encodeWithSelector(this.safeTransferFrom.selector, tokenInfo[id].receiver, id, count, "0x")); - if (!success) revert TransferFailed(); + // add partition details + partitionTokens[_partitionId].push(mintedId); + tokensPartition[mintedId] = _partitionId; // increase total supply - totalSupply[id] += count; - totalSupplyAll += count; - } - - /// @notice increases maxSupply for token id - /// @param id token id - /// @param amount newly added supply - function mintSupply(uint256 id, uint256 amount) external { - // validate - if (!isReceiverOf(id)) revert InvalidOwner(); - if (lastId < id) revert InvalidLastSupply(); + totalSupply[mintedId]++; + totalSupplyAll++; - // mint for existing id - _mint(address(this), id, amount, ""); + // deposit to ETH deposit contract + depositContract.deposit{value: ETH_DEPOSIT_AMOUNT}(depositInfo.pubkey, depositInfo.withdrawal_credentials, depositInfo.sig, depositInfo.root); + + // mint to sender + _mint(msg.sender, mintedId, 1, ""); + ownerOf[mintedId] = msg.sender; - // increase supply - tokenInfo[id].maxSupply += amount; + emit Minted(_partitionId, mintedId, msg.sender); } /// @notice decreases totalSupply for token id - /// @param id token id - /// @param amount newly removed supply - function burn(uint256 id, uint256 amount) external { + /// @param _tokenId token id + function burn(uint256 _tokenId) external { // validate - if (!isReceiverOf(id)) revert InvalidOwner(); - if (amount == 0) revert InvalidTokenAmount(); + if (!isOwnerOf(_tokenId)) revert InvalidOwner(); + + // retrieve OWR + address _owr = partitions[tokensPartition[_tokenId]].owr; + if (_owr == address(0)) revert OwrNotValid(); - uint256 minEthAmount = MIN_ETH_EXIT_AMOUNT * amount; uint256 ethBalanceBefore = address(this).balance; - IOptimisticWithdrawalRecipient(tokenInfo[id].owr).distributeFunds(); + IOptimisticWithdrawalRecipient(_owr).distributeFunds(); uint256 ethBalanceAfter = address(this).balance; uint256 ethReceived = ethBalanceAfter - ethBalanceBefore; + // TODO: check `ethReceived` amount - if(ethReceived < minEthAmount) revert InvalidBurnAmount(minEthAmount, ethReceived); + if(ethReceived < MIN_ETH_EXIT_AMOUNT) revert InvalidBurnAmount(MIN_ETH_EXIT_AMOUNT, ethReceived); - _burn(msg.sender, id, amount); + _burn(msg.sender, _tokenId, 1); - totalSupply[id] -= amount; - totalSupplyAll -= amount; + totalSupply[_tokenId]--; + totalSupplyAll--; - (bool sent,) = tokenInfo[id].receiver.call{value: ethReceived}(""); + (bool sent,) = msg.sender.call{value: ethReceived}(""); if (!sent) revert TransferFailed(); } + /// @notice triggers `OWR.distributeFunds` and updates claimable balances for partition + /// @param _tokenId token id + /// @param _splitConfig pull split configuration + function updateRewards(uint256 _tokenId, address _distributor, IPullSplit.PullSplitConfiguration calldata _splitConfig) external { + uint256 _partitionId = tokensPartition[_tokenId]; + address _owr = partitions[tokensPartition[_tokenId]].owr; + if (_owr == address(0)) revert OwrNotValid(); + + + // call .distribute() on OWR + uint256 balanceBefore = _getTokenBalance(_owr); + IOptimisticWithdrawalRecipient(_owr).distributeFunds(); + _distributeSplitsRewards(_owr, _distributor, _splitConfig); + uint256 balanceAfter = _getTokenBalance(_owr); + + uint256 _totalClaimable = balanceAfter - balanceBefore; + if (_totalClaimable > 0) { + uint256 count = partitionTokens[_partitionId].length; + uint256 _reward = _totalClaimable / count; + for(uint i; i < count; i++) { + address _owner = ownerOf[partitionTokens[_partitionId][i]]; + claimable[_owner] += _reward; + } + } + } + + /// @notice claim rewards + function claim() external { + if(claimable[msg.sender] > 0) { + (bool sent,) = msg.sender.call{value: claimable[msg.sender]}(""); + if (!sent) revert TransferFailed(); + } + claimable[msg.sender] = 0; + } + + + /// ----------------------------------------------------------------------- + /// functions - private + /// ----------------------------------------------------------------------- + + //TODO: refactor this + function _distributeSplitsRewards(address owr, address _distributor, IPullSplit.PullSplitConfiguration calldata _splitConfig) private { + (,address _split,) = IOptimisticWithdrawalRecipient(owr).getTranches(); + address _token = IOptimisticWithdrawalRecipient(owr).token(); + + IPullSplit(_split).distribute(_splitConfig, _token, _distributor); + address warehouse = IPullSplit(_split).SPLITS_WAREHOUSE(); + ISplitsWarehouse(warehouse).withdraw(address(this), _token); + } + /// @dev Hook that is called before any token transfer. /// Forces claim before a transfer happens function _beforeTokenTransfer(address from, address to, uint256[] memory ids, uint256[] memory, bytes memory) @@ -282,7 +235,7 @@ contract ObolErc1155Recipient is ERC1155, Ownable, IERC1155Receiver { // claim before transfer uint256 length = ids.length; for (uint256 i; i < length; i++) { - _claim(ids[i]); //allow transfer even if `claimed == 0` + ownerOf[ids[i]] = to; } } @@ -298,35 +251,13 @@ contract ObolErc1155Recipient is ERC1155, Ownable, IERC1155Receiver { lastId++; } - function _claim(uint256 id) private returns (uint256 claimed) { - address _owr = tokenInfo[id].owr; - if (_owr == address(0)) revert InvalidOWR(); - if (tokenInfo[id].claimable == 0) return 0; - - address token = IOptimisticWithdrawalRecipient(_owr).token(); - if (token == ETH_TOKEN_ADDRESS) { - (bool sent,) = tokenInfo[id].rewardAddress.call{value: tokenInfo[id].claimable}(""); - if (!sent) revert ClaimFailed(); - } else { - token.safeTransfer(tokenInfo[id].rewardAddress, tokenInfo[id].claimable); - } - tokenInfo[id].claimable = 0; - } - function _getOWRTokenBalance(address owr) private view returns (uint256 balance) { + function _getTokenBalance(address owr) private view returns (uint256 balance) { address token = IOptimisticWithdrawalRecipient(owr).token(); if (token == ETH_TOKEN_ADDRESS) balance = address(this).balance; else balance = ERC20(token).balanceOf(address(this)); } - function _assignInfoAndExtractId(address owr, address rewardAddress, address receiver, uint256 maxSupply) private returns (uint256 mintedId) { - mintedId = _incrementId(); - - TokenInfo memory info = TokenInfo({maxSupply: maxSupply, receiver: receiver, claimable: 0, owr: owr, rewardAddress: rewardAddress}); - tokenInfo[mintedId] = info; - owrTokens[info.owr] = mintedId; - } - /// ----------------------------------------------------------------------- /// IERC1155Receiver /// ----------------------------------------------------------------------- @@ -352,4 +283,4 @@ contract ObolErc1155Recipient is ERC1155, Ownable, IERC1155Receiver { ) public virtual override returns (bytes4) { return this.onERC1155BatchReceived.selector; } -} \ No newline at end of file +} diff --git a/src/test/owr/ObolErc1155ReceiverMock.sol b/src/test/owr/ObolErc1155ReceiverMock.sol.txt similarity index 100% rename from src/test/owr/ObolErc1155ReceiverMock.sol rename to src/test/owr/ObolErc1155ReceiverMock.sol.txt diff --git a/src/test/owr/ObolErc1155Recipient.t.sol b/src/test/owr/ObolErc1155Recipient.t.sol.txt similarity index 100% rename from src/test/owr/ObolErc1155Recipient.t.sol rename to src/test/owr/ObolErc1155Recipient.t.sol.txt diff --git a/src/test/owr/ObolErc1155RecipientMock.sol b/src/test/owr/ObolErc1155RecipientMock.sol.txt similarity index 100% rename from src/test/owr/ObolErc1155RecipientMock.sol rename to src/test/owr/ObolErc1155RecipientMock.sol.txt From c7e59fa19a0b0d2981579ac370334d5e93343b23 Mon Sep 17 00:00:00 2001 From: cosminobol Date: Tue, 4 Jun 2024 23:36:33 +0300 Subject: [PATCH 09/17] chore: fixed commit messages --- src/owr/ObolErc1155Recipient.sol | 74 ++++++++++++++++---------------- 1 file changed, 38 insertions(+), 36 deletions(-) diff --git a/src/owr/ObolErc1155Recipient.sol b/src/owr/ObolErc1155Recipient.sol index 43282c7..f8dedd4 100644 --- a/src/owr/ObolErc1155Recipient.sol +++ b/src/owr/ObolErc1155Recipient.sol @@ -24,6 +24,7 @@ contract ObolErc1155Recipient is ERC1155, Ownable, IERC1155Receiver { bytes sig; bytes32 root; } + struct Partition { uint256 maxSupply; address owr; @@ -46,7 +47,7 @@ contract ObolErc1155Recipient is ERC1155, Ownable, IERC1155Receiver { // BeaconChain deposit contract IDepositContract public immutable depositContract; - + string private _baseUri; address private constant ETH_TOKEN_ADDRESS = address(0x0); uint256 private constant ETH_DEPOSIT_AMOUNT = 32 ether; @@ -64,7 +65,7 @@ contract ObolErc1155Recipient is ERC1155, Ownable, IERC1155Receiver { error ClaimFailed(); error InvalidLastSupply(); error TransferFailed(); - error InvalidBurnAmount(uint256 necessary, uint received); + error InvalidBurnAmount(uint256 necessary, uint256 received); event PartitionCreated(address indexed _owr, uint256 indexed _partitionId, uint256 indexed _maxSupply); event Minted(uint256 indexed _partitionId, uint256 indexed _mintedId, address indexed _sender); @@ -121,7 +122,7 @@ contract ObolErc1155Recipient is ERC1155, Ownable, IERC1155Receiver { /// @return mintedId id of the minted NFT function mint(uint256 _partitionId, DepositInfo calldata depositInfo) external payable returns (uint256 mintedId) { // validation - if (partitions[_partitionId].owr == address(0)) revert PartitionNotValid(); + if (partitions[_partitionId].owr == address(0)) revert PartitionNotValid(); if (partitionTokens[_partitionId].length + 1 > partitions[_partitionId].maxSupply) revert PartitionSupplyReached(); if (msg.value != ETH_DEPOSIT_AMOUNT) revert DepositAmountNotValid(); @@ -137,8 +138,10 @@ contract ObolErc1155Recipient is ERC1155, Ownable, IERC1155Receiver { totalSupplyAll++; // deposit to ETH deposit contract - depositContract.deposit{value: ETH_DEPOSIT_AMOUNT}(depositInfo.pubkey, depositInfo.withdrawal_credentials, depositInfo.sig, depositInfo.root); - + depositContract.deposit{value: ETH_DEPOSIT_AMOUNT}( + depositInfo.pubkey, depositInfo.withdrawal_credentials, depositInfo.sig, depositInfo.root + ); + // mint to sender _mint(msg.sender, mintedId, 1, ""); ownerOf[mintedId] = msg.sender; @@ -162,7 +165,7 @@ contract ObolErc1155Recipient is ERC1155, Ownable, IERC1155Receiver { uint256 ethReceived = ethBalanceAfter - ethBalanceBefore; // TODO: check `ethReceived` amount - if(ethReceived < MIN_ETH_EXIT_AMOUNT) revert InvalidBurnAmount(MIN_ETH_EXIT_AMOUNT, ethReceived); + if (ethReceived < MIN_ETH_EXIT_AMOUNT) revert InvalidBurnAmount(MIN_ETH_EXIT_AMOUNT, ethReceived); _burn(msg.sender, _tokenId, 1); @@ -176,12 +179,15 @@ contract ObolErc1155Recipient is ERC1155, Ownable, IERC1155Receiver { /// @notice triggers `OWR.distributeFunds` and updates claimable balances for partition /// @param _tokenId token id /// @param _splitConfig pull split configuration - function updateRewards(uint256 _tokenId, address _distributor, IPullSplit.PullSplitConfiguration calldata _splitConfig) external { + function distributeRewards( + uint256 _tokenId, + address _distributor, + IPullSplit.PullSplitConfiguration calldata _splitConfig + ) external { uint256 _partitionId = tokensPartition[_tokenId]; address _owr = partitions[tokensPartition[_tokenId]].owr; if (_owr == address(0)) revert OwrNotValid(); - // call .distribute() on OWR uint256 balanceBefore = _getTokenBalance(_owr); IOptimisticWithdrawalRecipient(_owr).distributeFunds(); @@ -192,37 +198,40 @@ contract ObolErc1155Recipient is ERC1155, Ownable, IERC1155Receiver { if (_totalClaimable > 0) { uint256 count = partitionTokens[_partitionId].length; uint256 _reward = _totalClaimable / count; - for(uint i; i < count; i++) { + for (uint256 i; i < count; i++) { address _owner = ownerOf[partitionTokens[_partitionId][i]]; claimable[_owner] += _reward; } } - } + } /// @notice claim rewards - function claim() external { - if(claimable[msg.sender] > 0) { - (bool sent,) = msg.sender.call{value: claimable[msg.sender]}(""); + function claim(address _user) external { + if (claimable[_user] > 0) { + (bool sent,) = _user.call{value: claimable[_user]}(""); if (!sent) revert TransferFailed(); } - claimable[msg.sender] = 0; + claimable[_user] = 0; } - /// ----------------------------------------------------------------------- /// functions - private /// ----------------------------------------------------------------------- //TODO: refactor this - function _distributeSplitsRewards(address owr, address _distributor, IPullSplit.PullSplitConfiguration calldata _splitConfig) private { - (,address _split,) = IOptimisticWithdrawalRecipient(owr).getTranches(); + function _distributeSplitsRewards( + address owr, + address _distributor, + IPullSplit.PullSplitConfiguration calldata _splitConfig + ) private { + (, address _split,) = IOptimisticWithdrawalRecipient(owr).getTranches(); address _token = IOptimisticWithdrawalRecipient(owr).token(); IPullSplit(_split).distribute(_splitConfig, _token, _distributor); address warehouse = IPullSplit(_split).SPLITS_WAREHOUSE(); ISplitsWarehouse(warehouse).withdraw(address(this), _token); } - + /// @dev Hook that is called before any token transfer. /// Forces claim before a transfer happens function _beforeTokenTransfer(address from, address to, uint256[] memory ids, uint256[] memory, bytes memory) @@ -251,7 +260,6 @@ contract ObolErc1155Recipient is ERC1155, Ownable, IERC1155Receiver { lastId++; } - function _getTokenBalance(address owr) private view returns (uint256 balance) { address token = IOptimisticWithdrawalRecipient(owr).token(); if (token == ETH_TOKEN_ADDRESS) balance = address(this).balance; @@ -262,25 +270,19 @@ contract ObolErc1155Recipient is ERC1155, Ownable, IERC1155Receiver { /// IERC1155Receiver /// ----------------------------------------------------------------------- function supportsInterface(bytes4 interfaceId) public view virtual override(ERC1155, IERC165) returns (bool) { - return interfaceId == type(IERC1155Receiver).interfaceId || super.supportsInterface(interfaceId); + return interfaceId == type(IERC1155Receiver).interfaceId || super.supportsInterface(interfaceId); } - function onERC1155Received( - address, - address, - uint256, - uint256, - bytes memory - ) public virtual override returns (bytes4) { - return this.onERC1155Received.selector; + + function onERC1155Received(address, address, uint256, uint256, bytes memory) public virtual override returns (bytes4) { + return this.onERC1155Received.selector; } - function onERC1155BatchReceived( - address, - address, - uint256[] memory, - uint256[] memory, - bytes memory - ) public virtual override returns (bytes4) { - return this.onERC1155BatchReceived.selector; + function onERC1155BatchReceived(address, address, uint256[] memory, uint256[] memory, bytes memory) + public + virtual + override + returns (bytes4) + { + return this.onERC1155BatchReceived.selector; } } From 9555303601838bd88f0c8077c91cc9afd38e789f Mon Sep 17 00:00:00 2001 From: cosminobol Date: Mon, 10 Jun 2024 16:01:25 +0300 Subject: [PATCH 10/17] patch: updated ERC1155 rewards and burn flow + tests --- .DS_Store | Bin 0 -> 6148 bytes src/base/BaseSplit.sol | 208 +-- src/base/BaseSplitFactory.sol | 36 +- src/collector/ObolCollector.sol | 52 +- src/collector/ObolCollectorFactory.sol | 80 +- src/controllers/ImmutableSplitController.sol | 268 +-- .../ImmutableSplitControllerFactory.sol | 468 ++--- .../ObolEigenLayerPodController.sol | 316 ++-- .../ObolEigenLayerPodControllerFactory.sol | 146 +- src/etherfi/ObolEtherfiSplit.sol | 110 +- src/etherfi/ObolEtherfiSplitFactory.sol | 86 +- src/interfaces/IERC1155Receiver.sol | 152 +- .../IOptimisticPullWithdrawalRecipient.sol | 10 + .../IOptimisticWithdrawalRecipient.sol | 15 +- src/interfaces/ISplitWallet.sol | 6 - .../{ => external}/IDepositContract.sol | 48 +- .../{ => external}/IENSReverseRegistrar.sol | 0 src/interfaces/{ => external}/IEigenLayer.sol | 294 +-- src/interfaces/{ => external}/IweETH.sol | 18 +- src/interfaces/{ => external}/IwstETH.sol | 14 +- .../{ => external/splits}/IPullSplit.sol | 11 + .../{ => external/splits}/ISplitFactory.sol | 0 .../{ => external/splits}/ISplitMain.sol | 0 .../{ => external/splits}/ISplitMainV2.sol | 0 .../splits}/ISplitsWarehouse.sol | 0 src/lido/ObolLidoSplit.sol | 108 +- src/lido/ObolLidoSplitFactory.sol | 88 +- src/owr/ObolErc1155Recipient.sol | 181 +- src/owr/OptimisticPullWithdrawalRecipient.sol | 317 ++++ ...timisticPullWithdrawalRecipientFactory.sol | 125 ++ src/owr/OptimisticWithdrawalRecipient.sol | 686 +++---- .../OptimisticWithdrawalRecipientFactory.sol | 248 +-- src/test/collector/ObolCollector.t.sol | 394 ++-- src/test/collector/ObolCollectorFactory.t.sol | 64 +- src/test/controllers/IMSC.t.sol | 435 +++-- src/test/controllers/IMSCFactory.t.sol | 387 ++-- src/test/eigenlayer/EigenLayerTestBase.sol | 112 +- src/test/eigenlayer/OELPCFactory.t.sol | 161 +- .../ObolEigenLayerPodController.t.sol | 453 +++-- .../integration/OELPCIntegration.t.sol | 329 ++-- src/test/etherfi/ObolEtherfiSplit.t.sol | 378 ++-- .../etherfi/ObolEtherfiSplitFactory.t.sol | 100 +- .../etherfi/ObolEtherfiSplitTestHelper.sol | 16 +- .../ObolEtherfiSplitIntegrationTest.sol | 128 +- src/test/lido/ObolLIdoSplitFactory.t.sol | 100 +- src/test/lido/ObolLidoSplit.t.sol | 378 ++-- .../integration/LidoSplitIntegrationTest.sol | 128 +- src/test/owr/OWRTestHelper.t.sol | 82 +- src/test/owr/ObolErc1155Recipient.t.sol | 151 ++ src/test/owr/ObolErc1155Recipient.t.sol.txt | 254 --- ...k.sol.txt => ObolErc1155RecipientMock.sol} | 34 +- .../owr/OptimisticWithdrawalRecipient.t.sol | 1596 ++++++++--------- ...OptimisticWithdrawalRecipientFactory.t.sol | 376 ++-- .../owr/{ => mocks}/DepositContractMock.sol | 40 +- .../ObolErc1155ReceiverMock.sol} | 58 +- src/test/owr/mocks/PullSplitMock.sol | 39 + 56 files changed, 5359 insertions(+), 4925 deletions(-) create mode 100644 .DS_Store create mode 100644 src/interfaces/IOptimisticPullWithdrawalRecipient.sol delete mode 100644 src/interfaces/ISplitWallet.sol rename src/interfaces/{ => external}/IDepositContract.sol (97%) rename src/interfaces/{ => external}/IENSReverseRegistrar.sol (100%) rename src/interfaces/{ => external}/IEigenLayer.sol (97%) rename src/interfaces/{ => external}/IweETH.sol (97%) rename src/interfaces/{ => external}/IwstETH.sol (96%) rename src/interfaces/{ => external/splits}/IPullSplit.sol (71%) rename src/interfaces/{ => external/splits}/ISplitFactory.sol (100%) rename src/interfaces/{ => external/splits}/ISplitMain.sol (100%) rename src/interfaces/{ => external/splits}/ISplitMainV2.sol (100%) rename src/interfaces/{ => external/splits}/ISplitsWarehouse.sol (100%) create mode 100644 src/owr/OptimisticPullWithdrawalRecipient.sol create mode 100644 src/owr/OptimisticPullWithdrawalRecipientFactory.sol create mode 100644 src/test/owr/ObolErc1155Recipient.t.sol delete mode 100644 src/test/owr/ObolErc1155Recipient.t.sol.txt rename src/test/owr/{ObolErc1155RecipientMock.sol.txt => ObolErc1155RecipientMock.sol} (79%) rename src/test/owr/{ => mocks}/DepositContractMock.sol (62%) rename src/test/owr/{ObolErc1155ReceiverMock.sol.txt => mocks/ObolErc1155ReceiverMock.sol} (96%) create mode 100644 src/test/owr/mocks/PullSplitMock.sol diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..c169056d174c9ed0d7cadf6ba2692558d80a87d0 GIT binary patch literal 6148 zcmeHK%}T>S5Z-O8O({YS3Oz1(Eto=sf|n5M3mDOZN=;1BV9b^zwTDv3SzpK}@p+ut z-GIfMMeGdhe)GGV{UH0p7~}pT9x-M!#w=)v9F+z^cVnn!k`XzMQOu)MMqqtJa})dP zfZyI`1RNs4E_jm5pbX1J;fMJRT9;C$QLTs3RWe&n z?fo;Em0^<2a#fJTGf26;PO?}oeYwcuLe&O3VD+rt)E=x>BIBgb72#p-x`G8C@s zuGc;5;PB}5V)~rEWb#eZ$$@Ppdj@NG2gR)BHORA6W{=>hv+FEEVt^PR28e;}X26^a zR)4!`pw$xt#6SZBxIYMJh@QbpquM&4!|OA~JBTQt<68nz81xKQ8X*G0bt#}O<>raO zbvgKj$@2_W8g)72YGxS6%v?WSxSAdOLZvhAX{4SQAOE20# literal 0 HcmV?d00001 diff --git a/src/base/BaseSplit.sol b/src/base/BaseSplit.sol index f2a8063..cbcb204 100644 --- a/src/base/BaseSplit.sol +++ b/src/base/BaseSplit.sol @@ -1,104 +1,104 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity 0.8.19; - -import {ERC20} from "solmate/tokens/ERC20.sol"; -import {SafeTransferLib} from "solmate/utils/SafeTransferLib.sol"; -import {Clone} from "solady/utils/Clone.sol"; - -abstract contract BaseSplit is Clone { - error Invalid_Address(); - error Invalid_FeeShare(uint256 val); - error Invalid_FeeRecipient(); - - /// ----------------------------------------------------------------------- - /// libraries - /// ----------------------------------------------------------------------- - using SafeTransferLib for ERC20; - using SafeTransferLib for address; - - address internal constant ETH_ADDRESS = address(0); - uint256 internal constant PERCENTAGE_SCALE = 1e5; - - /// @notice fee share - uint256 public immutable feeShare; - - /// @notice fee address - address public immutable feeRecipient; - - // withdrawal (adress, 20 bytes) - // 0; first item - uint256 internal constant WITHDRAWAL_ADDRESS_OFFSET = 0; - // 20 = withdrawalAddress_offset (0) + withdrawalAddress_size (address, 20 bytes) - uint256 internal constant TOKEN_ADDRESS_OFFSET = 20; - - constructor(address _feeRecipient, uint256 _feeShare) { - if (_feeShare >= PERCENTAGE_SCALE) revert Invalid_FeeShare(_feeShare); - if (_feeShare > 0 && _feeRecipient == address(0)) revert Invalid_FeeRecipient(); - - feeShare = _feeShare; - feeRecipient = _feeRecipient; - } - - /// ----------------------------------------------------------------------- - /// View - /// ----------------------------------------------------------------------- - - /// Address to send funds to to - /// @dev equivalent to address public immutable withdrawalAddress - function withdrawalAddress() public pure returns (address) { - return _getArgAddress(WITHDRAWAL_ADDRESS_OFFSET); - } - - /// Token addresss - /// @dev equivalent to address public immutable token - function token() public pure virtual returns (address) { - return _getArgAddress(TOKEN_ADDRESS_OFFSET); - } - - /// ----------------------------------------------------------------------- - /// Public - /// ----------------------------------------------------------------------- - - /// @notice Rescue stuck ETH and tokens - /// Uses token == address(0) to represent ETH - /// @return balance Amount of ETH or tokens rescued - function rescueFunds(address tokenAddress) external virtual returns (uint256 balance) { - _beforeRescueFunds(tokenAddress); - - if (tokenAddress == ETH_ADDRESS) { - balance = address(this).balance; - if (balance > 0) withdrawalAddress().safeTransferETH(balance); - } else { - balance = ERC20(tokenAddress).balanceOf(address(this)); - if (balance > 0) ERC20(tokenAddress).safeTransfer(withdrawalAddress(), balance); - } - } - - /// @notice distribute funds to withdrawal address - function distribute() external virtual returns (uint256) { - (address tokenAddress, uint256 amount) = _beforeDistribute(); - - if (feeShare > 0) { - uint256 fee = (amount * feeShare) / PERCENTAGE_SCALE; - _transfer(tokenAddress, feeRecipient, fee); - _transfer(tokenAddress, withdrawalAddress(), amount -= fee); - } else { - _transfer(tokenAddress, withdrawalAddress(), amount); - } - - return amount; - } - - /// ----------------------------------------------------------------------- - /// Internal - /// ----------------------------------------------------------------------- - - function _beforeRescueFunds(address tokenAddress) internal virtual; - - function _beforeDistribute() internal virtual returns (address tokenAddress, uint256 amount); - - function _transfer(address tokenAddress, address receiver, uint256 amount) internal { - if (tokenAddress == ETH_ADDRESS) receiver.safeTransferETH(amount); - else ERC20(tokenAddress).safeTransfer(receiver, amount); - } -} +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.19; + +import {ERC20} from "solmate/tokens/ERC20.sol"; +import {SafeTransferLib} from "solmate/utils/SafeTransferLib.sol"; +import {Clone} from "solady/utils/Clone.sol"; + +abstract contract BaseSplit is Clone { + error Invalid_Address(); + error Invalid_FeeShare(uint256 val); + error Invalid_FeeRecipient(); + + /// ----------------------------------------------------------------------- + /// libraries + /// ----------------------------------------------------------------------- + using SafeTransferLib for ERC20; + using SafeTransferLib for address; + + address internal constant ETH_ADDRESS = address(0); + uint256 internal constant PERCENTAGE_SCALE = 1e5; + + /// @notice fee share + uint256 public immutable feeShare; + + /// @notice fee address + address public immutable feeRecipient; + + // withdrawal (adress, 20 bytes) + // 0; first item + uint256 internal constant WITHDRAWAL_ADDRESS_OFFSET = 0; + // 20 = withdrawalAddress_offset (0) + withdrawalAddress_size (address, 20 bytes) + uint256 internal constant TOKEN_ADDRESS_OFFSET = 20; + + constructor(address _feeRecipient, uint256 _feeShare) { + if (_feeShare >= PERCENTAGE_SCALE) revert Invalid_FeeShare(_feeShare); + if (_feeShare > 0 && _feeRecipient == address(0)) revert Invalid_FeeRecipient(); + + feeShare = _feeShare; + feeRecipient = _feeRecipient; + } + + /// ----------------------------------------------------------------------- + /// View + /// ----------------------------------------------------------------------- + + /// Address to send funds to to + /// @dev equivalent to address public immutable withdrawalAddress + function withdrawalAddress() public pure returns (address) { + return _getArgAddress(WITHDRAWAL_ADDRESS_OFFSET); + } + + /// Token addresss + /// @dev equivalent to address public immutable token + function token() public pure virtual returns (address) { + return _getArgAddress(TOKEN_ADDRESS_OFFSET); + } + + /// ----------------------------------------------------------------------- + /// Public + /// ----------------------------------------------------------------------- + + /// @notice Rescue stuck ETH and tokens + /// Uses token == address(0) to represent ETH + /// @return balance Amount of ETH or tokens rescued + function rescueFunds(address tokenAddress) external virtual returns (uint256 balance) { + _beforeRescueFunds(tokenAddress); + + if (tokenAddress == ETH_ADDRESS) { + balance = address(this).balance; + if (balance > 0) withdrawalAddress().safeTransferETH(balance); + } else { + balance = ERC20(tokenAddress).balanceOf(address(this)); + if (balance > 0) ERC20(tokenAddress).safeTransfer(withdrawalAddress(), balance); + } + } + + /// @notice distribute funds to withdrawal address + function distribute() external virtual returns (uint256) { + (address tokenAddress, uint256 amount) = _beforeDistribute(); + + if (feeShare > 0) { + uint256 fee = (amount * feeShare) / PERCENTAGE_SCALE; + _transfer(tokenAddress, feeRecipient, fee); + _transfer(tokenAddress, withdrawalAddress(), amount -= fee); + } else { + _transfer(tokenAddress, withdrawalAddress(), amount); + } + + return amount; + } + + /// ----------------------------------------------------------------------- + /// Internal + /// ----------------------------------------------------------------------- + + function _beforeRescueFunds(address tokenAddress) internal virtual; + + function _beforeDistribute() internal virtual returns (address tokenAddress, uint256 amount); + + function _transfer(address tokenAddress, address receiver, uint256 amount) internal { + if (tokenAddress == ETH_ADDRESS) receiver.safeTransferETH(amount); + else ERC20(tokenAddress).safeTransfer(receiver, amount); + } +} diff --git a/src/base/BaseSplitFactory.sol b/src/base/BaseSplitFactory.sol index 01944da..37feafc 100644 --- a/src/base/BaseSplitFactory.sol +++ b/src/base/BaseSplitFactory.sol @@ -1,18 +1,18 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity 0.8.19; - -abstract contract BaseSplitFactory { - /// ----------------------------------------------------------------------- - /// errors - /// ----------------------------------------------------------------------- - /// @dev Invalid address - error Invalid_Address(); - - /// ----------------------------------------------------------------------- - /// events - /// ----------------------------------------------------------------------- - /// Emitted on createCollector - event CreateSplit(address token, address withdrawalAddress); - - function createCollector(address token, address withdrawalAddress) external virtual returns (address collector); -} +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.19; + +abstract contract BaseSplitFactory { + /// ----------------------------------------------------------------------- + /// errors + /// ----------------------------------------------------------------------- + /// @dev Invalid address + error Invalid_Address(); + + /// ----------------------------------------------------------------------- + /// events + /// ----------------------------------------------------------------------- + /// Emitted on createCollector + event CreateSplit(address token, address withdrawalAddress); + + function createCollector(address token, address withdrawalAddress) external virtual returns (address collector); +} diff --git a/src/collector/ObolCollector.sol b/src/collector/ObolCollector.sol index 756455a..c0a4624 100644 --- a/src/collector/ObolCollector.sol +++ b/src/collector/ObolCollector.sol @@ -1,26 +1,26 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity 0.8.19; - -import {ERC20} from "solmate/tokens/ERC20.sol"; -import {SafeTransferLib} from "solmate/utils/SafeTransferLib.sol"; -import {Clone} from "solady/utils/Clone.sol"; -import {BaseSplit} from "../base/BaseSplit.sol"; - -/// @title ObolCollector -/// @author Obol -/// @notice An contract used to receive and distribute rewards minus fees -contract ObolCollector is BaseSplit { - constructor(address _feeRecipient, uint256 _feeShare) BaseSplit(_feeRecipient, _feeShare) {} - - function _beforeRescueFunds(address tokenAddress) internal pure override { - // prevent bypass - if (tokenAddress == token()) revert Invalid_Address(); - } - - function _beforeDistribute() internal view override returns (address tokenAddress, uint256 amount) { - tokenAddress = token(); - - if (tokenAddress == ETH_ADDRESS) amount = address(this).balance; - else amount = ERC20(tokenAddress).balanceOf(address(this)); - } -} +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.19; + +import {ERC20} from "solmate/tokens/ERC20.sol"; +import {SafeTransferLib} from "solmate/utils/SafeTransferLib.sol"; +import {Clone} from "solady/utils/Clone.sol"; +import {BaseSplit} from "../base/BaseSplit.sol"; + +/// @title ObolCollector +/// @author Obol +/// @notice An contract used to receive and distribute rewards minus fees +contract ObolCollector is BaseSplit { + constructor(address _feeRecipient, uint256 _feeShare) BaseSplit(_feeRecipient, _feeShare) {} + + function _beforeRescueFunds(address tokenAddress) internal pure override { + // prevent bypass + if (tokenAddress == token()) revert Invalid_Address(); + } + + function _beforeDistribute() internal view override returns (address tokenAddress, uint256 amount) { + tokenAddress = token(); + + if (tokenAddress == ETH_ADDRESS) amount = address(this).balance; + else amount = ERC20(tokenAddress).balanceOf(address(this)); + } +} diff --git a/src/collector/ObolCollectorFactory.sol b/src/collector/ObolCollectorFactory.sol index c76c6fd..47ef271 100644 --- a/src/collector/ObolCollectorFactory.sol +++ b/src/collector/ObolCollectorFactory.sol @@ -1,40 +1,40 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity 0.8.19; - -import {LibClone} from "solady/utils/LibClone.sol"; -import {ObolCollector} from "./ObolCollector.sol"; -import {BaseSplitFactory} from "../base/BaseSplitFactory.sol"; - -/// @title ObolCollector -/// @author Obol -/// @notice A factory contract for cheaply deploying ObolCollector. -/// @dev The address returned should be used to as reward address collecting rewards -contract ObolCollectorFactory is BaseSplitFactory { - /// ----------------------------------------------------------------------- - /// libraries - /// ----------------------------------------------------------------------- - using LibClone for address; - - /// ----------------------------------------------------------------------- - /// storage - /// ----------------------------------------------------------------------- - - /// @dev collector implementation - ObolCollector public immutable collectorImpl; - - constructor(address _feeRecipient, uint256 _feeShare) { - collectorImpl = new ObolCollector(_feeRecipient, _feeShare); - } - - /// @dev Create a new collector - /// @dev address(0) is used to represent ETH - /// @param token collector token address - /// @param withdrawalAddress withdrawalAddress to receive tokens - function createCollector(address token, address withdrawalAddress) external override returns (address collector) { - if (withdrawalAddress == address(0)) revert Invalid_Address(); - - collector = address(collectorImpl).clone(abi.encodePacked(withdrawalAddress, token)); - - emit CreateSplit(token, withdrawalAddress); - } -} +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.19; + +import {LibClone} from "solady/utils/LibClone.sol"; +import {ObolCollector} from "./ObolCollector.sol"; +import {BaseSplitFactory} from "../base/BaseSplitFactory.sol"; + +/// @title ObolCollector +/// @author Obol +/// @notice A factory contract for cheaply deploying ObolCollector. +/// @dev The address returned should be used to as reward address collecting rewards +contract ObolCollectorFactory is BaseSplitFactory { + /// ----------------------------------------------------------------------- + /// libraries + /// ----------------------------------------------------------------------- + using LibClone for address; + + /// ----------------------------------------------------------------------- + /// storage + /// ----------------------------------------------------------------------- + + /// @dev collector implementation + ObolCollector public immutable collectorImpl; + + constructor(address _feeRecipient, uint256 _feeShare) { + collectorImpl = new ObolCollector(_feeRecipient, _feeShare); + } + + /// @dev Create a new collector + /// @dev address(0) is used to represent ETH + /// @param token collector token address + /// @param withdrawalAddress withdrawalAddress to receive tokens + function createCollector(address token, address withdrawalAddress) external override returns (address collector) { + if (withdrawalAddress == address(0)) revert Invalid_Address(); + + collector = address(collectorImpl).clone(abi.encodePacked(withdrawalAddress, token)); + + emit CreateSplit(token, withdrawalAddress); + } +} diff --git a/src/controllers/ImmutableSplitController.sol b/src/controllers/ImmutableSplitController.sol index c9eea93..fc7f8ab 100644 --- a/src/controllers/ImmutableSplitController.sol +++ b/src/controllers/ImmutableSplitController.sol @@ -1,135 +1,135 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -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 -/// configuration is defined at deployment and cannot change -contract ImmutableSplitController is Clone { - /// @notice IMSC already initialized - error Initialized(); - - /// @notice - error Unauthorized(); - - /// @notice Revert if split balance is > 1 - /// @dev Prevent distribution of current balance - error Invalid_SplitBalance(); - - /// ----------------------------------------------------------------------- - /// 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(); - - // prevent distribution of existing money - if (address(split).balance > 1) revert Invalid_SplitBalance(); - - 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 = 0; - for (; 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)); - } - } +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.19; + +import {ISplitMain} from "../interfaces/external/splits/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 { + /// @notice IMSC already initialized + error Initialized(); + + /// @notice + error Unauthorized(); + + /// @notice Revert if split balance is > 1 + /// @dev Prevent distribution of current balance + error Invalid_SplitBalance(); + + /// ----------------------------------------------------------------------- + /// 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(); + + // prevent distribution of existing money + if (address(split).balance > 1) revert Invalid_SplitBalance(); + + 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 = 0; + for (; 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 d5506e2..09f32f4 100644 --- a/src/controllers/ImmutableSplitControllerFactory.sol +++ b/src/controllers/ImmutableSplitControllerFactory.sol @@ -1,235 +1,235 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -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 address - error Invalid_Address(); - /// @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); - /// @notice Array of accounts size - /// @param size acounts size - error InvalidSplit__TooManyAccounts(uint256 size); - - /// ----------------------------------------------------------------------- - /// 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); - _; - } - - /// Creates Factory - /// @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 - /// @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) - ); - } - - /// @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; - if (recipientsSize > type(uint8).max) revert InvalidSplit__TooManyAccounts(recipientsSize); - - uint256[] memory recipients = new uint[](recipientsSize); - - uint256 i = 0; - for (; i < recipientsSize;) { - recipients[i] = (uint256(percentAllocations[i]) << ADDRESS_BITS) | uint256(uint160(accounts[i])); - - unchecked { - i++; - } - } - - 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 - ++i; - } - } - } +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.19; + +import {ISplitMain} from "../interfaces/external/splits/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 address + error Invalid_Address(); + /// @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); + /// @notice Array of accounts size + /// @param size acounts size + error InvalidSplit__TooManyAccounts(uint256 size); + + /// ----------------------------------------------------------------------- + /// 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); + _; + } + + /// Creates Factory + /// @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 + /// @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) + ); + } + + /// @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; + if (recipientsSize > type(uint8).max) revert InvalidSplit__TooManyAccounts(recipientsSize); + + uint256[] memory recipients = new uint[](recipientsSize); + + uint256 i = 0; + for (; i < recipientsSize;) { + recipients[i] = (uint256(percentAllocations[i]) << ADDRESS_BITS) | uint256(uint160(accounts[i])); + + unchecked { + i++; + } + } + + 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 + ++i; + } + } + } } \ No newline at end of file diff --git a/src/eigenlayer/ObolEigenLayerPodController.sol b/src/eigenlayer/ObolEigenLayerPodController.sol index f47dd57..79e991f 100644 --- a/src/eigenlayer/ObolEigenLayerPodController.sol +++ b/src/eigenlayer/ObolEigenLayerPodController.sol @@ -1,158 +1,158 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity 0.8.19; - -import {SafeTransferLib} from "solmate/utils/SafeTransferLib.sol"; -import {ERC20} from "solmate/tokens/ERC20.sol"; -import {Ownable} from "solady/auth/Ownable.sol"; -import {IEigenLayerUtils, IEigenPodManager, IDelayedWithdrawalRouter} from "../interfaces/IEigenLayer.sol"; - -/// @title ObolEigenLayerPodController -/// @author Obol Labs -/// @notice A contract for controlling an Eigenpod and withdrawing the balance into an Obol Split -/// @dev The address returned should be used as the EigenPodController address -contract ObolEigenLayerPodController { - /// @dev returned on failed call - error CallFailed(bytes data); - /// @dev If Invalid fee setup - error Invalid_FeeSetup(); - /// @dev Invalid fee share - error Invalid_FeeShare(); - /// @dev user unauthorized - error Unauthorized(); - /// @dev contract already initialized - error AlreadyInitialized(); - - /// @dev Emiited on intialize - event Initialized(address eigenPod, address owner); - - /// ----------------------------------------------------------------------- - /// libraries - /// ----------------------------------------------------------------------- - using SafeTransferLib for address; - using SafeTransferLib for ERC20; - - uint256 internal constant PERCENTAGE_SCALE = 1e5; - - /// ----------------------------------------------------------------------- - /// storage - immutables - /// ----------------------------------------------------------------------- - - /// @notice address of Eigenlayer delegation manager - /// @dev This is the address of the delegation manager transparent proxy - address public immutable eigenLayerDelegationManager; - - /// @notice address of EigenLayerPod Manager - /// @dev this is the pod manager transparent proxy - IEigenPodManager public immutable eigenLayerPodManager; - - /// @notice address of delay withdrawal router - IDelayedWithdrawalRouter public immutable delayedWithdrawalRouter; - - /// @notice fee address - address public immutable feeRecipient; - - /// @notice fee share. Represented as an integer from 1->10000 (100%) - uint256 public immutable feeShare; - - /// ----------------------------------------------------------------------- - /// storage - /// ----------------------------------------------------------------------- - - /// @notice address of deployed Eigen pod - address public eigenPod; - - /// @notice address of a withdrawalAddress - address public withdrawalAddress; - - /// @notice address of owner - address public owner; - - modifier onlyOwner() { - if (msg.sender != owner) revert Unauthorized(); - _; - } - - constructor( - address recipient, - uint256 share, - address delegationManager, - address eigenPodManager, - address withdrawalRouter - ) { - if (recipient != address(0) && share == 0) revert Invalid_FeeSetup(); - if (share > PERCENTAGE_SCALE) revert Invalid_FeeShare(); - - feeRecipient = recipient; - feeShare = share; - eigenLayerDelegationManager = delegationManager; - eigenLayerPodManager = IEigenPodManager(eigenPodManager); - delayedWithdrawalRouter = IDelayedWithdrawalRouter(withdrawalRouter); - } - - /// @dev Enables contract to receive ETH - // defined on the clone implementation - // receive() external payable {} - - /// @notice initializes the controller - /// @param _owner address of the controller owner - /// @param _withdrawalAddress address to receive funds - function initialize(address _owner, address _withdrawalAddress) external { - if (owner != address(0)) revert AlreadyInitialized(); - - eigenPod = eigenLayerPodManager.createPod(); - owner = _owner; - withdrawalAddress = _withdrawalAddress; - - emit Initialized(eigenPod, _owner); - } - - /// @notice Call the eigenPod contract - /// @param data to call eigenPod contract - function callEigenPod(bytes calldata data) external payable onlyOwner { - _executeCall(address(eigenPod), msg.value, data); - } - - /// @notice Call the Eigenlayer delegation Manager contract - /// @param data to call eigenPod contract - function callDelegationManager(bytes calldata data) external payable onlyOwner { - _executeCall(address(eigenLayerDelegationManager), msg.value, data); - } - - /// @notice Call the Eigenlayer Manager contract - /// @param data to call contract - function callEigenPodManager(bytes calldata data) external payable onlyOwner { - _executeCall(address(eigenLayerPodManager), msg.value, data); - } - - /// @notice Withdraw funds from the delayed withdrawal router - /// @param numberOfDelayedWithdrawalsToClaim number of claims - function claimDelayedWithdrawals(uint256 numberOfDelayedWithdrawalsToClaim) external { - delayedWithdrawalRouter.claimDelayedWithdrawals(address(this), numberOfDelayedWithdrawalsToClaim); - - // transfer eth to withdrawalAddress - uint256 balance = address(this).balance; - if (feeShare > 0) { - uint256 fee = (balance * feeShare) / PERCENTAGE_SCALE; - feeRecipient.safeTransferETH(fee); - withdrawalAddress.safeTransferETH(balance -= fee); - } else { - withdrawalAddress.safeTransferETH(balance); - } - } - - /// @notice Rescue stuck tokens by sending them to the split contract. - /// @param token address of token - /// @param amount amount of token to rescue - function rescueFunds(address token, uint256 amount) external { - if (amount > 0) ERC20(token).safeTransfer(withdrawalAddress, amount); - } - - /// @notice Execute a low level call - /// @param to address to execute call - /// @param value amount of ETH to send with call - /// @param data bytes array to execute - function _executeCall(address to, uint256 value, bytes memory data) internal { - (bool success,) = address(to).call{value: value}(data); - if (!success) revert CallFailed(data); - } -} +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.19; + +import {SafeTransferLib} from "solmate/utils/SafeTransferLib.sol"; +import {ERC20} from "solmate/tokens/ERC20.sol"; +import {Ownable} from "solady/auth/Ownable.sol"; +import {IEigenLayerUtils, IEigenPodManager, IDelayedWithdrawalRouter} from "../interfaces/external/IEigenLayer.sol"; + +/// @title ObolEigenLayerPodController +/// @author Obol Labs +/// @notice A contract for controlling an Eigenpod and withdrawing the balance into an Obol Split +/// @dev The address returned should be used as the EigenPodController address +contract ObolEigenLayerPodController { + /// @dev returned on failed call + error CallFailed(bytes data); + /// @dev If Invalid fee setup + error Invalid_FeeSetup(); + /// @dev Invalid fee share + error Invalid_FeeShare(); + /// @dev user unauthorized + error Unauthorized(); + /// @dev contract already initialized + error AlreadyInitialized(); + + /// @dev Emiited on intialize + event Initialized(address eigenPod, address owner); + + /// ----------------------------------------------------------------------- + /// libraries + /// ----------------------------------------------------------------------- + using SafeTransferLib for address; + using SafeTransferLib for ERC20; + + uint256 internal constant PERCENTAGE_SCALE = 1e5; + + /// ----------------------------------------------------------------------- + /// storage - immutables + /// ----------------------------------------------------------------------- + + /// @notice address of Eigenlayer delegation manager + /// @dev This is the address of the delegation manager transparent proxy + address public immutable eigenLayerDelegationManager; + + /// @notice address of EigenLayerPod Manager + /// @dev this is the pod manager transparent proxy + IEigenPodManager public immutable eigenLayerPodManager; + + /// @notice address of delay withdrawal router + IDelayedWithdrawalRouter public immutable delayedWithdrawalRouter; + + /// @notice fee address + address public immutable feeRecipient; + + /// @notice fee share. Represented as an integer from 1->10000 (100%) + uint256 public immutable feeShare; + + /// ----------------------------------------------------------------------- + /// storage + /// ----------------------------------------------------------------------- + + /// @notice address of deployed Eigen pod + address public eigenPod; + + /// @notice address of a withdrawalAddress + address public withdrawalAddress; + + /// @notice address of owner + address public owner; + + modifier onlyOwner() { + if (msg.sender != owner) revert Unauthorized(); + _; + } + + constructor( + address recipient, + uint256 share, + address delegationManager, + address eigenPodManager, + address withdrawalRouter + ) { + if (recipient != address(0) && share == 0) revert Invalid_FeeSetup(); + if (share > PERCENTAGE_SCALE) revert Invalid_FeeShare(); + + feeRecipient = recipient; + feeShare = share; + eigenLayerDelegationManager = delegationManager; + eigenLayerPodManager = IEigenPodManager(eigenPodManager); + delayedWithdrawalRouter = IDelayedWithdrawalRouter(withdrawalRouter); + } + + /// @dev Enables contract to receive ETH + // defined on the clone implementation + // receive() external payable {} + + /// @notice initializes the controller + /// @param _owner address of the controller owner + /// @param _withdrawalAddress address to receive funds + function initialize(address _owner, address _withdrawalAddress) external { + if (owner != address(0)) revert AlreadyInitialized(); + + eigenPod = eigenLayerPodManager.createPod(); + owner = _owner; + withdrawalAddress = _withdrawalAddress; + + emit Initialized(eigenPod, _owner); + } + + /// @notice Call the eigenPod contract + /// @param data to call eigenPod contract + function callEigenPod(bytes calldata data) external payable onlyOwner { + _executeCall(address(eigenPod), msg.value, data); + } + + /// @notice Call the Eigenlayer delegation Manager contract + /// @param data to call eigenPod contract + function callDelegationManager(bytes calldata data) external payable onlyOwner { + _executeCall(address(eigenLayerDelegationManager), msg.value, data); + } + + /// @notice Call the Eigenlayer Manager contract + /// @param data to call contract + function callEigenPodManager(bytes calldata data) external payable onlyOwner { + _executeCall(address(eigenLayerPodManager), msg.value, data); + } + + /// @notice Withdraw funds from the delayed withdrawal router + /// @param numberOfDelayedWithdrawalsToClaim number of claims + function claimDelayedWithdrawals(uint256 numberOfDelayedWithdrawalsToClaim) external { + delayedWithdrawalRouter.claimDelayedWithdrawals(address(this), numberOfDelayedWithdrawalsToClaim); + + // transfer eth to withdrawalAddress + uint256 balance = address(this).balance; + if (feeShare > 0) { + uint256 fee = (balance * feeShare) / PERCENTAGE_SCALE; + feeRecipient.safeTransferETH(fee); + withdrawalAddress.safeTransferETH(balance -= fee); + } else { + withdrawalAddress.safeTransferETH(balance); + } + } + + /// @notice Rescue stuck tokens by sending them to the split contract. + /// @param token address of token + /// @param amount amount of token to rescue + function rescueFunds(address token, uint256 amount) external { + if (amount > 0) ERC20(token).safeTransfer(withdrawalAddress, amount); + } + + /// @notice Execute a low level call + /// @param to address to execute call + /// @param value amount of ETH to send with call + /// @param data bytes array to execute + function _executeCall(address to, uint256 value, bytes memory data) internal { + (bool success,) = address(to).call{value: value}(data); + if (!success) revert CallFailed(data); + } +} diff --git a/src/eigenlayer/ObolEigenLayerPodControllerFactory.sol b/src/eigenlayer/ObolEigenLayerPodControllerFactory.sol index 2716e01..09d543b 100644 --- a/src/eigenlayer/ObolEigenLayerPodControllerFactory.sol +++ b/src/eigenlayer/ObolEigenLayerPodControllerFactory.sol @@ -1,73 +1,73 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity 0.8.19; - -import {ObolEigenLayerPodController} from "./ObolEigenLayerPodController.sol"; -import {LibClone} from "solady/utils/LibClone.sol"; - -/// @title ObolEigenLayerFactory -/// @author Obol -/// @notice A factory contract for cheaply deploying ObolLidoEigenLayer. -/// @dev The address returned should be used to as the EigenPod address -contract ObolEigenLayerPodControllerFactory { - error Invalid_Owner(); - error Invalid_WithdrawalAddress(); - error Invalid_DelegationManager(); - error Invalid_EigenPodManaager(); - error Invalid_WithdrawalRouter(); - - using LibClone for address; - - event CreatePodController(address indexed controller, address indexed withdrawalAddress, address owner); - - ObolEigenLayerPodController public immutable controllerImplementation; - - constructor( - address feeRecipient, - uint256 feeShare, - address delegationManager, - address eigenPodManager, - address withdrawalRouter - ) { - if (delegationManager == address(0)) revert Invalid_DelegationManager(); - if (eigenPodManager == address(0)) revert Invalid_EigenPodManaager(); - if (withdrawalRouter == address(0)) revert Invalid_WithdrawalRouter(); - - controllerImplementation = - new ObolEigenLayerPodController(feeRecipient, feeShare, delegationManager, eigenPodManager, withdrawalRouter); - // initialize implementation - controllerImplementation.initialize(feeRecipient, feeRecipient); - } - - /// Creates a minimal proxy clone of implementation - /// @param owner address of owner - /// @param withdrawalAddress address of withdrawalAddress - /// @return controller Deployed obol eigen layer controller - function createPodController(address owner, address withdrawalAddress) external returns (address controller) { - if (owner == address(0)) revert Invalid_Owner(); - if (withdrawalAddress == address(0)) revert Invalid_WithdrawalAddress(); - - bytes32 salt = _createSalt(owner, withdrawalAddress); - - controller = address(controllerImplementation).cloneDeterministic("", salt); - - ObolEigenLayerPodController(controller).initialize(owner, withdrawalAddress); - - emit CreatePodController(controller, withdrawalAddress, owner); - } - - /// Predict the controller address - /// @param owner address of owner - /// @param withdrawalAddress address to withdraw funds to - function predictControllerAddress(address owner, address withdrawalAddress) - external - view - returns (address controller) - { - bytes32 salt = _createSalt(owner, withdrawalAddress); - controller = address(controllerImplementation).predictDeterministicAddress("", salt, address(this)); - } - - function _createSalt(address owner, address withdrawalAddress) internal pure returns (bytes32 salt) { - return keccak256(abi.encode(owner, withdrawalAddress)); - } -} +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.19; + +import {ObolEigenLayerPodController} from "./ObolEigenLayerPodController.sol"; +import {LibClone} from "solady/utils/LibClone.sol"; + +/// @title ObolEigenLayerFactory +/// @author Obol +/// @notice A factory contract for cheaply deploying ObolLidoEigenLayer. +/// @dev The address returned should be used to as the EigenPod address +contract ObolEigenLayerPodControllerFactory { + error Invalid_Owner(); + error Invalid_WithdrawalAddress(); + error Invalid_DelegationManager(); + error Invalid_EigenPodManaager(); + error Invalid_WithdrawalRouter(); + + using LibClone for address; + + event CreatePodController(address indexed controller, address indexed withdrawalAddress, address owner); + + ObolEigenLayerPodController public immutable controllerImplementation; + + constructor( + address feeRecipient, + uint256 feeShare, + address delegationManager, + address eigenPodManager, + address withdrawalRouter + ) { + if (delegationManager == address(0)) revert Invalid_DelegationManager(); + if (eigenPodManager == address(0)) revert Invalid_EigenPodManaager(); + if (withdrawalRouter == address(0)) revert Invalid_WithdrawalRouter(); + + controllerImplementation = + new ObolEigenLayerPodController(feeRecipient, feeShare, delegationManager, eigenPodManager, withdrawalRouter); + // initialize implementation + controllerImplementation.initialize(feeRecipient, feeRecipient); + } + + /// Creates a minimal proxy clone of implementation + /// @param owner address of owner + /// @param withdrawalAddress address of withdrawalAddress + /// @return controller Deployed obol eigen layer controller + function createPodController(address owner, address withdrawalAddress) external returns (address controller) { + if (owner == address(0)) revert Invalid_Owner(); + if (withdrawalAddress == address(0)) revert Invalid_WithdrawalAddress(); + + bytes32 salt = _createSalt(owner, withdrawalAddress); + + controller = address(controllerImplementation).cloneDeterministic("", salt); + + ObolEigenLayerPodController(controller).initialize(owner, withdrawalAddress); + + emit CreatePodController(controller, withdrawalAddress, owner); + } + + /// Predict the controller address + /// @param owner address of owner + /// @param withdrawalAddress address to withdraw funds to + function predictControllerAddress(address owner, address withdrawalAddress) + external + view + returns (address controller) + { + bytes32 salt = _createSalt(owner, withdrawalAddress); + controller = address(controllerImplementation).predictDeterministicAddress("", salt, address(this)); + } + + function _createSalt(address owner, address withdrawalAddress) internal pure returns (bytes32 salt) { + return keccak256(abi.encode(owner, withdrawalAddress)); + } +} diff --git a/src/etherfi/ObolEtherfiSplit.sol b/src/etherfi/ObolEtherfiSplit.sol index b9eebe0..40d6a5b 100644 --- a/src/etherfi/ObolEtherfiSplit.sol +++ b/src/etherfi/ObolEtherfiSplit.sol @@ -1,55 +1,55 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity 0.8.19; - -import {ERC20} from "solmate/tokens/ERC20.sol"; -import {SafeTransferLib} from "solmate/utils/SafeTransferLib.sol"; -import {Clone} from "solady/utils/Clone.sol"; -import {IweETH} from "src/interfaces/IweETH.sol"; - -import {BaseSplit} from "../base/BaseSplit.sol"; - -/// @title ObolEtherfiSplit -/// @author Obol -/// @notice A wrapper for 0xsplits/split-contracts SplitWallet that transforms -/// eEth token to weETH token because eEth is a rebasing token -/// @dev Wraps eETH to weETH and -contract ObolEtherfiSplit is BaseSplit { - /// @notice eETH token - ERC20 public immutable eETH; - - /// @notice weETH token - ERC20 public immutable weETH; - - /// @notice Constructor - /// @param _feeRecipient address to receive fee - /// @param _feeShare fee share scaled by PERCENTAGE_SCALE - /// @param _eETH eETH address - /// @param _weETH weETH address - constructor(address _feeRecipient, uint256 _feeShare, ERC20 _eETH, ERC20 _weETH) BaseSplit(_feeRecipient, _feeShare) { - eETH = _eETH; - weETH = _weETH; - } - - function _beforeRescueFunds(address tokenAddress) internal view override { - // we check weETH here so rescueFunds can't be used - // to bypass fee - if (tokenAddress == address(eETH) || tokenAddress == address(weETH)) revert Invalid_Address(); - } - - /// Wraps the current eETH token balance to weETH - /// transfers the weETH balance to withdrawalAddress for distribution - function _beforeDistribute() internal override returns (address tokenAddress, uint256 amount) { - tokenAddress = address(weETH); - - // get current balance - uint256 balance = eETH.balanceOf(address(this)); - // approve the weETH - eETH.approve(address(weETH), balance); - // wrap into wseth - // we ignore the return value - IweETH(address(weETH)).wrap(balance); - // we use balanceOf here in case some weETH is stuck in the - // contract we would be able to rescue it - amount = ERC20(weETH).balanceOf(address(this)); - } -} +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.19; + +import {ERC20} from "solmate/tokens/ERC20.sol"; +import {SafeTransferLib} from "solmate/utils/SafeTransferLib.sol"; +import {Clone} from "solady/utils/Clone.sol"; +import {IweETH} from "src/interfaces/external/IweETH.sol"; + +import {BaseSplit} from "../base/BaseSplit.sol"; + +/// @title ObolEtherfiSplit +/// @author Obol +/// @notice A wrapper for 0xsplits/split-contracts SplitWallet that transforms +/// eEth token to weETH token because eEth is a rebasing token +/// @dev Wraps eETH to weETH and +contract ObolEtherfiSplit is BaseSplit { + /// @notice eETH token + ERC20 public immutable eETH; + + /// @notice weETH token + ERC20 public immutable weETH; + + /// @notice Constructor + /// @param _feeRecipient address to receive fee + /// @param _feeShare fee share scaled by PERCENTAGE_SCALE + /// @param _eETH eETH address + /// @param _weETH weETH address + constructor(address _feeRecipient, uint256 _feeShare, ERC20 _eETH, ERC20 _weETH) BaseSplit(_feeRecipient, _feeShare) { + eETH = _eETH; + weETH = _weETH; + } + + function _beforeRescueFunds(address tokenAddress) internal view override { + // we check weETH here so rescueFunds can't be used + // to bypass fee + if (tokenAddress == address(eETH) || tokenAddress == address(weETH)) revert Invalid_Address(); + } + + /// Wraps the current eETH token balance to weETH + /// transfers the weETH balance to withdrawalAddress for distribution + function _beforeDistribute() internal override returns (address tokenAddress, uint256 amount) { + tokenAddress = address(weETH); + + // get current balance + uint256 balance = eETH.balanceOf(address(this)); + // approve the weETH + eETH.approve(address(weETH), balance); + // wrap into wseth + // we ignore the return value + IweETH(address(weETH)).wrap(balance); + // we use balanceOf here in case some weETH is stuck in the + // contract we would be able to rescue it + amount = ERC20(weETH).balanceOf(address(this)); + } +} diff --git a/src/etherfi/ObolEtherfiSplitFactory.sol b/src/etherfi/ObolEtherfiSplitFactory.sol index bca33cf..e7a91de 100644 --- a/src/etherfi/ObolEtherfiSplitFactory.sol +++ b/src/etherfi/ObolEtherfiSplitFactory.sol @@ -1,43 +1,43 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity 0.8.19; - -import {LibClone} from "solady/utils/LibClone.sol"; -import {ERC20} from "solmate/tokens/ERC20.sol"; -import "./ObolEtherfiSplit.sol"; -import {BaseSplitFactory} from "../base/BaseSplitFactory.sol"; - -/// @title ObolEtherfiSplitFactory -/// @author Obol -/// @notice A factory contract for cheaply deploying ObolEtherfiSplit. -/// @dev The address returned should be used to as reward address for EtherFi -contract ObolEtherfiSplitFactory is BaseSplitFactory { - /// ----------------------------------------------------------------------- - /// libraries - /// ----------------------------------------------------------------------- - using LibClone for address; - - /// ----------------------------------------------------------------------- - /// storage - /// ----------------------------------------------------------------------- - - /// @dev Ethersfi split implementation - ObolEtherfiSplit public immutable etherfiSplitImpl; - - constructor(address _feeRecipient, uint256 _feeShare, ERC20 _eETH, ERC20 _weETH) { - etherfiSplitImpl = new ObolEtherfiSplit(_feeRecipient, _feeShare, _eETH, _weETH); - } - - /// Creates a wrapper for splitWallet that transforms eETH token into - /// weETH - /// @dev Create a new collector - /// @dev address(0) is used to represent ETH - /// @param withdrawalAddress Address of the splitWallet to transfer weETH to - /// @return collector Address of the wrappper split - function createCollector(address, address withdrawalAddress) external override returns (address collector) { - if (withdrawalAddress == address(0)) revert Invalid_Address(); - - collector = address(etherfiSplitImpl).clone(abi.encodePacked(withdrawalAddress)); - - emit CreateSplit(address(0), collector); - } -} +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.19; + +import {LibClone} from "solady/utils/LibClone.sol"; +import {ERC20} from "solmate/tokens/ERC20.sol"; +import "./ObolEtherfiSplit.sol"; +import {BaseSplitFactory} from "../base/BaseSplitFactory.sol"; + +/// @title ObolEtherfiSplitFactory +/// @author Obol +/// @notice A factory contract for cheaply deploying ObolEtherfiSplit. +/// @dev The address returned should be used to as reward address for EtherFi +contract ObolEtherfiSplitFactory is BaseSplitFactory { + /// ----------------------------------------------------------------------- + /// libraries + /// ----------------------------------------------------------------------- + using LibClone for address; + + /// ----------------------------------------------------------------------- + /// storage + /// ----------------------------------------------------------------------- + + /// @dev Ethersfi split implementation + ObolEtherfiSplit public immutable etherfiSplitImpl; + + constructor(address _feeRecipient, uint256 _feeShare, ERC20 _eETH, ERC20 _weETH) { + etherfiSplitImpl = new ObolEtherfiSplit(_feeRecipient, _feeShare, _eETH, _weETH); + } + + /// Creates a wrapper for splitWallet that transforms eETH token into + /// weETH + /// @dev Create a new collector + /// @dev address(0) is used to represent ETH + /// @param withdrawalAddress Address of the splitWallet to transfer weETH to + /// @return collector Address of the wrappper split + function createCollector(address, address withdrawalAddress) external override returns (address collector) { + if (withdrawalAddress == address(0)) revert Invalid_Address(); + + collector = address(etherfiSplitImpl).clone(abi.encodePacked(withdrawalAddress)); + + emit CreateSplit(address(0), collector); + } +} diff --git a/src/interfaces/IERC1155Receiver.sol b/src/interfaces/IERC1155Receiver.sol index 81adf1f..2226b1e 100644 --- a/src/interfaces/IERC1155Receiver.sol +++ b/src/interfaces/IERC1155Receiver.sol @@ -1,77 +1,77 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.19; - - -/** - * @dev Interface of the ERC-165 standard, as defined in the - * https://eips.ethereum.org/EIPS/eip-165[ERC]. - * - * Implementers can declare support of contract interfaces, which can then be - * queried by others ({ERC165Checker}). - * - * For an implementation, see {ERC165}. - */ -interface IERC165 { - /** - * @dev Returns true if this contract implements the interface defined by - * `interfaceId`. See the corresponding - * https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[ERC section] - * to learn more about how these ids are created. - * - * This function call must use less than 30 000 gas. - */ - function supportsInterface(bytes4 interfaceId) external view returns (bool); -} - -/** - * @dev Interface that must be implemented by smart contracts in order to receive - * ERC-1155 token transfers. - */ -interface IERC1155Receiver is IERC165 { - /** - * @dev Handles the receipt of a single ERC-1155 token type. This function is - * called at the end of a `safeTransferFrom` after the balance has been updated. - * - * NOTE: To accept the transfer, this must return - * `bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)"))` - * (i.e. 0xf23a6e61, or its own function selector). - * - * @param operator The address which initiated the transfer (i.e. msg.sender) - * @param from The address which previously owned the token - * @param id The ID of the token being transferred - * @param value The amount of tokens being transferred - * @param data Additional data with no specified format - * @return `bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)"))` if transfer is allowed - */ - function onERC1155Received( - address operator, - address from, - uint256 id, - uint256 value, - bytes calldata data - ) external returns (bytes4); - - /** - * @dev Handles the receipt of a multiple ERC-1155 token types. This function - * is called at the end of a `safeBatchTransferFrom` after the balances have - * been updated. - * - * NOTE: To accept the transfer(s), this must return - * `bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)"))` - * (i.e. 0xbc197c81, or its own function selector). - * - * @param operator The address which initiated the batch transfer (i.e. msg.sender) - * @param from The address which previously owned the token - * @param ids An array containing ids of each token being transferred (order and length must match values array) - * @param values An array containing amounts of each token being transferred (order and length must match ids array) - * @param data Additional data with no specified format - * @return `bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)"))` if transfer is allowed - */ - function onERC1155BatchReceived( - address operator, - address from, - uint256[] calldata ids, - uint256[] calldata values, - bytes calldata data - ) external returns (bytes4); +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + + +/** + * @dev Interface of the ERC-165 standard, as defined in the + * https://eips.ethereum.org/EIPS/eip-165[ERC]. + * + * Implementers can declare support of contract interfaces, which can then be + * queried by others ({ERC165Checker}). + * + * For an implementation, see {ERC165}. + */ +interface IERC165 { + /** + * @dev Returns true if this contract implements the interface defined by + * `interfaceId`. See the corresponding + * https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[ERC section] + * to learn more about how these ids are created. + * + * This function call must use less than 30 000 gas. + */ + function supportsInterface(bytes4 interfaceId) external view returns (bool); +} + +/** + * @dev Interface that must be implemented by smart contracts in order to receive + * ERC-1155 token transfers. + */ +interface IERC1155Receiver is IERC165 { + /** + * @dev Handles the receipt of a single ERC-1155 token type. This function is + * called at the end of a `safeTransferFrom` after the balance has been updated. + * + * NOTE: To accept the transfer, this must return + * `bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)"))` + * (i.e. 0xf23a6e61, or its own function selector). + * + * @param operator The address which initiated the transfer (i.e. msg.sender) + * @param from The address which previously owned the token + * @param id The ID of the token being transferred + * @param value The amount of tokens being transferred + * @param data Additional data with no specified format + * @return `bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)"))` if transfer is allowed + */ + function onERC1155Received( + address operator, + address from, + uint256 id, + uint256 value, + bytes calldata data + ) external returns (bytes4); + + /** + * @dev Handles the receipt of a multiple ERC-1155 token types. This function + * is called at the end of a `safeBatchTransferFrom` after the balances have + * been updated. + * + * NOTE: To accept the transfer(s), this must return + * `bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)"))` + * (i.e. 0xbc197c81, or its own function selector). + * + * @param operator The address which initiated the batch transfer (i.e. msg.sender) + * @param from The address which previously owned the token + * @param ids An array containing ids of each token being transferred (order and length must match values array) + * @param values An array containing amounts of each token being transferred (order and length must match ids array) + * @param data Additional data with no specified format + * @return `bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)"))` if transfer is allowed + */ + function onERC1155BatchReceived( + address operator, + address from, + uint256[] calldata ids, + uint256[] calldata values, + bytes calldata data + ) external returns (bytes4); } \ No newline at end of file diff --git a/src/interfaces/IOptimisticPullWithdrawalRecipient.sol b/src/interfaces/IOptimisticPullWithdrawalRecipient.sol new file mode 100644 index 0000000..981c48e --- /dev/null +++ b/src/interfaces/IOptimisticPullWithdrawalRecipient.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +interface IOptimisticPullWithdrawalRecipient { + function token() external view returns (address); + function distributeFunds() external payable; + function distributeFundsPull() external payable; + function getTranches() external view returns (address principalRecipient, address rewardRecipient, uint256 amountOfPrincipalStake); + function withdraw(address account) external; +} \ No newline at end of file diff --git a/src/interfaces/IOptimisticWithdrawalRecipient.sol b/src/interfaces/IOptimisticWithdrawalRecipient.sol index 1485665..78be662 100644 --- a/src/interfaces/IOptimisticWithdrawalRecipient.sol +++ b/src/interfaces/IOptimisticWithdrawalRecipient.sol @@ -1,9 +1,8 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.19; - -interface IOptimisticWithdrawalRecipient { - function token() external view returns (address); - function distributeFunds() external payable; - function distributeFundsPull() external payable; - function getTranches() external view returns (address principalRecipient, address rewardRecipient, uint256 amountOfPrincipalStake); +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import "./IOptimisticPullWithdrawalRecipient.sol"; + +interface IOptimisticWithdrawalRecipient is IOptimisticPullWithdrawalRecipient{ + function distributeFundsPull() external payable; } \ No newline at end of file diff --git a/src/interfaces/ISplitWallet.sol b/src/interfaces/ISplitWallet.sol deleted file mode 100644 index 91981a2..0000000 --- a/src/interfaces/ISplitWallet.sol +++ /dev/null @@ -1,6 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.19; - -interface ISplitWallet { - function splitMain() external view returns (address); -} diff --git a/src/interfaces/IDepositContract.sol b/src/interfaces/external/IDepositContract.sol similarity index 97% rename from src/interfaces/IDepositContract.sol rename to src/interfaces/external/IDepositContract.sol index 17b0600..bebb478 100644 --- a/src/interfaces/IDepositContract.sol +++ b/src/interfaces/external/IDepositContract.sol @@ -1,25 +1,25 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.19; - -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; - - /// @notice Query the current deposit root hash. - /// @return The deposit root hash. - function get_deposit_root() external view returns (bytes32); - - /// @notice Query the current deposit count. - /// @return The deposit count encoded as a little endian 64-bit number. - function get_deposit_count() external view returns (bytes memory); +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +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; + + /// @notice Query the current deposit root hash. + /// @return The deposit root hash. + function get_deposit_root() external view returns (bytes32); + + /// @notice Query the current deposit count. + /// @return The deposit count encoded as a little endian 64-bit number. + function get_deposit_count() external view returns (bytes memory); } \ No newline at end of file diff --git a/src/interfaces/IENSReverseRegistrar.sol b/src/interfaces/external/IENSReverseRegistrar.sol similarity index 100% rename from src/interfaces/IENSReverseRegistrar.sol rename to src/interfaces/external/IENSReverseRegistrar.sol diff --git a/src/interfaces/IEigenLayer.sol b/src/interfaces/external/IEigenLayer.sol similarity index 97% rename from src/interfaces/IEigenLayer.sol rename to src/interfaces/external/IEigenLayer.sol index fdc4670..62cde25 100644 --- a/src/interfaces/IEigenLayer.sol +++ b/src/interfaces/external/IEigenLayer.sol @@ -1,147 +1,147 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.19; - -import {ERC20} from "solmate/tokens/ERC20.sol"; - -interface IEigenLayerUtils { - // @notice Struct that bundles together a signature and an expiration time for the signature. Used primarily for stack - // management. - struct SignatureWithExpiry { - // the signature itself, formatted as a single bytes object - bytes signature; - // the expiration timestamp (UTC) of the signature - uint256 expiry; - } - - // @notice Struct that bundles together a signature, a salt for uniqueness, and an expiration time for the signature. - // Used primarily for stack management. - struct SignatureWithSaltAndExpiry { - // the signature itself, formatted as a single bytes object - bytes signature; - // the salt used to generate the signature - bytes32 salt; - // the expiration timestamp (UTC) of the signature - uint256 expiry; - } -} - -interface IDelegationManager is IEigenLayerUtils { - /** - * @notice Caller delegates their stake to an operator. - * @param operator The account (`msg.sender`) is delegating its assets to for use in serving applications built on - * EigenLayer. - * @param approverSignatureAndExpiry Verifies the operator approves of this delegation - * @param approverSalt A unique single use value tied to an individual signature. - * @dev The approverSignatureAndExpiry is used in the event that: - * 1) the operator's `delegationApprover` address is set to a non-zero value. - * AND - * 2) neither the operator nor their `delegationApprover` is the `msg.sender`, since in the event that the - * operator - * or their delegationApprover is the `msg.sender`, then approval is assumed. - * @dev In the event that `approverSignatureAndExpiry` is not checked, its content is ignored entirely; it's - * recommended to use an empty input - * in this case to save on complexity + gas costs - */ - function delegateTo(address operator, SignatureWithExpiry memory approverSignatureAndExpiry, bytes32 approverSalt) - external; - - /** - * @notice Undelegates the staker from the operator who they are delegated to. Puts the staker into the "undelegation - * limbo" mode of the EigenPodManager - * and queues a withdrawal of all of the staker's shares in the StrategyManager (to the staker), if necessary. - * @param staker The account to be undelegated. - * @return withdrawalRoot The root of the newly queued withdrawal, if a withdrawal was queued. Otherwise just - * bytes32(0). - * - * @dev Reverts if the `staker` is also an operator, since operators are not allowed to undelegate from themselves. - * @dev Reverts if the caller is not the staker, nor the operator who the staker is delegated to, nor the operator's - * specified "delegationApprover" - * @dev Reverts if the `staker` is already undelegated. - */ - function undelegate(address staker) external returns (bytes32 withdrawalRoot); -} - -interface IEigenPodManager { - /** - * @notice Creates an EigenPod for the sender. - * @dev Function will revert if the `msg.sender` already has an EigenPod. - * @dev Returns EigenPod address - */ - function createPod() external returns (address); - - /** - * @notice Stakes for a new beacon chain validator on the sender's EigenPod. - * Also creates an EigenPod for the sender if they don't have one already. - * @param pubkey The 48 bytes public key of the beacon chain validator. - * @param signature The validator's signature of the deposit data. - * @param depositDataRoot The root/hash of the deposit data for the validator's deposit. - */ - function stake(bytes calldata pubkey, bytes calldata signature, bytes32 depositDataRoot) external payable; - - /// @notice Returns the address of the `podOwner`'s EigenPod (whether it is deployed yet or not). - function getPod(address podOwner) external returns (address); -} - -interface IDelayedWithdrawalRouter { - /** - * @notice Called in order to withdraw delayed withdrawals made to the `recipient` that have passed the - * `withdrawalDelayBlocks` period. - * @param recipient The address to claim delayedWithdrawals for. - * @param maxNumberOfDelayedWithdrawalsToClaim Used to limit the maximum number of delayedWithdrawals to loop through - * claiming. - * @dev - * WARNING: Note that the caller of this function cannot control where the funds are sent, but they can control - * when the - * funds are sent once the withdrawal becomes claimable. - */ - function claimDelayedWithdrawals(address recipient, uint256 maxNumberOfDelayedWithdrawalsToClaim) external; - - /** - * @notice Creates a delayed withdrawal for `msg.value` to the `recipient`. - * @dev Only callable by the `podOwner`'s EigenPod contract. - */ - function createDelayedWithdrawal(address podOwner, address recipient) external; - - /// @notice Owner-only function for modifying the value of the `withdrawalDelayBlocks` variable. - function setWithdrawalDelayBlocks(uint256 newValue) external; -} - -interface IEigenPod { - function activateRestaking() external; - - /// @notice Called by the pod owner to withdraw the balance of the pod when `hasRestaked` is set to false - function withdrawBeforeRestaking() external; - - /// @notice Called by the pod owner to withdraw the nonBeaconChainETHBalanceWei - function withdrawNonBeaconChainETHBalanceWei(address recipient, uint256 amountToWithdraw) external; - - /// @notice called by owner of a pod to remove any ERC20s deposited in the pod - function recoverTokens(ERC20[] memory tokenList, uint256[] memory amountsToWithdraw, address recipient) external; - - /// @notice The single EigenPodManager for EigenLayer - function eigenPodManager() external view returns (IEigenPodManager); - - /// @notice The owner of this EigenPod - function podOwner() external view returns (address); - - /// @notice an indicator of whether or not the podOwner has ever "fully restaked" by successfully calling - /// `verifyCorrectWithdrawalCredentials`. - function hasRestaked() external view returns (bool); - - /// @notice The max amount of eth, in gwei, that can be restaked per validator - function MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR() external view returns (uint64); - - /// @notice the amount of execution layer ETH in this contract that is staked in EigenLayer (i.e. withdrawn from - /// beaconchain but not EigenLayer), - function withdrawableRestakedExecutionLayerGwei() external view returns (uint64); - - /// @notice any ETH deposited into the EigenPod contract via the `receive` fallback function - function nonBeaconChainETHBalanceWei() external view returns (uint256); - - /// @notice Used to initialize the pointers to contracts crucial to the pod's functionality, in beacon proxy - /// construction from EigenPodManager - function initialize(address owner) external; - - /// @notice Called by EigenPodManager when the owner wants to create another ETH validator. - function stake(bytes calldata pubkey, bytes calldata signature, bytes32 depositDataRoot) external payable; -} +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import {ERC20} from "solmate/tokens/ERC20.sol"; + +interface IEigenLayerUtils { + // @notice Struct that bundles together a signature and an expiration time for the signature. Used primarily for stack + // management. + struct SignatureWithExpiry { + // the signature itself, formatted as a single bytes object + bytes signature; + // the expiration timestamp (UTC) of the signature + uint256 expiry; + } + + // @notice Struct that bundles together a signature, a salt for uniqueness, and an expiration time for the signature. + // Used primarily for stack management. + struct SignatureWithSaltAndExpiry { + // the signature itself, formatted as a single bytes object + bytes signature; + // the salt used to generate the signature + bytes32 salt; + // the expiration timestamp (UTC) of the signature + uint256 expiry; + } +} + +interface IDelegationManager is IEigenLayerUtils { + /** + * @notice Caller delegates their stake to an operator. + * @param operator The account (`msg.sender`) is delegating its assets to for use in serving applications built on + * EigenLayer. + * @param approverSignatureAndExpiry Verifies the operator approves of this delegation + * @param approverSalt A unique single use value tied to an individual signature. + * @dev The approverSignatureAndExpiry is used in the event that: + * 1) the operator's `delegationApprover` address is set to a non-zero value. + * AND + * 2) neither the operator nor their `delegationApprover` is the `msg.sender`, since in the event that the + * operator + * or their delegationApprover is the `msg.sender`, then approval is assumed. + * @dev In the event that `approverSignatureAndExpiry` is not checked, its content is ignored entirely; it's + * recommended to use an empty input + * in this case to save on complexity + gas costs + */ + function delegateTo(address operator, SignatureWithExpiry memory approverSignatureAndExpiry, bytes32 approverSalt) + external; + + /** + * @notice Undelegates the staker from the operator who they are delegated to. Puts the staker into the "undelegation + * limbo" mode of the EigenPodManager + * and queues a withdrawal of all of the staker's shares in the StrategyManager (to the staker), if necessary. + * @param staker The account to be undelegated. + * @return withdrawalRoot The root of the newly queued withdrawal, if a withdrawal was queued. Otherwise just + * bytes32(0). + * + * @dev Reverts if the `staker` is also an operator, since operators are not allowed to undelegate from themselves. + * @dev Reverts if the caller is not the staker, nor the operator who the staker is delegated to, nor the operator's + * specified "delegationApprover" + * @dev Reverts if the `staker` is already undelegated. + */ + function undelegate(address staker) external returns (bytes32 withdrawalRoot); +} + +interface IEigenPodManager { + /** + * @notice Creates an EigenPod for the sender. + * @dev Function will revert if the `msg.sender` already has an EigenPod. + * @dev Returns EigenPod address + */ + function createPod() external returns (address); + + /** + * @notice Stakes for a new beacon chain validator on the sender's EigenPod. + * Also creates an EigenPod for the sender if they don't have one already. + * @param pubkey The 48 bytes public key of the beacon chain validator. + * @param signature The validator's signature of the deposit data. + * @param depositDataRoot The root/hash of the deposit data for the validator's deposit. + */ + function stake(bytes calldata pubkey, bytes calldata signature, bytes32 depositDataRoot) external payable; + + /// @notice Returns the address of the `podOwner`'s EigenPod (whether it is deployed yet or not). + function getPod(address podOwner) external returns (address); +} + +interface IDelayedWithdrawalRouter { + /** + * @notice Called in order to withdraw delayed withdrawals made to the `recipient` that have passed the + * `withdrawalDelayBlocks` period. + * @param recipient The address to claim delayedWithdrawals for. + * @param maxNumberOfDelayedWithdrawalsToClaim Used to limit the maximum number of delayedWithdrawals to loop through + * claiming. + * @dev + * WARNING: Note that the caller of this function cannot control where the funds are sent, but they can control + * when the + * funds are sent once the withdrawal becomes claimable. + */ + function claimDelayedWithdrawals(address recipient, uint256 maxNumberOfDelayedWithdrawalsToClaim) external; + + /** + * @notice Creates a delayed withdrawal for `msg.value` to the `recipient`. + * @dev Only callable by the `podOwner`'s EigenPod contract. + */ + function createDelayedWithdrawal(address podOwner, address recipient) external; + + /// @notice Owner-only function for modifying the value of the `withdrawalDelayBlocks` variable. + function setWithdrawalDelayBlocks(uint256 newValue) external; +} + +interface IEigenPod { + function activateRestaking() external; + + /// @notice Called by the pod owner to withdraw the balance of the pod when `hasRestaked` is set to false + function withdrawBeforeRestaking() external; + + /// @notice Called by the pod owner to withdraw the nonBeaconChainETHBalanceWei + function withdrawNonBeaconChainETHBalanceWei(address recipient, uint256 amountToWithdraw) external; + + /// @notice called by owner of a pod to remove any ERC20s deposited in the pod + function recoverTokens(ERC20[] memory tokenList, uint256[] memory amountsToWithdraw, address recipient) external; + + /// @notice The single EigenPodManager for EigenLayer + function eigenPodManager() external view returns (IEigenPodManager); + + /// @notice The owner of this EigenPod + function podOwner() external view returns (address); + + /// @notice an indicator of whether or not the podOwner has ever "fully restaked" by successfully calling + /// `verifyCorrectWithdrawalCredentials`. + function hasRestaked() external view returns (bool); + + /// @notice The max amount of eth, in gwei, that can be restaked per validator + function MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR() external view returns (uint64); + + /// @notice the amount of execution layer ETH in this contract that is staked in EigenLayer (i.e. withdrawn from + /// beaconchain but not EigenLayer), + function withdrawableRestakedExecutionLayerGwei() external view returns (uint64); + + /// @notice any ETH deposited into the EigenPod contract via the `receive` fallback function + function nonBeaconChainETHBalanceWei() external view returns (uint256); + + /// @notice Used to initialize the pointers to contracts crucial to the pod's functionality, in beacon proxy + /// construction from EigenPodManager + function initialize(address owner) external; + + /// @notice Called by EigenPodManager when the owner wants to create another ETH validator. + function stake(bytes calldata pubkey, bytes calldata signature, bytes32 depositDataRoot) external payable; +} diff --git a/src/interfaces/IweETH.sol b/src/interfaces/external/IweETH.sol similarity index 97% rename from src/interfaces/IweETH.sol rename to src/interfaces/external/IweETH.sol index 199a460..f5b85e0 100644 --- a/src/interfaces/IweETH.sol +++ b/src/interfaces/external/IweETH.sol @@ -1,9 +1,9 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.19; - -interface IweETH { - function wrap(uint256 _eETHAmount) external returns (uint256); - function getEETHByWeETH(uint256 _weETHAmount) external view returns (uint256); - function getWeETHByeETH(uint256 _eETHAmount) external view returns (uint256); - function eETH() external view returns (address); -} +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +interface IweETH { + function wrap(uint256 _eETHAmount) external returns (uint256); + function getEETHByWeETH(uint256 _weETHAmount) external view returns (uint256); + function getWeETHByeETH(uint256 _eETHAmount) external view returns (uint256); + function eETH() external view returns (address); +} diff --git a/src/interfaces/IwstETH.sol b/src/interfaces/external/IwstETH.sol similarity index 96% rename from src/interfaces/IwstETH.sol rename to src/interfaces/external/IwstETH.sol index db7446e..e3ceebe 100644 --- a/src/interfaces/IwstETH.sol +++ b/src/interfaces/external/IwstETH.sol @@ -1,7 +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); -} +// 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); +} diff --git a/src/interfaces/IPullSplit.sol b/src/interfaces/external/splits/IPullSplit.sol similarity index 71% rename from src/interfaces/IPullSplit.sol rename to src/interfaces/external/splits/IPullSplit.sol index fb3bf47..96a516b 100644 --- a/src/interfaces/IPullSplit.sol +++ b/src/interfaces/external/splits/IPullSplit.sol @@ -3,6 +3,12 @@ pragma solidity ^0.8.19; interface IPullSplit { + struct Call { + address to; + uint256 value; + bytes data; + } + struct PullSplitConfiguration { address[] recipients; uint256[] allocations; @@ -24,5 +30,10 @@ interface IPullSplit { address _distributor ) external; + function execCalls(Call[] calldata _calls) + external + payable + returns (uint256 blockNumber, bytes[] memory returnData); + function SPLITS_WAREHOUSE() external view returns (address); } \ No newline at end of file diff --git a/src/interfaces/ISplitFactory.sol b/src/interfaces/external/splits/ISplitFactory.sol similarity index 100% rename from src/interfaces/ISplitFactory.sol rename to src/interfaces/external/splits/ISplitFactory.sol diff --git a/src/interfaces/ISplitMain.sol b/src/interfaces/external/splits/ISplitMain.sol similarity index 100% rename from src/interfaces/ISplitMain.sol rename to src/interfaces/external/splits/ISplitMain.sol diff --git a/src/interfaces/ISplitMainV2.sol b/src/interfaces/external/splits/ISplitMainV2.sol similarity index 100% rename from src/interfaces/ISplitMainV2.sol rename to src/interfaces/external/splits/ISplitMainV2.sol diff --git a/src/interfaces/ISplitsWarehouse.sol b/src/interfaces/external/splits/ISplitsWarehouse.sol similarity index 100% rename from src/interfaces/ISplitsWarehouse.sol rename to src/interfaces/external/splits/ISplitsWarehouse.sol diff --git a/src/lido/ObolLidoSplit.sol b/src/lido/ObolLidoSplit.sol index 2753899..66996db 100644 --- a/src/lido/ObolLidoSplit.sol +++ b/src/lido/ObolLidoSplit.sol @@ -1,54 +1,54 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity 0.8.19; - -import {ERC20} from "solmate/tokens/ERC20.sol"; -import {SafeTransferLib} from "solmate/utils/SafeTransferLib.sol"; -import {Clone} from "solady/utils/Clone.sol"; -import {IwstETH} from "src/interfaces/IwstETH.sol"; -import {BaseSplit} from "../base/BaseSplit.sol"; - -/// @title ObolLidoSplit -/// @author Obol -/// @notice A wrapper for 0xsplits/split-contracts SplitWallet that transforms -/// stETH token to wstETH token because stETH is a rebasing token -/// @dev Wraps stETH to wstETH and transfers to defined SplitWallet address -contract ObolLidoSplit is BaseSplit { - /// @notice stETH token - ERC20 public immutable stETH; - - /// @notice wstETH token - ERC20 public immutable wstETH; - - /// @notice Constructor - /// @param _feeRecipient address to receive fee - /// @param _feeShare fee share scaled by PERCENTAGE_SCALE - /// @param _stETH stETH address - /// @param _wstETH wstETH address - constructor(address _feeRecipient, uint256 _feeShare, ERC20 _stETH, ERC20 _wstETH) BaseSplit(_feeRecipient, _feeShare) { - stETH = _stETH; - wstETH = _wstETH; - } - - function _beforeRescueFunds(address tokenAddress) internal view override { - // we check weETH here so rescueFunds can't be used - // to bypass fee - if (tokenAddress == address(stETH) || tokenAddress == address(wstETH)) revert Invalid_Address(); - } - - /// Wraps the current stETH token balance to wstETH - /// transfers the wstETH balance to withdrawalAddress for distribution - function _beforeDistribute() internal override returns (address tokenAddress, uint256 amount) { - tokenAddress = address(wstETH); - - // get current balance - uint256 balance = stETH.balanceOf(address(this)); - // approve the wstETH - stETH.approve(address(wstETH), balance); - // wrap into wstETH - // we ignore the return value - IwstETH(address(wstETH)).wrap(balance); - // we use balanceOf here in case some wstETH is stuck in the - // contract we would be able to rescue it - amount = ERC20(wstETH).balanceOf(address(this)); - } -} +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.19; + +import {ERC20} from "solmate/tokens/ERC20.sol"; +import {SafeTransferLib} from "solmate/utils/SafeTransferLib.sol"; +import {Clone} from "solady/utils/Clone.sol"; +import {IwstETH} from "src/interfaces/external/IwstETH.sol"; +import {BaseSplit} from "../base/BaseSplit.sol"; + +/// @title ObolLidoSplit +/// @author Obol +/// @notice A wrapper for 0xsplits/split-contracts SplitWallet that transforms +/// stETH token to wstETH token because stETH is a rebasing token +/// @dev Wraps stETH to wstETH and transfers to defined SplitWallet address +contract ObolLidoSplit is BaseSplit { + /// @notice stETH token + ERC20 public immutable stETH; + + /// @notice wstETH token + ERC20 public immutable wstETH; + + /// @notice Constructor + /// @param _feeRecipient address to receive fee + /// @param _feeShare fee share scaled by PERCENTAGE_SCALE + /// @param _stETH stETH address + /// @param _wstETH wstETH address + constructor(address _feeRecipient, uint256 _feeShare, ERC20 _stETH, ERC20 _wstETH) BaseSplit(_feeRecipient, _feeShare) { + stETH = _stETH; + wstETH = _wstETH; + } + + function _beforeRescueFunds(address tokenAddress) internal view override { + // we check weETH here so rescueFunds can't be used + // to bypass fee + if (tokenAddress == address(stETH) || tokenAddress == address(wstETH)) revert Invalid_Address(); + } + + /// Wraps the current stETH token balance to wstETH + /// transfers the wstETH balance to withdrawalAddress for distribution + function _beforeDistribute() internal override returns (address tokenAddress, uint256 amount) { + tokenAddress = address(wstETH); + + // get current balance + uint256 balance = stETH.balanceOf(address(this)); + // approve the wstETH + stETH.approve(address(wstETH), balance); + // wrap into wstETH + // we ignore the return value + IwstETH(address(wstETH)).wrap(balance); + // we use balanceOf here in case some wstETH is stuck in the + // contract we would be able to rescue it + amount = ERC20(wstETH).balanceOf(address(this)); + } +} diff --git a/src/lido/ObolLidoSplitFactory.sol b/src/lido/ObolLidoSplitFactory.sol index bdb872f..29ef787 100644 --- a/src/lido/ObolLidoSplitFactory.sol +++ b/src/lido/ObolLidoSplitFactory.sol @@ -1,44 +1,44 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity 0.8.19; - -import {LibClone} from "solady/utils/LibClone.sol"; -import {ERC20} from "solmate/tokens/ERC20.sol"; -import {BaseSplitFactory} from "../base/BaseSplitFactory.sol"; -import "./ObolLidoSplit.sol"; - -/// @title ObolLidoSplitFactory -/// @author Obol -/// @notice A factory contract for cheaply deploying ObolLidoSplit. -/// @dev The address returned should be used to as reward address for Lido -contract ObolLidoSplitFactory is BaseSplitFactory { - - /// ----------------------------------------------------------------------- - /// libraries - /// ----------------------------------------------------------------------- - using LibClone for address; - - /// ----------------------------------------------------------------------- - /// storage - /// ----------------------------------------------------------------------- - - /// @dev lido split implementation - ObolLidoSplit public immutable lidoSplitImpl; - - constructor(address _feeRecipient, uint256 _feeShare, ERC20 _stETH, ERC20 _wstETH) { - lidoSplitImpl = new ObolLidoSplit(_feeRecipient, _feeShare, _stETH, _wstETH); - } - - // Creates a wrapper for splitWallet that transforms stETH token into - /// wstETH - /// @dev Create a new collector - /// @dev address(0) is used to represent ETH - /// @param withdrawalAddress Address of the splitWallet to transfer wstETH to - /// @return collector Address of the wrappper split - function createCollector(address, address withdrawalAddress) external override returns (address collector) { - if (withdrawalAddress == address(0)) revert Invalid_Address(); - - collector = address(lidoSplitImpl).clone(abi.encodePacked(withdrawalAddress)); - - emit CreateSplit(address(0), collector); - } -} +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.19; + +import {LibClone} from "solady/utils/LibClone.sol"; +import {ERC20} from "solmate/tokens/ERC20.sol"; +import {BaseSplitFactory} from "../base/BaseSplitFactory.sol"; +import "./ObolLidoSplit.sol"; + +/// @title ObolLidoSplitFactory +/// @author Obol +/// @notice A factory contract for cheaply deploying ObolLidoSplit. +/// @dev The address returned should be used to as reward address for Lido +contract ObolLidoSplitFactory is BaseSplitFactory { + + /// ----------------------------------------------------------------------- + /// libraries + /// ----------------------------------------------------------------------- + using LibClone for address; + + /// ----------------------------------------------------------------------- + /// storage + /// ----------------------------------------------------------------------- + + /// @dev lido split implementation + ObolLidoSplit public immutable lidoSplitImpl; + + constructor(address _feeRecipient, uint256 _feeShare, ERC20 _stETH, ERC20 _wstETH) { + lidoSplitImpl = new ObolLidoSplit(_feeRecipient, _feeShare, _stETH, _wstETH); + } + + // Creates a wrapper for splitWallet that transforms stETH token into + /// wstETH + /// @dev Create a new collector + /// @dev address(0) is used to represent ETH + /// @param withdrawalAddress Address of the splitWallet to transfer wstETH to + /// @return collector Address of the wrappper split + function createCollector(address, address withdrawalAddress) external override returns (address collector) { + if (withdrawalAddress == address(0)) revert Invalid_Address(); + + collector = address(lidoSplitImpl).clone(abi.encodePacked(withdrawalAddress)); + + emit CreateSplit(address(0), collector); + } +} diff --git a/src/owr/ObolErc1155Recipient.sol b/src/owr/ObolErc1155Recipient.sol index f8dedd4..db2684d 100644 --- a/src/owr/ObolErc1155Recipient.sol +++ b/src/owr/ObolErc1155Recipient.sol @@ -1,17 +1,18 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity 0.8.19; -import {ERC1155} from "solady/tokens/ERC1155.sol"; +import {ERC20} from "solmate/tokens/ERC20.sol"; import {Ownable} from "solady/auth/Ownable.sol"; +import {ERC1155} from "solady/tokens/ERC1155.sol"; import {LibString} from "solady/utils/LibString.sol"; -import {IDepositContract} from "../interfaces/IDepositContract.sol"; -import {IOptimisticWithdrawalRecipient} from "../interfaces/IOptimisticWithdrawalRecipient.sol"; -import {ERC20} from "solmate/tokens/ERC20.sol"; import {SafeTransferLib} from "solady/utils/SafeTransferLib.sol"; import {IERC165, IERC1155Receiver} from "src/interfaces/IERC1155Receiver.sol"; -import {ISplitMain, SplitConfiguration} from "src/interfaces/ISplitMain.sol"; -import {IPullSplit} from "src/interfaces/IPullSplit.sol"; -import {ISplitsWarehouse} from "src/interfaces/ISplitsWarehouse.sol"; + +import {IPullSplit} from "src/interfaces/external/splits/IPullSplit.sol"; +import {IDepositContract} from "../interfaces/external/IDepositContract.sol"; +import {ISplitsWarehouse} from "src/interfaces/external/splits/ISplitsWarehouse.sol"; +import {ISplitMain, SplitConfiguration} from "src/interfaces/external/splits/ISplitMain.sol"; +import {IOptimisticPullWithdrawalRecipient} from "../interfaces/IOptimisticPullWithdrawalRecipient.sol"; /// @notice OWR principal recipient /// @dev handles rewards and principal of OWR @@ -31,16 +32,17 @@ contract ObolErc1155Recipient is ERC1155, Ownable, IERC1155Receiver { address operator; } - uint256 public lastId; - uint256 public partitionCount; + uint256 public tokenId; + uint256 public partitionId; mapping(uint256 _partitionId => Partition) public partitions; mapping(uint256 _partitionId => uint256[] _tokenIds) public partitionTokens; - mapping(address _owr => uint256 _partitionId) public owrsPartition; + mapping(address _owr => uint256 _partitionId) public owrsPartition; mapping(uint256 _tokenId => uint256 _partitionId) public tokensPartition; mapping(uint256 _tokenId => address _owner) public ownerOf; - mapping(address _owner => uint256 _amount) public claimable; + mapping(address _owner => mapping(address _token => uint256 _amount)) public claimable; + mapping(address _token => uint256 _claimable) public totalClaimable; mapping(uint256 id => uint256) public totalSupply; uint256 public totalSupplyAll; @@ -49,29 +51,25 @@ contract ObolErc1155Recipient is ERC1155, Ownable, IERC1155Receiver { IDepositContract public immutable depositContract; string private _baseUri; - address private constant ETH_TOKEN_ADDRESS = address(0x0); + address private constant ETH_ADDRESS = address(0x0); uint256 private constant ETH_DEPOSIT_AMOUNT = 32 ether; uint256 private constant MIN_ETH_EXIT_AMOUNT = 16 ether; - error PartitionSupplyReached(); - error DepositAmountNotValid(); - error PartitionNotValid(); error OwrNotValid(); - - error LengthMismatch(); - error InvalidTokenAmount(); - error InvalidOwner(); - error InvalidOWR(); error ClaimFailed(); - error InvalidLastSupply(); + error InvalidOwner(); error TransferFailed(); + error PartitionNotValid(); + error DepositAmountNotValid(); + error PartitionSupplyReached(); error InvalidBurnAmount(uint256 necessary, uint256 received); event PartitionCreated(address indexed _owr, uint256 indexed _partitionId, uint256 indexed _maxSupply); event Minted(uint256 indexed _partitionId, uint256 indexed _mintedId, address indexed _sender); + event Claimed(address indexed _account, address indexed _token, uint256 _amount); + event RewardsDistributed(address indexed _token, uint256 indexed _tokenId, address indexed _account, uint256 _amount, uint256 _totalRewards); constructor(string memory baseUri_, address _owner, address _depositContract) { - lastId = 1; _baseUri = baseUri_; depositContract = IDepositContract(_depositContract); @@ -90,7 +88,7 @@ contract ObolErc1155Recipient is ERC1155, Ownable, IERC1155Receiver { /// @dev Returns true if `msg.sender` is the owner of the contract function isOwnerOf(uint256 _tokenId) public view returns (bool) { - return balanceOf(msg.sender, _tokenId) > 0; + return ownerOf[_tokenId] == msg.sender; } /// @dev Returns max supply for id @@ -99,20 +97,29 @@ contract ObolErc1155Recipient is ERC1155, Ownable, IERC1155Receiver { return partitions[_partition].maxSupply; } + /// @dev Returns active supply for id + function getActiveSupply(uint256 _tokenId) public view returns (uint256) { + uint256 _partition = tokensPartition[_tokenId]; + return partitionTokens[_partition].length; + } + + function getPartitionTokensLength(uint256 _partitionId) external view returns (uint256) { + return partitionTokens[_partitionId].length; + } + /// ----------------------------------------------------------------------- /// functions - public & external /// ----------------------------------------------------------------------- /// @notice creates a new partition - /// @dev TODO: TBD DOS risk /// @param maxSupply the maximum number of unique tokens /// @param owr the Optimistic Withdrawal Recipient address - function createPartition(uint256 maxSupply, address owr) external { - uint256 _id = partitionCount; + function createPartition(uint256 maxSupply, address owr) external onlyOwner { + uint256 _id = partitionId; partitions[_id] = Partition({maxSupply: maxSupply, owr: owr, operator: msg.sender}); owrsPartition[owr] = _id; - partitionCount++; + partitionId++; emit PartitionCreated(owr, _id, maxSupply); } @@ -126,8 +133,14 @@ contract ObolErc1155Recipient is ERC1155, Ownable, IERC1155Receiver { if (partitionTokens[_partitionId].length + 1 > partitions[_partitionId].maxSupply) revert PartitionSupplyReached(); if (msg.value != ETH_DEPOSIT_AMOUNT) revert DepositAmountNotValid(); + // deposit first to ETH deposit contract + depositContract.deposit{value: ETH_DEPOSIT_AMOUNT}( + depositInfo.pubkey, depositInfo.withdrawal_credentials, depositInfo.sig, depositInfo.root + ); + // retrieve id - mintedId = _incrementId(); + mintedId = tokenId; + tokenId++; // add partition details partitionTokens[_partitionId].push(mintedId); @@ -137,11 +150,6 @@ contract ObolErc1155Recipient is ERC1155, Ownable, IERC1155Receiver { totalSupply[mintedId]++; totalSupplyAll++; - // deposit to ETH deposit contract - depositContract.deposit{value: ETH_DEPOSIT_AMOUNT}( - depositInfo.pubkey, depositInfo.withdrawal_credentials, depositInfo.sig, depositInfo.root - ); - // mint to sender _mint(msg.sender, mintedId, 1, ""); ownerOf[mintedId] = msg.sender; @@ -159,12 +167,15 @@ contract ObolErc1155Recipient is ERC1155, Ownable, IERC1155Receiver { address _owr = partitions[tokensPartition[_tokenId]].owr; if (_owr == address(0)) revert OwrNotValid(); + // retrieve ETH from the OWR uint256 ethBalanceBefore = address(this).balance; - IOptimisticWithdrawalRecipient(_owr).distributeFunds(); + IOptimisticPullWithdrawalRecipient(_owr).distributeFunds(); + IOptimisticPullWithdrawalRecipient(_owr).withdraw(address(this)); uint256 ethBalanceAfter = address(this).balance; uint256 ethReceived = ethBalanceAfter - ethBalanceBefore; - // TODO: check `ethReceived` amount + // TODO: what if ethReceived > 32 + // : should we distribute 32 to sender and remaining split between active validators ? if (ethReceived < MIN_ETH_EXIT_AMOUNT) revert InvalidBurnAmount(MIN_ETH_EXIT_AMOUNT, ethReceived); _burn(msg.sender, _tokenId, 1); @@ -176,59 +187,110 @@ contract ObolErc1155Recipient is ERC1155, Ownable, IERC1155Receiver { if (!sent) revert TransferFailed(); } + /// @notice triggers `OWR.distributeFunds` and updates claimable balances for partition /// @param _tokenId token id - /// @param _splitConfig pull split configuration + /// @param _distributor `PullSplit` distributor address + /// @param _splitConfig `PullSplit` configuration function distributeRewards( uint256 _tokenId, address _distributor, IPullSplit.PullSplitConfiguration calldata _splitConfig ) external { + // validate params uint256 _partitionId = tokensPartition[_tokenId]; address _owr = partitions[tokensPartition[_tokenId]].owr; if (_owr == address(0)) revert OwrNotValid(); - // call .distribute() on OWR - uint256 balanceBefore = _getTokenBalance(_owr); - IOptimisticWithdrawalRecipient(_owr).distributeFunds(); + // call `.distribute()` on OWR and `distribute()` on PullSplit + address _token = IOptimisticPullWithdrawalRecipient(_owr).token(); + uint256 balanceBefore = _getTokenBalance(_token); + IOptimisticPullWithdrawalRecipient(_owr).distributeFunds(); _distributeSplitsRewards(_owr, _distributor, _splitConfig); - uint256 balanceAfter = _getTokenBalance(_owr); + uint256 balanceAfter = _getTokenBalance(_token); + // update `claimable` for partition's active supply uint256 _totalClaimable = balanceAfter - balanceBefore; + totalClaimable[_token] += _totalClaimable; + + // update active validators claimable amounts if (_totalClaimable > 0) { uint256 count = partitionTokens[_partitionId].length; uint256 _reward = _totalClaimable / count; for (uint256 i; i < count; i++) { address _owner = ownerOf[partitionTokens[_partitionId][i]]; - claimable[_owner] += _reward; + claimable[_owner][_token] += _reward; + emit RewardsDistributed(_token, _tokenId, _owner, _reward, _totalClaimable); } } } - + /// @notice claim rewards - function claim(address _user) external { - if (claimable[_user] > 0) { - (bool sent,) = _user.call{value: claimable[_user]}(""); - if (!sent) revert TransferFailed(); + /// @dev for ETH, `_token` should be `address(0)` + /// @param _user the account to claim for + /// @param _token the token address + function claim(address _user, address _token) external { + uint256 _amount = claimable[_user][_token]; + + // send `_token` to user + if (_amount > 0) { + if (_token == ETH_ADDRESS) _user.safeTransferETH(_amount); + else _token.safeTransfer(_user, _amount); } - claimable[_user] = 0; + + // reset `claimable` for `_user` and `_token` + claimable[_user][_token] = 0; + totalClaimable[_token] -= _amount; + + emit Claimed(_user, _token, _amount); } /// ----------------------------------------------------------------------- - /// functions - private + /// functions - owner /// ----------------------------------------------------------------------- + /// @notice recover airdropped tokens + /// @dev for ETH, `_token` should be `address(0)` + /// @param _token the token address + function recoverTokens(address _token) external onlyOwner { + // validate token amounts + uint256 _balance = ERC20(_token).balanceOf(address(this)); + if (_balance <= totalClaimable[_token]) revert ClaimFailed(); + + // compoute airdropped amount + uint256 _amount = _balance - totalClaimable[_token]; + + // send `_token` to owner + if (_amount > 0) { + if (_token == ETH_ADDRESS) msg.sender.safeTransferETH(_amount); + else _token.safeTransfer(msg.sender, _amount); + } + } - //TODO: refactor this + /// ----------------------------------------------------------------------- + /// functions - private + /// ----------------------------------------------------------------------- function _distributeSplitsRewards( address owr, address _distributor, IPullSplit.PullSplitConfiguration calldata _splitConfig ) private { - (, address _split,) = IOptimisticWithdrawalRecipient(owr).getTranches(); - address _token = IOptimisticWithdrawalRecipient(owr).token(); - + (, address _split,) = IOptimisticPullWithdrawalRecipient(owr).getTranches(); + address _token = IOptimisticPullWithdrawalRecipient(owr).token(); + + // retrieve funds from OWR + IPullSplit.Call[] memory _calls = new IPullSplit.Call[](1); + _calls[0] = IPullSplit.Call({ + to: owr, + value: 0, + data: abi.encodeWithSelector(IOptimisticPullWithdrawalRecipient.withdraw.selector, _split) + }); + IPullSplit(_split).execCalls(_calls); + + // distribute IPullSplit(_split).distribute(_splitConfig, _token, _distributor); address warehouse = IPullSplit(_split).SPLITS_WAREHOUSE(); + + // retrieve funds from PullSplits ISplitsWarehouse(warehouse).withdraw(address(this), _token); } @@ -241,7 +303,6 @@ contract ObolErc1155Recipient is ERC1155, Ownable, IERC1155Receiver { // skip for mint or burn if (from == address(0) || to == address(0)) return; - // claim before transfer uint256 length = ids.length; for (uint256 i; i < length; i++) { ownerOf[ids[i]] = to; @@ -255,15 +316,9 @@ contract ObolErc1155Recipient is ERC1155, Ownable, IERC1155Receiver { return true; } - function _incrementId() public returns (uint256 mintedId) { - mintedId = lastId; - lastId++; - } - - function _getTokenBalance(address owr) private view returns (uint256 balance) { - address token = IOptimisticWithdrawalRecipient(owr).token(); - if (token == ETH_TOKEN_ADDRESS) balance = address(this).balance; - else balance = ERC20(token).balanceOf(address(this)); + function _getTokenBalance(address _token) private view returns (uint256 balance) { + if (_token == ETH_ADDRESS) balance = address(this).balance; + else balance = ERC20(_token).balanceOf(address(this)); } /// ----------------------------------------------------------------------- @@ -285,4 +340,4 @@ contract ObolErc1155Recipient is ERC1155, Ownable, IERC1155Receiver { { return this.onERC1155BatchReceived.selector; } -} +} \ No newline at end of file diff --git a/src/owr/OptimisticPullWithdrawalRecipient.sol b/src/owr/OptimisticPullWithdrawalRecipient.sol new file mode 100644 index 0000000..c34490f --- /dev/null +++ b/src/owr/OptimisticPullWithdrawalRecipient.sol @@ -0,0 +1,317 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.19; + +import {Clone} from "solady/utils/Clone.sol"; +import {ERC20} from "solmate/tokens/ERC20.sol"; +import {SafeTransferLib} from "solady/utils/SafeTransferLib.sol"; + +/// @title OptimisticPullOnlyWithdrawalRecipient +/// @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. +contract OptimisticPullWithdrawalRecipient 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 principalPayout Amount of principal paid out + /// @param rewardPayout Amount of reward paid out + /// pulling + event DistributeFunds(uint256 principalPayout, uint256 rewardPayout); + + /// 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 ONE_WORD = 32; + uint256 internal constant ADDRESS_BITS = 160; + + /// @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); + } + } + + /// ----------------------------------------------------------------------- + /// storage - mutables + /// ----------------------------------------------------------------------- + + /// 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 principal + /// @dev Would be less than or equal to amount of stake + /// @dev ERC20s with very large decimals may overflow & cause issues + uint128 public claimedPrincipalFunds; + + /// 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 { + /// checks + + /// effects + + // load storage into memory + // fetch the token we want to distribute + address _token = token(); + uint256 currentbalance = _token == ETH_ADDRESS ? address(this).balance : ERC20(_token).balanceOf(address(this)); + uint256 _claimedPrincipalFunds = uint256(claimedPrincipalFunds); + uint256 _memoryFundsPendingWithdrawal = uint256(fundsPendingWithdrawal); + + uint256 _fundsToBeDistributed = currentbalance - _memoryFundsPendingWithdrawal; + + (address principalRecipient, address rewardRecipient, uint256 amountOfPrincipalStake) = getTranches(); + + // determine which recipeint is getting paid based on funds to be + // distributed + uint256 _principalPayout = 0; + uint256 _rewardPayout = 0; + + unchecked { + // _claimedPrincipalFunds should always be <= amountOfPrincipalStake + uint256 principalStakeRemaining = amountOfPrincipalStake - _claimedPrincipalFunds; + + if (_fundsToBeDistributed >= BALANCE_CLASSIFICATION_THRESHOLD && principalStakeRemaining > 0) { + if (_fundsToBeDistributed > principalStakeRemaining) { + // 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 + _principalPayout = _fundsToBeDistributed; + } + } else { + _rewardPayout = _fundsToBeDistributed; + } + } + + { + if (_fundsToBeDistributed > type(uint128).max) revert InvalidDistribution_TooLarge(); + // Write to storage + // the principal value + // it cannot overflow because _principalPayout < _fundsToBeDistributed + if (_principalPayout > 0) claimedPrincipalFunds += uint128(_principalPayout); + } + + /// 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) + + // pay out principal + _payout(principalRecipient, _principalPayout); + // pay out reward + _payout(rewardRecipient, _rewardPayout); + + if (_principalPayout > 0 || _rewardPayout > 0) { + // Write to storage + fundsPendingWithdrawal = uint128(_memoryFundsPendingWithdrawal + _principalPayout + _rewardPayout); + } + + emit DistributeFunds(_principalPayout, _rewardPayout); + } + + /// 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 principalRecipient, address rewardRecipient,) = getTranches(); + if (recipient != principalRecipient && recipient != rewardRecipient) { + revert InvalidTokenRecovery_InvalidRecipient(); + } + } else if (recipient != _recoveryAddress) { + revert InvalidTokenRecovery_InvalidRecipient(); + } + + /// effects + + /// 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); + } + + 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 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); + principalRecipient = address(uint160(tranche)); + amountOfPrincipalStake = tranche >> ADDRESS_BITS; + + rewardRecipient = 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 + /// ----------------------------------------------------------------------- + function _payout(address recipient, uint256 payoutAmount) internal { + if (payoutAmount > 0) { + pullBalances[recipient] += payoutAmount; + } + } +} \ No newline at end of file diff --git a/src/owr/OptimisticPullWithdrawalRecipientFactory.sol b/src/owr/OptimisticPullWithdrawalRecipientFactory.sol new file mode 100644 index 0000000..b30f4a1 --- /dev/null +++ b/src/owr/OptimisticPullWithdrawalRecipientFactory.sol @@ -0,0 +1,125 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.19; + +import {OptimisticPullWithdrawalRecipient} from "./OptimisticPullWithdrawalRecipient.sol"; +import {LibClone} from "solady/utils/LibClone.sol"; +import {IENSReverseRegistrar} from "../interfaces/external/IENSReverseRegistrar.sol"; + +/// @title OptimisticPullWithdrawalRecipientFactory +/// @author Obol +/// @notice A factory contract for cheaply deploying +/// OptimisticPullWithdrawalRecipient. +/// @dev This contract uses token = address(0) to refer to ETH. +contract OptimisticPullWithdrawalRecipientFactory { + /// ----------------------------------------------------------------------- + /// 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 OptimisticPullWithdrawalRecipient module is deployed + /// @param owr Address of newly created OptimisticPullWithdrawalRecipient 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; + + /// OptimisticWithdrawalRecipient implementation address + OptimisticPullWithdrawalRecipient public immutable owrImpl; + + /// ----------------------------------------------------------------------- + /// constructor + /// ----------------------------------------------------------------------- + + constructor( + string memory _ensName, + address _ensReverseRegistrar, + address _ensOwner + ) { + owrImpl = new OptimisticPullWithdrawalRecipient(); + IENSReverseRegistrar(_ensReverseRegistrar).setName(_ensName); + IENSReverseRegistrar(_ensReverseRegistrar).claim(_ensOwner); + } + + /// ----------------------------------------------------------------------- + /// 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 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 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 + function createOWRecipient( + address token, + address recoveryAddress, + address principalRecipient, + address rewardRecipient, + uint256 amountOfPrincipalStake + ) external returns (OptimisticPullWithdrawalRecipient owr) { + /// checks + + // ensure doesn't have address(0) + if (principalRecipient == address(0) || rewardRecipient == address(0)) revert Invalid__Recipients(); + // ensure threshold isn't zero + if (amountOfPrincipalStake == 0) revert Invalid__ZeroThreshold(); + // ensure threshold isn't too large + if (amountOfPrincipalStake > type(uint96).max) revert Invalid__ThresholdTooLarge(amountOfPrincipalStake); + + /// effects + uint256 principalData = (amountOfPrincipalStake << ADDRESS_BITS) | uint256(uint160(principalRecipient)); + uint256 rewardData = uint256(uint160(rewardRecipient)); + + // would not exceed contract size limits + // important to not reorder + bytes memory data = abi.encodePacked(token, recoveryAddress, principalData, rewardData); + owr = OptimisticPullWithdrawalRecipient(address(owrImpl).clone(data)); + + emit CreateOWRecipient(address(owr), token, recoveryAddress, principalRecipient, rewardRecipient, amountOfPrincipalStake); + } +} \ No newline at end of file diff --git a/src/owr/OptimisticWithdrawalRecipient.sol b/src/owr/OptimisticWithdrawalRecipient.sol index 4bd6c5a..a76b295 100644 --- a/src/owr/OptimisticWithdrawalRecipient.sol +++ b/src/owr/OptimisticWithdrawalRecipient.sol @@ -1,344 +1,344 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity 0.8.19; - -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 -/// 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. -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 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 principalPayout, uint256 rewardPayout, 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; - - /// @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); - } - } - - /// ----------------------------------------------------------------------- - /// storage - mutables - /// ----------------------------------------------------------------------- - - /// 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 principal - /// @dev Would be less than or equal to amount of stake - /// @dev ERC20s with very large decimals may overflow & cause issues - uint128 public claimedPrincipalFunds; - - /// 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 principalRecipient, address rewardRecipient,) = getTranches(); - if (recipient != principalRecipient && recipient != rewardRecipient) { - revert InvalidTokenRecovery_InvalidRecipient(); - } - } else if (recipient != _recoveryAddress) { - revert InvalidTokenRecovery_InvalidRecipient(); - } - - /// effects - - /// 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); - } - - 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 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); - principalRecipient = address(uint160(tranche)); - amountOfPrincipalStake = tranche >> ADDRESS_BITS; - - rewardRecipient = 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(); - uint256 currentbalance = _token == ETH_ADDRESS ? address(this).balance : ERC20(_token).balanceOf(address(this)); - uint256 _claimedPrincipalFunds = uint256(claimedPrincipalFunds); - uint256 _memoryFundsPendingWithdrawal = uint256(fundsPendingWithdrawal); - - uint256 _fundsToBeDistributed = currentbalance - _memoryFundsPendingWithdrawal; - - (address principalRecipient, address rewardRecipient, uint256 amountOfPrincipalStake) = getTranches(); - - // determine which recipeint is getting paid based on funds to be - // distributed - uint256 _principalPayout = 0; - uint256 _rewardPayout = 0; - - unchecked { - // _claimedPrincipalFunds should always be <= amountOfPrincipalStake - uint256 principalStakeRemaining = amountOfPrincipalStake - _claimedPrincipalFunds; - - if (_fundsToBeDistributed >= BALANCE_CLASSIFICATION_THRESHOLD && principalStakeRemaining > 0) { - if (_fundsToBeDistributed > principalStakeRemaining) { - // 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 - _principalPayout = _fundsToBeDistributed; - } - } else { - _rewardPayout = _fundsToBeDistributed; - } - } - - { - if (_fundsToBeDistributed > type(uint128).max) revert InvalidDistribution_TooLarge(); - // Write to storage - // the principal value - // it cannot overflow because _principalPayout < _fundsToBeDistributed - if (_principalPayout > 0) claimedPrincipalFunds += uint128(_principalPayout); - } - - /// 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) - - // pay out principal - _payout(_token, principalRecipient, _principalPayout, pullFlowFlag); - // pay out reward - _payout(_token, rewardRecipient, _rewardPayout, pullFlowFlag); - - if (pullFlowFlag == PULL) { - if (_principalPayout > 0 || _rewardPayout > 0) { - // Write to storage - fundsPendingWithdrawal = uint128(_memoryFundsPendingWithdrawal + _principalPayout + _rewardPayout); - } - } - - 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); - } - } - } +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.19; + +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 +/// 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. +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 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 principalPayout, uint256 rewardPayout, 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; + + /// @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); + } + } + + /// ----------------------------------------------------------------------- + /// storage - mutables + /// ----------------------------------------------------------------------- + + /// 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 principal + /// @dev Would be less than or equal to amount of stake + /// @dev ERC20s with very large decimals may overflow & cause issues + uint128 public claimedPrincipalFunds; + + /// 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 principalRecipient, address rewardRecipient,) = getTranches(); + if (recipient != principalRecipient && recipient != rewardRecipient) { + revert InvalidTokenRecovery_InvalidRecipient(); + } + } else if (recipient != _recoveryAddress) { + revert InvalidTokenRecovery_InvalidRecipient(); + } + + /// effects + + /// 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); + } + + 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 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); + principalRecipient = address(uint160(tranche)); + amountOfPrincipalStake = tranche >> ADDRESS_BITS; + + rewardRecipient = 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(); + uint256 currentbalance = _token == ETH_ADDRESS ? address(this).balance : ERC20(_token).balanceOf(address(this)); + uint256 _claimedPrincipalFunds = uint256(claimedPrincipalFunds); + uint256 _memoryFundsPendingWithdrawal = uint256(fundsPendingWithdrawal); + + uint256 _fundsToBeDistributed = currentbalance - _memoryFundsPendingWithdrawal; + + (address principalRecipient, address rewardRecipient, uint256 amountOfPrincipalStake) = getTranches(); + + // determine which recipeint is getting paid based on funds to be + // distributed + uint256 _principalPayout = 0; + uint256 _rewardPayout = 0; + + unchecked { + // _claimedPrincipalFunds should always be <= amountOfPrincipalStake + uint256 principalStakeRemaining = amountOfPrincipalStake - _claimedPrincipalFunds; + + if (_fundsToBeDistributed >= BALANCE_CLASSIFICATION_THRESHOLD && principalStakeRemaining > 0) { + if (_fundsToBeDistributed > principalStakeRemaining) { + // 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 + _principalPayout = _fundsToBeDistributed; + } + } else { + _rewardPayout = _fundsToBeDistributed; + } + } + + { + if (_fundsToBeDistributed > type(uint128).max) revert InvalidDistribution_TooLarge(); + // Write to storage + // the principal value + // it cannot overflow because _principalPayout < _fundsToBeDistributed + if (_principalPayout > 0) claimedPrincipalFunds += uint128(_principalPayout); + } + + /// 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) + + // pay out principal + _payout(_token, principalRecipient, _principalPayout, pullFlowFlag); + // pay out reward + _payout(_token, rewardRecipient, _rewardPayout, pullFlowFlag); + + if (pullFlowFlag == PULL) { + if (_principalPayout > 0 || _rewardPayout > 0) { + // Write to storage + fundsPendingWithdrawal = uint128(_memoryFundsPendingWithdrawal + _principalPayout + _rewardPayout); + } + } + + 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); + } + } + } } \ No newline at end of file diff --git a/src/owr/OptimisticWithdrawalRecipientFactory.sol b/src/owr/OptimisticWithdrawalRecipientFactory.sol index 0dee73f..b70541e 100644 --- a/src/owr/OptimisticWithdrawalRecipientFactory.sol +++ b/src/owr/OptimisticWithdrawalRecipientFactory.sol @@ -1,125 +1,125 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -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 -/// @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; - - /// OptimisticWithdrawalRecipient implementation address - OptimisticWithdrawalRecipient public immutable owrImpl; - - /// ----------------------------------------------------------------------- - /// constructor - /// ----------------------------------------------------------------------- - - constructor( - string memory _ensName, - address _ensReverseRegistrar, - address _ensOwner - ) { - owrImpl = new OptimisticWithdrawalRecipient(); - IENSReverseRegistrar(_ensReverseRegistrar).setName(_ensName); - IENSReverseRegistrar(_ensReverseRegistrar).claim(_ensOwner); - } - - /// ----------------------------------------------------------------------- - /// 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 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 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 - function createOWRecipient( - address token, - address recoveryAddress, - address principalRecipient, - address rewardRecipient, - 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 (amountOfPrincipalStake == 0) revert Invalid__ZeroThreshold(); - // ensure threshold isn't too large - if (amountOfPrincipalStake > type(uint96).max) revert Invalid__ThresholdTooLarge(amountOfPrincipalStake); - - /// effects - uint256 principalData = (amountOfPrincipalStake << ADDRESS_BITS) | uint256(uint160(principalRecipient)); - uint256 rewardData = uint256(uint160(rewardRecipient)); - - // would not exceed contract size limits - // 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, amountOfPrincipalStake); - } +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.19; + +import {OptimisticWithdrawalRecipient} from "./OptimisticWithdrawalRecipient.sol"; +import {LibClone} from "solady/utils/LibClone.sol"; +import {IENSReverseRegistrar} from "../interfaces/external/IENSReverseRegistrar.sol"; + +/// @title OptimisticWithdrawalRecipientFactory +/// @author Obol +/// @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; + + /// OptimisticWithdrawalRecipient implementation address + OptimisticWithdrawalRecipient public immutable owrImpl; + + /// ----------------------------------------------------------------------- + /// constructor + /// ----------------------------------------------------------------------- + + constructor( + string memory _ensName, + address _ensReverseRegistrar, + address _ensOwner + ) { + owrImpl = new OptimisticWithdrawalRecipient(); + IENSReverseRegistrar(_ensReverseRegistrar).setName(_ensName); + IENSReverseRegistrar(_ensReverseRegistrar).claim(_ensOwner); + } + + /// ----------------------------------------------------------------------- + /// 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 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 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 + function createOWRecipient( + address token, + address recoveryAddress, + address principalRecipient, + address rewardRecipient, + 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 (amountOfPrincipalStake == 0) revert Invalid__ZeroThreshold(); + // ensure threshold isn't too large + if (amountOfPrincipalStake > type(uint96).max) revert Invalid__ThresholdTooLarge(amountOfPrincipalStake); + + /// effects + uint256 principalData = (amountOfPrincipalStake << ADDRESS_BITS) | uint256(uint160(principalRecipient)); + uint256 rewardData = uint256(uint160(rewardRecipient)); + + // would not exceed contract size limits + // 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, amountOfPrincipalStake); + } } \ No newline at end of file diff --git a/src/test/collector/ObolCollector.t.sol b/src/test/collector/ObolCollector.t.sol index dd53844..f5d1242 100644 --- a/src/test/collector/ObolCollector.t.sol +++ b/src/test/collector/ObolCollector.t.sol @@ -1,197 +1,197 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.8.19; - -import "forge-std/Test.sol"; -import {ObolCollectorFactory, ObolCollector} from "src/collector/ObolCollectorFactory.sol"; -import {MockERC20} from "src/test/utils/mocks/MockERC20.sol"; -import {BaseSplit} from "src/base/BaseSplit.sol"; - -contract ObolCollectorTest is Test { - - uint256 internal constant PERCENTAGE_SCALE = 1e5; - - address feeRecipient; - address withdrawalAddress; - address ethWithdrawalAddress; - - uint256 feeShare; - MockERC20 mERC20; - MockERC20 rescueERC20; - - ObolCollectorFactory collectorFactoryWithFee; - - ObolCollector collectorWithFee; - ObolCollector ethCollectorWithFee; - - function setUp() public { - feeRecipient = makeAddr("feeRecipient"); - withdrawalAddress = makeAddr("withdrawalAddress"); - ethWithdrawalAddress = makeAddr("ethWithdrawalAddress"); - mERC20 = new MockERC20("Test Token", "TOK", 18); - rescueERC20 = new MockERC20("Rescue Test Token", "TOK", 18); - - feeShare = 1e4; // 10% - collectorFactoryWithFee = new ObolCollectorFactory(feeRecipient, feeShare); - - collectorWithFee = ObolCollector(collectorFactoryWithFee.createCollector(address(mERC20), withdrawalAddress)); - ethCollectorWithFee = ObolCollector(collectorFactoryWithFee.createCollector(address(0), ethWithdrawalAddress)); - - mERC20.mint(type(uint256).max); - rescueERC20.mint(type(uint256).max); - } - - function test_InvalidFeeShare() public { - vm.expectRevert(abi.encodeWithSelector(BaseSplit.Invalid_FeeShare.selector, 1e10)); - new ObolCollectorFactory(address(0), 1e10); - - vm.expectRevert(abi.encodeWithSelector(BaseSplit.Invalid_FeeShare.selector, 1e5)); - new ObolCollectorFactory(address(0), 1e5); - } - - function test_feeShare() public { - assertEq(collectorWithFee.feeShare(), feeShare, "invalid collector fee"); - - assertEq(ethCollectorWithFee.feeShare(), feeShare, "invalid collector value fee"); - } - - function test_feeRecipient() public { - assertEq(collectorWithFee.feeRecipient(), feeRecipient, "invalid collector feeRecipient"); - - assertEq(ethCollectorWithFee.feeRecipient(), feeRecipient, "invalid collector feeRecipient 2"); - } - - function test_withdrawalAddress() public { - assertEq(collectorWithFee.withdrawalAddress(), withdrawalAddress, "invalid split wallet"); - - assertEq(ethCollectorWithFee.withdrawalAddress(), ethWithdrawalAddress, "invalid eth split wallet"); - } - - function test_token() public { - assertEq(collectorWithFee.token(), address(mERC20), "invalid token"); - - assertEq(ethCollectorWithFee.token(), address(0), "ivnalid token eth"); - } - - function test_DistributeERC20WithFee() public { - uint256 amountToDistribute = 10 ether; - - mERC20.transfer(address(collectorWithFee), amountToDistribute); - - collectorWithFee.distribute(); - - uint256 fee = amountToDistribute * feeShare / PERCENTAGE_SCALE; - - assertEq(mERC20.balanceOf(feeRecipient), fee, "invalid fee share"); - - assertEq(mERC20.balanceOf(withdrawalAddress), amountToDistribute - fee, "invalid amount to split"); - } - - function testFuzz_DistributeERC20WithFee( - uint256 amountToDistribute, - uint256 fuzzFeeShare, - address fuzzFeeRecipient, - address fuzzWithdrawalAddress - ) public { - vm.assume(amountToDistribute > 0); - vm.assume(fuzzWithdrawalAddress != address(0)); - vm.assume(fuzzFeeRecipient != address(0)); - - amountToDistribute = bound(amountToDistribute, 1, type(uint128).max); - fuzzFeeShare = bound(fuzzFeeShare, 1, 8 * 1e4); - - ObolCollectorFactory fuzzCollectorFactoryWithFee = new ObolCollectorFactory(fuzzFeeRecipient, fuzzFeeShare); - ObolCollector fuzzCollectorWithFee = - ObolCollector(fuzzCollectorFactoryWithFee.createCollector(address(mERC20), fuzzWithdrawalAddress)); - - uint256 feeRecipientBalancePrev = mERC20.balanceOf(fuzzFeeRecipient); - uint256 fuzzWithdrawalAddressBalancePrev = mERC20.balanceOf(fuzzWithdrawalAddress); - - mERC20.transfer(address(fuzzCollectorWithFee), amountToDistribute); - - fuzzCollectorWithFee.distribute(); - - uint256 fee = amountToDistribute * fuzzFeeShare / PERCENTAGE_SCALE; - - assertEq(mERC20.balanceOf(fuzzFeeRecipient), feeRecipientBalancePrev + fee, "invalid fee share"); - - assertEq( - mERC20.balanceOf(fuzzWithdrawalAddress), - fuzzWithdrawalAddressBalancePrev + amountToDistribute - fee, - "invalid amount to split" - ); - } - - function test_DistributeETHWithFee() public { - uint256 amountToDistribute = 10 ether; - - vm.deal(address(ethCollectorWithFee), amountToDistribute); - - ethCollectorWithFee.distribute(); - - uint256 fee = amountToDistribute * feeShare / PERCENTAGE_SCALE; - - assertEq(address(feeRecipient).balance, fee, "invalid fee share"); - - assertEq(address(ethWithdrawalAddress).balance, amountToDistribute - fee, "invalid amount to split"); - } - - function testFuzz_DistributeETHWithFee(uint256 amountToDistribute, uint256 fuzzFeeShare) public { - vm.assume(amountToDistribute > 0); - vm.assume(fuzzFeeShare > 0); - - address fuzzWithdrawalAddress = makeAddr("fuzzWithdrawalAddress"); - address fuzzFeeRecipient = makeAddr("fuzzFeeRecipient"); - - amountToDistribute = bound(amountToDistribute, 1, type(uint96).max); - fuzzFeeShare = bound(fuzzFeeShare, 1, 9 * 1e4); - - ObolCollectorFactory fuzzCollectorFactoryWithFee = new ObolCollectorFactory(fuzzFeeRecipient, fuzzFeeShare); - ObolCollector fuzzETHCollectorWithFee = - ObolCollector(fuzzCollectorFactoryWithFee.createCollector(address(0), fuzzWithdrawalAddress)); - - vm.deal(address(fuzzETHCollectorWithFee), amountToDistribute); - - uint256 fuzzFeeRecipientBalance = address(fuzzFeeRecipient).balance; - uint256 fuzzWithdrawalAddressBalance = address(fuzzWithdrawalAddress).balance; - - fuzzETHCollectorWithFee.distribute(); - - uint256 fee = amountToDistribute * fuzzFeeShare / PERCENTAGE_SCALE; - - assertEq(address(fuzzFeeRecipient).balance, fuzzFeeRecipientBalance + fee, "invalid fee share"); - - assertEq( - address(fuzzWithdrawalAddress).balance, - fuzzWithdrawalAddressBalance + amountToDistribute - fee, - "invalid amount to split" - ); - } - - function testCannot_RescueControllerToken() public { - deal(address(ethCollectorWithFee), 1 ether); - vm.expectRevert(BaseSplit.Invalid_Address.selector); - ethCollectorWithFee.rescueFunds(address(0)); - - mERC20.transfer(address(collectorWithFee), 1 ether); - vm.expectRevert(BaseSplit.Invalid_Address.selector); - collectorWithFee.rescueFunds(address(mERC20)); - } - - function test_RescueTokens() public { - uint256 amountToRescue = 1 ether; - deal(address(collectorWithFee), amountToRescue); - collectorWithFee.rescueFunds(address(0)); - - assertEq(address(withdrawalAddress).balance, amountToRescue, "invalid amount"); - - rescueERC20.transfer(address(collectorWithFee), amountToRescue); - collectorWithFee.rescueFunds(address(rescueERC20)); - assertEq(rescueERC20.balanceOf(withdrawalAddress), amountToRescue, "invalid erc20 amount"); - - // ETH - rescueERC20.transfer(address(ethCollectorWithFee), amountToRescue); - ethCollectorWithFee.rescueFunds(address(rescueERC20)); - - assertEq(rescueERC20.balanceOf(ethWithdrawalAddress), amountToRescue, "invalid erc20 amount"); - } -} +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.19; + +import "forge-std/Test.sol"; +import {ObolCollectorFactory, ObolCollector} from "src/collector/ObolCollectorFactory.sol"; +import {MockERC20} from "src/test/utils/mocks/MockERC20.sol"; +import {BaseSplit} from "src/base/BaseSplit.sol"; + +contract ObolCollectorTest is Test { + + uint256 internal constant PERCENTAGE_SCALE = 1e5; + + address feeRecipient; + address withdrawalAddress; + address ethWithdrawalAddress; + + uint256 feeShare; + MockERC20 mERC20; + MockERC20 rescueERC20; + + ObolCollectorFactory collectorFactoryWithFee; + + ObolCollector collectorWithFee; + ObolCollector ethCollectorWithFee; + + function setUp() public { + feeRecipient = makeAddr("feeRecipient"); + withdrawalAddress = makeAddr("withdrawalAddress"); + ethWithdrawalAddress = makeAddr("ethWithdrawalAddress"); + mERC20 = new MockERC20("Test Token", "TOK", 18); + rescueERC20 = new MockERC20("Rescue Test Token", "TOK", 18); + + feeShare = 1e4; // 10% + collectorFactoryWithFee = new ObolCollectorFactory(feeRecipient, feeShare); + + collectorWithFee = ObolCollector(collectorFactoryWithFee.createCollector(address(mERC20), withdrawalAddress)); + ethCollectorWithFee = ObolCollector(collectorFactoryWithFee.createCollector(address(0), ethWithdrawalAddress)); + + mERC20.mint(type(uint256).max); + rescueERC20.mint(type(uint256).max); + } + + function test_InvalidFeeShare() public { + vm.expectRevert(abi.encodeWithSelector(BaseSplit.Invalid_FeeShare.selector, 1e10)); + new ObolCollectorFactory(address(0), 1e10); + + vm.expectRevert(abi.encodeWithSelector(BaseSplit.Invalid_FeeShare.selector, 1e5)); + new ObolCollectorFactory(address(0), 1e5); + } + + function test_feeShare() public { + assertEq(collectorWithFee.feeShare(), feeShare, "invalid collector fee"); + + assertEq(ethCollectorWithFee.feeShare(), feeShare, "invalid collector value fee"); + } + + function test_feeRecipient() public { + assertEq(collectorWithFee.feeRecipient(), feeRecipient, "invalid collector feeRecipient"); + + assertEq(ethCollectorWithFee.feeRecipient(), feeRecipient, "invalid collector feeRecipient 2"); + } + + function test_withdrawalAddress() public { + assertEq(collectorWithFee.withdrawalAddress(), withdrawalAddress, "invalid split wallet"); + + assertEq(ethCollectorWithFee.withdrawalAddress(), ethWithdrawalAddress, "invalid eth split wallet"); + } + + function test_token() public { + assertEq(collectorWithFee.token(), address(mERC20), "invalid token"); + + assertEq(ethCollectorWithFee.token(), address(0), "ivnalid token eth"); + } + + function test_DistributeERC20WithFee() public { + uint256 amountToDistribute = 10 ether; + + mERC20.transfer(address(collectorWithFee), amountToDistribute); + + collectorWithFee.distribute(); + + uint256 fee = amountToDistribute * feeShare / PERCENTAGE_SCALE; + + assertEq(mERC20.balanceOf(feeRecipient), fee, "invalid fee share"); + + assertEq(mERC20.balanceOf(withdrawalAddress), amountToDistribute - fee, "invalid amount to split"); + } + + function testFuzz_DistributeERC20WithFee( + uint256 amountToDistribute, + uint256 fuzzFeeShare, + address fuzzFeeRecipient, + address fuzzWithdrawalAddress + ) public { + vm.assume(amountToDistribute > 0); + vm.assume(fuzzWithdrawalAddress != address(0)); + vm.assume(fuzzFeeRecipient != address(0)); + + amountToDistribute = bound(amountToDistribute, 1, type(uint128).max); + fuzzFeeShare = bound(fuzzFeeShare, 1, 8 * 1e4); + + ObolCollectorFactory fuzzCollectorFactoryWithFee = new ObolCollectorFactory(fuzzFeeRecipient, fuzzFeeShare); + ObolCollector fuzzCollectorWithFee = + ObolCollector(fuzzCollectorFactoryWithFee.createCollector(address(mERC20), fuzzWithdrawalAddress)); + + uint256 feeRecipientBalancePrev = mERC20.balanceOf(fuzzFeeRecipient); + uint256 fuzzWithdrawalAddressBalancePrev = mERC20.balanceOf(fuzzWithdrawalAddress); + + mERC20.transfer(address(fuzzCollectorWithFee), amountToDistribute); + + fuzzCollectorWithFee.distribute(); + + uint256 fee = amountToDistribute * fuzzFeeShare / PERCENTAGE_SCALE; + + assertEq(mERC20.balanceOf(fuzzFeeRecipient), feeRecipientBalancePrev + fee, "invalid fee share"); + + assertEq( + mERC20.balanceOf(fuzzWithdrawalAddress), + fuzzWithdrawalAddressBalancePrev + amountToDistribute - fee, + "invalid amount to split" + ); + } + + function test_DistributeETHWithFee() public { + uint256 amountToDistribute = 10 ether; + + vm.deal(address(ethCollectorWithFee), amountToDistribute); + + ethCollectorWithFee.distribute(); + + uint256 fee = amountToDistribute * feeShare / PERCENTAGE_SCALE; + + assertEq(address(feeRecipient).balance, fee, "invalid fee share"); + + assertEq(address(ethWithdrawalAddress).balance, amountToDistribute - fee, "invalid amount to split"); + } + + function testFuzz_DistributeETHWithFee(uint256 amountToDistribute, uint256 fuzzFeeShare) public { + vm.assume(amountToDistribute > 0); + vm.assume(fuzzFeeShare > 0); + + address fuzzWithdrawalAddress = makeAddr("fuzzWithdrawalAddress"); + address fuzzFeeRecipient = makeAddr("fuzzFeeRecipient"); + + amountToDistribute = bound(amountToDistribute, 1, type(uint96).max); + fuzzFeeShare = bound(fuzzFeeShare, 1, 9 * 1e4); + + ObolCollectorFactory fuzzCollectorFactoryWithFee = new ObolCollectorFactory(fuzzFeeRecipient, fuzzFeeShare); + ObolCollector fuzzETHCollectorWithFee = + ObolCollector(fuzzCollectorFactoryWithFee.createCollector(address(0), fuzzWithdrawalAddress)); + + vm.deal(address(fuzzETHCollectorWithFee), amountToDistribute); + + uint256 fuzzFeeRecipientBalance = address(fuzzFeeRecipient).balance; + uint256 fuzzWithdrawalAddressBalance = address(fuzzWithdrawalAddress).balance; + + fuzzETHCollectorWithFee.distribute(); + + uint256 fee = amountToDistribute * fuzzFeeShare / PERCENTAGE_SCALE; + + assertEq(address(fuzzFeeRecipient).balance, fuzzFeeRecipientBalance + fee, "invalid fee share"); + + assertEq( + address(fuzzWithdrawalAddress).balance, + fuzzWithdrawalAddressBalance + amountToDistribute - fee, + "invalid amount to split" + ); + } + + function testCannot_RescueControllerToken() public { + deal(address(ethCollectorWithFee), 1 ether); + vm.expectRevert(BaseSplit.Invalid_Address.selector); + ethCollectorWithFee.rescueFunds(address(0)); + + mERC20.transfer(address(collectorWithFee), 1 ether); + vm.expectRevert(BaseSplit.Invalid_Address.selector); + collectorWithFee.rescueFunds(address(mERC20)); + } + + function test_RescueTokens() public { + uint256 amountToRescue = 1 ether; + deal(address(collectorWithFee), amountToRescue); + collectorWithFee.rescueFunds(address(0)); + + assertEq(address(withdrawalAddress).balance, amountToRescue, "invalid amount"); + + rescueERC20.transfer(address(collectorWithFee), amountToRescue); + collectorWithFee.rescueFunds(address(rescueERC20)); + assertEq(rescueERC20.balanceOf(withdrawalAddress), amountToRescue, "invalid erc20 amount"); + + // ETH + rescueERC20.transfer(address(ethCollectorWithFee), amountToRescue); + ethCollectorWithFee.rescueFunds(address(rescueERC20)); + + assertEq(rescueERC20.balanceOf(ethWithdrawalAddress), amountToRescue, "invalid erc20 amount"); + } +} diff --git a/src/test/collector/ObolCollectorFactory.t.sol b/src/test/collector/ObolCollectorFactory.t.sol index a7f376a..0690b88 100644 --- a/src/test/collector/ObolCollectorFactory.t.sol +++ b/src/test/collector/ObolCollectorFactory.t.sol @@ -1,32 +1,32 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.8.19; - -import "forge-std/Test.sol"; -import {ObolCollectorFactory, ObolCollector} from "src/collector/ObolCollectorFactory.sol"; -import {MockERC20} from "src/test/utils/mocks/MockERC20.sol"; - -contract ObolCollectorFactoryTest is Test { - error Invalid_Address(); - - address feeRecipient; - uint256 feeShare; - address splitWallet; - - ObolCollectorFactory collectorFactory; - - function setUp() public { - feeRecipient = makeAddr("feeRecipient"); - splitWallet = makeAddr("splitWallet"); - feeShare = 1e4; // 10% - collectorFactory = new ObolCollectorFactory(feeRecipient, feeShare); - } - - function testCannot_CreateCollectorInvalidWithdrawalAddress() public { - vm.expectRevert(Invalid_Address.selector); - collectorFactory.createCollector(address(0), address(0)); - } - - function test_CreateCollector() public { - collectorFactory.createCollector(address(0), splitWallet); - } -} +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.19; + +import "forge-std/Test.sol"; +import {ObolCollectorFactory, ObolCollector} from "src/collector/ObolCollectorFactory.sol"; +import {MockERC20} from "src/test/utils/mocks/MockERC20.sol"; + +contract ObolCollectorFactoryTest is Test { + error Invalid_Address(); + + address feeRecipient; + uint256 feeShare; + address splitWallet; + + ObolCollectorFactory collectorFactory; + + function setUp() public { + feeRecipient = makeAddr("feeRecipient"); + splitWallet = makeAddr("splitWallet"); + feeShare = 1e4; // 10% + collectorFactory = new ObolCollectorFactory(feeRecipient, feeShare); + } + + function testCannot_CreateCollectorInvalidWithdrawalAddress() public { + vm.expectRevert(Invalid_Address.selector); + collectorFactory.createCollector(address(0), address(0)); + } + + function test_CreateCollector() public { + collectorFactory.createCollector(address(0), splitWallet); + } +} diff --git a/src/test/controllers/IMSC.t.sol b/src/test/controllers/IMSC.t.sol index d6dd517..b3a3206 100644 --- a/src/test/controllers/IMSC.t.sol +++ b/src/test/controllers/IMSC.t.sol @@ -1,218 +1,217 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.8.19; - -import "forge-std/Test.sol"; -import { - ImmutableSplitControllerFactory, - ImmutableSplitController -} from "src/controllers/ImmutableSplitControllerFactory.sol"; -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; - - 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); - - 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); - } - - 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 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(); - - 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; - } - } - - 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); - } -} +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.19; + +import "forge-std/Test.sol"; +import { + ImmutableSplitControllerFactory, + ImmutableSplitController +} from "src/controllers/ImmutableSplitControllerFactory.sol"; +import {ISplitMain} from "src/interfaces/external/splits/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; + + ImmutableSplitControllerFactory public factory; + ImmutableSplitController public cntrlImpl; + + ImmutableSplitController public controller; + + address[] accounts; + uint32[] percentAllocations; + + address[] controllerAccounts; + uint32[] controllerPercentAllocations; + + address split; + address owner; + + function setUp() public { + vm.createSelectFork(getChain("goerli").rpcUrl); + + 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); + } + + 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 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(); + + 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; + } + } + + 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); + } +} diff --git a/src/test/controllers/IMSCFactory.t.sol b/src/test/controllers/IMSCFactory.t.sol index e9e79de..8149845 100644 --- a/src/test/controllers/IMSCFactory.t.sol +++ b/src/test/controllers/IMSCFactory.t.sol @@ -1,194 +1,193 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.8.19; - -import "forge-std/Test.sol"; -import { - ImmutableSplitControllerFactory, - ImmutableSplitController -} from "src/controllers/ImmutableSplitControllerFactory.sol"; -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); - 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; - uint256 public constant 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); - - 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_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"); - - 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_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))); - - 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"); - } - - 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); - } -} +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.19; + +import "forge-std/Test.sol"; +import { + ImmutableSplitControllerFactory, + ImmutableSplitController +} from "src/controllers/ImmutableSplitControllerFactory.sol"; +import {ISplitMain} from "src/interfaces/external/splits/ISplitMain.sol"; + +contract IMSCFactory is Test { + error Invalid_Address(); + 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; + uint256 public constant PERCENTAGE_SCALE = 1e6; + + ImmutableSplitControllerFactory public factory; + ImmutableSplitController public cntrlImpl; + + address owner; + + address[] accounts; + uint32[] percentAllocations; + + function setUp() public { + vm.createSelectFork(getChain("goerli").rpcUrl); + + 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_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"); + + 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_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))); + + 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"); + } + + 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); + } +} diff --git a/src/test/eigenlayer/EigenLayerTestBase.sol b/src/test/eigenlayer/EigenLayerTestBase.sol index 3c3853a..bc8f2e8 100644 --- a/src/test/eigenlayer/EigenLayerTestBase.sol +++ b/src/test/eigenlayer/EigenLayerTestBase.sol @@ -1,56 +1,56 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.8.19; - -import "forge-std/Test.sol"; -import { - IEigenPod, - IDelegationManager, - IEigenPodManager, - IEigenLayerUtils, - IDelayedWithdrawalRouter -} from "src/interfaces/IEigenLayer.sol"; - -abstract contract EigenLayerTestBase is Test { - address public constant ETH_ADDRESS = address(0); - - uint256 public constant PERCENTAGE_SCALE = 1e5; - - address public constant SPLIT_MAIN_GOERLI = 0x2ed6c4B5dA6378c7897AC67Ba9e43102Feb694EE; - - address public constant ENS_REVERSE_REGISTRAR_GOERLI = 0x084b1c3C81545d370f3634392De611CaaBFf8148; - - address public constant DEPOSIT_CONTRACT_GOERLI = 0xff50ed3d0ec03aC01D4C79aAd74928BFF48a7b2b; - address public constant DELEGATION_MANAGER_GOERLI = 0x1b7b8F6b258f95Cf9596EabB9aa18B62940Eb0a8; - address public constant POD_MANAGER_GOERLI = 0xa286b84C96aF280a49Fe1F40B9627C2A2827df41; - address public constant DELAY_ROUTER_GOERLI = 0x89581561f1F98584F88b0d57c2180fb89225388f; - // eigenlayer admin - address public constant DELAY_ROUTER_OWNER_GOERLI = 0x37bAFb55BC02056c5fD891DFa503ee84a97d89bF; - address public constant EIGEN_LAYER_OPERATOR_GOERLI = 0x3DeD1CB5E25FE3eC9811B918A809A371A4965A5D; - - uint256 internal constant BALANCE_CLASSIFICATION_THRESHOLD = 16 ether; - - function encodeEigenPodCall(address recipient, uint256 amount) internal pure returns (bytes memory callData) { - callData = abi.encodeCall(IEigenPod.withdrawNonBeaconChainETHBalanceWei, (recipient, amount)); - } - - function encodeDelegationManagerCall(address operator) internal pure returns (bytes memory callData) { - IEigenLayerUtils.SignatureWithExpiry memory signature = IEigenLayerUtils.SignatureWithExpiry(bytes(""), 0); - callData = abi.encodeCall(IDelegationManager.delegateTo, (operator, signature, bytes32(0))); - } - - function encodeEigenPodManagerCall(uint256) internal pure returns (bytes memory callData) { - bytes memory pubkey = bytes(""); - bytes memory signature = bytes(""); - bytes32 dataRoot = bytes32(0); - - callData = abi.encodeCall(IEigenPodManager.stake, (pubkey, signature, dataRoot)); - } - - function _min(uint256 a, uint256 b) internal pure returns (uint256 min) { - min = a > b ? b : a; - } - - function _max(uint256 a, uint256 b) internal pure returns (uint256 max) { - max = a > b ? a : b; - } -} +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.19; + +import "forge-std/Test.sol"; +import { + IEigenPod, + IDelegationManager, + IEigenPodManager, + IEigenLayerUtils, + IDelayedWithdrawalRouter +} from "src/interfaces/external/IEigenLayer.sol"; + +abstract contract EigenLayerTestBase is Test { + address public constant ETH_ADDRESS = address(0); + + uint256 public constant PERCENTAGE_SCALE = 1e5; + + address public constant SPLIT_MAIN_GOERLI = 0x2ed6c4B5dA6378c7897AC67Ba9e43102Feb694EE; + + address public constant ENS_REVERSE_REGISTRAR_GOERLI = 0x084b1c3C81545d370f3634392De611CaaBFf8148; + + address public constant DEPOSIT_CONTRACT_GOERLI = 0xff50ed3d0ec03aC01D4C79aAd74928BFF48a7b2b; + address public constant DELEGATION_MANAGER_GOERLI = 0x1b7b8F6b258f95Cf9596EabB9aa18B62940Eb0a8; + address public constant POD_MANAGER_GOERLI = 0xa286b84C96aF280a49Fe1F40B9627C2A2827df41; + address public constant DELAY_ROUTER_GOERLI = 0x89581561f1F98584F88b0d57c2180fb89225388f; + // eigenlayer admin + address public constant DELAY_ROUTER_OWNER_GOERLI = 0x37bAFb55BC02056c5fD891DFa503ee84a97d89bF; + address public constant EIGEN_LAYER_OPERATOR_GOERLI = 0x3DeD1CB5E25FE3eC9811B918A809A371A4965A5D; + + uint256 internal constant BALANCE_CLASSIFICATION_THRESHOLD = 16 ether; + + function encodeEigenPodCall(address recipient, uint256 amount) internal pure returns (bytes memory callData) { + callData = abi.encodeCall(IEigenPod.withdrawNonBeaconChainETHBalanceWei, (recipient, amount)); + } + + function encodeDelegationManagerCall(address operator) internal pure returns (bytes memory callData) { + IEigenLayerUtils.SignatureWithExpiry memory signature = IEigenLayerUtils.SignatureWithExpiry(bytes(""), 0); + callData = abi.encodeCall(IDelegationManager.delegateTo, (operator, signature, bytes32(0))); + } + + function encodeEigenPodManagerCall(uint256) internal pure returns (bytes memory callData) { + bytes memory pubkey = bytes(""); + bytes memory signature = bytes(""); + bytes32 dataRoot = bytes32(0); + + callData = abi.encodeCall(IEigenPodManager.stake, (pubkey, signature, dataRoot)); + } + + function _min(uint256 a, uint256 b) internal pure returns (uint256 min) { + min = a > b ? b : a; + } + + function _max(uint256 a, uint256 b) internal pure returns (uint256 max) { + max = a > b ? a : b; + } +} diff --git a/src/test/eigenlayer/OELPCFactory.t.sol b/src/test/eigenlayer/OELPCFactory.t.sol index 78ecf40..948bc6f 100644 --- a/src/test/eigenlayer/OELPCFactory.t.sol +++ b/src/test/eigenlayer/OELPCFactory.t.sol @@ -1,81 +1,80 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity 0.8.19; - -import "forge-std/Test.sol"; -import {ObolEigenLayerPodControllerFactory} from "src/eigenlayer/ObolEigenLayerPodControllerFactory.sol"; -import {EigenLayerTestBase} from "src/test/eigenlayer/EigenLayerTestBase.sol"; - -contract ObolEigenLayerPodControllerFactoryTest is EigenLayerTestBase { - error Invalid_Owner(); - error Invalid_WithdrawalAddress(); - error Invalid_DelegationManager(); - error Invalid_EigenPodManaager(); - error Invalid_WithdrawalRouter(); - - event CreatePodController(address indexed controller, address indexed split, address owner); - - ObolEigenLayerPodControllerFactory factory; - - address owner; - address user1; - address withdrawalAddress; - address feeRecipient; - - uint256 feeShare; - - function setUp() public { - uint256 goerliBlock = 10_205_449; - vm.createSelectFork(getChain("goerli").rpcUrl); - - owner = makeAddr("owner"); - user1 = makeAddr("user1"); - withdrawalAddress = makeAddr("withdrawalAddress"); - feeRecipient = makeAddr("feeRecipient"); - feeShare = 1e3; - - factory = new ObolEigenLayerPodControllerFactory( - feeRecipient, feeShare, DELEGATION_MANAGER_GOERLI, POD_MANAGER_GOERLI, DELAY_ROUTER_GOERLI - ); - } - - function test_RevertIfInvalidDelegationManger() external { - vm.expectRevert(Invalid_DelegationManager.selector); - new ObolEigenLayerPodControllerFactory(feeRecipient, feeShare, address(0), POD_MANAGER_GOERLI, DELAY_ROUTER_GOERLI); - } - - function test_RevertIfInvalidPodManger() external { - vm.expectRevert(Invalid_EigenPodManaager.selector); - new ObolEigenLayerPodControllerFactory( - feeRecipient, feeShare, DELEGATION_MANAGER_GOERLI, address(0), DELAY_ROUTER_GOERLI - ); - } - - function test_RevertIfInvalidWithdrawalRouter() external { - vm.expectRevert(Invalid_WithdrawalRouter.selector); - new ObolEigenLayerPodControllerFactory( - feeRecipient, feeShare, DELEGATION_MANAGER_GOERLI, POD_MANAGER_GOERLI, address(0) - ); - } - - function test_RevertIfOwnerIsZero() external { - vm.expectRevert(Invalid_Owner.selector); - factory.createPodController(address(0), withdrawalAddress); - } - - function test_RevertIfOWRIsZero() external { - vm.expectRevert(Invalid_WithdrawalAddress.selector); - factory.createPodController(user1, address(0)); - } - - function test_CreatePodController() external { - vm.expectEmit(false, false, false, true); - - emit CreatePodController(address(0), withdrawalAddress, user1); - - address predictedAddress = factory.predictControllerAddress(user1, withdrawalAddress); - - address createdAddress = factory.createPodController(user1, withdrawalAddress); - - assertEq(predictedAddress, createdAddress, "predicted address is equivalent"); - } -} +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.19; + +import "forge-std/Test.sol"; +import {ObolEigenLayerPodControllerFactory} from "src/eigenlayer/ObolEigenLayerPodControllerFactory.sol"; +import {EigenLayerTestBase} from "src/test/eigenlayer/EigenLayerTestBase.sol"; + +contract ObolEigenLayerPodControllerFactoryTest is EigenLayerTestBase { + error Invalid_Owner(); + error Invalid_WithdrawalAddress(); + error Invalid_DelegationManager(); + error Invalid_EigenPodManaager(); + error Invalid_WithdrawalRouter(); + + event CreatePodController(address indexed controller, address indexed split, address owner); + + ObolEigenLayerPodControllerFactory factory; + + address owner; + address user1; + address withdrawalAddress; + address feeRecipient; + + uint256 feeShare; + + function setUp() public { + vm.createSelectFork(getChain("goerli").rpcUrl); + + owner = makeAddr("owner"); + user1 = makeAddr("user1"); + withdrawalAddress = makeAddr("withdrawalAddress"); + feeRecipient = makeAddr("feeRecipient"); + feeShare = 1e3; + + factory = new ObolEigenLayerPodControllerFactory( + feeRecipient, feeShare, DELEGATION_MANAGER_GOERLI, POD_MANAGER_GOERLI, DELAY_ROUTER_GOERLI + ); + } + + function test_RevertIfInvalidDelegationManger() external { + vm.expectRevert(Invalid_DelegationManager.selector); + new ObolEigenLayerPodControllerFactory(feeRecipient, feeShare, address(0), POD_MANAGER_GOERLI, DELAY_ROUTER_GOERLI); + } + + function test_RevertIfInvalidPodManger() external { + vm.expectRevert(Invalid_EigenPodManaager.selector); + new ObolEigenLayerPodControllerFactory( + feeRecipient, feeShare, DELEGATION_MANAGER_GOERLI, address(0), DELAY_ROUTER_GOERLI + ); + } + + function test_RevertIfInvalidWithdrawalRouter() external { + vm.expectRevert(Invalid_WithdrawalRouter.selector); + new ObolEigenLayerPodControllerFactory( + feeRecipient, feeShare, DELEGATION_MANAGER_GOERLI, POD_MANAGER_GOERLI, address(0) + ); + } + + function test_RevertIfOwnerIsZero() external { + vm.expectRevert(Invalid_Owner.selector); + factory.createPodController(address(0), withdrawalAddress); + } + + function test_RevertIfOWRIsZero() external { + vm.expectRevert(Invalid_WithdrawalAddress.selector); + factory.createPodController(user1, address(0)); + } + + function test_CreatePodController() external { + vm.expectEmit(false, false, false, true); + + emit CreatePodController(address(0), withdrawalAddress, user1); + + address predictedAddress = factory.predictControllerAddress(user1, withdrawalAddress); + + address createdAddress = factory.createPodController(user1, withdrawalAddress); + + assertEq(predictedAddress, createdAddress, "predicted address is equivalent"); + } +} diff --git a/src/test/eigenlayer/ObolEigenLayerPodController.t.sol b/src/test/eigenlayer/ObolEigenLayerPodController.t.sol index c07ae90..ef84f48 100644 --- a/src/test/eigenlayer/ObolEigenLayerPodController.t.sol +++ b/src/test/eigenlayer/ObolEigenLayerPodController.t.sol @@ -1,227 +1,226 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.8.19; - -import "forge-std/Test.sol"; -import {ObolEigenLayerPodController} from "src/eigenlayer/ObolEigenLayerPodController.sol"; -import {ObolEigenLayerPodControllerFactory} from "src/eigenlayer/ObolEigenLayerPodControllerFactory.sol"; -import { - IEigenPod, - IDelegationManager, - IEigenPodManager, - IEigenLayerUtils, - IDelayedWithdrawalRouter -} from "src/interfaces/IEigenLayer.sol"; -import {MockERC20} from "src/test/utils/mocks/MockERC20.sol"; -import { - OptimisticWithdrawalRecipientFactory, - OptimisticWithdrawalRecipient -} from "src/owr/OptimisticWithdrawalRecipientFactory.sol"; -import {EigenLayerTestBase} from "src/test/eigenlayer/EigenLayerTestBase.sol"; - -interface IDepositContract { - function deposit( - bytes calldata pubkey, - bytes calldata withdrawal_credentials, - bytes calldata signature, - bytes32 deposit_data_root - ) external payable; -} - -contract ObolEigenLayerPodControllerTest is EigenLayerTestBase { - error Unauthorized(); - error AlreadyInitialized(); - error Invalid_FeeShare(); - error CallFailed(bytes); - - ObolEigenLayerPodControllerFactory factory; - ObolEigenLayerPodControllerFactory zeroFeeFactory; - - ObolEigenLayerPodController controller; - ObolEigenLayerPodController zeroFeeController; - - address owner; - address user1; - address user2; - address withdrawalAddress; - address principalRecipient; - address feeRecipient; - - uint256 feeShare; - - MockERC20 mERC20; - - function setUp() public { - uint256 goerliBlock = 10_205_449; - vm.createSelectFork(getChain("goerli").rpcUrl); - - vm.mockCall( - DEPOSIT_CONTRACT_GOERLI, abi.encodeWithSelector(IDepositContract.deposit.selector), bytes.concat(bytes32(0)) - ); - - owner = makeAddr("owner"); - user1 = makeAddr("user1"); - user1 = makeAddr("user2"); - principalRecipient = makeAddr("principalRecipient"); - withdrawalAddress = makeAddr("withdrawalAddress"); - feeRecipient = makeAddr("feeRecipient"); - feeShare = 1e3; - - factory = new ObolEigenLayerPodControllerFactory( - feeRecipient, feeShare, DELEGATION_MANAGER_GOERLI, POD_MANAGER_GOERLI, DELAY_ROUTER_GOERLI - ); - - zeroFeeFactory = new ObolEigenLayerPodControllerFactory( - address(0), 0, DELEGATION_MANAGER_GOERLI, POD_MANAGER_GOERLI, DELAY_ROUTER_GOERLI - ); - - controller = ObolEigenLayerPodController(factory.createPodController(owner, withdrawalAddress)); - zeroFeeController = ObolEigenLayerPodController(zeroFeeFactory.createPodController(owner, withdrawalAddress)); - - mERC20 = new MockERC20("Test Token", "TOK", 18); - mERC20.mint(type(uint256).max); - - vm.prank(DELAY_ROUTER_OWNER_GOERLI); - // set the delay withdrawal duration to zero - IDelayedWithdrawalRouter(DELAY_ROUTER_GOERLI).setWithdrawalDelayBlocks(0); - } - - function test_RevertIfInvalidFeeShare() external { - vm.expectRevert(Invalid_FeeShare.selector); - new ObolEigenLayerPodControllerFactory( - feeRecipient, 1e7, DELEGATION_MANAGER_GOERLI, POD_MANAGER_GOERLI, DELAY_ROUTER_GOERLI - ); - } - - function test_RevertIfNotOwnerCallEigenPod() external { - vm.prank(user1); - vm.expectRevert(Unauthorized.selector); - controller.callEigenPod(encodeEigenPodCall(user1, 1 ether)); - } - - function test_RevertIfDoubleInitialize() external { - vm.prank(user1); - vm.expectRevert(AlreadyInitialized.selector); - controller.initialize(owner, withdrawalAddress); - } - - function test_CallEigenPod() external { - address pod = controller.eigenPod(); - uint256 amount = 1 ether; - - // airdrop ether to pod - (bool success,) = pod.call{value: amount}(""); - require(success, "call failed"); - - vm.prank(owner); - controller.callEigenPod(encodeEigenPodCall(user1, amount)); - } - - function test_CallDelegationManager() external { - vm.prank(owner); - controller.callDelegationManager(encodeDelegationManagerCall(EIGEN_LAYER_OPERATOR_GOERLI)); - } - - function test_OnlyOwnerCallDelegationManager() external { - vm.prank(user1); - vm.expectRevert(Unauthorized.selector); - controller.callDelegationManager(encodeDelegationManagerCall(EIGEN_LAYER_OPERATOR_GOERLI)); - } - - function test_CallEigenPodManager() external { - uint256 etherStake = 32 ether; - vm.deal(owner, etherStake + 1 ether); - vm.prank(owner); - controller.callEigenPodManager{value: etherStake}(encodeEigenPodManagerCall(0)); - } - - function test_OnlyOwnerEigenPodManager() external { - vm.expectRevert(Unauthorized.selector); - controller.callEigenPodManager(encodeEigenPodManagerCall(0)); - } - - function test_ClaimDelayedWithdrawals() external { - uint256 amountToDeposit = 2 ether; - - // transfer unstake beacon eth to eigenPod - (bool success,) = address(controller.eigenPod()).call{value: amountToDeposit}(""); - require(success, "call failed"); - - vm.startPrank(owner); - { - controller.callEigenPod(encodeEigenPodCall(address(controller), amountToDeposit)); - controller.claimDelayedWithdrawals(1); - } - vm.stopPrank(); - - assertEq(address(feeRecipient).balance, 20_000_000_000_000_000, "fee recipient balance increased"); - assertEq(address(withdrawalAddress).balance, 1_980_000_000_000_000_000, "withdrawal balance increased"); - } - - function test_ClaimDelayedWithdrawalsZeroFee() external { - uint256 amountToDeposit = 20 ether; - - // transfer unstake beacon eth to eigenPod - (bool success,) = address(zeroFeeController.eigenPod()).call{value: amountToDeposit}(""); - require(success, "call failed"); - - vm.startPrank(owner); - { - zeroFeeController.callEigenPod(encodeEigenPodCall(address(zeroFeeController), amountToDeposit)); - zeroFeeController.claimDelayedWithdrawals(1); - } - vm.stopPrank(); - - assertEq(address(withdrawalAddress).balance, amountToDeposit, "withdrawal balance increased"); - } - - function test_InvalidCallReverts() external { - uint256 amountToDeposit = 20 ether; - bytes memory data = encodeEigenPodCall(address(0x2), amountToDeposit); - vm.expectRevert(abi.encodeWithSelector(CallFailed.selector, data)); - vm.prank(owner); - zeroFeeController.callEigenPod(data); - vm.stopPrank(); - } - - function testFuzz_ClaimDelayedWithdrawals(uint256 amount) external { - amount = bound(amount, _min(amount, address(this).balance), type(uint96).max); - - address DELAY_ROUTER_OWNER = 0x37bAFb55BC02056c5fD891DFa503ee84a97d89bF; - vm.prank(DELAY_ROUTER_OWNER); - // set the delay withdrawal duration to zero - IDelayedWithdrawalRouter(DELAY_ROUTER_GOERLI).setWithdrawalDelayBlocks(0); - - // transfer unstake beacon eth to eigenPod - (bool success,) = address(controller.eigenPod()).call{value: amount}(""); - require(success, "call failed"); - - vm.startPrank(owner); - { - controller.callEigenPod(encodeEigenPodCall(address(controller), amount)); - controller.claimDelayedWithdrawals(1); - } - vm.stopPrank(); - - uint256 fee = amount * feeShare / PERCENTAGE_SCALE; - - assertEq(address(feeRecipient).balance, fee, "invalid fee"); - - assertEq(address(withdrawalAddress).balance, amount -= fee, "invalid withdrawalAddress balance"); - } - - function test_RescueFunds() external { - uint256 amount = 1e18; - mERC20.transfer(address(controller), amount); - - controller.rescueFunds(address(mERC20), amount); - - assertEq(mERC20.balanceOf(withdrawalAddress), amount, "could not rescue funds"); - } - - function test_RescueFundsZero() external { - uint256 amount = 0; - controller.rescueFunds(address(mERC20), amount); - - assertEq(mERC20.balanceOf(withdrawalAddress), amount, "balance should be zero"); - } -} +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.19; + +import "forge-std/Test.sol"; +import {ObolEigenLayerPodController} from "src/eigenlayer/ObolEigenLayerPodController.sol"; +import {ObolEigenLayerPodControllerFactory} from "src/eigenlayer/ObolEigenLayerPodControllerFactory.sol"; +import { + IEigenPod, + IDelegationManager, + IEigenPodManager, + IEigenLayerUtils, + IDelayedWithdrawalRouter +} from "src/interfaces/external/IEigenLayer.sol"; +import {MockERC20} from "src/test/utils/mocks/MockERC20.sol"; +import { + OptimisticWithdrawalRecipientFactory, + OptimisticWithdrawalRecipient +} from "src/owr/OptimisticWithdrawalRecipientFactory.sol"; +import {EigenLayerTestBase} from "src/test/eigenlayer/EigenLayerTestBase.sol"; + +interface IDepositContract { + function deposit( + bytes calldata pubkey, + bytes calldata withdrawal_credentials, + bytes calldata signature, + bytes32 deposit_data_root + ) external payable; +} + +contract ObolEigenLayerPodControllerTest is EigenLayerTestBase { + error Unauthorized(); + error AlreadyInitialized(); + error Invalid_FeeShare(); + error CallFailed(bytes); + + ObolEigenLayerPodControllerFactory factory; + ObolEigenLayerPodControllerFactory zeroFeeFactory; + + ObolEigenLayerPodController controller; + ObolEigenLayerPodController zeroFeeController; + + address owner; + address user1; + address user2; + address withdrawalAddress; + address principalRecipient; + address feeRecipient; + + uint256 feeShare; + + MockERC20 mERC20; + + function setUp() public { + vm.createSelectFork(getChain("goerli").rpcUrl); + + vm.mockCall( + DEPOSIT_CONTRACT_GOERLI, abi.encodeWithSelector(IDepositContract.deposit.selector), bytes.concat(bytes32(0)) + ); + + owner = makeAddr("owner"); + user1 = makeAddr("user1"); + user1 = makeAddr("user2"); + principalRecipient = makeAddr("principalRecipient"); + withdrawalAddress = makeAddr("withdrawalAddress"); + feeRecipient = makeAddr("feeRecipient"); + feeShare = 1e3; + + factory = new ObolEigenLayerPodControllerFactory( + feeRecipient, feeShare, DELEGATION_MANAGER_GOERLI, POD_MANAGER_GOERLI, DELAY_ROUTER_GOERLI + ); + + zeroFeeFactory = new ObolEigenLayerPodControllerFactory( + address(0), 0, DELEGATION_MANAGER_GOERLI, POD_MANAGER_GOERLI, DELAY_ROUTER_GOERLI + ); + + controller = ObolEigenLayerPodController(factory.createPodController(owner, withdrawalAddress)); + zeroFeeController = ObolEigenLayerPodController(zeroFeeFactory.createPodController(owner, withdrawalAddress)); + + mERC20 = new MockERC20("Test Token", "TOK", 18); + mERC20.mint(type(uint256).max); + + vm.prank(DELAY_ROUTER_OWNER_GOERLI); + // set the delay withdrawal duration to zero + IDelayedWithdrawalRouter(DELAY_ROUTER_GOERLI).setWithdrawalDelayBlocks(0); + } + + function test_RevertIfInvalidFeeShare() external { + vm.expectRevert(Invalid_FeeShare.selector); + new ObolEigenLayerPodControllerFactory( + feeRecipient, 1e7, DELEGATION_MANAGER_GOERLI, POD_MANAGER_GOERLI, DELAY_ROUTER_GOERLI + ); + } + + function test_RevertIfNotOwnerCallEigenPod() external { + vm.prank(user1); + vm.expectRevert(Unauthorized.selector); + controller.callEigenPod(encodeEigenPodCall(user1, 1 ether)); + } + + function test_RevertIfDoubleInitialize() external { + vm.prank(user1); + vm.expectRevert(AlreadyInitialized.selector); + controller.initialize(owner, withdrawalAddress); + } + + function test_CallEigenPod() external { + address pod = controller.eigenPod(); + uint256 amount = 1 ether; + + // airdrop ether to pod + (bool success,) = pod.call{value: amount}(""); + require(success, "call failed"); + + vm.prank(owner); + controller.callEigenPod(encodeEigenPodCall(user1, amount)); + } + + function test_CallDelegationManager() external { + vm.prank(owner); + controller.callDelegationManager(encodeDelegationManagerCall(EIGEN_LAYER_OPERATOR_GOERLI)); + } + + function test_OnlyOwnerCallDelegationManager() external { + vm.prank(user1); + vm.expectRevert(Unauthorized.selector); + controller.callDelegationManager(encodeDelegationManagerCall(EIGEN_LAYER_OPERATOR_GOERLI)); + } + + function test_CallEigenPodManager() external { + uint256 etherStake = 32 ether; + vm.deal(owner, etherStake + 1 ether); + vm.prank(owner); + controller.callEigenPodManager{value: etherStake}(encodeEigenPodManagerCall(0)); + } + + function test_OnlyOwnerEigenPodManager() external { + vm.expectRevert(Unauthorized.selector); + controller.callEigenPodManager(encodeEigenPodManagerCall(0)); + } + + function test_ClaimDelayedWithdrawals() external { + uint256 amountToDeposit = 2 ether; + + // transfer unstake beacon eth to eigenPod + (bool success,) = address(controller.eigenPod()).call{value: amountToDeposit}(""); + require(success, "call failed"); + + vm.startPrank(owner); + { + controller.callEigenPod(encodeEigenPodCall(address(controller), amountToDeposit)); + controller.claimDelayedWithdrawals(1); + } + vm.stopPrank(); + + assertEq(address(feeRecipient).balance, 20_000_000_000_000_000, "fee recipient balance increased"); + assertEq(address(withdrawalAddress).balance, 1_980_000_000_000_000_000, "withdrawal balance increased"); + } + + function test_ClaimDelayedWithdrawalsZeroFee() external { + uint256 amountToDeposit = 20 ether; + + // transfer unstake beacon eth to eigenPod + (bool success,) = address(zeroFeeController.eigenPod()).call{value: amountToDeposit}(""); + require(success, "call failed"); + + vm.startPrank(owner); + { + zeroFeeController.callEigenPod(encodeEigenPodCall(address(zeroFeeController), amountToDeposit)); + zeroFeeController.claimDelayedWithdrawals(1); + } + vm.stopPrank(); + + assertEq(address(withdrawalAddress).balance, amountToDeposit, "withdrawal balance increased"); + } + + function test_InvalidCallReverts() external { + uint256 amountToDeposit = 20 ether; + bytes memory data = encodeEigenPodCall(address(0x2), amountToDeposit); + vm.expectRevert(abi.encodeWithSelector(CallFailed.selector, data)); + vm.prank(owner); + zeroFeeController.callEigenPod(data); + vm.stopPrank(); + } + + function testFuzz_ClaimDelayedWithdrawals(uint256 amount) external { + amount = bound(amount, _min(amount, address(this).balance), type(uint96).max); + + address DELAY_ROUTER_OWNER = 0x37bAFb55BC02056c5fD891DFa503ee84a97d89bF; + vm.prank(DELAY_ROUTER_OWNER); + // set the delay withdrawal duration to zero + IDelayedWithdrawalRouter(DELAY_ROUTER_GOERLI).setWithdrawalDelayBlocks(0); + + // transfer unstake beacon eth to eigenPod + (bool success,) = address(controller.eigenPod()).call{value: amount}(""); + require(success, "call failed"); + + vm.startPrank(owner); + { + controller.callEigenPod(encodeEigenPodCall(address(controller), amount)); + controller.claimDelayedWithdrawals(1); + } + vm.stopPrank(); + + uint256 fee = amount * feeShare / PERCENTAGE_SCALE; + + assertEq(address(feeRecipient).balance, fee, "invalid fee"); + + assertEq(address(withdrawalAddress).balance, amount -= fee, "invalid withdrawalAddress balance"); + } + + function test_RescueFunds() external { + uint256 amount = 1e18; + mERC20.transfer(address(controller), amount); + + controller.rescueFunds(address(mERC20), amount); + + assertEq(mERC20.balanceOf(withdrawalAddress), amount, "could not rescue funds"); + } + + function test_RescueFundsZero() external { + uint256 amount = 0; + controller.rescueFunds(address(mERC20), amount); + + assertEq(mERC20.balanceOf(withdrawalAddress), amount, "balance should be zero"); + } +} diff --git a/src/test/eigenlayer/integration/OELPCIntegration.t.sol b/src/test/eigenlayer/integration/OELPCIntegration.t.sol index 83d10c4..da00d88 100644 --- a/src/test/eigenlayer/integration/OELPCIntegration.t.sol +++ b/src/test/eigenlayer/integration/OELPCIntegration.t.sol @@ -1,165 +1,164 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.8.19; - -import "forge-std/Test.sol"; -import {ObolEigenLayerPodController} from "src/eigenlayer/ObolEigenLayerPodController.sol"; -import {ObolEigenLayerPodControllerFactory} from "src/eigenlayer/ObolEigenLayerPodControllerFactory.sol"; -import { - IEigenPod, - IDelegationManager, - IEigenPodManager, - IEigenLayerUtils, - IDelayedWithdrawalRouter -} from "src/interfaces/IEigenLayer.sol"; -import {MockERC20} from "src/test/utils/mocks/MockERC20.sol"; -import {ISplitMain} from "src/interfaces/ISplitMain.sol"; -import { - OptimisticWithdrawalRecipientFactory, - OptimisticWithdrawalRecipient -} from "src/owr/OptimisticWithdrawalRecipientFactory.sol"; -import {IENSReverseRegistrar} from "../../../interfaces/IENSReverseRegistrar.sol"; -import {EigenLayerTestBase} from "src/test/eigenlayer/EigenLayerTestBase.sol"; - -contract OELPCIntegration is EigenLayerTestBase { - ObolEigenLayerPodControllerFactory factory; - ObolEigenLayerPodController owrController; - ObolEigenLayerPodController splitController; - - address[] accounts; - uint32[] percentAllocations; - - address owner; - address user1; - address user2; - - address owrWithdrawalAddress; - address splitWithdrawalAddress; - - address principalRecipient; - address rewardRecipient; - address feeRecipient; - - uint256 feeShare; - - function setUp() public { - uint256 goerliBlock = 10_653_080; - vm.createSelectFork(getChain("goerli").rpcUrl); - - 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)) - ); - - owner = makeAddr("owner"); - user1 = makeAddr("user1"); - user1 = makeAddr("user2"); - principalRecipient = makeAddr("principalRecipient"); - rewardRecipient = makeAddr("rewardRecipient"); - feeRecipient = makeAddr("feeRecipient"); - feeShare = 1e3; - - OptimisticWithdrawalRecipientFactory owrFactory = - new OptimisticWithdrawalRecipientFactory("demo.obol.eth", ENS_REVERSE_REGISTRAR_GOERLI, address(this)); - - owrWithdrawalAddress = - address(owrFactory.createOWRecipient(ETH_ADDRESS, address(0), principalRecipient, rewardRecipient, 32 ether)); - - factory = new ObolEigenLayerPodControllerFactory( - feeRecipient, feeShare, DELEGATION_MANAGER_GOERLI, POD_MANAGER_GOERLI, DELAY_ROUTER_GOERLI - ); - - owrController = ObolEigenLayerPodController(factory.createPodController(owner, owrWithdrawalAddress)); - - accounts = new address[](2); - accounts[0] = makeAddr("accounts0"); - accounts[1] = makeAddr("accounts1"); - - percentAllocations = new uint32[](2); - percentAllocations[0] = 300_000; - percentAllocations[1] = 700_000; - - splitWithdrawalAddress = ISplitMain(SPLIT_MAIN_GOERLI).createSplit(accounts, percentAllocations, 0, address(0)); - - splitController = ObolEigenLayerPodController(factory.createPodController(owner, splitWithdrawalAddress)); - - vm.prank(DELAY_ROUTER_OWNER_GOERLI); - // set the delay withdrawal duration to zero - IDelayedWithdrawalRouter(DELAY_ROUTER_GOERLI).setWithdrawalDelayBlocks(0); - } - - function testFuzz_WithdrawOWR(uint256 amountToDeposit) external { - vm.assume(amountToDeposit > 0); - - uint256 stakeSize = 32 ether; - - amountToDeposit = boundETH(amountToDeposit); - // transfer unstake beacon eth to eigenPod - (bool success,) = address(owrController.eigenPod()).call{value: amountToDeposit}(""); - require(success, "call failed"); - - vm.startPrank(owner); - { - owrController.callEigenPod(encodeEigenPodCall(address(owrController), amountToDeposit)); - owrController.claimDelayedWithdrawals(1); - } - vm.stopPrank(); - - uint256 fee = amountToDeposit * feeShare / PERCENTAGE_SCALE; - - assertEq(address(feeRecipient).balance, fee, "fee recipient balance increased"); - - uint256 owrBalance = amountToDeposit - fee; - assertEq(address(owrWithdrawalAddress).balance, owrBalance, "owr balance increased"); - - // call distribute on owrWithdrawal address - OptimisticWithdrawalRecipient(owrWithdrawalAddress).distributeFunds(); - - // check the princiapl recipient - if (owrBalance >= BALANCE_CLASSIFICATION_THRESHOLD) { - if (owrBalance > stakeSize) { - // prinicipal rexeives 32 eth and reward recieves remainder - assertEq(address(principalRecipient).balance, stakeSize, "invalid principal balance"); - assertEq(address(rewardRecipient).balance, owrBalance - stakeSize, "invalid reward balance"); - } else { - // principal receives everything - assertEq(address(principalRecipient).balance, owrBalance, "invalid principal balance"); - } - } else { - // reward recipient receives everything - assertEq(address(rewardRecipient).balance, owrBalance, "invalid reward balance"); - } - } - - function testFuzz_WithdrawSplit(uint256 amountToDeposit) external { - vm.assume(amountToDeposit > 0); - - amountToDeposit = boundETH(amountToDeposit); - // transfer unstake beacon eth to eigenPod - (bool success,) = address(splitController.eigenPod()).call{value: amountToDeposit}(""); - require(success, "call failed"); - - vm.startPrank(owner); - { - splitController.callEigenPod(encodeEigenPodCall(address(splitController), amountToDeposit)); - splitController.claimDelayedWithdrawals(1); - } - vm.stopPrank(); - - uint256 fee = amountToDeposit * feeShare / PERCENTAGE_SCALE; - assertEq(address(feeRecipient).balance, fee, "fee recipient balance increased"); - - uint256 splitBalance = amountToDeposit - fee; - - assertEq(address(splitWithdrawalAddress).balance, splitBalance, "invalid balance"); - } - - function boundETH(uint256 amount) internal view returns (uint256 result) { - result = bound(amount, 1, type(uint96).max); - } -} +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.19; + +import "forge-std/Test.sol"; +import {ObolEigenLayerPodController} from "src/eigenlayer/ObolEigenLayerPodController.sol"; +import {ObolEigenLayerPodControllerFactory} from "src/eigenlayer/ObolEigenLayerPodControllerFactory.sol"; +import { + IEigenPod, + IDelegationManager, + IEigenPodManager, + IEigenLayerUtils, + IDelayedWithdrawalRouter +} from "src/interfaces/external/IEigenLayer.sol"; +import {MockERC20} from "src/test/utils/mocks/MockERC20.sol"; +import {ISplitMain} from "src/interfaces/external/splits/ISplitMain.sol"; +import { + OptimisticWithdrawalRecipientFactory, + OptimisticWithdrawalRecipient +} from "src/owr/OptimisticWithdrawalRecipientFactory.sol"; +import {IENSReverseRegistrar} from "../../../interfaces/external/IENSReverseRegistrar.sol"; +import {EigenLayerTestBase} from "src/test/eigenlayer/EigenLayerTestBase.sol"; + +contract OELPCIntegration is EigenLayerTestBase { + ObolEigenLayerPodControllerFactory factory; + ObolEigenLayerPodController owrController; + ObolEigenLayerPodController splitController; + + address[] accounts; + uint32[] percentAllocations; + + address owner; + address user1; + address user2; + + address owrWithdrawalAddress; + address splitWithdrawalAddress; + + address principalRecipient; + address rewardRecipient; + address feeRecipient; + + uint256 feeShare; + + function setUp() public { + vm.createSelectFork(getChain("goerli").rpcUrl); + + 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)) + ); + + owner = makeAddr("owner"); + user1 = makeAddr("user1"); + user1 = makeAddr("user2"); + principalRecipient = makeAddr("principalRecipient"); + rewardRecipient = makeAddr("rewardRecipient"); + feeRecipient = makeAddr("feeRecipient"); + feeShare = 1e3; + + OptimisticWithdrawalRecipientFactory owrFactory = + new OptimisticWithdrawalRecipientFactory("demo.obol.eth", ENS_REVERSE_REGISTRAR_GOERLI, address(this)); + + owrWithdrawalAddress = + address(owrFactory.createOWRecipient(ETH_ADDRESS, address(0), principalRecipient, rewardRecipient, 32 ether)); + + factory = new ObolEigenLayerPodControllerFactory( + feeRecipient, feeShare, DELEGATION_MANAGER_GOERLI, POD_MANAGER_GOERLI, DELAY_ROUTER_GOERLI + ); + + owrController = ObolEigenLayerPodController(factory.createPodController(owner, owrWithdrawalAddress)); + + accounts = new address[](2); + accounts[0] = makeAddr("accounts0"); + accounts[1] = makeAddr("accounts1"); + + percentAllocations = new uint32[](2); + percentAllocations[0] = 300_000; + percentAllocations[1] = 700_000; + + splitWithdrawalAddress = ISplitMain(SPLIT_MAIN_GOERLI).createSplit(accounts, percentAllocations, 0, address(0)); + + splitController = ObolEigenLayerPodController(factory.createPodController(owner, splitWithdrawalAddress)); + + vm.prank(DELAY_ROUTER_OWNER_GOERLI); + // set the delay withdrawal duration to zero + IDelayedWithdrawalRouter(DELAY_ROUTER_GOERLI).setWithdrawalDelayBlocks(0); + } + + function testFuzz_WithdrawOWR(uint256 amountToDeposit) external { + vm.assume(amountToDeposit > 0); + + uint256 stakeSize = 32 ether; + + amountToDeposit = boundETH(amountToDeposit); + // transfer unstake beacon eth to eigenPod + (bool success,) = address(owrController.eigenPod()).call{value: amountToDeposit}(""); + require(success, "call failed"); + + vm.startPrank(owner); + { + owrController.callEigenPod(encodeEigenPodCall(address(owrController), amountToDeposit)); + owrController.claimDelayedWithdrawals(1); + } + vm.stopPrank(); + + uint256 fee = amountToDeposit * feeShare / PERCENTAGE_SCALE; + + assertEq(address(feeRecipient).balance, fee, "fee recipient balance increased"); + + uint256 owrBalance = amountToDeposit - fee; + assertEq(address(owrWithdrawalAddress).balance, owrBalance, "owr balance increased"); + + // call distribute on owrWithdrawal address + OptimisticWithdrawalRecipient(owrWithdrawalAddress).distributeFunds(); + + // check the princiapl recipient + if (owrBalance >= BALANCE_CLASSIFICATION_THRESHOLD) { + if (owrBalance > stakeSize) { + // prinicipal rexeives 32 eth and reward recieves remainder + assertEq(address(principalRecipient).balance, stakeSize, "invalid principal balance"); + assertEq(address(rewardRecipient).balance, owrBalance - stakeSize, "invalid reward balance"); + } else { + // principal receives everything + assertEq(address(principalRecipient).balance, owrBalance, "invalid principal balance"); + } + } else { + // reward recipient receives everything + assertEq(address(rewardRecipient).balance, owrBalance, "invalid reward balance"); + } + } + + function testFuzz_WithdrawSplit(uint256 amountToDeposit) external { + vm.assume(amountToDeposit > 0); + + amountToDeposit = boundETH(amountToDeposit); + // transfer unstake beacon eth to eigenPod + (bool success,) = address(splitController.eigenPod()).call{value: amountToDeposit}(""); + require(success, "call failed"); + + vm.startPrank(owner); + { + splitController.callEigenPod(encodeEigenPodCall(address(splitController), amountToDeposit)); + splitController.claimDelayedWithdrawals(1); + } + vm.stopPrank(); + + uint256 fee = amountToDeposit * feeShare / PERCENTAGE_SCALE; + assertEq(address(feeRecipient).balance, fee, "fee recipient balance increased"); + + uint256 splitBalance = amountToDeposit - fee; + + assertEq(address(splitWithdrawalAddress).balance, splitBalance, "invalid balance"); + } + + function boundETH(uint256 amount) internal view returns (uint256 result) { + result = bound(amount, 1, type(uint96).max); + } +} diff --git a/src/test/etherfi/ObolEtherfiSplit.t.sol b/src/test/etherfi/ObolEtherfiSplit.t.sol index 5fc9188..a43bdd4 100644 --- a/src/test/etherfi/ObolEtherfiSplit.t.sol +++ b/src/test/etherfi/ObolEtherfiSplit.t.sol @@ -1,189 +1,189 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.8.19; - -import "forge-std/Test.sol"; -import {ObolEtherfiSplitFactory, ObolEtherfiSplit, IweETH} from "src/etherfi/ObolEtherfiSplitFactory.sol"; -import {BaseSplit} from "src/base/BaseSplit.sol"; -import {ERC20} from "solmate/tokens/ERC20.sol"; -import {ObolEtherfiSplitTestHelper} from "./ObolEtherfiSplitTestHelper.sol"; -import {MockERC20} from "src/test/utils/mocks/MockERC20.sol"; - -contract ObolEtherfiSplitTest is ObolEtherfiSplitTestHelper, Test { - uint256 internal constant PERCENTAGE_SCALE = 1e5; - - ObolEtherfiSplitFactory internal etherfiSplitFactory; - ObolEtherfiSplitFactory internal etherfiSplitFactoryWithFee; - - ObolEtherfiSplit internal etherfiSplit; - ObolEtherfiSplit internal etherfiSplitWithFee; - - address demoSplit; - address feeRecipient; - uint256 feeShare; - - MockERC20 mERC20; - - function setUp() public { - uint256 mainnetBlock = 19_393_100; - vm.createSelectFork(getChain("mainnet").rpcUrl, mainnetBlock); - - feeRecipient = makeAddr("feeRecipient"); - feeShare = 1e4; - - etherfiSplitFactory = - new ObolEtherfiSplitFactory(address(0), 0, ERC20(EETH_MAINNET_ADDRESS), ERC20(WEETH_MAINNET_ADDRESS)); - - etherfiSplitFactoryWithFee = - new ObolEtherfiSplitFactory(feeRecipient, feeShare, ERC20(EETH_MAINNET_ADDRESS), ERC20(WEETH_MAINNET_ADDRESS)); - - demoSplit = makeAddr("demoSplit"); - - etherfiSplit = ObolEtherfiSplit(etherfiSplitFactory.createCollector(address(0), demoSplit)); - etherfiSplitWithFee = ObolEtherfiSplit(etherfiSplitFactoryWithFee.createCollector(address(0), demoSplit)); - - mERC20 = new MockERC20("Test Token", "TOK", 18); - mERC20.mint(type(uint256).max); - } - - function test_etherfi_CannotCreateInvalidFeeRecipient() public { - vm.expectRevert(BaseSplit.Invalid_FeeRecipient.selector); - new ObolEtherfiSplit(address(0), 10, ERC20(EETH_MAINNET_ADDRESS), ERC20(WEETH_MAINNET_ADDRESS)); - } - - function test_etherfi_CannotCreateInvalidFeeShare() public { - vm.expectRevert(abi.encodeWithSelector(BaseSplit.Invalid_FeeShare.selector, PERCENTAGE_SCALE + 1)); - new ObolEtherfiSplit(address(1), PERCENTAGE_SCALE + 1, ERC20(EETH_MAINNET_ADDRESS), ERC20(WEETH_MAINNET_ADDRESS)); - - vm.expectRevert(abi.encodeWithSelector(BaseSplit.Invalid_FeeShare.selector, PERCENTAGE_SCALE)); - new ObolEtherfiSplit(address(1), PERCENTAGE_SCALE, ERC20(EETH_MAINNET_ADDRESS), ERC20(WEETH_MAINNET_ADDRESS)); - } - - function test_etherfi_CloneArgsIsCorrect() public { - assertEq(etherfiSplit.withdrawalAddress(), demoSplit, "invalid address"); - assertEq(address(etherfiSplit.eETH()), EETH_MAINNET_ADDRESS, "invalid eETH address"); - assertEq(address(etherfiSplit.weETH()), WEETH_MAINNET_ADDRESS, "invalid weETH address"); - assertEq(etherfiSplit.feeRecipient(), address(0), "invalid fee recipient"); - assertEq(etherfiSplit.feeShare(), 0, "invalid fee amount"); - - assertEq(etherfiSplitWithFee.withdrawalAddress(), demoSplit, "invalid address"); - assertEq(address(etherfiSplitWithFee.eETH()), EETH_MAINNET_ADDRESS, "invalid eETH address"); - assertEq(address(etherfiSplitWithFee.weETH()), WEETH_MAINNET_ADDRESS, "invalid weETH address"); - assertEq(etherfiSplitWithFee.feeRecipient(), feeRecipient, "invalid fee recipient /2"); - assertEq(etherfiSplitWithFee.feeShare(), feeShare, "invalid fee share /2"); - } - - function test_etherfi_CanRescueFunds() public { - // rescue ETH - uint256 amountOfEther = 1 ether; - deal(address(etherfiSplit), amountOfEther); - - uint256 balance = etherfiSplit.rescueFunds(address(0)); - assertEq(balance, amountOfEther, "balance not rescued"); - assertEq(address(etherfiSplit).balance, 0, "balance is not zero"); - assertEq(address(etherfiSplit.withdrawalAddress()).balance, amountOfEther, "rescue not successful"); - - // rescue tokens - mERC20.transfer(address(etherfiSplit), amountOfEther); - uint256 tokenBalance = etherfiSplit.rescueFunds(address(mERC20)); - assertEq(tokenBalance, amountOfEther, "token - balance not rescued"); - assertEq(mERC20.balanceOf(address(etherfiSplit)), 0, "token - balance is not zero"); - assertEq(mERC20.balanceOf(etherfiSplit.withdrawalAddress()), amountOfEther, "token - rescue not successful"); - } - - function test_etherfi_Cannot_RescueEtherfiTokens() public { - vm.expectRevert(BaseSplit.Invalid_Address.selector); - etherfiSplit.rescueFunds(address(EETH_MAINNET_ADDRESS)); - - vm.expectRevert(BaseSplit.Invalid_Address.selector); - etherfiSplit.rescueFunds(address(WEETH_MAINNET_ADDRESS)); - } - - function test_etherfi_CanDistributeWithoutFee() public { - // we use a random account on Etherscan to credit the etherfiSplit address - // with 10 ether worth of eETH on mainnet - vm.prank(RANDOM_EETH_ACCOUNT_ADDRESS); - ERC20(EETH_MAINNET_ADDRESS).transfer(address(etherfiSplit), 100 ether); - - uint256 prevBalance = ERC20(WEETH_MAINNET_ADDRESS).balanceOf(demoSplit); - - uint256 amount = etherfiSplit.distribute(); - - assertTrue(amount > 0, "invalid amount"); - - uint256 afterBalance = ERC20(WEETH_MAINNET_ADDRESS).balanceOf(demoSplit); - - assertGe(afterBalance, prevBalance, "after balance greater"); - } - - function test_etherfi_CanDistributeWithFee() public { - // we use a random account on Etherscan to credit the etherfiSplit address - // with 10 ether worth of eETH on mainnet - vm.prank(RANDOM_EETH_ACCOUNT_ADDRESS); - uint256 amountToDistribute = 100 ether; - ERC20(EETH_MAINNET_ADDRESS).transfer(address(etherfiSplitWithFee), amountToDistribute); - - uint256 prevBalance = ERC20(WEETH_MAINNET_ADDRESS).balanceOf(demoSplit); - - uint256 balance = ERC20(EETH_MAINNET_ADDRESS).balanceOf(address(etherfiSplitWithFee)); - - uint256 weETHDistributed = IweETH(WEETH_MAINNET_ADDRESS).getWeETHByeETH(balance); - - uint256 amount = etherfiSplitWithFee.distribute(); - - assertTrue(amount > 0, "invalid amount"); - - uint256 afterBalance = ERC20(WEETH_MAINNET_ADDRESS).balanceOf(demoSplit); - - assertGe(afterBalance, prevBalance, "after balance greater"); - - uint256 expectedFee = (weETHDistributed * feeShare) / PERCENTAGE_SCALE; - - assertEq(ERC20(WEETH_MAINNET_ADDRESS).balanceOf(feeRecipient), expectedFee, "invalid fee transferred"); - - assertEq(ERC20(WEETH_MAINNET_ADDRESS).balanceOf(demoSplit), weETHDistributed - expectedFee, "invalid amount"); - } - - function testFuzz_etherfi_CanDistributeWithFee( - address anotherSplit, - uint256 amountToDistribute, - address fuzzFeeRecipient, - uint256 fuzzFeeShare - ) public { - vm.assume(anotherSplit != address(0)); - vm.assume(fuzzFeeRecipient != anotherSplit); - vm.assume(fuzzFeeShare > 0 && fuzzFeeShare < PERCENTAGE_SCALE); - vm.assume(fuzzFeeRecipient != address(0)); - vm.assume(amountToDistribute > 1 ether); - vm.assume(amountToDistribute < 10 ether); - - ObolEtherfiSplitFactory fuzzFactorySplitWithFee = new ObolEtherfiSplitFactory( - fuzzFeeRecipient, fuzzFeeShare, ERC20(EETH_MAINNET_ADDRESS), ERC20(WEETH_MAINNET_ADDRESS) - ); - - ObolEtherfiSplit fuzzSplitWithFee = ObolEtherfiSplit(fuzzFactorySplitWithFee.createCollector(address(0), anotherSplit)); - - vm.prank(RANDOM_EETH_ACCOUNT_ADDRESS); - - ERC20(EETH_MAINNET_ADDRESS).transfer(address(fuzzSplitWithFee), amountToDistribute); - - uint256 prevBalance = ERC20(WEETH_MAINNET_ADDRESS).balanceOf(anotherSplit); - - uint256 balance = ERC20(EETH_MAINNET_ADDRESS).balanceOf(address(fuzzSplitWithFee)); - - uint256 weETHDistributed = IweETH(WEETH_MAINNET_ADDRESS).getWeETHByeETH(balance); - - uint256 amount = fuzzSplitWithFee.distribute(); - - assertTrue(amount > 0, "invalid amount"); - - uint256 afterBalance = ERC20(WEETH_MAINNET_ADDRESS).balanceOf(anotherSplit); - - assertGe(afterBalance, prevBalance, "after balance greater"); - - uint256 expectedFee = (weETHDistributed * fuzzFeeShare) / PERCENTAGE_SCALE; - - assertEq(ERC20(WEETH_MAINNET_ADDRESS).balanceOf(fuzzFeeRecipient), expectedFee, "invalid fee transferred"); - - assertEq(ERC20(WEETH_MAINNET_ADDRESS).balanceOf(anotherSplit), weETHDistributed - expectedFee, "invalid amount"); - } -} +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.19; + +import "forge-std/Test.sol"; +import {ObolEtherfiSplitFactory, ObolEtherfiSplit, IweETH} from "src/etherfi/ObolEtherfiSplitFactory.sol"; +import {BaseSplit} from "src/base/BaseSplit.sol"; +import {ERC20} from "solmate/tokens/ERC20.sol"; +import {ObolEtherfiSplitTestHelper} from "./ObolEtherfiSplitTestHelper.sol"; +import {MockERC20} from "src/test/utils/mocks/MockERC20.sol"; + +contract ObolEtherfiSplitTest is ObolEtherfiSplitTestHelper, Test { + uint256 internal constant PERCENTAGE_SCALE = 1e5; + + ObolEtherfiSplitFactory internal etherfiSplitFactory; + ObolEtherfiSplitFactory internal etherfiSplitFactoryWithFee; + + ObolEtherfiSplit internal etherfiSplit; + ObolEtherfiSplit internal etherfiSplitWithFee; + + address demoSplit; + address feeRecipient; + uint256 feeShare; + + MockERC20 mERC20; + + function setUp() public { + uint256 mainnetBlock = 19_393_100; + vm.createSelectFork(getChain("mainnet").rpcUrl, mainnetBlock); + + feeRecipient = makeAddr("feeRecipient"); + feeShare = 1e4; + + etherfiSplitFactory = + new ObolEtherfiSplitFactory(address(0), 0, ERC20(EETH_MAINNET_ADDRESS), ERC20(WEETH_MAINNET_ADDRESS)); + + etherfiSplitFactoryWithFee = + new ObolEtherfiSplitFactory(feeRecipient, feeShare, ERC20(EETH_MAINNET_ADDRESS), ERC20(WEETH_MAINNET_ADDRESS)); + + demoSplit = makeAddr("demoSplit"); + + etherfiSplit = ObolEtherfiSplit(etherfiSplitFactory.createCollector(address(0), demoSplit)); + etherfiSplitWithFee = ObolEtherfiSplit(etherfiSplitFactoryWithFee.createCollector(address(0), demoSplit)); + + mERC20 = new MockERC20("Test Token", "TOK", 18); + mERC20.mint(type(uint256).max); + } + + function test_etherfi_CannotCreateInvalidFeeRecipient() public { + vm.expectRevert(BaseSplit.Invalid_FeeRecipient.selector); + new ObolEtherfiSplit(address(0), 10, ERC20(EETH_MAINNET_ADDRESS), ERC20(WEETH_MAINNET_ADDRESS)); + } + + function test_etherfi_CannotCreateInvalidFeeShare() public { + vm.expectRevert(abi.encodeWithSelector(BaseSplit.Invalid_FeeShare.selector, PERCENTAGE_SCALE + 1)); + new ObolEtherfiSplit(address(1), PERCENTAGE_SCALE + 1, ERC20(EETH_MAINNET_ADDRESS), ERC20(WEETH_MAINNET_ADDRESS)); + + vm.expectRevert(abi.encodeWithSelector(BaseSplit.Invalid_FeeShare.selector, PERCENTAGE_SCALE)); + new ObolEtherfiSplit(address(1), PERCENTAGE_SCALE, ERC20(EETH_MAINNET_ADDRESS), ERC20(WEETH_MAINNET_ADDRESS)); + } + + function test_etherfi_CloneArgsIsCorrect() public { + assertEq(etherfiSplit.withdrawalAddress(), demoSplit, "invalid address"); + assertEq(address(etherfiSplit.eETH()), EETH_MAINNET_ADDRESS, "invalid eETH address"); + assertEq(address(etherfiSplit.weETH()), WEETH_MAINNET_ADDRESS, "invalid weETH address"); + assertEq(etherfiSplit.feeRecipient(), address(0), "invalid fee recipient"); + assertEq(etherfiSplit.feeShare(), 0, "invalid fee amount"); + + assertEq(etherfiSplitWithFee.withdrawalAddress(), demoSplit, "invalid address"); + assertEq(address(etherfiSplitWithFee.eETH()), EETH_MAINNET_ADDRESS, "invalid eETH address"); + assertEq(address(etherfiSplitWithFee.weETH()), WEETH_MAINNET_ADDRESS, "invalid weETH address"); + assertEq(etherfiSplitWithFee.feeRecipient(), feeRecipient, "invalid fee recipient /2"); + assertEq(etherfiSplitWithFee.feeShare(), feeShare, "invalid fee share /2"); + } + + function test_etherfi_CanRescueFunds() public { + // rescue ETH + uint256 amountOfEther = 1 ether; + deal(address(etherfiSplit), amountOfEther); + + uint256 balance = etherfiSplit.rescueFunds(address(0)); + assertEq(balance, amountOfEther, "balance not rescued"); + assertEq(address(etherfiSplit).balance, 0, "balance is not zero"); + assertEq(address(etherfiSplit.withdrawalAddress()).balance, amountOfEther, "rescue not successful"); + + // rescue tokens + mERC20.transfer(address(etherfiSplit), amountOfEther); + uint256 tokenBalance = etherfiSplit.rescueFunds(address(mERC20)); + assertEq(tokenBalance, amountOfEther, "token - balance not rescued"); + assertEq(mERC20.balanceOf(address(etherfiSplit)), 0, "token - balance is not zero"); + assertEq(mERC20.balanceOf(etherfiSplit.withdrawalAddress()), amountOfEther, "token - rescue not successful"); + } + + function test_etherfi_Cannot_RescueEtherfiTokens() public { + vm.expectRevert(BaseSplit.Invalid_Address.selector); + etherfiSplit.rescueFunds(address(EETH_MAINNET_ADDRESS)); + + vm.expectRevert(BaseSplit.Invalid_Address.selector); + etherfiSplit.rescueFunds(address(WEETH_MAINNET_ADDRESS)); + } + + function test_etherfi_CanDistributeWithoutFee() public { + // we use a random account on Etherscan to credit the etherfiSplit address + // with 10 ether worth of eETH on mainnet + vm.prank(RANDOM_EETH_ACCOUNT_ADDRESS); + ERC20(EETH_MAINNET_ADDRESS).transfer(address(etherfiSplit), 100 ether); + + uint256 prevBalance = ERC20(WEETH_MAINNET_ADDRESS).balanceOf(demoSplit); + + uint256 amount = etherfiSplit.distribute(); + + assertTrue(amount > 0, "invalid amount"); + + uint256 afterBalance = ERC20(WEETH_MAINNET_ADDRESS).balanceOf(demoSplit); + + assertGe(afterBalance, prevBalance, "after balance greater"); + } + + function test_etherfi_CanDistributeWithFee() public { + // we use a random account on Etherscan to credit the etherfiSplit address + // with 10 ether worth of eETH on mainnet + vm.prank(RANDOM_EETH_ACCOUNT_ADDRESS); + uint256 amountToDistribute = 100 ether; + ERC20(EETH_MAINNET_ADDRESS).transfer(address(etherfiSplitWithFee), amountToDistribute); + + uint256 prevBalance = ERC20(WEETH_MAINNET_ADDRESS).balanceOf(demoSplit); + + uint256 balance = ERC20(EETH_MAINNET_ADDRESS).balanceOf(address(etherfiSplitWithFee)); + + uint256 weETHDistributed = IweETH(WEETH_MAINNET_ADDRESS).getWeETHByeETH(balance); + + uint256 amount = etherfiSplitWithFee.distribute(); + + assertTrue(amount > 0, "invalid amount"); + + uint256 afterBalance = ERC20(WEETH_MAINNET_ADDRESS).balanceOf(demoSplit); + + assertGe(afterBalance, prevBalance, "after balance greater"); + + uint256 expectedFee = (weETHDistributed * feeShare) / PERCENTAGE_SCALE; + + assertEq(ERC20(WEETH_MAINNET_ADDRESS).balanceOf(feeRecipient), expectedFee, "invalid fee transferred"); + + assertEq(ERC20(WEETH_MAINNET_ADDRESS).balanceOf(demoSplit), weETHDistributed - expectedFee, "invalid amount"); + } + + function testFuzz_etherfi_CanDistributeWithFee( + address anotherSplit, + uint256 amountToDistribute, + address fuzzFeeRecipient, + uint256 fuzzFeeShare + ) public { + vm.assume(anotherSplit != address(0)); + vm.assume(fuzzFeeRecipient != anotherSplit); + vm.assume(fuzzFeeShare > 0 && fuzzFeeShare < PERCENTAGE_SCALE); + vm.assume(fuzzFeeRecipient != address(0)); + vm.assume(amountToDistribute > 1 ether); + vm.assume(amountToDistribute < 10 ether); + + ObolEtherfiSplitFactory fuzzFactorySplitWithFee = new ObolEtherfiSplitFactory( + fuzzFeeRecipient, fuzzFeeShare, ERC20(EETH_MAINNET_ADDRESS), ERC20(WEETH_MAINNET_ADDRESS) + ); + + ObolEtherfiSplit fuzzSplitWithFee = ObolEtherfiSplit(fuzzFactorySplitWithFee.createCollector(address(0), anotherSplit)); + + vm.prank(RANDOM_EETH_ACCOUNT_ADDRESS); + + ERC20(EETH_MAINNET_ADDRESS).transfer(address(fuzzSplitWithFee), amountToDistribute); + + uint256 prevBalance = ERC20(WEETH_MAINNET_ADDRESS).balanceOf(anotherSplit); + + uint256 balance = ERC20(EETH_MAINNET_ADDRESS).balanceOf(address(fuzzSplitWithFee)); + + uint256 weETHDistributed = IweETH(WEETH_MAINNET_ADDRESS).getWeETHByeETH(balance); + + uint256 amount = fuzzSplitWithFee.distribute(); + + assertTrue(amount > 0, "invalid amount"); + + uint256 afterBalance = ERC20(WEETH_MAINNET_ADDRESS).balanceOf(anotherSplit); + + assertGe(afterBalance, prevBalance, "after balance greater"); + + uint256 expectedFee = (weETHDistributed * fuzzFeeShare) / PERCENTAGE_SCALE; + + assertEq(ERC20(WEETH_MAINNET_ADDRESS).balanceOf(fuzzFeeRecipient), expectedFee, "invalid fee transferred"); + + assertEq(ERC20(WEETH_MAINNET_ADDRESS).balanceOf(anotherSplit), weETHDistributed - expectedFee, "invalid amount"); + } +} diff --git a/src/test/etherfi/ObolEtherfiSplitFactory.t.sol b/src/test/etherfi/ObolEtherfiSplitFactory.t.sol index 08b3e9d..ed98660 100644 --- a/src/test/etherfi/ObolEtherfiSplitFactory.t.sol +++ b/src/test/etherfi/ObolEtherfiSplitFactory.t.sol @@ -1,50 +1,50 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.8.19; - -import "forge-std/Test.sol"; -import {ObolEtherfiSplitFactory} from "src/etherfi/ObolEtherfiSplitFactory.sol"; -import {BaseSplitFactory} from "src/base/BaseSplitFactory.sol"; -import {ERC20} from "solmate/tokens/ERC20.sol"; -import {ObolEtherfiSplitTestHelper} from "./ObolEtherfiSplitTestHelper.sol"; - -contract ObolEtherfiSplitFactoryTest is ObolEtherfiSplitTestHelper, Test { - ObolEtherfiSplitFactory internal etherfiSplitFactory; - ObolEtherfiSplitFactory internal etherfiSplitFactoryWithFee; - - address demoSplit; - - event CreateSplit(address token, address split); - - function setUp() public { - uint256 mainnetBlock = 19_228_949; - vm.createSelectFork(getChain("mainnet").rpcUrl, mainnetBlock); - - etherfiSplitFactory = - new ObolEtherfiSplitFactory(address(0), 0, ERC20(EETH_MAINNET_ADDRESS), ERC20(WEETH_MAINNET_ADDRESS)); - - etherfiSplitFactoryWithFee = - new ObolEtherfiSplitFactory(address(this), 1e3, ERC20(EETH_MAINNET_ADDRESS), ERC20(WEETH_MAINNET_ADDRESS)); - - demoSplit = makeAddr("demoSplit"); - } - - function testCan_CreateSplit() public { - vm.expectEmit(true, true, true, false, address(etherfiSplitFactory)); - emit CreateSplit(address(0), address(0x1)); - - etherfiSplitFactory.createCollector(address(0), demoSplit); - - vm.expectEmit(true, true, true, false, address(etherfiSplitFactoryWithFee)); - emit CreateSplit(address(0), address(0x1)); - - etherfiSplitFactoryWithFee.createCollector(address(0), demoSplit); - } - - function testCannot_CreateSplitInvalidAddress() public { - vm.expectRevert(BaseSplitFactory.Invalid_Address.selector); - etherfiSplitFactory.createCollector(address(0), address(0)); - - vm.expectRevert(BaseSplitFactory.Invalid_Address.selector); - etherfiSplitFactoryWithFee.createCollector(address(0), address(0)); - } -} +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.19; + +import "forge-std/Test.sol"; +import {ObolEtherfiSplitFactory} from "src/etherfi/ObolEtherfiSplitFactory.sol"; +import {BaseSplitFactory} from "src/base/BaseSplitFactory.sol"; +import {ERC20} from "solmate/tokens/ERC20.sol"; +import {ObolEtherfiSplitTestHelper} from "./ObolEtherfiSplitTestHelper.sol"; + +contract ObolEtherfiSplitFactoryTest is ObolEtherfiSplitTestHelper, Test { + ObolEtherfiSplitFactory internal etherfiSplitFactory; + ObolEtherfiSplitFactory internal etherfiSplitFactoryWithFee; + + address demoSplit; + + event CreateSplit(address token, address split); + + function setUp() public { + uint256 mainnetBlock = 19_228_949; + vm.createSelectFork(getChain("mainnet").rpcUrl, mainnetBlock); + + etherfiSplitFactory = + new ObolEtherfiSplitFactory(address(0), 0, ERC20(EETH_MAINNET_ADDRESS), ERC20(WEETH_MAINNET_ADDRESS)); + + etherfiSplitFactoryWithFee = + new ObolEtherfiSplitFactory(address(this), 1e3, ERC20(EETH_MAINNET_ADDRESS), ERC20(WEETH_MAINNET_ADDRESS)); + + demoSplit = makeAddr("demoSplit"); + } + + function testCan_CreateSplit() public { + vm.expectEmit(true, true, true, false, address(etherfiSplitFactory)); + emit CreateSplit(address(0), address(0x1)); + + etherfiSplitFactory.createCollector(address(0), demoSplit); + + vm.expectEmit(true, true, true, false, address(etherfiSplitFactoryWithFee)); + emit CreateSplit(address(0), address(0x1)); + + etherfiSplitFactoryWithFee.createCollector(address(0), demoSplit); + } + + function testCannot_CreateSplitInvalidAddress() public { + vm.expectRevert(BaseSplitFactory.Invalid_Address.selector); + etherfiSplitFactory.createCollector(address(0), address(0)); + + vm.expectRevert(BaseSplitFactory.Invalid_Address.selector); + etherfiSplitFactoryWithFee.createCollector(address(0), address(0)); + } +} diff --git a/src/test/etherfi/ObolEtherfiSplitTestHelper.sol b/src/test/etherfi/ObolEtherfiSplitTestHelper.sol index fcb4b13..f31d639 100644 --- a/src/test/etherfi/ObolEtherfiSplitTestHelper.sol +++ b/src/test/etherfi/ObolEtherfiSplitTestHelper.sol @@ -1,8 +1,8 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.8.19; - -contract ObolEtherfiSplitTestHelper { - address internal EETH_MAINNET_ADDRESS = address(0x35fA164735182de50811E8e2E824cFb9B6118ac2); - address internal WEETH_MAINNET_ADDRESS = address(0xCd5fE23C85820F7B72D0926FC9b05b43E359b7ee); - address internal RANDOM_EETH_ACCOUNT_ADDRESS = address(0x30653c83162ff00918842D8bFe016935Fdd6Ab84); -} +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.19; + +contract ObolEtherfiSplitTestHelper { + address internal EETH_MAINNET_ADDRESS = address(0x35fA164735182de50811E8e2E824cFb9B6118ac2); + address internal WEETH_MAINNET_ADDRESS = address(0xCd5fE23C85820F7B72D0926FC9b05b43E359b7ee); + address internal RANDOM_EETH_ACCOUNT_ADDRESS = address(0x30653c83162ff00918842D8bFe016935Fdd6Ab84); +} diff --git a/src/test/etherfi/integration/ObolEtherfiSplitIntegrationTest.sol b/src/test/etherfi/integration/ObolEtherfiSplitIntegrationTest.sol index 62e2e00..baad28f 100644 --- a/src/test/etherfi/integration/ObolEtherfiSplitIntegrationTest.sol +++ b/src/test/etherfi/integration/ObolEtherfiSplitIntegrationTest.sol @@ -1,64 +1,64 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.8.19; - -import "forge-std/Test.sol"; -import {ObolEtherfiSplitFactory, ObolEtherfiSplit} from "src/etherfi/ObolEtherfiSplitFactory.sol"; -import {ERC20} from "solmate/tokens/ERC20.sol"; -import {ObolEtherfiSplitTestHelper} from "../ObolEtherfiSplitTestHelper.sol"; -import {ISplitMain} from "src/interfaces/ISplitMain.sol"; - -contract ObolEtherfiSplitIntegrationTest is ObolEtherfiSplitTestHelper, Test { - ObolEtherfiSplitFactory internal etherfiSplitFactory; - ObolEtherfiSplit internal etherfiSplit; - - address splitter; - - address[] accounts; - uint32[] percentAllocations; - - address internal SPLIT_MAIN_MAINNET = 0x2ed6c4B5dA6378c7897AC67Ba9e43102Feb694EE; - - function setUp() public { - uint256 mainnetBlock = 19_228_949; - vm.createSelectFork(getChain("mainnet").rpcUrl, mainnetBlock); - - etherfiSplitFactory = - new ObolEtherfiSplitFactory(address(0), 0, ERC20(EETH_MAINNET_ADDRESS), ERC20(WEETH_MAINNET_ADDRESS)); - - accounts = new address[](2); - accounts[0] = makeAddr("accounts0"); - accounts[1] = makeAddr("accounts1"); - - percentAllocations = new uint32[](2); - percentAllocations[0] = 400_000; - percentAllocations[1] = 600_000; - - splitter = ISplitMain(SPLIT_MAIN_MAINNET).createSplit(accounts, percentAllocations, 0, address(0)); - - etherfiSplit = ObolEtherfiSplit(etherfiSplitFactory.createCollector(address(0), splitter)); - } - - function test_etherfi_integration_CanDistribute() public { - vm.prank(RANDOM_EETH_ACCOUNT_ADDRESS); - ERC20(EETH_MAINNET_ADDRESS).transfer(address(etherfiSplit), 100 ether); - - etherfiSplit.distribute(); - - ISplitMain(SPLIT_MAIN_MAINNET).distributeERC20( - splitter, ERC20(WEETH_MAINNET_ADDRESS), accounts, percentAllocations, 0, address(0) - ); - - ERC20[] memory tokens = new ERC20[](1); - tokens[0] = ERC20(WEETH_MAINNET_ADDRESS); - - ISplitMain(SPLIT_MAIN_MAINNET).withdraw(accounts[0], 0, tokens); - ISplitMain(SPLIT_MAIN_MAINNET).withdraw(accounts[1], 0, tokens); - - assertEq( - ERC20(WEETH_MAINNET_ADDRESS).balanceOf(accounts[0]), 38_787_430_925_418_583_374, "invalid account 0 balance" - ); - assertEq( - ERC20(WEETH_MAINNET_ADDRESS).balanceOf(accounts[1]), 58_181_146_388_127_875_061, "invalid account 1 balance" - ); - } -} +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.19; + +import "forge-std/Test.sol"; +import {ObolEtherfiSplitFactory, ObolEtherfiSplit} from "src/etherfi/ObolEtherfiSplitFactory.sol"; +import {ERC20} from "solmate/tokens/ERC20.sol"; +import {ObolEtherfiSplitTestHelper} from "../ObolEtherfiSplitTestHelper.sol"; +import {ISplitMain} from "src/interfaces/external/splits/ISplitMain.sol"; + +contract ObolEtherfiSplitIntegrationTest is ObolEtherfiSplitTestHelper, Test { + ObolEtherfiSplitFactory internal etherfiSplitFactory; + ObolEtherfiSplit internal etherfiSplit; + + address splitter; + + address[] accounts; + uint32[] percentAllocations; + + address internal SPLIT_MAIN_MAINNET = 0x2ed6c4B5dA6378c7897AC67Ba9e43102Feb694EE; + + function setUp() public { + uint256 mainnetBlock = 19_228_949; + vm.createSelectFork(getChain("mainnet").rpcUrl, mainnetBlock); + + etherfiSplitFactory = + new ObolEtherfiSplitFactory(address(0), 0, ERC20(EETH_MAINNET_ADDRESS), ERC20(WEETH_MAINNET_ADDRESS)); + + accounts = new address[](2); + accounts[0] = makeAddr("accounts0"); + accounts[1] = makeAddr("accounts1"); + + percentAllocations = new uint32[](2); + percentAllocations[0] = 400_000; + percentAllocations[1] = 600_000; + + splitter = ISplitMain(SPLIT_MAIN_MAINNET).createSplit(accounts, percentAllocations, 0, address(0)); + + etherfiSplit = ObolEtherfiSplit(etherfiSplitFactory.createCollector(address(0), splitter)); + } + + function test_etherfi_integration_CanDistribute() public { + vm.prank(RANDOM_EETH_ACCOUNT_ADDRESS); + ERC20(EETH_MAINNET_ADDRESS).transfer(address(etherfiSplit), 100 ether); + + etherfiSplit.distribute(); + + ISplitMain(SPLIT_MAIN_MAINNET).distributeERC20( + splitter, ERC20(WEETH_MAINNET_ADDRESS), accounts, percentAllocations, 0, address(0) + ); + + ERC20[] memory tokens = new ERC20[](1); + tokens[0] = ERC20(WEETH_MAINNET_ADDRESS); + + ISplitMain(SPLIT_MAIN_MAINNET).withdraw(accounts[0], 0, tokens); + ISplitMain(SPLIT_MAIN_MAINNET).withdraw(accounts[1], 0, tokens); + + assertEq( + ERC20(WEETH_MAINNET_ADDRESS).balanceOf(accounts[0]), 38_787_430_925_418_583_374, "invalid account 0 balance" + ); + assertEq( + ERC20(WEETH_MAINNET_ADDRESS).balanceOf(accounts[1]), 58_181_146_388_127_875_061, "invalid account 1 balance" + ); + } +} diff --git a/src/test/lido/ObolLIdoSplitFactory.t.sol b/src/test/lido/ObolLIdoSplitFactory.t.sol index 99bbc13..8a5739b 100644 --- a/src/test/lido/ObolLIdoSplitFactory.t.sol +++ b/src/test/lido/ObolLIdoSplitFactory.t.sol @@ -1,50 +1,50 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.8.19; - -import "forge-std/Test.sol"; -import {ObolLidoSplitFactory} from "src/lido/ObolLidoSplitFactory.sol"; -import {ERC20} from "solmate/tokens/ERC20.sol"; -import {BaseSplitFactory} from "src/base/BaseSplitFactory.sol"; -import {ObolLidoSplitTestHelper} from "./ObolLidoSplitTestHelper.sol"; - -contract ObolLidoSplitFactoryTest is ObolLidoSplitTestHelper, Test { - ObolLidoSplitFactory internal lidoSplitFactory; - ObolLidoSplitFactory internal lidoSplitFactoryWithFee; - - address demoSplit; - - event CreateSplit(address token, address split); - - function setUp() public { - uint256 mainnetBlock = 17_421_005; - vm.createSelectFork(getChain("mainnet").rpcUrl, mainnetBlock); - - lidoSplitFactory = - new ObolLidoSplitFactory(address(0), 0, ERC20(STETH_MAINNET_ADDRESS), ERC20(WSTETH_MAINNET_ADDRESS)); - - lidoSplitFactoryWithFee = - new ObolLidoSplitFactory(address(this), 1e3, ERC20(STETH_MAINNET_ADDRESS), ERC20(WSTETH_MAINNET_ADDRESS)); - - demoSplit = makeAddr("demoSplit"); - } - - function testCan_CreateSplit() public { - vm.expectEmit(true, true, true, false, address(lidoSplitFactory)); - emit CreateSplit(address(0), address(0x1)); - - lidoSplitFactory.createCollector(address(0), demoSplit); - - vm.expectEmit(true, true, true, false, address(lidoSplitFactoryWithFee)); - emit CreateSplit(address(0), address(0x1)); - - lidoSplitFactoryWithFee.createCollector(address(0), demoSplit); - } - - function testCannot_CreateSplitInvalidAddress() public { - vm.expectRevert(BaseSplitFactory.Invalid_Address.selector); - lidoSplitFactory.createCollector(address(0), address(0)); - - vm.expectRevert(BaseSplitFactory.Invalid_Address.selector); - lidoSplitFactoryWithFee.createCollector(address(0), address(0)); - } -} +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.19; + +import "forge-std/Test.sol"; +import {ObolLidoSplitFactory} from "src/lido/ObolLidoSplitFactory.sol"; +import {ERC20} from "solmate/tokens/ERC20.sol"; +import {BaseSplitFactory} from "src/base/BaseSplitFactory.sol"; +import {ObolLidoSplitTestHelper} from "./ObolLidoSplitTestHelper.sol"; + +contract ObolLidoSplitFactoryTest is ObolLidoSplitTestHelper, Test { + ObolLidoSplitFactory internal lidoSplitFactory; + ObolLidoSplitFactory internal lidoSplitFactoryWithFee; + + address demoSplit; + + event CreateSplit(address token, address split); + + function setUp() public { + uint256 mainnetBlock = 17_421_005; + vm.createSelectFork(getChain("mainnet").rpcUrl, mainnetBlock); + + lidoSplitFactory = + new ObolLidoSplitFactory(address(0), 0, ERC20(STETH_MAINNET_ADDRESS), ERC20(WSTETH_MAINNET_ADDRESS)); + + lidoSplitFactoryWithFee = + new ObolLidoSplitFactory(address(this), 1e3, ERC20(STETH_MAINNET_ADDRESS), ERC20(WSTETH_MAINNET_ADDRESS)); + + demoSplit = makeAddr("demoSplit"); + } + + function testCan_CreateSplit() public { + vm.expectEmit(true, true, true, false, address(lidoSplitFactory)); + emit CreateSplit(address(0), address(0x1)); + + lidoSplitFactory.createCollector(address(0), demoSplit); + + vm.expectEmit(true, true, true, false, address(lidoSplitFactoryWithFee)); + emit CreateSplit(address(0), address(0x1)); + + lidoSplitFactoryWithFee.createCollector(address(0), demoSplit); + } + + function testCannot_CreateSplitInvalidAddress() public { + vm.expectRevert(BaseSplitFactory.Invalid_Address.selector); + lidoSplitFactory.createCollector(address(0), address(0)); + + vm.expectRevert(BaseSplitFactory.Invalid_Address.selector); + lidoSplitFactoryWithFee.createCollector(address(0), address(0)); + } +} diff --git a/src/test/lido/ObolLidoSplit.t.sol b/src/test/lido/ObolLidoSplit.t.sol index 2a63b37..80d6a89 100644 --- a/src/test/lido/ObolLidoSplit.t.sol +++ b/src/test/lido/ObolLidoSplit.t.sol @@ -1,189 +1,189 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.8.19; - -import "forge-std/Test.sol"; -import {ObolLidoSplitFactory, ObolLidoSplit, IwstETH} from "src/lido/ObolLidoSplitFactory.sol"; -import {BaseSplit} from "src/base/BaseSplit.sol"; -import {ERC20} from "solmate/tokens/ERC20.sol"; -import {ObolLidoSplitTestHelper} from "./ObolLidoSplitTestHelper.sol"; -import {MockERC20} from "src/test/utils/mocks/MockERC20.sol"; - -contract ObolLidoSplitTest is ObolLidoSplitTestHelper, Test { - uint256 internal constant PERCENTAGE_SCALE = 1e5; - - ObolLidoSplitFactory internal lidoSplitFactory; - ObolLidoSplitFactory internal lidoSplitFactoryWithFee; - - ObolLidoSplit internal lidoSplit; - ObolLidoSplit internal lidoSplitWithFee; - - address demoSplit; - address feeRecipient; - uint256 feeShare; - - MockERC20 mERC20; - - function setUp() public { - uint256 mainnetBlock = 17_421_005; - vm.createSelectFork(getChain("mainnet").rpcUrl, mainnetBlock); - - feeRecipient = makeAddr("feeRecipient"); - feeShare = 1e4; - - lidoSplitFactory = - new ObolLidoSplitFactory(address(0), 0, ERC20(STETH_MAINNET_ADDRESS), ERC20(WSTETH_MAINNET_ADDRESS)); - - lidoSplitFactoryWithFee = - new ObolLidoSplitFactory(feeRecipient, feeShare, ERC20(STETH_MAINNET_ADDRESS), ERC20(WSTETH_MAINNET_ADDRESS)); - - demoSplit = makeAddr("demoSplit"); - - lidoSplit = ObolLidoSplit(lidoSplitFactory.createCollector(address(0), demoSplit)); - lidoSplitWithFee = ObolLidoSplit(lidoSplitFactoryWithFee.createCollector(address(0), demoSplit)); - - mERC20 = new MockERC20("Test Token", "TOK", 18); - mERC20.mint(type(uint256).max); - } - - function test_CannotCreateInvalidFeeRecipient() public { - vm.expectRevert(BaseSplit.Invalid_FeeRecipient.selector); - new ObolLidoSplit(address(0), 10, ERC20(STETH_MAINNET_ADDRESS), ERC20(WSTETH_MAINNET_ADDRESS)); - } - - function test_CannotCreateInvalidFeeShare() public { - vm.expectRevert(abi.encodeWithSelector(BaseSplit.Invalid_FeeShare.selector, PERCENTAGE_SCALE + 1)); - new ObolLidoSplit(address(1), PERCENTAGE_SCALE + 1, ERC20(STETH_MAINNET_ADDRESS), ERC20(WSTETH_MAINNET_ADDRESS)); - - vm.expectRevert(abi.encodeWithSelector(BaseSplit.Invalid_FeeShare.selector, PERCENTAGE_SCALE)); - new ObolLidoSplit(address(1), PERCENTAGE_SCALE, ERC20(STETH_MAINNET_ADDRESS), ERC20(WSTETH_MAINNET_ADDRESS)); - } - - function test_CloneArgsIsCorrect() public { - assertEq(lidoSplit.withdrawalAddress(), demoSplit, "invalid address"); - assertEq(address(lidoSplit.stETH()), STETH_MAINNET_ADDRESS, "invalid stETH address"); - assertEq(address(lidoSplit.wstETH()), WSTETH_MAINNET_ADDRESS, "invalid wstETH address"); - assertEq(lidoSplit.feeRecipient(), address(0), "invalid fee recipient"); - assertEq(lidoSplit.feeShare(), 0, "invalid fee amount"); - - assertEq(lidoSplitWithFee.withdrawalAddress(), demoSplit, "invalid address"); - assertEq(address(lidoSplitWithFee.stETH()), STETH_MAINNET_ADDRESS, "invalid stETH address"); - assertEq(address(lidoSplitWithFee.wstETH()), WSTETH_MAINNET_ADDRESS, "invalid wstETH address"); - assertEq(lidoSplitWithFee.feeRecipient(), feeRecipient, "invalid fee recipient /2"); - assertEq(lidoSplitWithFee.feeShare(), feeShare, "invalid fee share /2"); - } - - 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.withdrawalAddress()).balance, amountOfEther, "rescue not successful"); - - // rescue tokens - mERC20.transfer(address(lidoSplit), amountOfEther); - uint256 tokenBalance = lidoSplit.rescueFunds(address(mERC20)); - assertEq(tokenBalance, amountOfEther, "token - balance not rescued"); - assertEq(mERC20.balanceOf(address(lidoSplit)), 0, "token - balance is not zero"); - assertEq(mERC20.balanceOf(lidoSplit.withdrawalAddress()), amountOfEther, "token - rescue not successful"); - } - - function testCannot_RescueLidoTokens() public { - vm.expectRevert(BaseSplit.Invalid_Address.selector); - lidoSplit.rescueFunds(address(STETH_MAINNET_ADDRESS)); - - vm.expectRevert(BaseSplit.Invalid_Address.selector); - lidoSplit.rescueFunds(address(WSTETH_MAINNET_ADDRESS)); - } - - 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); - ERC20(STETH_MAINNET_ADDRESS).transfer(address(lidoSplit), 100 ether); - - uint256 prevBalance = ERC20(WSTETH_MAINNET_ADDRESS).balanceOf(demoSplit); - - uint256 amount = lidoSplit.distribute(); - - assertTrue(amount > 0, "invalid amount"); - - uint256 afterBalance = ERC20(WSTETH_MAINNET_ADDRESS).balanceOf(demoSplit); - - 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 anotherSplit, - uint256 amountToDistribute, - address fuzzFeeRecipient, - uint256 fuzzFeeShare - ) public { - vm.assume(anotherSplit != address(0)); - vm.assume(fuzzFeeRecipient != anotherSplit); - vm.assume(fuzzFeeShare > 0 && fuzzFeeShare < PERCENTAGE_SCALE); - vm.assume(fuzzFeeRecipient != address(0)); - vm.assume(amountToDistribute > 1 ether); - vm.assume(amountToDistribute < 10 ether); - - ObolLidoSplitFactory fuzzFactorySplitWithFee = new ObolLidoSplitFactory( - fuzzFeeRecipient, fuzzFeeShare, ERC20(STETH_MAINNET_ADDRESS), ERC20(WSTETH_MAINNET_ADDRESS) - ); - - ObolLidoSplit fuzzSplitWithFee = ObolLidoSplit(fuzzFactorySplitWithFee.createCollector(address(0), 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"); - } -} +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.19; + +import "forge-std/Test.sol"; +import {ObolLidoSplitFactory, ObolLidoSplit, IwstETH} from "src/lido/ObolLidoSplitFactory.sol"; +import {BaseSplit} from "src/base/BaseSplit.sol"; +import {ERC20} from "solmate/tokens/ERC20.sol"; +import {ObolLidoSplitTestHelper} from "./ObolLidoSplitTestHelper.sol"; +import {MockERC20} from "src/test/utils/mocks/MockERC20.sol"; + +contract ObolLidoSplitTest is ObolLidoSplitTestHelper, Test { + uint256 internal constant PERCENTAGE_SCALE = 1e5; + + ObolLidoSplitFactory internal lidoSplitFactory; + ObolLidoSplitFactory internal lidoSplitFactoryWithFee; + + ObolLidoSplit internal lidoSplit; + ObolLidoSplit internal lidoSplitWithFee; + + address demoSplit; + address feeRecipient; + uint256 feeShare; + + MockERC20 mERC20; + + function setUp() public { + uint256 mainnetBlock = 17_421_005; + vm.createSelectFork(getChain("mainnet").rpcUrl, mainnetBlock); + + feeRecipient = makeAddr("feeRecipient"); + feeShare = 1e4; + + lidoSplitFactory = + new ObolLidoSplitFactory(address(0), 0, ERC20(STETH_MAINNET_ADDRESS), ERC20(WSTETH_MAINNET_ADDRESS)); + + lidoSplitFactoryWithFee = + new ObolLidoSplitFactory(feeRecipient, feeShare, ERC20(STETH_MAINNET_ADDRESS), ERC20(WSTETH_MAINNET_ADDRESS)); + + demoSplit = makeAddr("demoSplit"); + + lidoSplit = ObolLidoSplit(lidoSplitFactory.createCollector(address(0), demoSplit)); + lidoSplitWithFee = ObolLidoSplit(lidoSplitFactoryWithFee.createCollector(address(0), demoSplit)); + + mERC20 = new MockERC20("Test Token", "TOK", 18); + mERC20.mint(type(uint256).max); + } + + function test_CannotCreateInvalidFeeRecipient() public { + vm.expectRevert(BaseSplit.Invalid_FeeRecipient.selector); + new ObolLidoSplit(address(0), 10, ERC20(STETH_MAINNET_ADDRESS), ERC20(WSTETH_MAINNET_ADDRESS)); + } + + function test_CannotCreateInvalidFeeShare() public { + vm.expectRevert(abi.encodeWithSelector(BaseSplit.Invalid_FeeShare.selector, PERCENTAGE_SCALE + 1)); + new ObolLidoSplit(address(1), PERCENTAGE_SCALE + 1, ERC20(STETH_MAINNET_ADDRESS), ERC20(WSTETH_MAINNET_ADDRESS)); + + vm.expectRevert(abi.encodeWithSelector(BaseSplit.Invalid_FeeShare.selector, PERCENTAGE_SCALE)); + new ObolLidoSplit(address(1), PERCENTAGE_SCALE, ERC20(STETH_MAINNET_ADDRESS), ERC20(WSTETH_MAINNET_ADDRESS)); + } + + function test_CloneArgsIsCorrect() public { + assertEq(lidoSplit.withdrawalAddress(), demoSplit, "invalid address"); + assertEq(address(lidoSplit.stETH()), STETH_MAINNET_ADDRESS, "invalid stETH address"); + assertEq(address(lidoSplit.wstETH()), WSTETH_MAINNET_ADDRESS, "invalid wstETH address"); + assertEq(lidoSplit.feeRecipient(), address(0), "invalid fee recipient"); + assertEq(lidoSplit.feeShare(), 0, "invalid fee amount"); + + assertEq(lidoSplitWithFee.withdrawalAddress(), demoSplit, "invalid address"); + assertEq(address(lidoSplitWithFee.stETH()), STETH_MAINNET_ADDRESS, "invalid stETH address"); + assertEq(address(lidoSplitWithFee.wstETH()), WSTETH_MAINNET_ADDRESS, "invalid wstETH address"); + assertEq(lidoSplitWithFee.feeRecipient(), feeRecipient, "invalid fee recipient /2"); + assertEq(lidoSplitWithFee.feeShare(), feeShare, "invalid fee share /2"); + } + + 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.withdrawalAddress()).balance, amountOfEther, "rescue not successful"); + + // rescue tokens + mERC20.transfer(address(lidoSplit), amountOfEther); + uint256 tokenBalance = lidoSplit.rescueFunds(address(mERC20)); + assertEq(tokenBalance, amountOfEther, "token - balance not rescued"); + assertEq(mERC20.balanceOf(address(lidoSplit)), 0, "token - balance is not zero"); + assertEq(mERC20.balanceOf(lidoSplit.withdrawalAddress()), amountOfEther, "token - rescue not successful"); + } + + function testCannot_RescueLidoTokens() public { + vm.expectRevert(BaseSplit.Invalid_Address.selector); + lidoSplit.rescueFunds(address(STETH_MAINNET_ADDRESS)); + + vm.expectRevert(BaseSplit.Invalid_Address.selector); + lidoSplit.rescueFunds(address(WSTETH_MAINNET_ADDRESS)); + } + + 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); + ERC20(STETH_MAINNET_ADDRESS).transfer(address(lidoSplit), 100 ether); + + uint256 prevBalance = ERC20(WSTETH_MAINNET_ADDRESS).balanceOf(demoSplit); + + uint256 amount = lidoSplit.distribute(); + + assertTrue(amount > 0, "invalid amount"); + + uint256 afterBalance = ERC20(WSTETH_MAINNET_ADDRESS).balanceOf(demoSplit); + + 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 anotherSplit, + uint256 amountToDistribute, + address fuzzFeeRecipient, + uint256 fuzzFeeShare + ) public { + vm.assume(anotherSplit != address(0)); + vm.assume(fuzzFeeRecipient != anotherSplit); + vm.assume(fuzzFeeShare > 0 && fuzzFeeShare < PERCENTAGE_SCALE); + vm.assume(fuzzFeeRecipient != address(0)); + vm.assume(amountToDistribute > 1 ether); + vm.assume(amountToDistribute < 10 ether); + + ObolLidoSplitFactory fuzzFactorySplitWithFee = new ObolLidoSplitFactory( + fuzzFeeRecipient, fuzzFeeShare, ERC20(STETH_MAINNET_ADDRESS), ERC20(WSTETH_MAINNET_ADDRESS) + ); + + ObolLidoSplit fuzzSplitWithFee = ObolLidoSplit(fuzzFactorySplitWithFee.createCollector(address(0), 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"); + } +} diff --git a/src/test/lido/integration/LidoSplitIntegrationTest.sol b/src/test/lido/integration/LidoSplitIntegrationTest.sol index 2f89c51..1531df9 100644 --- a/src/test/lido/integration/LidoSplitIntegrationTest.sol +++ b/src/test/lido/integration/LidoSplitIntegrationTest.sol @@ -1,64 +1,64 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.8.19; - -import "forge-std/Test.sol"; -import {ObolLidoSplitFactory, ObolLidoSplit} from "src/lido/ObolLidoSplitFactory.sol"; -import {ERC20} from "solmate/tokens/ERC20.sol"; -import {ObolLidoSplitTestHelper} from "../ObolLidoSplitTestHelper.sol"; -import {ISplitMain} from "src/interfaces/ISplitMain.sol"; - -contract ObolLidoSplitIntegrationTest is ObolLidoSplitTestHelper, Test { - ObolLidoSplitFactory internal lidoSplitFactory; - ObolLidoSplit internal lidoSplit; - - address splitter; - - address[] accounts; - uint32[] percentAllocations; - - address internal SPLIT_MAIN_MAINNET = 0x2ed6c4B5dA6378c7897AC67Ba9e43102Feb694EE; - - function setUp() public { - uint256 mainnetBlock = 17_421_005; - vm.createSelectFork(getChain("mainnet").rpcUrl, mainnetBlock); - - lidoSplitFactory = - new ObolLidoSplitFactory(address(0), 0, ERC20(STETH_MAINNET_ADDRESS), ERC20(WSTETH_MAINNET_ADDRESS)); - - 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 = ObolLidoSplit(lidoSplitFactory.createCollector(address(0), splitter)); - } - - function test_CanDistribute() public { - vm.prank(RANDOM_stETH_ACCOUNT_ADDRESS); - ERC20(STETH_MAINNET_ADDRESS).transfer(address(lidoSplit), 100 ether); - - lidoSplit.distribute(); - - ISplitMain(SPLIT_MAIN_MAINNET).distributeERC20( - splitter, ERC20(WSTETH_MAINNET_ADDRESS), accounts, percentAllocations, 0, address(0) - ); - - 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" - ); - } -} +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.19; + +import "forge-std/Test.sol"; +import {ObolLidoSplitFactory, ObolLidoSplit} from "src/lido/ObolLidoSplitFactory.sol"; +import {ERC20} from "solmate/tokens/ERC20.sol"; +import {ObolLidoSplitTestHelper} from "../ObolLidoSplitTestHelper.sol"; +import {ISplitMain} from "src/interfaces/external/splits/ISplitMain.sol"; + +contract ObolLidoSplitIntegrationTest is ObolLidoSplitTestHelper, Test { + ObolLidoSplitFactory internal lidoSplitFactory; + ObolLidoSplit internal lidoSplit; + + address splitter; + + address[] accounts; + uint32[] percentAllocations; + + address internal SPLIT_MAIN_MAINNET = 0x2ed6c4B5dA6378c7897AC67Ba9e43102Feb694EE; + + function setUp() public { + uint256 mainnetBlock = 17_421_005; + vm.createSelectFork(getChain("mainnet").rpcUrl, mainnetBlock); + + lidoSplitFactory = + new ObolLidoSplitFactory(address(0), 0, ERC20(STETH_MAINNET_ADDRESS), ERC20(WSTETH_MAINNET_ADDRESS)); + + 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 = ObolLidoSplit(lidoSplitFactory.createCollector(address(0), splitter)); + } + + function test_CanDistribute() public { + vm.prank(RANDOM_stETH_ACCOUNT_ADDRESS); + ERC20(STETH_MAINNET_ADDRESS).transfer(address(lidoSplit), 100 ether); + + lidoSplit.distribute(); + + ISplitMain(SPLIT_MAIN_MAINNET).distributeERC20( + splitter, ERC20(WSTETH_MAINNET_ADDRESS), accounts, percentAllocations, 0, address(0) + ); + + 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/owr/OWRTestHelper.t.sol b/src/test/owr/OWRTestHelper.t.sol index 8884e85..0f85e21 100644 --- a/src/test/owr/OWRTestHelper.t.sol +++ b/src/test/owr/OWRTestHelper.t.sol @@ -1,41 +1,41 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.8.17; - -contract OWRTestHelper { - 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); - } -} +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.17; + +contract OWRTestHelper { + 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); + } +} diff --git a/src/test/owr/ObolErc1155Recipient.t.sol b/src/test/owr/ObolErc1155Recipient.t.sol new file mode 100644 index 0000000..30d1128 --- /dev/null +++ b/src/test/owr/ObolErc1155Recipient.t.sol @@ -0,0 +1,151 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.19; + +import "forge-std/Test.sol"; + +import {SafeTransferLib} from "solady/utils/SafeTransferLib.sol"; +import {ObolErc1155Recipient} from "src/owr/ObolErc1155Recipient.sol"; +import {ObolErc1155RecipientMock} from "./ObolErc1155RecipientMock.sol"; +import {OptimisticPullWithdrawalRecipient} from "src/owr/OptimisticPullWithdrawalRecipient.sol"; +import {OptimisticPullWithdrawalRecipientFactory} from "src/owr/OptimisticPullWithdrawalRecipientFactory.sol"; + +import {IERC1155Receiver} from "src/interfaces/IERC1155Receiver.sol"; +import {ISplitMain} from "src/interfaces/external/splits/ISplitMain.sol"; +import {IPullSplit} from "src/interfaces/external/splits/IPullSplit.sol"; +import {IENSReverseRegistrar} from "../../interfaces/external/IENSReverseRegistrar.sol"; + +import {PullSplitMock} from "./mocks/PullSplitMock.sol"; +import {DepositContractMock} from "./mocks/DepositContractMock.sol"; +import {ObolErc1155ReceiverMock} from "./mocks/ObolErc1155ReceiverMock.sol"; + +contract ObolErc1155RecipientTest is Test, IERC1155Receiver { + using SafeTransferLib for address; + + ObolErc1155RecipientMock recipient; + DepositContractMock depositContract; + PullSplitMock pullSplitMock; + string constant BASE_URI = "https://github.com"; + uint256 internal constant ETH_STAKE = 32 ether; + address internal constant ETH_ADDRESS = address(0); + address internal constant ENS_REVERSE_REGISTRAR_GOERLI = 0x084b1c3C81545d370f3634392De611CaaBFf8148; + + receive() external payable {} + + function setUp() public { + depositContract = new DepositContractMock(); + recipient = new ObolErc1155RecipientMock(BASE_URI, address(this), address(depositContract)); + pullSplitMock = new PullSplitMock(); + } + + function onERC1155Received(address, address, uint256, uint256, bytes calldata) + external + pure + override + returns (bytes4) + { + return this.onERC1155Received.selector; + } + + function onERC1155BatchReceived(address, address, uint256[] calldata, uint256[] calldata, bytes calldata) + external + pure + override + returns (bytes4) + { + return this.onERC1155Received.selector; + } + + function supportsInterface(bytes4) external pure override returns (bool) { + return true; + } + + function testInitialSupply_owrErc1155() public { + assertEq(recipient.totalSupplyAll(), 0); + } + + function testCreatePartition_owrErc1155() public { + address owrAddress = makeAddr("owrAddress"); + recipient.createPartition(10, owrAddress); + (uint256 maxSupply, address owr, address operator) = recipient.partitions(0); + assertEq(maxSupply, 10); + assertEq(owr, owrAddress); + assertEq(operator, address(this)); + assertEq(recipient.getPartitionTokensLength(0), 0); + } + + function testMint_owrErc1155() public { + address owrAddress = makeAddr("owrAddress"); + recipient.createPartition(10, owrAddress); + recipient.mint{value: 32 ether}(0, ObolErc1155Recipient.DepositInfo({pubkey: "0x", withdrawal_credentials: "0x", sig: "0x", root: bytes32(0)})); + recipient.mint{value: 32 ether}(0, ObolErc1155Recipient.DepositInfo({pubkey: "0x", withdrawal_credentials: "0x", sig: "0x", root: bytes32(0)})); + + uint256 firstToken = recipient.partitionTokens(0, 0); + assertEq(recipient.ownerOf(firstToken), address(this)); + assertEq(recipient.ownerOf(1), address(this)); + assertEq(recipient.getPartitionTokensLength(0), 2); + } + + + function testRewards_owrErc1155() public { + 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)) + ); + OptimisticPullWithdrawalRecipientFactory owrFactory = + new OptimisticPullWithdrawalRecipientFactory("demo.obol.eth", ENS_REVERSE_REGISTRAR_GOERLI, address(this)); + + OptimisticPullWithdrawalRecipient owrETH = + owrFactory.createOWRecipient(ETH_ADDRESS, address(pullSplitMock), address(this), address(pullSplitMock), ETH_STAKE); + + recipient.createPartition(10, address(owrETH)); + recipient.mint{value: 32 ether}(0, ObolErc1155Recipient.DepositInfo({pubkey: "0x", withdrawal_credentials: "0x", sig: "0x", root: bytes32(0)})); + + address(owrETH).safeTransferETH(1 ether); + assertEq(address(owrETH).balance, 1 ether); + + recipient.distributeRewards(0, address(this), IPullSplit.PullSplitConfiguration({ + recipients: new address[](0), + allocations: new uint256[](0), + totalAllocation: 0, + distributionIncentive: 0 + })); + + uint256 claimable = recipient.claimable(address(this), ETH_ADDRESS); + assertEq(claimable, 1 ether); + } + + function testBurn_owrErc1155() public { + 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)) + ); + OptimisticPullWithdrawalRecipientFactory owrFactory = + new OptimisticPullWithdrawalRecipientFactory("demo.obol.eth", ENS_REVERSE_REGISTRAR_GOERLI, address(this)); + + OptimisticPullWithdrawalRecipient owrETH = + owrFactory.createOWRecipient(ETH_ADDRESS, address(pullSplitMock), address(recipient), address(pullSplitMock), ETH_STAKE); + + recipient.createPartition(2, address(owrETH)); + recipient.mint{value: 32 ether}(0, ObolErc1155Recipient.DepositInfo({pubkey: "0x", withdrawal_credentials: "0x", sig: "0x", root: bytes32(0)})); + + address(owrETH).safeTransferETH(32 ether); + assertEq(address(owrETH).balance, 32 ether); + + uint256 balanceBefore = address(this).balance; + recipient.burn(0); + uint256 balanceAfter = address(this).balance; + assertEq(balanceBefore + 32 ether, balanceAfter); + } +} diff --git a/src/test/owr/ObolErc1155Recipient.t.sol.txt b/src/test/owr/ObolErc1155Recipient.t.sol.txt deleted file mode 100644 index 7ffa9f9..0000000 --- a/src/test/owr/ObolErc1155Recipient.t.sol.txt +++ /dev/null @@ -1,254 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.8.19; - -import "forge-std/Test.sol"; -import {IERC1155Receiver} from "src/interfaces/IERC1155Receiver.sol"; -import {ObolErc1155Recipient} from "src/owr/ObolErc1155Recipient.sol"; -import {ObolErc1155RecipientMock} from "./ObolErc1155RecipientMock.sol"; -import {OptimisticWithdrawalRecipient} from "src/owr/OptimisticWithdrawalRecipient.sol"; -import {OptimisticWithdrawalRecipientFactory} from "src/owr/OptimisticWithdrawalRecipientFactory.sol"; -import {IENSReverseRegistrar} from "../../interfaces/IENSReverseRegistrar.sol"; -import {SafeTransferLib} from "solady/utils/SafeTransferLib.sol"; -import {ObolErc1155ReceiverMock} from "./ObolErc1155ReceiverMock.sol"; -import {DepositContractMock} from "./DepositContractMock.sol"; -import {ISplitMain} from "src/interfaces/ISplitMain.sol"; - - -contract ObolErc1155RecipientTest is Test, IERC1155Receiver { - using SafeTransferLib for address; - - ObolErc1155RecipientMock recipient; - DepositContractMock depositContract; - string constant BASE_URI = "https://github.com"; - uint256 internal constant ETH_STAKE = 32 ether; - address internal constant ETH_ADDRESS = address(0); - address internal constant ENS_REVERSE_REGISTRAR_GOERLI = 0x084b1c3C81545d370f3634392De611CaaBFf8148; - - function setUp() public { - depositContract = new DepositContractMock(); - recipient = new ObolErc1155RecipientMock(BASE_URI, address(this), address(depositContract)); - } - - function onERC1155Received(address, address, uint256, uint256, bytes calldata) - external - pure - override - returns (bytes4) - { - return this.onERC1155Received.selector; - } - - function onERC1155BatchReceived(address, address, uint256[] calldata, uint256[] calldata, bytes calldata) - external - pure - override - returns (bytes4) - { - return this.onERC1155Received.selector; - } - - function supportsInterface(bytes4) external pure override returns (bool) { - return true; - } - - function testInitialSupply_owrErc1155() public { - assertEq(recipient.totalSupplyAll(), 0); - } - - function testTransferFrom_owrErc1155() public { - recipient.mint(address(this), 1, address(0), address(0)); - recipient.mint(address(this), 1, address(0), address(0)); - - vm.expectRevert(); - recipient.safeTransferFrom(address(this), address(this), 1, 0, ""); - - uint256[] memory batchTokens = new uint256[](2); - batchTokens[0] = 1; - batchTokens[1] = 2; - uint256[] memory batchAmounts = new uint256[](2); - batchAmounts[0] = 0; - batchAmounts[0] = 1; - - vm.expectRevert(); - recipient.safeBatchTransferFrom(address(this), address(this), batchTokens, batchAmounts, ""); - } - - function testMint_owrErc1155() public { - recipient.mint(address(this), 1, address(0), address(0)); - bool ownerOf1 = recipient.isReceiverOf(1); - assertEq(ownerOf1, true); - - uint256[] memory amounts = new uint256[](2); - amounts[0] = 1; - amounts[1] = 1; - - address[] memory owrs = new address[](2); - owrs[0] = address(0); - owrs[1] = address(0); - address[] memory rewardAddresses = new address[](2); - rewardAddresses[0] = address(0); - rewardAddresses[1] = address(0); - - recipient.mintBatch(address(this), 2, amounts, owrs, rewardAddresses); - bool ownerOf2 = recipient.isReceiverOf(2); - bool ownerOf3 = recipient.isReceiverOf(3); - assertEq(ownerOf2, true); - assertEq(ownerOf3, true); - } - - function testMintSupply_owrErc1155() public { - recipient.mint(address(this), 1, address(0), address(0)); - bool ownerOf1 = recipient.isReceiverOf(1); - assertEq(ownerOf1, true); - - uint256 maxSupplyBefore = recipient.getMaxSupply(1); - recipient.mintSupply(1, 100); - uint256 maxSupplyAfter = recipient.getMaxSupply(1); - assertGt(maxSupplyAfter, maxSupplyBefore); - } - - // function testBurn_owrErc1155() public { - // recipient.mint(address(this), 100, address(0), address(0)); - // bool ownerOf1 = recipient.isOwnerOf(1); - // assertEq(ownerOf1, true); - - // uint256 totalSupplyBefore = recipient.totalSupply(1); - // recipient.burn(1, 50); - // uint256 totalSupplyAfter = recipient.totalSupply(1); - // assertLt(totalSupplyAfter, totalSupplyBefore); - // } - - function testClaim_owrErc1155() public { - address rewardAddress = makeAddr("rewardAddress"); - 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)) - ); - OptimisticWithdrawalRecipientFactory owrFactory = - new OptimisticWithdrawalRecipientFactory("demo.obol.eth", ENS_REVERSE_REGISTRAR_GOERLI, address(this)); - - OptimisticWithdrawalRecipient owrETH = - owrFactory.createOWRecipient(ETH_ADDRESS, rewardAddress, rewardAddress, rewardAddress, ETH_STAKE); - - recipient.mint(address(this), 1, address(owrETH), rewardAddress); - - address(recipient).safeTransferETH(1 ether); - assertEq(address(recipient).balance, 1 ether); - - recipient.setRewards(1, 1 ether); - assertEq(_getRewards(1), 1 ether); - - recipient.claim(1); - assertEq(rewardAddress.balance, 1 ether); - } - - function testTransferWithRewards_owrErc1155() public { - address rewardAddress = makeAddr("rewardAddress"); - address receiverAddress = address(new ObolErc1155ReceiverMock()); - - 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)) - ); - OptimisticWithdrawalRecipientFactory owrFactory = - new OptimisticWithdrawalRecipientFactory("demo.obol.eth", ENS_REVERSE_REGISTRAR_GOERLI, address(this)); - - OptimisticWithdrawalRecipient owrETH = - owrFactory.createOWRecipient(ETH_ADDRESS, rewardAddress, rewardAddress, rewardAddress, ETH_STAKE); - - recipient.mint(address(this), 1, address(owrETH), rewardAddress); - recipient.simulateReceiverMint(1, 1); - - address(recipient).safeTransferETH(1 ether); - assertEq(address(recipient).balance, 1 ether); - - recipient.setRewards(1, 1 ether); - assertEq(_getRewards(1), 1 ether); - - recipient.safeTransferFrom(address(this), receiverAddress, 1, 1, "0x"); - assertEq(rewardAddress.balance, 1 ether); - } - - function testTransferWithoutRewards_owrErc1155() public { - address rewardAddress = makeAddr("rewardAddress"); - address receiverAddress = address(new ObolErc1155ReceiverMock()); - - 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)) - ); - OptimisticWithdrawalRecipientFactory owrFactory = - new OptimisticWithdrawalRecipientFactory("demo.obol.eth", ENS_REVERSE_REGISTRAR_GOERLI, address(this)); - - OptimisticWithdrawalRecipient owrETH = - owrFactory.createOWRecipient(ETH_ADDRESS, rewardAddress, rewardAddress, rewardAddress, ETH_STAKE); - - recipient.mint(address(this), 1, address(owrETH), rewardAddress); - recipient.simulateReceiverMint(1, 1); - - recipient.safeTransferFrom(address(this), receiverAddress, 1, 1, "0x"); - assertFalse(recipient.isOwnerOf(1)); - - vm.prank(receiverAddress); - assertTrue(recipient.isOwnerOf(1)); - } - - // function testReceiveRewards_owrErc1155() public { - // address rewardAddress = makeAddr("rewardAddress"); - // 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)) - // ); - // OptimisticWithdrawalRecipientFactory owrFactory = - // new OptimisticWithdrawalRecipientFactory("demo.obol.eth", ENS_REVERSE_REGISTRAR_GOERLI, address(this)); - - // OptimisticWithdrawalRecipient owrETH = - // owrFactory.createOWRecipient(ETH_ADDRESS, rewardAddress, rewardAddress, address(recipient), ETH_STAKE); - - // address(owrETH).safeTransferETH(1 ether); - - // recipient.mint(address(this), 1, address(owrETH), rewardAddress); - // recipient.simulateReceiverMint(1, 1); - // bool ownerOf1 = recipient.isOwnerOf(1); - // assertEq(ownerOf1, true); - - // uint256 registeredRewards = _getRewards(1); - // assertEq(registeredRewards, 0); - - // recipient.receiveRewards(address(owrETH)); - // assertEq(address(owrETH).balance, 0 ether); - - // registeredRewards = _getRewards(1); - // assertEq(registeredRewards, 1 ether); - // } - - - function _getRewards(uint256 id) private view returns (uint256) { - (, , uint256 claimable , ,) = recipient.tokenInfo(id); - return claimable; - } -} diff --git a/src/test/owr/ObolErc1155RecipientMock.sol.txt b/src/test/owr/ObolErc1155RecipientMock.sol similarity index 79% rename from src/test/owr/ObolErc1155RecipientMock.sol.txt rename to src/test/owr/ObolErc1155RecipientMock.sol index b2701bc..029d349 100644 --- a/src/test/owr/ObolErc1155RecipientMock.sol.txt +++ b/src/test/owr/ObolErc1155RecipientMock.sol @@ -1,18 +1,18 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.8.19; - -import {ObolErc1155Recipient} from "src/owr/ObolErc1155Recipient.sol"; - -contract ObolErc1155RecipientMock is ObolErc1155Recipient { - constructor(string memory baseUri_, address _owner, address _depositContract) ObolErc1155Recipient(baseUri_, _owner, _depositContract) { - } - - function setRewards(uint256 id, uint256 amount) external { - tokenInfo[id].claimable += amount; - } - - function simulateReceiverMint(uint256 id, uint256 amount) external { - (bool success,) = address(this).call(abi.encodeWithSelector(this.safeTransferFrom.selector, address(this), tokenInfo[id].receiver, id, amount, "0x")); - if (!success) revert TransferFailed(); - } +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.19; + +import {ObolErc1155Recipient} from "src/owr/ObolErc1155Recipient.sol"; + +contract ObolErc1155RecipientMock is ObolErc1155Recipient { + constructor(string memory baseUri_, address _owner, address _depositContract) ObolErc1155Recipient(baseUri_, _owner, _depositContract) { + } + + function setRewards(uint256 id, uint256 amount) external { + claimable[ownerOf[id]][address(0)] += amount; + } + + function simulateReceiverMint(uint256 id, uint256 amount) external { + (bool success,) = address(this).call(abi.encodeWithSelector(this.safeTransferFrom.selector, address(this), ownerOf[id], id, amount, "0x")); + if (!success) revert TransferFailed(); + } } \ No newline at end of file diff --git a/src/test/owr/OptimisticWithdrawalRecipient.t.sol b/src/test/owr/OptimisticWithdrawalRecipient.t.sol index c30d1d5..384c562 100644 --- a/src/test/owr/OptimisticWithdrawalRecipient.t.sol +++ b/src/test/owr/OptimisticWithdrawalRecipient.t.sol @@ -1,799 +1,799 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.8.19; - -import "forge-std/Test.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"; -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; - - OptimisticWithdrawalRecipient owrETH; - OptimisticWithdrawalRecipient owrERC20; - OptimisticWithdrawalRecipient owrETH_OR; - OptimisticWithdrawalRecipient owrERC20_OR; - MockERC20 mERC20; - - address public principalRecipient; - address public rewardRecipient; - uint256 internal trancheThreshold; - - function setUp() public { - mERC20 = new MockERC20("demo", "DMT", 18); - mERC20.mint(type(uint256).max); - - 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(); - - (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 _principalRecipient, address _rewardRecipient, uint256 wtrancheThreshold) = owrETH.getTranches(); - - 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 { - 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 principalPayout; - uint256 rewardPayout; - - vm.expectEmit(true, true, true, true); - emit DistributeFunds(principalPayout, rewardPayout, 0); - owrETH.distributeFunds(); - } - - function testCan_distributeToSecondRecipient() public { - address(owrETH).safeTransferETH(1 ether); - - uint256 rewardPayout = 1 ether; - uint256 principalPayout; - - vm.expectEmit(true, true, true, true); - emit DistributeFunds(principalPayout, rewardPayout, 0); - owrETH.distributeFunds(); - assertEq(address(owrETH).balance, 0 ether); - assertEq(rewardRecipient.balance, 1 ether); - - rewardPayout = 0; - vm.expectEmit(true, true, true, true); - emit DistributeFunds(principalPayout, rewardPayout, 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); - - 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 { - 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 principalPayout = 32 ether; - uint256 rewardPayout = 4 ether; - - vm.expectEmit(true, true, true, true); - emit DistributeFunds(principalPayout, rewardPayout, 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(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 { - 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); - - 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 { - // eth - vm.deal(address(owrETH), type(uint128).max); - owrETH.distributeFunds(); - vm.deal(address(owrETH), 1); - - vm.deal(address(owrETH), type(uint136).max); - vm.expectRevert(OptimisticWithdrawalRecipient.InvalidDistribution_TooLarge.selector); - owrETH.distributeFunds(); - - vm.expectRevert(OptimisticWithdrawalRecipient.InvalidDistribution_TooLarge.selector); - owrETH.distributeFundsPull(); - - // token - address(mERC20).safeTransfer(address(owrERC20_OR), type(uint128).max); - owrERC20_OR.distributeFunds(); - address(mERC20).safeTransfer(address(owrERC20_OR), 1); - - address(mERC20).safeTransfer(address(owrERC20_OR), type(uint136).max); - 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); - address(owrETH).safeTransferETH(33 ether); - - vm.expectRevert(SafeTransferLib.ETHTransferFailed.selector); - owrETH.distributeFunds(); - - assertEq(address(owrETH).balance, 33 ether); - assertEq(address(wr).balance, 0 ether); - assertEq(address(0).balance, 0 ether); - } - - function testCan_distributeToPullFlow() public { - // test eth - address(owrETH).safeTransferETH(36 ether); - owrETH.distributeFundsPull(); - - assertEq(address(owrETH).balance, 36 ether); - assertEq(principalRecipient.balance, 0 ether); - assertEq(rewardRecipient.balance, 0 ether); - - assertEq(owrETH.getPullBalance(principalRecipient), 32 ether); - assertEq(owrETH.getPullBalance(rewardRecipient), 4 ether); - - assertEq(owrETH.fundsPendingWithdrawal(), 36 ether); - - owrETH.withdraw(rewardRecipient); - - assertEq(address(owrETH).balance, 32 ether); - assertEq(principalRecipient.balance, 0); - assertEq(rewardRecipient.balance, 4 ether); - - assertEq(owrETH.getPullBalance(principalRecipient), 32 ether); - assertEq(owrETH.getPullBalance(rewardRecipient), 0); - - assertEq(owrETH.fundsPendingWithdrawal(), 32 ether); - - owrETH.withdraw(principalRecipient); - - assertEq(address(owrETH).balance, 0 ether); - assertEq(principalRecipient.balance, 32 ether); - assertEq(rewardRecipient.balance, 4 ether); - - assertEq(owrETH.getPullBalance(principalRecipient), 0); - assertEq(owrETH.getPullBalance(rewardRecipient), 0); - - 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.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.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.fundsPendingWithdrawal(), 0 ether); - } - - function testCan_distributePushAndPull() public { - // test eth - address(owrETH).safeTransferETH(0.5 ether); - assertEq(address(owrETH).balance, 0.5 ether, "2/incorrect balance"); - - owrETH.distributeFunds(); - - assertEq(address(owrETH).balance, 0, "3/incorrect balance"); - assertEq(principalRecipient.balance, 0 ether); - assertEq(rewardRecipient.balance, 0.5 ether); - - assertEq(owrETH.getPullBalance(principalRecipient), 0 ether); - assertEq(owrETH.getPullBalance(rewardRecipient), 0 ether); - - assertEq(owrETH.fundsPendingWithdrawal(), 0 ether); - - address(owrETH).safeTransferETH(1 ether); - assertEq(address(owrETH).balance, 1 ether); - - owrETH.distributeFundsPull(); - - assertEq(address(owrETH).balance, 1 ether); - assertEq(principalRecipient.balance, 0 ether); - assertEq(rewardRecipient.balance, 0.5 ether); - - assertEq(owrETH.getPullBalance(principalRecipient), 0 ether); - assertEq(owrETH.getPullBalance(rewardRecipient), 1 ether); - - assertEq(owrETH.fundsPendingWithdrawal(), 1 ether); - - owrETH.distributeFunds(); - - assertEq(address(owrETH).balance, 1 ether); - assertEq(principalRecipient.balance, 0 ether); - assertEq(rewardRecipient.balance, 0.5 ether); - - assertEq(owrETH.getPullBalance(principalRecipient), 0); - assertEq(owrETH.getPullBalance(rewardRecipient), 1 ether); - - assertEq(owrETH.fundsPendingWithdrawal(), 1 ether); - - owrETH.distributeFundsPull(); - - assertEq(address(owrETH).balance, 1 ether); - assertEq(principalRecipient.balance, 0 ether); - assertEq(rewardRecipient.balance, 0.5 ether); - - assertEq(owrETH.getPullBalance(principalRecipient), 0); - assertEq(owrETH.getPullBalance(rewardRecipient), 1 ether); - - assertEq(owrETH.fundsPendingWithdrawal(), 1 ether); - - address(owrETH).safeTransferETH(1 ether); - assertEq(address(owrETH).balance, 2 ether); - - owrETH.distributeFunds(); - - assertEq(address(owrETH).balance, 1 ether); - assertEq(principalRecipient.balance, 0); - assertEq(rewardRecipient.balance, 1.5 ether); - - assertEq(owrETH.getPullBalance(principalRecipient), 0 ether); - assertEq(owrETH.getPullBalance(rewardRecipient), 1 ether); - - assertEq(owrETH.fundsPendingWithdrawal(), 1 ether); - - owrETH.withdraw(rewardRecipient); - - assertEq(address(owrETH).balance, 0 ether); - assertEq(principalRecipient.balance, 0); - assertEq(rewardRecipient.balance, 2.5 ether); - - assertEq(owrETH.getPullBalance(principalRecipient), 0 ether); - assertEq(owrETH.getPullBalance(rewardRecipient), 0 ether); - - assertEq(owrETH.fundsPendingWithdrawal(), 0); - - address(owrETH).safeTransferETH(1 ether); - owrETH.withdraw(rewardRecipient); - - assertEq(address(owrETH).balance, 1 ether); - assertEq(principalRecipient.balance, 0 ether); - assertEq(rewardRecipient.balance, 2.5 ether); - - assertEq(owrETH.getPullBalance(principalRecipient), 0 ether); - assertEq(owrETH.getPullBalance(rewardRecipient), 0 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.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.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.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.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.fundsPendingWithdrawal(), 0 ether); - } - - 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 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.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.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( - 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 - ); - - /// test eth - - 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.fundsPendingWithdrawal(), _totalETHAmount); - - uint256 principal = owrETH.getPullBalance(_principalRecipient); - assertEq( - owrETH.getPullBalance(_principalRecipient), - (_ethAmount >= BALANCE_CLASSIFICATION_THRESHOLD) - ? _trancheThreshold > _totalETHAmount ? _totalETHAmount : _trancheThreshold - : 0, - "5/invalid recipient balance" - ); - - uint256 reward = owrETH.getPullBalance(_rewardRecipient); - assertEq( - owrETH.getPullBalance(_rewardRecipient), - (_ethAmount >= BALANCE_CLASSIFICATION_THRESHOLD) - ? _totalETHAmount > _trancheThreshold ? (_totalETHAmount - _trancheThreshold) : 0 - : _totalETHAmount, - "6/invalid recipient balance" - ); - - owrETH.withdraw(_principalRecipient); - owrETH.withdraw(_rewardRecipient); - - assertEq(address(owrETH).balance, 0); - 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.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.fundsPendingWithdrawal(), 0, "20/invalid funds pending"); - - assertEq(mERC20.balanceOf(_principalRecipient), principal, "21/invalid principal balance"); - assertEq(mERC20.balanceOf(_rewardRecipient), reward, "22/invalid reward balance"); - } +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.19; + +import "forge-std/Test.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"; +import {IENSReverseRegistrar} from "../../interfaces/external/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; + + OptimisticWithdrawalRecipient owrETH; + OptimisticWithdrawalRecipient owrERC20; + OptimisticWithdrawalRecipient owrETH_OR; + OptimisticWithdrawalRecipient owrERC20_OR; + MockERC20 mERC20; + + address public principalRecipient; + address public rewardRecipient; + uint256 internal trancheThreshold; + + function setUp() public { + mERC20 = new MockERC20("demo", "DMT", 18); + mERC20.mint(type(uint256).max); + + 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(); + + (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 _principalRecipient, address _rewardRecipient, uint256 wtrancheThreshold) = owrETH.getTranches(); + + 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 { + 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 principalPayout; + uint256 rewardPayout; + + vm.expectEmit(true, true, true, true); + emit DistributeFunds(principalPayout, rewardPayout, 0); + owrETH.distributeFunds(); + } + + function testCan_distributeToSecondRecipient() public { + address(owrETH).safeTransferETH(1 ether); + + uint256 rewardPayout = 1 ether; + uint256 principalPayout; + + vm.expectEmit(true, true, true, true); + emit DistributeFunds(principalPayout, rewardPayout, 0); + owrETH.distributeFunds(); + assertEq(address(owrETH).balance, 0 ether); + assertEq(rewardRecipient.balance, 1 ether); + + rewardPayout = 0; + vm.expectEmit(true, true, true, true); + emit DistributeFunds(principalPayout, rewardPayout, 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); + + 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 { + 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 principalPayout = 32 ether; + uint256 rewardPayout = 4 ether; + + vm.expectEmit(true, true, true, true); + emit DistributeFunds(principalPayout, rewardPayout, 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(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 { + 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); + + 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 { + // eth + vm.deal(address(owrETH), type(uint128).max); + owrETH.distributeFunds(); + vm.deal(address(owrETH), 1); + + vm.deal(address(owrETH), type(uint136).max); + vm.expectRevert(OptimisticWithdrawalRecipient.InvalidDistribution_TooLarge.selector); + owrETH.distributeFunds(); + + vm.expectRevert(OptimisticWithdrawalRecipient.InvalidDistribution_TooLarge.selector); + owrETH.distributeFundsPull(); + + // token + address(mERC20).safeTransfer(address(owrERC20_OR), type(uint128).max); + owrERC20_OR.distributeFunds(); + address(mERC20).safeTransfer(address(owrERC20_OR), 1); + + address(mERC20).safeTransfer(address(owrERC20_OR), type(uint136).max); + 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); + address(owrETH).safeTransferETH(33 ether); + + vm.expectRevert(SafeTransferLib.ETHTransferFailed.selector); + owrETH.distributeFunds(); + + assertEq(address(owrETH).balance, 33 ether); + assertEq(address(wr).balance, 0 ether); + assertEq(address(0).balance, 0 ether); + } + + function testCan_distributeToPullFlow() public { + // test eth + address(owrETH).safeTransferETH(36 ether); + owrETH.distributeFundsPull(); + + assertEq(address(owrETH).balance, 36 ether); + assertEq(principalRecipient.balance, 0 ether); + assertEq(rewardRecipient.balance, 0 ether); + + assertEq(owrETH.getPullBalance(principalRecipient), 32 ether); + assertEq(owrETH.getPullBalance(rewardRecipient), 4 ether); + + assertEq(owrETH.fundsPendingWithdrawal(), 36 ether); + + owrETH.withdraw(rewardRecipient); + + assertEq(address(owrETH).balance, 32 ether); + assertEq(principalRecipient.balance, 0); + assertEq(rewardRecipient.balance, 4 ether); + + assertEq(owrETH.getPullBalance(principalRecipient), 32 ether); + assertEq(owrETH.getPullBalance(rewardRecipient), 0); + + assertEq(owrETH.fundsPendingWithdrawal(), 32 ether); + + owrETH.withdraw(principalRecipient); + + assertEq(address(owrETH).balance, 0 ether); + assertEq(principalRecipient.balance, 32 ether); + assertEq(rewardRecipient.balance, 4 ether); + + assertEq(owrETH.getPullBalance(principalRecipient), 0); + assertEq(owrETH.getPullBalance(rewardRecipient), 0); + + 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.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.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.fundsPendingWithdrawal(), 0 ether); + } + + function testCan_distributePushAndPull() public { + // test eth + address(owrETH).safeTransferETH(0.5 ether); + assertEq(address(owrETH).balance, 0.5 ether, "2/incorrect balance"); + + owrETH.distributeFunds(); + + assertEq(address(owrETH).balance, 0, "3/incorrect balance"); + assertEq(principalRecipient.balance, 0 ether); + assertEq(rewardRecipient.balance, 0.5 ether); + + assertEq(owrETH.getPullBalance(principalRecipient), 0 ether); + assertEq(owrETH.getPullBalance(rewardRecipient), 0 ether); + + assertEq(owrETH.fundsPendingWithdrawal(), 0 ether); + + address(owrETH).safeTransferETH(1 ether); + assertEq(address(owrETH).balance, 1 ether); + + owrETH.distributeFundsPull(); + + assertEq(address(owrETH).balance, 1 ether); + assertEq(principalRecipient.balance, 0 ether); + assertEq(rewardRecipient.balance, 0.5 ether); + + assertEq(owrETH.getPullBalance(principalRecipient), 0 ether); + assertEq(owrETH.getPullBalance(rewardRecipient), 1 ether); + + assertEq(owrETH.fundsPendingWithdrawal(), 1 ether); + + owrETH.distributeFunds(); + + assertEq(address(owrETH).balance, 1 ether); + assertEq(principalRecipient.balance, 0 ether); + assertEq(rewardRecipient.balance, 0.5 ether); + + assertEq(owrETH.getPullBalance(principalRecipient), 0); + assertEq(owrETH.getPullBalance(rewardRecipient), 1 ether); + + assertEq(owrETH.fundsPendingWithdrawal(), 1 ether); + + owrETH.distributeFundsPull(); + + assertEq(address(owrETH).balance, 1 ether); + assertEq(principalRecipient.balance, 0 ether); + assertEq(rewardRecipient.balance, 0.5 ether); + + assertEq(owrETH.getPullBalance(principalRecipient), 0); + assertEq(owrETH.getPullBalance(rewardRecipient), 1 ether); + + assertEq(owrETH.fundsPendingWithdrawal(), 1 ether); + + address(owrETH).safeTransferETH(1 ether); + assertEq(address(owrETH).balance, 2 ether); + + owrETH.distributeFunds(); + + assertEq(address(owrETH).balance, 1 ether); + assertEq(principalRecipient.balance, 0); + assertEq(rewardRecipient.balance, 1.5 ether); + + assertEq(owrETH.getPullBalance(principalRecipient), 0 ether); + assertEq(owrETH.getPullBalance(rewardRecipient), 1 ether); + + assertEq(owrETH.fundsPendingWithdrawal(), 1 ether); + + owrETH.withdraw(rewardRecipient); + + assertEq(address(owrETH).balance, 0 ether); + assertEq(principalRecipient.balance, 0); + assertEq(rewardRecipient.balance, 2.5 ether); + + assertEq(owrETH.getPullBalance(principalRecipient), 0 ether); + assertEq(owrETH.getPullBalance(rewardRecipient), 0 ether); + + assertEq(owrETH.fundsPendingWithdrawal(), 0); + + address(owrETH).safeTransferETH(1 ether); + owrETH.withdraw(rewardRecipient); + + assertEq(address(owrETH).balance, 1 ether); + assertEq(principalRecipient.balance, 0 ether); + assertEq(rewardRecipient.balance, 2.5 ether); + + assertEq(owrETH.getPullBalance(principalRecipient), 0 ether); + assertEq(owrETH.getPullBalance(rewardRecipient), 0 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.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.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.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.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.fundsPendingWithdrawal(), 0 ether); + } + + 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 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.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.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( + 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 + ); + + /// test eth + + 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.fundsPendingWithdrawal(), _totalETHAmount); + + uint256 principal = owrETH.getPullBalance(_principalRecipient); + assertEq( + owrETH.getPullBalance(_principalRecipient), + (_ethAmount >= BALANCE_CLASSIFICATION_THRESHOLD) + ? _trancheThreshold > _totalETHAmount ? _totalETHAmount : _trancheThreshold + : 0, + "5/invalid recipient balance" + ); + + uint256 reward = owrETH.getPullBalance(_rewardRecipient); + assertEq( + owrETH.getPullBalance(_rewardRecipient), + (_ethAmount >= BALANCE_CLASSIFICATION_THRESHOLD) + ? _totalETHAmount > _trancheThreshold ? (_totalETHAmount - _trancheThreshold) : 0 + : _totalETHAmount, + "6/invalid recipient balance" + ); + + owrETH.withdraw(_principalRecipient); + owrETH.withdraw(_rewardRecipient); + + assertEq(address(owrETH).balance, 0); + 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.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.fundsPendingWithdrawal(), 0, "20/invalid funds pending"); + + assertEq(mERC20.balanceOf(_principalRecipient), principal, "21/invalid principal balance"); + assertEq(mERC20.balanceOf(_rewardRecipient), reward, "22/invalid reward balance"); + } } \ No newline at end of file diff --git a/src/test/owr/OptimisticWithdrawalRecipientFactory.t.sol b/src/test/owr/OptimisticWithdrawalRecipientFactory.t.sol index 8743304..fbea40c 100644 --- a/src/test/owr/OptimisticWithdrawalRecipientFactory.t.sol +++ b/src/test/owr/OptimisticWithdrawalRecipientFactory.t.sol @@ -1,188 +1,188 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.8.19; - -import "forge-std/Test.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"; -import {IENSReverseRegistrar} from "../../interfaces/IENSReverseRegistrar.sol"; - -contract OptimisticWithdrawalRecipientFactoryTest is OWRTestHelper, Test { - event CreateOWRecipient( - address indexed owr, - address token, - address recoveryAddress, - address principalRecipient, - address rewardRecipient, - uint256 threshold - ); - - address public ENS_REVERSE_REGISTRAR_GOERLI = 0x084b1c3C81545d370f3634392De611CaaBFf8148; - - 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); - - 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; - } - - 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); - } -} +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.19; + +import "forge-std/Test.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"; +import {IENSReverseRegistrar} from "../../interfaces/external/IENSReverseRegistrar.sol"; + +contract OptimisticWithdrawalRecipientFactoryTest is OWRTestHelper, Test { + event CreateOWRecipient( + address indexed owr, + address token, + address recoveryAddress, + address principalRecipient, + address rewardRecipient, + uint256 threshold + ); + + address public ENS_REVERSE_REGISTRAR_GOERLI = 0x084b1c3C81545d370f3634392De611CaaBFf8148; + + 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); + + 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; + } + + 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); + } +} diff --git a/src/test/owr/DepositContractMock.sol b/src/test/owr/mocks/DepositContractMock.sol similarity index 62% rename from src/test/owr/DepositContractMock.sol rename to src/test/owr/mocks/DepositContractMock.sol index de53405..c0a3296 100644 --- a/src/test/owr/DepositContractMock.sol +++ b/src/test/owr/mocks/DepositContractMock.sol @@ -1,25 +1,17 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.8.19; - -contract DepositContractMock { - /// @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 {} - - /// @notice Query the current deposit root hash. - /// @return The deposit root hash. - function get_deposit_root() external pure returns (bytes32) {return bytes32(uint256(1));} - - /// @notice Query the current deposit count. - /// @return The deposit count encoded as a little endian 64-bit number. - function get_deposit_count() external pure returns (bytes memory) {return "0x";} +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.19; + +contract DepositContractMock { + /// @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 {} } \ No newline at end of file diff --git a/src/test/owr/ObolErc1155ReceiverMock.sol.txt b/src/test/owr/mocks/ObolErc1155ReceiverMock.sol similarity index 96% rename from src/test/owr/ObolErc1155ReceiverMock.sol.txt rename to src/test/owr/mocks/ObolErc1155ReceiverMock.sol index 1d3554d..7671775 100644 --- a/src/test/owr/ObolErc1155ReceiverMock.sol.txt +++ b/src/test/owr/mocks/ObolErc1155ReceiverMock.sol @@ -1,30 +1,30 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.8.19; - -import {IERC1155Receiver} from "src/interfaces/IERC1155Receiver.sol"; - -contract ObolErc1155ReceiverMock is IERC1155Receiver { - function onERC1155Received( - address, - address, - uint256, - uint256, - bytes calldata - ) external pure override returns (bytes4) { - return this.onERC1155Received.selector; - } - - function onERC1155BatchReceived( - address, - address, - uint256[] calldata, - uint256[] calldata, - bytes calldata - ) external pure override returns (bytes4){ - return this.onERC1155Received.selector; - } - - function supportsInterface(bytes4) external pure override returns (bool) { - return true; - } +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.19; + +import {IERC1155Receiver} from "src/interfaces/IERC1155Receiver.sol"; + +contract ObolErc1155ReceiverMock is IERC1155Receiver { + function onERC1155Received( + address, + address, + uint256, + uint256, + bytes calldata + ) external pure override returns (bytes4) { + return this.onERC1155Received.selector; + } + + function onERC1155BatchReceived( + address, + address, + uint256[] calldata, + uint256[] calldata, + bytes calldata + ) external pure override returns (bytes4){ + return this.onERC1155Received.selector; + } + + function supportsInterface(bytes4) external pure override returns (bool) { + return true; + } } \ No newline at end of file diff --git a/src/test/owr/mocks/PullSplitMock.sol b/src/test/owr/mocks/PullSplitMock.sol new file mode 100644 index 0000000..c74cd64 --- /dev/null +++ b/src/test/owr/mocks/PullSplitMock.sol @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.19; + +import {IPullSplit} from "src/interfaces/external/splits/IPullSplit.sol"; + + +contract PullSplitMock { + address public SPLITS_WAREHOUSE; + + constructor() { + SPLITS_WAREHOUSE = address(this); + } + + function distribute( + IPullSplit.PullSplitConfiguration calldata, + address, + address + ) external { + payable(msg.sender).transfer(address(this).balance); + } + + function execCalls(IPullSplit.Call[] calldata _calls) + external + payable + returns (uint256, bytes[] memory) { + + IPullSplit.Call memory firstCall = _calls[0]; + (bool success, ) = firstCall.to.call{value: firstCall.value}(firstCall.data); + require(success, "failed"); + + return (1, new bytes[](1)); + } + + receive() external payable {} + + function withdraw(address _to, address) external { + payable(_to).transfer(address(this).balance); + } +} \ No newline at end of file From 243862b30c932a2080ec57f155e01371f4aedb77 Mon Sep 17 00:00:00 2001 From: cosminobol Date: Mon, 17 Jun 2024 10:22:59 +0300 Subject: [PATCH 11/17] patch: updated burn for ERC1155 recipient --- script/ObolLidoSetupScript.sol | 2 +- src/.DS_Store | Bin 0 -> 6148 bytes .../IOptimisticPullWithdrawalRecipient.sol | 3 +- src/owr/ObolErc1155Recipient.sol | 43 +++++++++++++----- src/owr/OptimisticPullWithdrawalRecipient.sol | 21 +++++---- 5 files changed, 48 insertions(+), 21 deletions(-) create mode 100644 src/.DS_Store diff --git a/script/ObolLidoSetupScript.sol b/script/ObolLidoSetupScript.sol index ed2f7c8..bd10a72 100644 --- a/script/ObolLidoSetupScript.sol +++ b/script/ObolLidoSetupScript.sol @@ -2,7 +2,7 @@ pragma solidity 0.8.19; import "forge-std/Script.sol"; -import {ISplitMain, SplitConfiguration} from "src/interfaces/ISplitMain.sol"; +import {ISplitMain, SplitConfiguration} from "src/interfaces/external/splits/ISplitMain.sol"; import {ObolLidoSplitFactory} from "src/lido/ObolLidoSplitFactory.sol"; /// @title ObolLidoScript diff --git a/src/.DS_Store b/src/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..1abfc226fe780b5aa258da8cdcaa0ee090795717 GIT binary patch literal 6148 zcmeHK%}T>S5T0$TO%fDt{Y)WlW|#;hc%Ig~=K`a-^m&*RMQ zR_u>g5t)J6Z+3PjVZIH!82}LNL2n134gd}+VX2109HDj687Wy$1ySf{Bya~l+(IAH znP_(WMFwc^mLP-}GTgOq`xoTta}cQj*WZKZC`_~M?GI5ZR~8nlPSsgYy*a31#aZhiGw#hD+*{X{2(y*`FqU4(H@Hd#TR#%z{&MYTf$! zaJaX7*pRLLQ9}+}dj}2KY&J)unzOdKb#&T&jGkikYWNiR>1kQ9IEFVg=9c#2C2^$U zdt9g7GAPI)`5y(_Vx~$QQ^Lp$FaykhVZa`o&N36i3@`)Cz!U>?K1ftT*J5E%A00U8 z766fcBekGSy#(b*i>}4OAg-VYlZt3kg>5l}Nk_l5ajwO}ph*W|n-5{%ENq7&^xN_I zQip?Z4f4ngFawhel+CnG_y5Vy{r_YV&zJ#b;9oHyDjl!W#**yaI#V3owG#CPm4xC7 mgI_6V=&KlG=_=kp)q;LW2BK@RFo+%${t?hL@W2fGDg&SH%ZRlA literal 0 HcmV?d00001 diff --git a/src/interfaces/IOptimisticPullWithdrawalRecipient.sol b/src/interfaces/IOptimisticPullWithdrawalRecipient.sol index 981c48e..bccb336 100644 --- a/src/interfaces/IOptimisticPullWithdrawalRecipient.sol +++ b/src/interfaces/IOptimisticPullWithdrawalRecipient.sol @@ -6,5 +6,6 @@ interface IOptimisticPullWithdrawalRecipient { function distributeFunds() external payable; function distributeFundsPull() external payable; function getTranches() external view returns (address principalRecipient, address rewardRecipient, uint256 amountOfPrincipalStake); - function withdraw(address account) external; + function withdraw(address account, uint256 amount) external; + function pullBalances(address account) external view returns (uint256); } \ No newline at end of file diff --git a/src/owr/ObolErc1155Recipient.sol b/src/owr/ObolErc1155Recipient.sol index db2684d..1e424f5 100644 --- a/src/owr/ObolErc1155Recipient.sol +++ b/src/owr/ObolErc1155Recipient.sol @@ -164,26 +164,47 @@ contract ObolErc1155Recipient is ERC1155, Ownable, IERC1155Receiver { if (!isOwnerOf(_tokenId)) revert InvalidOwner(); // retrieve OWR - address _owr = partitions[tokensPartition[_tokenId]].owr; - if (_owr == address(0)) revert OwrNotValid(); + IOptimisticPullWithdrawalRecipient _owr = IOptimisticPullWithdrawalRecipient(partitions[tokensPartition[_tokenId]].owr); + if (address(_owr) == address(0)) revert OwrNotValid(); // retrieve ETH from the OWR - uint256 ethBalanceBefore = address(this).balance; - IOptimisticPullWithdrawalRecipient(_owr).distributeFunds(); - IOptimisticPullWithdrawalRecipient(_owr).withdraw(address(this)); - uint256 ethBalanceAfter = address(this).balance; - uint256 ethReceived = ethBalanceAfter - ethBalanceBefore; + _owr.distributeFunds(); + _owr.withdraw(address(this), ETH_DEPOSIT_AMOUNT); + + _burn(msg.sender, _tokenId, 1); + + totalSupply[_tokenId]--; + totalSupplyAll--; + + (bool sent,) = msg.sender.call{value: ETH_DEPOSIT_AMOUNT}(""); + if (!sent) revert TransferFailed(); + } + + /// @notice decreases totalSupply for token id + /// @param _tokenId token id + function burnSlashed(uint256 _tokenId) external { + // validate + if (!isOwnerOf(_tokenId)) revert InvalidOwner(); + + // retrieve OWR + IOptimisticPullWithdrawalRecipient _owr = IOptimisticPullWithdrawalRecipient(partitions[tokensPartition[_tokenId]].owr); + if (address(_owr) == address(0)) revert OwrNotValid(); + + // retrieve ETH from the OWR + _owr.distributeFunds(); - // TODO: what if ethReceived > 32 - // : should we distribute 32 to sender and remaining split between active validators ? - if (ethReceived < MIN_ETH_EXIT_AMOUNT) revert InvalidBurnAmount(MIN_ETH_EXIT_AMOUNT, ethReceived); + // withdraw from the OWR + uint256 pullBalance = _owr.pullBalances(address(this)); + uint256 toWithdraw = pullBalance < ETH_DEPOSIT_AMOUNT ? pullBalance: ETH_DEPOSIT_AMOUNT; + if (toWithdraw < MIN_ETH_EXIT_AMOUNT) revert InvalidBurnAmount(MIN_ETH_EXIT_AMOUNT, toWithdraw); + _owr.withdraw(address(this), toWithdraw); _burn(msg.sender, _tokenId, 1); totalSupply[_tokenId]--; totalSupplyAll--; - (bool sent,) = msg.sender.call{value: ethReceived}(""); + (bool sent,) = msg.sender.call{value: toWithdraw}(""); if (!sent) revert TransferFailed(); } diff --git a/src/owr/OptimisticPullWithdrawalRecipient.sol b/src/owr/OptimisticPullWithdrawalRecipient.sol index c34490f..170d815 100644 --- a/src/owr/OptimisticPullWithdrawalRecipient.sol +++ b/src/owr/OptimisticPullWithdrawalRecipient.sol @@ -33,6 +33,9 @@ contract OptimisticPullWithdrawalRecipient is Clone { /// Invalid distribution error InvalidDistribution_TooLarge(); + /// Invalid withdraw + error InvalidWithdrawAmount_TooLarge(); + /// ----------------------------------------------------------------------- /// events /// ----------------------------------------------------------------------- @@ -265,18 +268,20 @@ contract OptimisticPullWithdrawalRecipient is Clone { /// 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]; + /// @param amount Amount to withdraw + function withdraw(address account, uint256 amount) external { + if (pullBalances[account] < amount) revert InvalidWithdrawAmount_TooLarge(); unchecked { // shouldn't underflow; fundsPendingWithdrawal = sum(pullBalances) - fundsPendingWithdrawal -= uint128(tokenAmount); + fundsPendingWithdrawal -= uint128(amount); } - pullBalances[account] = 0; - if (_token == ETH_ADDRESS) account.safeTransferETH(tokenAmount); - else _token.safeTransfer(account, tokenAmount); + pullBalances[account] -= amount; + + address _token = token(); + if (_token == ETH_ADDRESS) account.safeTransferETH(amount); + else _token.safeTransfer(account, amount); - emit Withdrawal(account, tokenAmount); + emit Withdrawal(account, amount); } /// ----------------------------------------------------------------------- From 9cc613a3abc5896bb262a96caf5731a61651f693 Mon Sep 17 00:00:00 2001 From: cosminobol Date: Wed, 19 Jun 2024 10:52:46 +0300 Subject: [PATCH 12/17] patch: added holesky deployment data for erc1155 --- script/CreateERC1155RecipientTestData.s.sol | 43 +++ script/DeployERC1155Recipient.s.sol | 19 ++ script/DeployMockDepositContract.s.sol | 17 + script/DeployPullOWRAndSplit.s.sol | 77 +++++ script/OWRFactoryScript.s.sol | 38 +-- script/ObolLidoSetupScript.sol | 310 +++++++++--------- script/ObolLidoSplitFactoryScript.s.sol | 40 +-- script/PullOWRFactoryScript.s.sol | 19 ++ script/SplitterConfiguration.sol | 89 +++++ script/data/deploy-pullOwr-sample.json | 20 ++ script/data/lido-data-sample.json | 86 ++--- src/interfaces/IObolErc1155Recipient.sol | 25 ++ .../IOptimisticPullWithdrawalRecipient.sol | 2 +- src/owr/ObolErc1155Recipient.sol | 22 +- src/test/owr/ObolErc1155Recipient.t.sol | 10 +- src/test/owr/mocks/DepositContractMock.sol | 18 +- 16 files changed, 571 insertions(+), 264 deletions(-) create mode 100644 script/CreateERC1155RecipientTestData.s.sol create mode 100644 script/DeployERC1155Recipient.s.sol create mode 100644 script/DeployMockDepositContract.s.sol create mode 100644 script/DeployPullOWRAndSplit.s.sol create mode 100644 script/PullOWRFactoryScript.s.sol create mode 100644 script/SplitterConfiguration.sol create mode 100644 script/data/deploy-pullOwr-sample.json create mode 100644 src/interfaces/IObolErc1155Recipient.sol diff --git a/script/CreateERC1155RecipientTestData.s.sol b/script/CreateERC1155RecipientTestData.s.sol new file mode 100644 index 0000000..aa23ae8 --- /dev/null +++ b/script/CreateERC1155RecipientTestData.s.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.19; + +import "forge-std/Script.sol"; +import {IObolErc1155Recipient} from "src/interfaces/IObolErc1155Recipient.sol"; + +contract CreateERC1155RecipientTestData is Script { + function run(address _erc1155Recipient, address _owr) external { + IObolErc1155Recipient _recipient = IObolErc1155Recipient(_erc1155Recipient); + + IObolErc1155Recipient.DepositInfo memory depositInfo = IObolErc1155Recipient.DepositInfo({ + pubkey: "", + withdrawal_credentials: "", + sig: "", + root: bytes32(0) + }); + + // Create 3 partitions with the following configuration: + // - 1 with a max supply of 20 out of which 3 are active + // - 1 with a max supply of 50 out of which 2 are active + // - 1 with a max supply of 10, all of them active + + //0 + _recipient.createPartition(20, _owr); + + //1 + _recipient.createPartition(50, _owr); + + //2 + _recipient.createPartition(10, _owr); + + // Recipient should use a mock version of the deposit contract + // Have 1 active validator in each partition + for(uint256 i; i < 3; i++) { + _recipient.mint{value: 0}(i, depositInfo); + } + + // Activate the rest of the validators in the 3rd partition + for(uint256 i; i < 9; i++) { + _recipient.mint{value: 0}(2, depositInfo); + } + } +} \ No newline at end of file diff --git a/script/DeployERC1155Recipient.s.sol b/script/DeployERC1155Recipient.s.sol new file mode 100644 index 0000000..4eb8e4d --- /dev/null +++ b/script/DeployERC1155Recipient.s.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.19; + +import "forge-std/Script.sol"; +import {ObolErc1155Recipient} from "src/owr/ObolErc1155Recipient.sol"; + +contract DeployERC1155Recipient is Script { + + // @dev `_depositContract` is passed to allow + function run(string memory _baseUri, address _owner, address _depositContract) external { + uint256 privKey = vm.envUint("PRIVATE_KEY"); + + vm.startBroadcast(privKey); + + new ObolErc1155Recipient{salt: keccak256("owr.erc1155recipient")}(_baseUri, _owner, _depositContract); + + vm.stopBroadcast(); + } +} diff --git a/script/DeployMockDepositContract.s.sol b/script/DeployMockDepositContract.s.sol new file mode 100644 index 0000000..e676d32 --- /dev/null +++ b/script/DeployMockDepositContract.s.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.19; + +import "forge-std/Script.sol"; +import {DepositContractMock} from "src/test/owr/mocks/DepositContractMock.sol"; + +contract DeployMockDepositContract is Script { + function run() external { + uint256 privKey = vm.envUint("PRIVATE_KEY"); + + vm.startBroadcast(privKey); + + new DepositContractMock{salt: keccak256("depositContractMock")}(); + + vm.stopBroadcast(); + } +} \ No newline at end of file diff --git a/script/DeployPullOWRAndSplit.s.sol b/script/DeployPullOWRAndSplit.s.sol new file mode 100644 index 0000000..42dce01 --- /dev/null +++ b/script/DeployPullOWRAndSplit.s.sol @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.19; + +import "forge-std/Script.sol"; +import {OptimisticPullWithdrawalRecipientFactory} from "src/owr/OptimisticPullWithdrawalRecipientFactory.sol"; +import {ISplitMain, SplitConfiguration} from "src/interfaces/external/splits/ISplitMain.sol"; +import {SplitterConfiguration} from "./SplitterConfiguration.sol"; + +contract DeployPullOWRAndSplit is Script, SplitterConfiguration { + address private constant ETH_ADDRESS = address(0); + + error Invalid_PrincipalRecipient(); + + struct ConfigurationData { + address principalRecipient; + address recoveryRecipient; + JsonSplitData split; + } + + /// @param jsonFilePath the data format can be seen in ./data/deploy-pullOWR-sample.json + /// @param splitMain address for 0xsplits splitMain + /// @param pullOwrFactory address for factory + function run(string memory jsonFilePath, address splitMain, address pullOwrFactory, uint256 stakeSize) + external + { + uint256 privKey = vm.envUint("PRIVATE_KEY"); + bytes memory parsedJson = vm.parseJson(vm.readFile(jsonFilePath)); + + ConfigurationData[] memory data = abi.decode(parsedJson, (ConfigurationData[])); + _validateInputJson(data); + + // deploy the split and obol script + string memory jsonKey = "pullOwrDeploy"; + string memory finalJSON; + + uint256 stakeAmount = stakeSize * 1 ether; + + for (uint256 i = 0; i < data.length; i++) { + // deploy split + ConfigurationData memory currentConfiguration = data[i]; + + vm.startBroadcast(privKey); + + address split = ISplitMain(splitMain).createSplit( + currentConfiguration.split.accounts, + currentConfiguration.split.percentAllocations, + currentConfiguration.split.distributorFee, + currentConfiguration.split.controller + ); + + // create obol split + address pullOwrAddress = address( + OptimisticPullWithdrawalRecipientFactory(pullOwrFactory).createOWRecipient( + ETH_ADDRESS, currentConfiguration.recoveryRecipient, currentConfiguration.principalRecipient, split, stakeAmount + ) + ); + + vm.stopBroadcast(); + + string memory objKey = vm.toString(i); + + vm.serializeAddress(objKey, "splitAddress", split); + string memory repsonse = vm.serializeAddress(objKey, "pullOWRAddress", pullOwrAddress); + + finalJSON = vm.serializeString(jsonKey, objKey, repsonse); + } + + vm.writeJson(finalJSON, "./pullOwr-split.json"); + } + + function _validateInputJson(ConfigurationData[] memory configuration) internal pure { + for (uint256 i = 0; i < configuration.length; i++) { + if (configuration[i].principalRecipient == address(0)) revert Invalid_PrincipalRecipient(); + _validateSplitInputJson(configuration[i].split); + } + } +} \ No newline at end of file diff --git a/script/OWRFactoryScript.s.sol b/script/OWRFactoryScript.s.sol index 6568e95..dc59a51 100644 --- a/script/OWRFactoryScript.s.sol +++ b/script/OWRFactoryScript.s.sol @@ -1,19 +1,19 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.19; - -import "forge-std/Script.sol"; -import {OptimisticWithdrawalRecipientFactory} from "src/owr/OptimisticWithdrawalRecipientFactory.sol"; - -contract OWRFactoryScript is Script { - function run(string memory _name, address _ensReverseRegistrar, address _ensOwner) external { - uint256 privKey = vm.envUint("PRIVATE_KEY"); - - vm.startBroadcast(privKey); - - new OptimisticWithdrawalRecipientFactory{salt: keccak256("obol.owrFactory.v1")}( - _name, _ensReverseRegistrar, _ensOwner - ); - - vm.stopBroadcast(); - } -} +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import "forge-std/Script.sol"; +import {OptimisticWithdrawalRecipientFactory} from "src/owr/OptimisticWithdrawalRecipientFactory.sol"; + +contract OWRFactoryScript is Script { + function run(string memory _name, address _ensReverseRegistrar, address _ensOwner) external { + uint256 privKey = vm.envUint("PRIVATE_KEY"); + + vm.startBroadcast(privKey); + + new OptimisticWithdrawalRecipientFactory{salt: keccak256("obol.owrFactory.v1")}( + _name, _ensReverseRegistrar, _ensOwner + ); + + vm.stopBroadcast(); + } +} diff --git a/script/ObolLidoSetupScript.sol b/script/ObolLidoSetupScript.sol index bd10a72..ab085b8 100644 --- a/script/ObolLidoSetupScript.sol +++ b/script/ObolLidoSetupScript.sol @@ -1,155 +1,155 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.19; - -import "forge-std/Script.sol"; -import {ISplitMain, SplitConfiguration} from "src/interfaces/external/splits/ISplitMain.sol"; -import {ObolLidoSplitFactory} from "src/lido/ObolLidoSplitFactory.sol"; - -/// @title ObolLidoScript -/// @author Obol -/// @notice Creates Split and ObolLidoSplit Adddresses -/// -/// @dev Takes a json file following the format defined at ./data/lido-data-sample.json -/// and deploys split and ObolLido split contracts. -/// -/// It outputs the result of the script to "./result.json" -/// -/// NOTE: It's COMPULSORY the json file supplied follows the arrangement format defined -/// in the sample file else the json parse will fail. -/// -/// -/// To Run -/// -/// Step 1 fill in the appropriate details for env vars -/// > cp .env.deployment .env -/// -/// Step 2 add to environment -/// > source .env -/// -/// Step 3 Run forge script to simulate the execution of the transaction -/// -/// > forge script script/ObolLidoSetupScript.sol:ObolLidoSetupScript --fork-url $RPC_URL -vvvv --sig -/// "run(string,address,address)" "" $SPLITMAIN -/// $OBOL_LIDO_SPLIT_FACTORY -/// -/// add --broadcast flag to broadcast to the public blockchain - -contract ObolLidoSetupScript is Script { - /// @dev invalid split accounts configuration - error InvalidSplit__TooFewAccounts(uint256 accountsLength); - /// @notice Array lengths of accounts & percentAllocations don't match - /// (`accountsLength` != `allocationsLength`) - /// @param accountsLength Length of accounts array - /// @param allocationsLength Length of percentAllocations array - error InvalidSplit__AccountsAndAllocationsMismatch(uint256 accountsLength, uint256 allocationsLength); - /// @notice Invalid percentAllocations sum `allocationsSum` must equal - /// `PERCENTAGE_SCALE` - /// @param allocationsSum Sum of percentAllocations array - error InvalidSplit__InvalidAllocationsSum(uint32 allocationsSum); - /// @notice Invalid accounts ordering at `index` - /// @param index Index of out-of-order account - error InvalidSplit__AccountsOutOfOrder(uint256 index); - /// @notice Invalid percentAllocation of zero at `index` - /// @param index Index of zero percentAllocation - error InvalidSplit__AllocationMustBePositive(uint256 index); - /// @notice Invalid distributorFee `distributorFee` cannot be greater than - /// 10% (1e5) - /// @param distributorFee Invalid distributorFee amount - error InvalidSplit__InvalidDistributorFee(uint32 distributorFee); - /// @notice Array of accounts size - /// @param size acounts size - error InvalidSplit__TooManyAccounts(uint256 size); - - uint256 internal constant PERCENTAGE_SCALE = 1e6; - uint256 internal constant MAX_DISTRIBUTOR_FEE = 1e5; - - struct JsonSplitConfiguration { - address[] accounts; - address controller; - uint32 distributorFee; - uint32[] percentAllocations; - } - - function run(string memory jsonFilePath, address splitMain, address obolLidoSplitFactory) external { - uint256 privKey = vm.envUint("PRIVATE_KEY"); - - string memory file = vm.readFile(jsonFilePath); - bytes memory parsedJson = vm.parseJson(file); - JsonSplitConfiguration[] memory configuration = abi.decode(parsedJson, (JsonSplitConfiguration[])); - _validateInputJson(configuration); - - // deploy the split and obol script - string memory jsonKey = "lidoObolDeploy"; - string memory finalJSON; - - for (uint256 j = 0; j < configuration.length; j++) { - string memory objKey = vm.toString(j); - // deploy split - JsonSplitConfiguration memory currentConfiguration = configuration[j]; - - vm.startBroadcast(privKey); - - address split = ISplitMain(splitMain).createSplit( - currentConfiguration.accounts, - currentConfiguration.percentAllocations, - currentConfiguration.distributorFee, - currentConfiguration.controller - ); - - // create obol split - address obolLidoSplitAdress = ObolLidoSplitFactory(obolLidoSplitFactory).createCollector(address(0), split); - - vm.stopBroadcast(); - - vm.serializeAddress(objKey, "splitAddress", split); - string memory repsonse = vm.serializeAddress(objKey, "obolLidoSplitAddress", obolLidoSplitAdress); - - finalJSON = vm.serializeString(jsonKey, objKey, repsonse); - } - - vm.writeJson(finalJSON, "./result.json"); - } - - function _validateInputJson(JsonSplitConfiguration[] memory configuration) internal pure { - for (uint256 i = 0; i < configuration.length; i++) { - address[] memory splitAddresses = configuration[i].accounts; - uint32[] memory percents = configuration[i].percentAllocations; - uint32 distributorFee = configuration[i].distributorFee; - _validSplit(splitAddresses, percents, distributorFee); - } - } - - function _validSplit(address[] memory accounts, uint32[] memory percentAllocations, uint32 distributorFee) - internal - pure - { - if (accounts.length < 2) revert InvalidSplit__TooFewAccounts(accounts.length); - if (accounts.length != percentAllocations.length) { - revert InvalidSplit__AccountsAndAllocationsMismatch(accounts.length, percentAllocations.length); - } - // _getSum should overflow if any percentAllocation[i] < 0 - if (_getSum(percentAllocations) != PERCENTAGE_SCALE) { - revert InvalidSplit__InvalidAllocationsSum(_getSum(percentAllocations)); - } - unchecked { - // overflow should be impossible in for-loop index - // cache accounts length to save gas - uint256 loopLength = accounts.length - 1; - for (uint256 i = 0; i < loopLength; ++i) { - // overflow should be impossible in array access math - if (accounts[i] >= accounts[i + 1]) revert InvalidSplit__AccountsOutOfOrder(i); - if (percentAllocations[i] == uint32(0)) revert InvalidSplit__AllocationMustBePositive(i); - } - // overflow should be impossible in array access math with validated - // equal array lengths - if (percentAllocations[loopLength] == uint32(0)) revert InvalidSplit__AllocationMustBePositive(loopLength); - } - if (distributorFee > MAX_DISTRIBUTOR_FEE) revert InvalidSplit__InvalidDistributorFee(distributorFee); - } - - function _getSum(uint32[] memory percents) internal pure returns (uint32 sum) { - for (uint32 i = 0; i < percents.length; i++) { - sum += percents[i]; - } - } -} +// SPDX-License-Identifier: MIT +pragma solidity 0.8.19; + +import "forge-std/Script.sol"; +import {ISplitMain, SplitConfiguration} from "src/interfaces/external/splits/ISplitMain.sol"; +import {ObolLidoSplitFactory} from "src/lido/ObolLidoSplitFactory.sol"; + +/// @title ObolLidoScript +/// @author Obol +/// @notice Creates Split and ObolLidoSplit Adddresses +/// +/// @dev Takes a json file following the format defined at ./data/lido-data-sample.json +/// and deploys split and ObolLido split contracts. +/// +/// It outputs the result of the script to "./result.json" +/// +/// NOTE: It's COMPULSORY the json file supplied follows the arrangement format defined +/// in the sample file else the json parse will fail. +/// +/// +/// To Run +/// +/// Step 1 fill in the appropriate details for env vars +/// > cp .env.deployment .env +/// +/// Step 2 add to environment +/// > source .env +/// +/// Step 3 Run forge script to simulate the execution of the transaction +/// +/// > forge script script/ObolLidoSetupScript.sol:ObolLidoSetupScript --fork-url $RPC_URL -vvvv --sig +/// "run(string,address,address)" "" $SPLITMAIN +/// $OBOL_LIDO_SPLIT_FACTORY +/// +/// add --broadcast flag to broadcast to the public blockchain + +contract ObolLidoSetupScript is Script { + /// @dev invalid split accounts configuration + error InvalidSplit__TooFewAccounts(uint256 accountsLength); + /// @notice Array lengths of accounts & percentAllocations don't match + /// (`accountsLength` != `allocationsLength`) + /// @param accountsLength Length of accounts array + /// @param allocationsLength Length of percentAllocations array + error InvalidSplit__AccountsAndAllocationsMismatch(uint256 accountsLength, uint256 allocationsLength); + /// @notice Invalid percentAllocations sum `allocationsSum` must equal + /// `PERCENTAGE_SCALE` + /// @param allocationsSum Sum of percentAllocations array + error InvalidSplit__InvalidAllocationsSum(uint32 allocationsSum); + /// @notice Invalid accounts ordering at `index` + /// @param index Index of out-of-order account + error InvalidSplit__AccountsOutOfOrder(uint256 index); + /// @notice Invalid percentAllocation of zero at `index` + /// @param index Index of zero percentAllocation + error InvalidSplit__AllocationMustBePositive(uint256 index); + /// @notice Invalid distributorFee `distributorFee` cannot be greater than + /// 10% (1e5) + /// @param distributorFee Invalid distributorFee amount + error InvalidSplit__InvalidDistributorFee(uint32 distributorFee); + /// @notice Array of accounts size + /// @param size acounts size + error InvalidSplit__TooManyAccounts(uint256 size); + + uint256 internal constant PERCENTAGE_SCALE = 1e6; + uint256 internal constant MAX_DISTRIBUTOR_FEE = 1e5; + + struct JsonSplitConfiguration { + address[] accounts; + address controller; + uint32 distributorFee; + uint32[] percentAllocations; + } + + function run(string memory jsonFilePath, address splitMain, address obolLidoSplitFactory) external { + uint256 privKey = vm.envUint("PRIVATE_KEY"); + + string memory file = vm.readFile(jsonFilePath); + bytes memory parsedJson = vm.parseJson(file); + JsonSplitConfiguration[] memory configuration = abi.decode(parsedJson, (JsonSplitConfiguration[])); + _validateInputJson(configuration); + + // deploy the split and obol script + string memory jsonKey = "lidoObolDeploy"; + string memory finalJSON; + + for (uint256 j = 0; j < configuration.length; j++) { + string memory objKey = vm.toString(j); + // deploy split + JsonSplitConfiguration memory currentConfiguration = configuration[j]; + + vm.startBroadcast(privKey); + + address split = ISplitMain(splitMain).createSplit( + currentConfiguration.accounts, + currentConfiguration.percentAllocations, + currentConfiguration.distributorFee, + currentConfiguration.controller + ); + + // create obol split + address obolLidoSplitAdress = ObolLidoSplitFactory(obolLidoSplitFactory).createCollector(address(0), split); + + vm.stopBroadcast(); + + vm.serializeAddress(objKey, "splitAddress", split); + string memory repsonse = vm.serializeAddress(objKey, "obolLidoSplitAddress", obolLidoSplitAdress); + + finalJSON = vm.serializeString(jsonKey, objKey, repsonse); + } + + vm.writeJson(finalJSON, "./result.json"); + } + + function _validateInputJson(JsonSplitConfiguration[] memory configuration) internal pure { + for (uint256 i = 0; i < configuration.length; i++) { + address[] memory splitAddresses = configuration[i].accounts; + uint32[] memory percents = configuration[i].percentAllocations; + uint32 distributorFee = configuration[i].distributorFee; + _validSplit(splitAddresses, percents, distributorFee); + } + } + + function _validSplit(address[] memory accounts, uint32[] memory percentAllocations, uint32 distributorFee) + internal + pure + { + if (accounts.length < 2) revert InvalidSplit__TooFewAccounts(accounts.length); + if (accounts.length != percentAllocations.length) { + revert InvalidSplit__AccountsAndAllocationsMismatch(accounts.length, percentAllocations.length); + } + // _getSum should overflow if any percentAllocation[i] < 0 + if (_getSum(percentAllocations) != PERCENTAGE_SCALE) { + revert InvalidSplit__InvalidAllocationsSum(_getSum(percentAllocations)); + } + unchecked { + // overflow should be impossible in for-loop index + // cache accounts length to save gas + uint256 loopLength = accounts.length - 1; + for (uint256 i = 0; i < loopLength; ++i) { + // overflow should be impossible in array access math + if (accounts[i] >= accounts[i + 1]) revert InvalidSplit__AccountsOutOfOrder(i); + if (percentAllocations[i] == uint32(0)) revert InvalidSplit__AllocationMustBePositive(i); + } + // overflow should be impossible in array access math with validated + // equal array lengths + if (percentAllocations[loopLength] == uint32(0)) revert InvalidSplit__AllocationMustBePositive(loopLength); + } + if (distributorFee > MAX_DISTRIBUTOR_FEE) revert InvalidSplit__InvalidDistributorFee(distributorFee); + } + + function _getSum(uint32[] memory percents) internal pure returns (uint32 sum) { + for (uint32 i = 0; i < percents.length; i++) { + sum += percents[i]; + } + } +} diff --git a/script/ObolLidoSplitFactoryScript.s.sol b/script/ObolLidoSplitFactoryScript.s.sol index ffa778d..88ff282 100644 --- a/script/ObolLidoSplitFactoryScript.s.sol +++ b/script/ObolLidoSplitFactoryScript.s.sol @@ -1,20 +1,20 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity 0.8.19; - -import "forge-std/Script.sol"; -import {ObolLidoSplitFactory} from "src/lido/ObolLidoSplitFactory.sol"; -import {ERC20} from "solmate/tokens/ERC20.sol"; - -contract ObolLidoSplitFactoryScript is Script { - function run(address _feeRecipient, uint256 _feeShare, address _stETH, address _wstETH) external { - uint256 privKey = vm.envUint("PRIVATE_KEY"); - vm.startBroadcast(privKey); - - ERC20 stETH = ERC20(_stETH); - ERC20 wstETH = ERC20(_wstETH); - - new ObolLidoSplitFactory{salt: keccak256("obol.lidoSplitFactory.v1")}(_feeRecipient, _feeShare, stETH, wstETH); - - vm.stopBroadcast(); - } -} +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.19; + +import "forge-std/Script.sol"; +import {ObolLidoSplitFactory} from "src/lido/ObolLidoSplitFactory.sol"; +import {ERC20} from "solmate/tokens/ERC20.sol"; + +contract ObolLidoSplitFactoryScript is Script { + function run(address _feeRecipient, uint256 _feeShare, address _stETH, address _wstETH) external { + uint256 privKey = vm.envUint("PRIVATE_KEY"); + vm.startBroadcast(privKey); + + ERC20 stETH = ERC20(_stETH); + ERC20 wstETH = ERC20(_wstETH); + + new ObolLidoSplitFactory{salt: keccak256("obol.lidoSplitFactory.v1")}(_feeRecipient, _feeShare, stETH, wstETH); + + vm.stopBroadcast(); + } +} diff --git a/script/PullOWRFactoryScript.s.sol b/script/PullOWRFactoryScript.s.sol new file mode 100644 index 0000000..8aa59a3 --- /dev/null +++ b/script/PullOWRFactoryScript.s.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import "forge-std/Script.sol"; +import {OptimisticPullWithdrawalRecipientFactory} from "src/owr/OptimisticPullWithdrawalRecipientFactory.sol"; + +contract PullOWRFactoryScript is Script { + function run(string memory _name, address _ensReverseRegistrar, address _ensOwner) external { + uint256 privKey = vm.envUint("PRIVATE_KEY"); + + vm.startBroadcast(privKey); + + new OptimisticPullWithdrawalRecipientFactory{salt: keccak256("obol.pullOwrFactory.v1")}( + _name, _ensReverseRegistrar, _ensOwner + ); + + vm.stopBroadcast(); + } +} diff --git a/script/SplitterConfiguration.sol b/script/SplitterConfiguration.sol new file mode 100644 index 0000000..fbc6ea5 --- /dev/null +++ b/script/SplitterConfiguration.sol @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.19; + +abstract contract SplitterConfiguration { + /// @dev invalid split accounts configuration + error InvalidSplit__TooFewAccounts(uint256 accountsLength); + /// @notice Array lengths of accounts & percentAllocations don't match + /// (`accountsLength` != `allocationsLength`) + /// @param accountsLength Length of accounts array + /// @param allocationsLength Length of percentAllocations array + error InvalidSplit__AccountsAndAllocationsMismatch(uint256 accountsLength, uint256 allocationsLength); + /// @notice Invalid percentAllocations sum `allocationsSum` must equal + /// `PERCENTAGE_SCALE` + /// @param allocationsSum Sum of percentAllocations array + error InvalidSplit__InvalidAllocationsSum(uint32 allocationsSum); + /// @notice Invalid accounts ordering at `index` + /// @param index Index of out-of-order account + error InvalidSplit__AccountsOutOfOrder(uint256 index); + /// @notice Invalid percentAllocation of zero at `index` + /// @param index Index of zero percentAllocation + error InvalidSplit__AllocationMustBePositive(uint256 index); + /// @notice Invalid distributorFee `distributorFee` cannot be greater than + /// 10% (1e5) + /// @param distributorFee Invalid distributorFee amount + error InvalidSplit__InvalidDistributorFee(uint32 distributorFee); + /// @notice Array of accounts size + /// @param size acounts size + error InvalidSplit__TooManyAccounts(uint256 size); + + uint256 internal constant PERCENTAGE_SCALE = 1e6; + uint256 internal constant MAX_DISTRIBUTOR_FEE = 1e5; + + struct JsonSplitData { + address[] accounts; + address controller; + uint32 distributorFee; + uint32[] percentAllocations; + } + + function _validateSplitInputJson(JsonSplitData[] memory configuration) internal pure { + for (uint256 i = 0; i < configuration.length; i++) { + address[] memory splitAddresses = configuration[i].accounts; + uint32[] memory percents = configuration[i].percentAllocations; + uint32 distributorFee = configuration[i].distributorFee; + _validSplit(splitAddresses, percents, distributorFee); + } + } + + function _validateSplitInputJson(JsonSplitData memory configuration) internal pure { + address[] memory splitAddresses = configuration.accounts; + uint32[] memory percents = configuration.percentAllocations; + uint32 distributorFee = configuration.distributorFee; + _validSplit(splitAddresses, percents, distributorFee); + } + + function _validSplit(address[] memory accounts, uint32[] memory percentAllocations, uint32 distributorFee) + internal + pure + { + if (accounts.length < 2) revert InvalidSplit__TooFewAccounts(accounts.length); + if (accounts.length != percentAllocations.length) { + revert InvalidSplit__AccountsAndAllocationsMismatch(accounts.length, percentAllocations.length); + } + // _getSum should overflow if any percentAllocation[i] < 0 + if (_getSum(percentAllocations) != PERCENTAGE_SCALE) { + revert InvalidSplit__InvalidAllocationsSum(_getSum(percentAllocations)); + } + unchecked { + // overflow should be impossible in for-loop index + // cache accounts length to save gas + uint256 loopLength = accounts.length - 1; + for (uint256 i = 0; i < loopLength; ++i) { + // overflow should be impossible in array access math + if (accounts[i] >= accounts[i + 1]) revert InvalidSplit__AccountsOutOfOrder(i); + if (percentAllocations[i] == uint32(0)) revert InvalidSplit__AllocationMustBePositive(i); + } + // overflow should be impossible in array access math with validated + // equal array lengths + if (percentAllocations[loopLength] == uint32(0)) revert InvalidSplit__AllocationMustBePositive(loopLength); + } + if (distributorFee > MAX_DISTRIBUTOR_FEE) revert InvalidSplit__InvalidDistributorFee(distributorFee); + } + + function _getSum(uint32[] memory percents) internal pure returns (uint32 sum) { + for (uint32 i = 0; i < percents.length; i++) { + sum += percents[i]; + } + } +} \ No newline at end of file diff --git a/script/data/deploy-pullOwr-sample.json b/script/data/deploy-pullOwr-sample.json new file mode 100644 index 0000000..f5d06e3 --- /dev/null +++ b/script/data/deploy-pullOwr-sample.json @@ -0,0 +1,20 @@ +[ + { + "principalRecipient": "0x0000000000000000000000000000000000000001", + "recoveryRecipient": "0x0000000000000000000000000000000000000002", + "split": { + "accounts": [ + "0x0000000000000000000000000000000000000001", + "0x0000000000000000000000000000000000000002", + "0x0000000000000000000000000000000000000003" + ], + "controller": "0x0000000000000000000000000000000000000004", + "distributorFee": 1, + "percentAllocations": [ + 840000, + 60000, + 100000 + ] + } + } +] \ No newline at end of file diff --git a/script/data/lido-data-sample.json b/script/data/lido-data-sample.json index a7e36bc..baee89f 100644 --- a/script/data/lido-data-sample.json +++ b/script/data/lido-data-sample.json @@ -1,44 +1,44 @@ -[ - { - "accounts": [ - "0x0000000000000000000000000000000000000001", - "0x0000000000000000000000000000000000000002", - "0x0000000000000000000000000000000000000003" - ], - "controller": "0x0000000000000000000000000000000000000004", - "distributorFee": 1, - "percentAllocations": [ - 840000, - 60000, - 100000 - ] - }, - { - "accounts": [ - "0x0000000000000000000000000000000000000001", - "0x0000000000000000000000000000000000000002", - "0x0000000000000000000000000000000000000003" - ], - "controller": "0x0000000000000000000000000000000000000004", - "distributorFee": 1, - "percentAllocations": [ - 840000, - 60000, - 100000 - ] - }, - { - "accounts": [ - "0x0000000000000000000000000000000000000001", - "0x0000000000000000000000000000000000000002", - "0x0000000000000000000000000000000000000003" - ], - "controller": "0x0000000000000000000000000000000000000004", - "distributorFee": 1, - "percentAllocations": [ - 840000, - 60000, - 100000 - ] - } +[ + { + "accounts": [ + "0x0000000000000000000000000000000000000001", + "0x0000000000000000000000000000000000000002", + "0x0000000000000000000000000000000000000003" + ], + "controller": "0x0000000000000000000000000000000000000004", + "distributorFee": 1, + "percentAllocations": [ + 840000, + 60000, + 100000 + ] + }, + { + "accounts": [ + "0x0000000000000000000000000000000000000001", + "0x0000000000000000000000000000000000000002", + "0x0000000000000000000000000000000000000003" + ], + "controller": "0x0000000000000000000000000000000000000004", + "distributorFee": 1, + "percentAllocations": [ + 840000, + 60000, + 100000 + ] + }, + { + "accounts": [ + "0x0000000000000000000000000000000000000001", + "0x0000000000000000000000000000000000000002", + "0x0000000000000000000000000000000000000003" + ], + "controller": "0x0000000000000000000000000000000000000004", + "distributorFee": 1, + "percentAllocations": [ + 840000, + 60000, + 100000 + ] + } ] \ No newline at end of file diff --git a/src/interfaces/IObolErc1155Recipient.sol b/src/interfaces/IObolErc1155Recipient.sol new file mode 100644 index 0000000..eb21931 --- /dev/null +++ b/src/interfaces/IObolErc1155Recipient.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import {IPullSplit} from "src/interfaces/external/splits/IPullSplit.sol"; + +interface IObolErc1155Recipient { + struct DepositInfo { + bytes pubkey; + bytes withdrawal_credentials; + bytes sig; + bytes32 root; + } + + + function createPartition(uint256 maxSupply, address owr) external; + function mint(uint256 _partitionId, DepositInfo calldata depositInfo) external payable returns (uint256 mintedId); + function burn(uint256 _tokenId) external; + function burnSlashed(uint256 _tokenId) external; + function distributeRewards( + uint256 _tokenId, + address _distributor, + IPullSplit.PullSplitConfiguration calldata _splitConfig + ) external; + function claim(address _user, address _token) external; +} \ No newline at end of file diff --git a/src/interfaces/IOptimisticPullWithdrawalRecipient.sol b/src/interfaces/IOptimisticPullWithdrawalRecipient.sol index bccb336..22b0310 100644 --- a/src/interfaces/IOptimisticPullWithdrawalRecipient.sol +++ b/src/interfaces/IOptimisticPullWithdrawalRecipient.sol @@ -7,5 +7,5 @@ interface IOptimisticPullWithdrawalRecipient { function distributeFundsPull() external payable; function getTranches() external view returns (address principalRecipient, address rewardRecipient, uint256 amountOfPrincipalStake); function withdraw(address account, uint256 amount) external; - function pullBalances(address account) external view returns (uint256); + function getPullBalance(address account) external view returns (uint256); } \ No newline at end of file diff --git a/src/owr/ObolErc1155Recipient.sol b/src/owr/ObolErc1155Recipient.sol index 1e424f5..f174d62 100644 --- a/src/owr/ObolErc1155Recipient.sol +++ b/src/owr/ObolErc1155Recipient.sol @@ -13,19 +13,13 @@ import {IDepositContract} from "../interfaces/external/IDepositContract.sol"; import {ISplitsWarehouse} from "src/interfaces/external/splits/ISplitsWarehouse.sol"; import {ISplitMain, SplitConfiguration} from "src/interfaces/external/splits/ISplitMain.sol"; import {IOptimisticPullWithdrawalRecipient} from "../interfaces/IOptimisticPullWithdrawalRecipient.sol"; +import {IObolErc1155Recipient} from "../interfaces/IObolErc1155Recipient.sol"; /// @notice OWR principal recipient /// @dev handles rewards and principal of OWR contract ObolErc1155Recipient is ERC1155, Ownable, IERC1155Receiver { using SafeTransferLib for address; - struct DepositInfo { - bytes pubkey; - bytes withdrawal_credentials; - bytes sig; - bytes32 root; - } - struct Partition { uint256 maxSupply; address owr; @@ -114,7 +108,7 @@ contract ObolErc1155Recipient is ERC1155, Ownable, IERC1155Receiver { /// @notice creates a new partition /// @param maxSupply the maximum number of unique tokens /// @param owr the Optimistic Withdrawal Recipient address - function createPartition(uint256 maxSupply, address owr) external onlyOwner { + function createPartition(uint256 maxSupply, address owr) external { uint256 _id = partitionId; partitions[_id] = Partition({maxSupply: maxSupply, owr: owr, operator: msg.sender}); owrsPartition[owr] = _id; @@ -127,7 +121,7 @@ contract ObolErc1155Recipient is ERC1155, Ownable, IERC1155Receiver { /// @param _partitionId the partition to assign it to /// @param depositInfo deposit data needed for `DepositContract` /// @return mintedId id of the minted NFT - function mint(uint256 _partitionId, DepositInfo calldata depositInfo) external payable returns (uint256 mintedId) { + function mint(uint256 _partitionId, IObolErc1155Recipient.DepositInfo calldata depositInfo) external payable returns (uint256 mintedId) { // validation if (partitions[_partitionId].owr == address(0)) revert PartitionNotValid(); if (partitionTokens[_partitionId].length + 1 > partitions[_partitionId].maxSupply) revert PartitionSupplyReached(); @@ -194,7 +188,7 @@ contract ObolErc1155Recipient is ERC1155, Ownable, IERC1155Receiver { _owr.distributeFunds(); // withdraw from the OWR - uint256 pullBalance = _owr.pullBalances(address(this)); + uint256 pullBalance = _owr.getPullBalance(address(this)); uint256 toWithdraw = pullBalance < ETH_DEPOSIT_AMOUNT ? pullBalance: ETH_DEPOSIT_AMOUNT; if (toWithdraw < MIN_ETH_EXIT_AMOUNT) revert InvalidBurnAmount(MIN_ETH_EXIT_AMOUNT, toWithdraw); _owr.withdraw(address(this), toWithdraw); @@ -295,15 +289,17 @@ contract ObolErc1155Recipient is ERC1155, Ownable, IERC1155Receiver { address _distributor, IPullSplit.PullSplitConfiguration calldata _splitConfig ) private { - (, address _split,) = IOptimisticPullWithdrawalRecipient(owr).getTranches(); - address _token = IOptimisticPullWithdrawalRecipient(owr).token(); + IOptimisticPullWithdrawalRecipient _owr = IOptimisticPullWithdrawalRecipient(owr); + (, address _split,) = _owr.getTranches(); + address _token = _owr.token(); + uint256 _pullBalance = _owr.getPullBalance(_split); // retrieve funds from OWR IPullSplit.Call[] memory _calls = new IPullSplit.Call[](1); _calls[0] = IPullSplit.Call({ to: owr, value: 0, - data: abi.encodeWithSelector(IOptimisticPullWithdrawalRecipient.withdraw.selector, _split) + data: abi.encodeWithSelector(IOptimisticPullWithdrawalRecipient.withdraw.selector, _split, _pullBalance) }); IPullSplit(_split).execCalls(_calls); diff --git a/src/test/owr/ObolErc1155Recipient.t.sol b/src/test/owr/ObolErc1155Recipient.t.sol index 30d1128..1396fa4 100644 --- a/src/test/owr/ObolErc1155Recipient.t.sol +++ b/src/test/owr/ObolErc1155Recipient.t.sol @@ -13,6 +13,8 @@ import {IERC1155Receiver} from "src/interfaces/IERC1155Receiver.sol"; import {ISplitMain} from "src/interfaces/external/splits/ISplitMain.sol"; import {IPullSplit} from "src/interfaces/external/splits/IPullSplit.sol"; import {IENSReverseRegistrar} from "../../interfaces/external/IENSReverseRegistrar.sol"; +import {IObolErc1155Recipient} from "src/interfaces/IObolErc1155Recipient.sol"; + import {PullSplitMock} from "./mocks/PullSplitMock.sol"; import {DepositContractMock} from "./mocks/DepositContractMock.sol"; @@ -76,8 +78,8 @@ contract ObolErc1155RecipientTest is Test, IERC1155Receiver { function testMint_owrErc1155() public { address owrAddress = makeAddr("owrAddress"); recipient.createPartition(10, owrAddress); - recipient.mint{value: 32 ether}(0, ObolErc1155Recipient.DepositInfo({pubkey: "0x", withdrawal_credentials: "0x", sig: "0x", root: bytes32(0)})); - recipient.mint{value: 32 ether}(0, ObolErc1155Recipient.DepositInfo({pubkey: "0x", withdrawal_credentials: "0x", sig: "0x", root: bytes32(0)})); + recipient.mint{value: 32 ether}(0, IObolErc1155Recipient.DepositInfo({pubkey: "0x", withdrawal_credentials: "0x", sig: "0x", root: bytes32(0)})); + recipient.mint{value: 32 ether}(0, IObolErc1155Recipient.DepositInfo({pubkey: "0x", withdrawal_credentials: "0x", sig: "0x", root: bytes32(0)})); uint256 firstToken = recipient.partitionTokens(0, 0); assertEq(recipient.ownerOf(firstToken), address(this)); @@ -104,7 +106,7 @@ contract ObolErc1155RecipientTest is Test, IERC1155Receiver { owrFactory.createOWRecipient(ETH_ADDRESS, address(pullSplitMock), address(this), address(pullSplitMock), ETH_STAKE); recipient.createPartition(10, address(owrETH)); - recipient.mint{value: 32 ether}(0, ObolErc1155Recipient.DepositInfo({pubkey: "0x", withdrawal_credentials: "0x", sig: "0x", root: bytes32(0)})); + recipient.mint{value: 32 ether}(0, IObolErc1155Recipient.DepositInfo({pubkey: "0x", withdrawal_credentials: "0x", sig: "0x", root: bytes32(0)})); address(owrETH).safeTransferETH(1 ether); assertEq(address(owrETH).balance, 1 ether); @@ -138,7 +140,7 @@ contract ObolErc1155RecipientTest is Test, IERC1155Receiver { owrFactory.createOWRecipient(ETH_ADDRESS, address(pullSplitMock), address(recipient), address(pullSplitMock), ETH_STAKE); recipient.createPartition(2, address(owrETH)); - recipient.mint{value: 32 ether}(0, ObolErc1155Recipient.DepositInfo({pubkey: "0x", withdrawal_credentials: "0x", sig: "0x", root: bytes32(0)})); + recipient.mint{value: 32 ether}(0, IObolErc1155Recipient.DepositInfo({pubkey: "0x", withdrawal_credentials: "0x", sig: "0x", root: bytes32(0)})); address(owrETH).safeTransferETH(32 ether); assertEq(address(owrETH).balance, 32 ether); diff --git a/src/test/owr/mocks/DepositContractMock.sol b/src/test/owr/mocks/DepositContractMock.sol index c0a3296..00b9c28 100644 --- a/src/test/owr/mocks/DepositContractMock.sol +++ b/src/test/owr/mocks/DepositContractMock.sol @@ -3,15 +3,15 @@ pragma solidity ^0.8.19; contract DepositContractMock { /// @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 {} + bytes calldata, + bytes calldata, + bytes calldata, + bytes32 + ) external payable { + if (msg.value > 0) { + payable(msg.sender).transfer(msg.value); //send ether back + } + } } \ No newline at end of file From 73c5bd6ebc202a468b2102f15291894df9ca2e10 Mon Sep 17 00:00:00 2001 From: cosminobol Date: Mon, 24 Jun 2024 16:44:52 +0300 Subject: [PATCH 13/17] patch: small fixes for ERC1155 and holesky deployments --- script/DeployPullOWRAndSplit.s.sol | 5 +- script/PullOWRFactoryScript.s.sol | 8 ++- script/data/deploy-pullOwr-sample.json | 16 +++--- script/data/pullOwr-split.json | 6 ++ src/owr/ObolErc1155Recipient.sol | 67 ++++++++++++---------- src/test/.DS_Store | Bin 0 -> 6148 bytes src/test/owr/ObolErc1155Recipient.t.sol | 49 +++++++++++++--- src/test/owr/ObolErc1155RecipientMock.sol | 2 +- 8 files changed, 102 insertions(+), 51 deletions(-) create mode 100644 script/data/pullOwr-split.json create mode 100644 src/test/.DS_Store diff --git a/script/DeployPullOWRAndSplit.s.sol b/script/DeployPullOWRAndSplit.s.sol index 42dce01..f198392 100644 --- a/script/DeployPullOWRAndSplit.s.sol +++ b/script/DeployPullOWRAndSplit.s.sol @@ -21,11 +21,12 @@ contract DeployPullOWRAndSplit is Script, SplitterConfiguration { /// @param splitMain address for 0xsplits splitMain /// @param pullOwrFactory address for factory function run(string memory jsonFilePath, address splitMain, address pullOwrFactory, uint256 stakeSize) + // function run() external - { + { uint256 privKey = vm.envUint("PRIVATE_KEY"); bytes memory parsedJson = vm.parseJson(vm.readFile(jsonFilePath)); - + ConfigurationData[] memory data = abi.decode(parsedJson, (ConfigurationData[])); _validateInputJson(data); diff --git a/script/PullOWRFactoryScript.s.sol b/script/PullOWRFactoryScript.s.sol index 8aa59a3..fb6a719 100644 --- a/script/PullOWRFactoryScript.s.sol +++ b/script/PullOWRFactoryScript.s.sol @@ -5,12 +5,16 @@ import "forge-std/Script.sol"; import {OptimisticPullWithdrawalRecipientFactory} from "src/owr/OptimisticPullWithdrawalRecipientFactory.sol"; contract PullOWRFactoryScript is Script { - function run(string memory _name, address _ensReverseRegistrar, address _ensOwner) external { + function run() external { uint256 privKey = vm.envUint("PRIVATE_KEY"); + string memory _name = "HoleskyTest"; + address _ensReverseRegistrar = address(0x132AC0B116a73add4225029D1951A9A707Ef673f); + address _ensOwner = vm.envAddress("PUBLIC_KEY"); + vm.startBroadcast(privKey); - new OptimisticPullWithdrawalRecipientFactory{salt: keccak256("obol.pullOwrFactory.v1")}( + new OptimisticPullWithdrawalRecipientFactory{salt: keccak256("obol.pullOwrFactory.v1.holesky")}( _name, _ensReverseRegistrar, _ensOwner ); diff --git a/script/data/deploy-pullOwr-sample.json b/script/data/deploy-pullOwr-sample.json index f5d06e3..7057c0a 100644 --- a/script/data/deploy-pullOwr-sample.json +++ b/script/data/deploy-pullOwr-sample.json @@ -1,19 +1,17 @@ [ { - "principalRecipient": "0x0000000000000000000000000000000000000001", - "recoveryRecipient": "0x0000000000000000000000000000000000000002", + "principalRecipient": "0x0000e6203DB925DbFB9e3d650A80a1E2f4A78e94", + "recoveryRecipient": "0x0000e6203DB925DbFB9e3d650A80a1E2f4A78e94", "split": { "accounts": [ - "0x0000000000000000000000000000000000000001", - "0x0000000000000000000000000000000000000002", - "0x0000000000000000000000000000000000000003" + "0x0000e6203DB925DbFB9e3d650A80a1E2f4A78e94", + "0x08610E2A424c23169F79108D263Ed04f570A44FC" ], - "controller": "0x0000000000000000000000000000000000000004", + "controller": "0x0000e6203DB925DbFB9e3d650A80a1E2f4A78e94", "distributorFee": 1, "percentAllocations": [ - 840000, - 60000, - 100000 + 500000, + 500000 ] } } diff --git a/script/data/pullOwr-split.json b/script/data/pullOwr-split.json new file mode 100644 index 0000000..8a6372d --- /dev/null +++ b/script/data/pullOwr-split.json @@ -0,0 +1,6 @@ +{ + "0": { + "pullOWRAddress": "0x74203853De2726e3Bb65ac9a27D8B3E2A8f42c60", + "splitAddress": "0xdd8aE284363516703fB933ADbA33249e8C0a3Ffd" + } +} \ No newline at end of file diff --git a/src/owr/ObolErc1155Recipient.sol b/src/owr/ObolErc1155Recipient.sol index f174d62..ea6a837 100644 --- a/src/owr/ObolErc1155Recipient.sol +++ b/src/owr/ObolErc1155Recipient.sol @@ -15,6 +15,8 @@ import {ISplitMain, SplitConfiguration} from "src/interfaces/external/splits/ISp import {IOptimisticPullWithdrawalRecipient} from "../interfaces/IOptimisticPullWithdrawalRecipient.sol"; import {IObolErc1155Recipient} from "../interfaces/IObolErc1155Recipient.sol"; +import "forge-std/console.sol"; + /// @notice OWR principal recipient /// @dev handles rewards and principal of OWR contract ObolErc1155Recipient is ERC1155, Ownable, IERC1155Receiver { @@ -35,12 +37,14 @@ contract ObolErc1155Recipient is ERC1155, Ownable, IERC1155Receiver { mapping(uint256 _tokenId => uint256 _partitionId) public tokensPartition; mapping(uint256 _tokenId => address _owner) public ownerOf; - mapping(address _owner => mapping(address _token => uint256 _amount)) public claimable; - mapping(address _token => uint256 _claimable) public totalClaimable; + mapping(address _owner => uint256 _amount) public claimable; + uint256 public totalClaimable; mapping(uint256 id => uint256) public totalSupply; uint256 public totalSupplyAll; + mapping(bytes => bool) private _usedPubKeys; + // BeaconChain deposit contract IDepositContract public immutable depositContract; @@ -57,6 +61,8 @@ contract ObolErc1155Recipient is ERC1155, Ownable, IERC1155Receiver { error DepositAmountNotValid(); error PartitionSupplyReached(); error InvalidBurnAmount(uint256 necessary, uint256 received); + error PubKeyUsed(); + error WithdrawCredentialsNotValid(); event PartitionCreated(address indexed _owr, uint256 indexed _partitionId, uint256 indexed _maxSupply); event Minted(uint256 indexed _partitionId, uint256 indexed _mintedId, address indexed _sender); @@ -122,10 +128,15 @@ contract ObolErc1155Recipient is ERC1155, Ownable, IERC1155Receiver { /// @param depositInfo deposit data needed for `DepositContract` /// @return mintedId id of the minted NFT function mint(uint256 _partitionId, IObolErc1155Recipient.DepositInfo calldata depositInfo) external payable returns (uint256 mintedId) { + // validation if (partitions[_partitionId].owr == address(0)) revert PartitionNotValid(); if (partitionTokens[_partitionId].length + 1 > partitions[_partitionId].maxSupply) revert PartitionSupplyReached(); if (msg.value != ETH_DEPOSIT_AMOUNT) revert DepositAmountNotValid(); + if (_usedPubKeys[depositInfo.pubkey]) revert PubKeyUsed(); + if (!_validateWithdrawalCredentials(depositInfo.withdrawal_credentials, partitions[_partitionId].owr)) revert WithdrawCredentialsNotValid(); + + _usedPubKeys[depositInfo.pubkey] = true; // deposit first to ETH deposit contract depositContract.deposit{value: ETH_DEPOSIT_AMOUNT}( @@ -218,15 +229,13 @@ contract ObolErc1155Recipient is ERC1155, Ownable, IERC1155Receiver { if (_owr == address(0)) revert OwrNotValid(); // call `.distribute()` on OWR and `distribute()` on PullSplit - address _token = IOptimisticPullWithdrawalRecipient(_owr).token(); - uint256 balanceBefore = _getTokenBalance(_token); + uint256 balanceBefore = address(this).balance; IOptimisticPullWithdrawalRecipient(_owr).distributeFunds(); _distributeSplitsRewards(_owr, _distributor, _splitConfig); - uint256 balanceAfter = _getTokenBalance(_token); // update `claimable` for partition's active supply - uint256 _totalClaimable = balanceAfter - balanceBefore; - totalClaimable[_token] += _totalClaimable; + uint256 _totalClaimable = address(this).balance - balanceBefore; + totalClaimable += _totalClaimable; // update active validators claimable amounts if (_totalClaimable > 0) { @@ -234,8 +243,8 @@ contract ObolErc1155Recipient is ERC1155, Ownable, IERC1155Receiver { uint256 _reward = _totalClaimable / count; for (uint256 i; i < count; i++) { address _owner = ownerOf[partitionTokens[_partitionId][i]]; - claimable[_owner][_token] += _reward; - emit RewardsDistributed(_token, _tokenId, _owner, _reward, _totalClaimable); + claimable[_owner] += _reward; + emit RewardsDistributed(ETH_ADDRESS, _tokenId, _owner, _reward, _totalClaimable); } } } @@ -243,21 +252,19 @@ contract ObolErc1155Recipient is ERC1155, Ownable, IERC1155Receiver { /// @notice claim rewards /// @dev for ETH, `_token` should be `address(0)` /// @param _user the account to claim for - /// @param _token the token address - function claim(address _user, address _token) external { - uint256 _amount = claimable[_user][_token]; + function claim(address _user) external { + uint256 _amount = claimable[_user]; // send `_token` to user if (_amount > 0) { - if (_token == ETH_ADDRESS) _user.safeTransferETH(_amount); - else _token.safeTransfer(_user, _amount); + _user.safeTransferETH(_amount); } // reset `claimable` for `_user` and `_token` - claimable[_user][_token] = 0; - totalClaimable[_token] -= _amount; + claimable[_user] = 0; + totalClaimable -= _amount; - emit Claimed(_user, _token, _amount); + emit Claimed(_user, ETH_ADDRESS, _amount); } /// ----------------------------------------------------------------------- @@ -267,18 +274,19 @@ contract ObolErc1155Recipient is ERC1155, Ownable, IERC1155Receiver { /// @dev for ETH, `_token` should be `address(0)` /// @param _token the token address function recoverTokens(address _token) external onlyOwner { + if (_token != ETH_ADDRESS) { + uint256 _tokenBalance = ERC20(_token).balanceOf(address(this)); + _token.safeTransfer(msg.sender, _tokenBalance); + return; + } + // validate token amounts - uint256 _balance = ERC20(_token).balanceOf(address(this)); - if (_balance <= totalClaimable[_token]) revert ClaimFailed(); + uint256 _balance = address(this).balance; + if (_balance <= totalClaimable) revert ClaimFailed(); // compoute airdropped amount - uint256 _amount = _balance - totalClaimable[_token]; - - // send `_token` to owner - if (_amount > 0) { - if (_token == ETH_ADDRESS) msg.sender.safeTransferETH(_amount); - else _token.safeTransfer(msg.sender, _amount); - } + uint256 _amount = _balance - totalClaimable; + msg.sender.safeTransferETH(_amount); } /// ----------------------------------------------------------------------- @@ -332,10 +340,9 @@ contract ObolErc1155Recipient is ERC1155, Ownable, IERC1155Receiver { function _useBeforeTokenTransfer() internal pure override returns (bool) { return true; } - - function _getTokenBalance(address _token) private view returns (uint256 balance) { - if (_token == ETH_ADDRESS) balance = address(this).balance; - else balance = ERC20(_token).balanceOf(address(this)); + function _validateWithdrawalCredentials(bytes calldata _credentials, address _owr) private pure returns (bool) { + address _address = address(uint160(bytes20(_credentials[12:32]))); + return _address == _owr; } /// ----------------------------------------------------------------------- diff --git a/src/test/.DS_Store b/src/test/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..ce1fafde5bd03fa652bd67647da606feaee8d373 GIT binary patch literal 6148 zcmeHK%}T>S5T5OiO({YTiXIod7Hn-PikDF93mDOZN^NM-V9b^#HHT8jRbR+A@p+ut z-AZfiRiw_q?6*5RlQ3Vx&JF;G&ScO4r~-h4N|-C-@Pp7k>5Am6rGO~t8S%&um5-}; zf3)JwhQG)FeY-p;NMY=!i{CE{;-p^x5V?GzxK(mWPQ|(N9#!fk{bUk%`r{i~T`Cp& z^LgLD3WjN~x^t$Yq#s1Xu?`3ZLv*>l4x)idJ8BXQVjb(54X5msd)2+^w0YdB$#!E_ zlhc!>eDRUi6;A=SaO81_l0XTDC3D;SDD?nHO&yg(`YL3wak|0to~V zLcAGiE+q6V)j1iA%m6dM46J9s9-~fWy$kO!1I)lbWq{5HiAv~N%na(I0|(s#AksBb z3(l#RpcrY16nvbA+3IjXf1^&XXk{4#@IAvn>c g7=5V}@1tr#yQB%CYcVs378Je+Xc~B627Z-+Pnso$IsgCw literal 0 HcmV?d00001 diff --git a/src/test/owr/ObolErc1155Recipient.t.sol b/src/test/owr/ObolErc1155Recipient.t.sol index 1396fa4..34db455 100644 --- a/src/test/owr/ObolErc1155Recipient.t.sol +++ b/src/test/owr/ObolErc1155Recipient.t.sol @@ -30,6 +30,7 @@ contract ObolErc1155RecipientTest is Test, IERC1155Receiver { uint256 internal constant ETH_STAKE = 32 ether; address internal constant ETH_ADDRESS = address(0); address internal constant ENS_REVERSE_REGISTRAR_GOERLI = 0x084b1c3C81545d370f3634392De611CaaBFf8148; + address internal constant OWR_ADDRESS = 0x747515655BaC1A8CcD1dA01ed0F9aeEac464c8B6; receive() external payable {} @@ -75,11 +76,28 @@ contract ObolErc1155RecipientTest is Test, IERC1155Receiver { assertEq(recipient.getPartitionTokensLength(0), 0); } - function testMint_owrErc1155() public { - address owrAddress = makeAddr("owrAddress"); + function testWithdrawCredentials_owrErc1155() public { + address owrAddress = 0x747515655BaC1A8CcD1dA01ed0F9aeEac464c8B6; recipient.createPartition(10, owrAddress); + + vm.expectRevert(); recipient.mint{value: 32 ether}(0, IObolErc1155Recipient.DepositInfo({pubkey: "0x", withdrawal_credentials: "0x", sig: "0x", root: bytes32(0)})); - recipient.mint{value: 32 ether}(0, IObolErc1155Recipient.DepositInfo({pubkey: "0x", withdrawal_credentials: "0x", sig: "0x", root: bytes32(0)})); + } + + function testPubKey_owrErc1155() public { + bytes memory withdrawalCreds = _createBytesWithAddress(address(OWR_ADDRESS)); + recipient.createPartition(10, OWR_ADDRESS); + recipient.mint{value: 32 ether}(0, IObolErc1155Recipient.DepositInfo({pubkey: "0x", withdrawal_credentials: withdrawalCreds, sig: "0x", root: bytes32(0)})); + vm.expectRevert(); + recipient.mint{value: 32 ether}(0, IObolErc1155Recipient.DepositInfo({pubkey: "0x", withdrawal_credentials: withdrawalCreds, sig: "0x", root: bytes32(0)})); + + } + + function testMint_owrErc1155() public { + bytes memory withdrawalCreds = _createBytesWithAddress(address(OWR_ADDRESS)); + recipient.createPartition(10, OWR_ADDRESS); + recipient.mint{value: 32 ether}(0, IObolErc1155Recipient.DepositInfo({pubkey: "0x", withdrawal_credentials: withdrawalCreds, sig: "0x", root: bytes32(0)})); + recipient.mint{value: 32 ether}(0, IObolErc1155Recipient.DepositInfo({pubkey: "0x1", withdrawal_credentials: withdrawalCreds, sig: "0x", root: bytes32(0)})); uint256 firstToken = recipient.partitionTokens(0, 0); assertEq(recipient.ownerOf(firstToken), address(this)); @@ -104,9 +122,10 @@ contract ObolErc1155RecipientTest is Test, IERC1155Receiver { OptimisticPullWithdrawalRecipient owrETH = owrFactory.createOWRecipient(ETH_ADDRESS, address(pullSplitMock), address(this), address(pullSplitMock), ETH_STAKE); - + + bytes memory withdrawalCreds = _createBytesWithAddress(address(owrETH)); recipient.createPartition(10, address(owrETH)); - recipient.mint{value: 32 ether}(0, IObolErc1155Recipient.DepositInfo({pubkey: "0x", withdrawal_credentials: "0x", sig: "0x", root: bytes32(0)})); + recipient.mint{value: 32 ether}(0, IObolErc1155Recipient.DepositInfo({pubkey: "0x", withdrawal_credentials: withdrawalCreds, sig: "0x", root: bytes32(0)})); address(owrETH).safeTransferETH(1 ether); assertEq(address(owrETH).balance, 1 ether); @@ -118,10 +137,12 @@ contract ObolErc1155RecipientTest is Test, IERC1155Receiver { distributionIncentive: 0 })); - uint256 claimable = recipient.claimable(address(this), ETH_ADDRESS); + uint256 claimable = recipient.claimable(address(this)); assertEq(claimable, 1 ether); } + + function testBurn_owrErc1155() public { vm.mockCall( ENS_REVERSE_REGISTRAR_GOERLI, @@ -139,8 +160,9 @@ contract ObolErc1155RecipientTest is Test, IERC1155Receiver { OptimisticPullWithdrawalRecipient owrETH = owrFactory.createOWRecipient(ETH_ADDRESS, address(pullSplitMock), address(recipient), address(pullSplitMock), ETH_STAKE); + bytes memory withdrawalCreds = _createBytesWithAddress(address(owrETH)); recipient.createPartition(2, address(owrETH)); - recipient.mint{value: 32 ether}(0, IObolErc1155Recipient.DepositInfo({pubkey: "0x", withdrawal_credentials: "0x", sig: "0x", root: bytes32(0)})); + recipient.mint{value: 32 ether}(0, IObolErc1155Recipient.DepositInfo({pubkey: "0x", withdrawal_credentials: withdrawalCreds, sig: "0x", root: bytes32(0)})); address(owrETH).safeTransferETH(32 ether); assertEq(address(owrETH).balance, 32 ether); @@ -150,4 +172,17 @@ contract ObolErc1155RecipientTest is Test, IERC1155Receiver { uint256 balanceAfter = address(this).balance; assertEq(balanceBefore + 32 ether, balanceAfter); } + + + function _createBytesWithAddress(address addr) private pure returns (bytes memory) { + bytes20 addrBytes = bytes20(addr); + bytes memory result = new bytes(32); + result[0] = 0x01; + + for (uint256 i = 12; i < 32; i++) { + result[i] = addrBytes[i-12]; + } + + return result; + } } diff --git a/src/test/owr/ObolErc1155RecipientMock.sol b/src/test/owr/ObolErc1155RecipientMock.sol index 029d349..b7deb3e 100644 --- a/src/test/owr/ObolErc1155RecipientMock.sol +++ b/src/test/owr/ObolErc1155RecipientMock.sol @@ -8,7 +8,7 @@ contract ObolErc1155RecipientMock is ObolErc1155Recipient { } function setRewards(uint256 id, uint256 amount) external { - claimable[ownerOf[id]][address(0)] += amount; + claimable[ownerOf[id]] += amount; } function simulateReceiverMint(uint256 id, uint256 amount) external { From 9bd793219dd0e36f7a65833d585cd3d4a72c6d50 Mon Sep 17 00:00:00 2001 From: cosminobol Date: Mon, 1 Jul 2024 09:25:41 +0300 Subject: [PATCH 14/17] patch: small fixes for ERC1155 --- script/DeployPullOWRAndSplit.s.sol | 49 ++++++++++++-------------- script/DeployPullSplitMock.sol | 17 +++++++++ script/data/deploy-pullOwr-sample.json | 16 ++------- src/owr/ObolErc1155Recipient.sol | 1 - 4 files changed, 41 insertions(+), 42 deletions(-) create mode 100644 script/DeployPullSplitMock.sol diff --git a/script/DeployPullOWRAndSplit.s.sol b/script/DeployPullOWRAndSplit.s.sol index f198392..59924f7 100644 --- a/script/DeployPullOWRAndSplit.s.sol +++ b/script/DeployPullOWRAndSplit.s.sol @@ -18,12 +18,16 @@ contract DeployPullOWRAndSplit is Script, SplitterConfiguration { } /// @param jsonFilePath the data format can be seen in ./data/deploy-pullOWR-sample.json - /// @param splitMain address for 0xsplits splitMain + /// @param split address for 0xsplits PullSplit /// @param pullOwrFactory address for factory - function run(string memory jsonFilePath, address splitMain, address pullOwrFactory, uint256 stakeSize) - // function run() + function run(string memory jsonFilePath, address split, address pullOwrFactory, uint256 stakeSize) external { + // string memory jsonFilePath = "script/data/deploy-pullOwr-sample.json"; + // address pullOwrFactory = 0xcFf568fBD1386f0d7784C174411341C8588d4Ba4; + // address split = 0x2636b017110c4d8977C6a7351D1de09e95fd595a; + // uint256 stakeSize = 32; + uint256 privKey = vm.envUint("PRIVATE_KEY"); bytes memory parsedJson = vm.parseJson(vm.readFile(jsonFilePath)); @@ -34,36 +38,27 @@ contract DeployPullOWRAndSplit is Script, SplitterConfiguration { string memory jsonKey = "pullOwrDeploy"; string memory finalJSON; - uint256 stakeAmount = stakeSize * 1 ether; - for (uint256 i = 0; i < data.length; i++) { - // deploy split - ConfigurationData memory currentConfiguration = data[i]; - - vm.startBroadcast(privKey); - - address split = ISplitMain(splitMain).createSplit( - currentConfiguration.split.accounts, - currentConfiguration.split.percentAllocations, - currentConfiguration.split.distributorFee, - currentConfiguration.split.controller - ); + { + vm.startBroadcast(privKey); - // create obol split - address pullOwrAddress = address( - OptimisticPullWithdrawalRecipientFactory(pullOwrFactory).createOWRecipient( - ETH_ADDRESS, currentConfiguration.recoveryRecipient, currentConfiguration.principalRecipient, split, stakeAmount - ) - ); + ConfigurationData memory currentConfiguration = data[i]; + address pullOwrAddress = address( + OptimisticPullWithdrawalRecipientFactory(pullOwrFactory).createOWRecipient( + ETH_ADDRESS, currentConfiguration.recoveryRecipient, currentConfiguration.principalRecipient, split, stakeSize * 1 ether + ) + ); - vm.stopBroadcast(); + vm.stopBroadcast(); - string memory objKey = vm.toString(i); + string memory objKey = vm.toString(i); - vm.serializeAddress(objKey, "splitAddress", split); - string memory repsonse = vm.serializeAddress(objKey, "pullOWRAddress", pullOwrAddress); + vm.serializeAddress(objKey, "splitAddress", split); + string memory repsonse = vm.serializeAddress(objKey, "pullOWRAddress", pullOwrAddress); - finalJSON = vm.serializeString(jsonKey, objKey, repsonse); + finalJSON = vm.serializeString(jsonKey, objKey, repsonse); + } + } vm.writeJson(finalJSON, "./pullOwr-split.json"); diff --git a/script/DeployPullSplitMock.sol b/script/DeployPullSplitMock.sol new file mode 100644 index 0000000..ad7cff1 --- /dev/null +++ b/script/DeployPullSplitMock.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.19; + +import "forge-std/Script.sol"; +import {PullSplitMock} from "src/test/owr/mocks/PullSplitMock.sol"; + +contract DeployPullSplitMock is Script { + function run() external { + uint256 privKey = vm.envUint("PRIVATE_KEY"); + + vm.startBroadcast(privKey); + + new PullSplitMock{salt: keccak256("pullSplitMock.1")}(); + + vm.stopBroadcast(); + } +} \ No newline at end of file diff --git a/script/data/deploy-pullOwr-sample.json b/script/data/deploy-pullOwr-sample.json index 7057c0a..b70bc33 100644 --- a/script/data/deploy-pullOwr-sample.json +++ b/script/data/deploy-pullOwr-sample.json @@ -1,18 +1,6 @@ [ { - "principalRecipient": "0x0000e6203DB925DbFB9e3d650A80a1E2f4A78e94", - "recoveryRecipient": "0x0000e6203DB925DbFB9e3d650A80a1E2f4A78e94", - "split": { - "accounts": [ - "0x0000e6203DB925DbFB9e3d650A80a1E2f4A78e94", - "0x08610E2A424c23169F79108D263Ed04f570A44FC" - ], - "controller": "0x0000e6203DB925DbFB9e3d650A80a1E2f4A78e94", - "distributorFee": 1, - "percentAllocations": [ - 500000, - 500000 - ] - } + "principalRecipient": "0x7712657E5a734fb19E3b6Bbb463dFa92BcDDb2DD", + "recoveryRecipient": "0x0000e6203DB925DbFB9e3d650A80a1E2f4A78e94" } ] \ No newline at end of file diff --git a/src/owr/ObolErc1155Recipient.sol b/src/owr/ObolErc1155Recipient.sol index ea6a837..3658d1e 100644 --- a/src/owr/ObolErc1155Recipient.sol +++ b/src/owr/ObolErc1155Recipient.sol @@ -201,7 +201,6 @@ contract ObolErc1155Recipient is ERC1155, Ownable, IERC1155Receiver { // withdraw from the OWR uint256 pullBalance = _owr.getPullBalance(address(this)); uint256 toWithdraw = pullBalance < ETH_DEPOSIT_AMOUNT ? pullBalance: ETH_DEPOSIT_AMOUNT; - if (toWithdraw < MIN_ETH_EXIT_AMOUNT) revert InvalidBurnAmount(MIN_ETH_EXIT_AMOUNT, toWithdraw); _owr.withdraw(address(this), toWithdraw); _burn(msg.sender, _tokenId, 1); From 7612569f534bcf4646889426774fee8e95c5017c Mon Sep 17 00:00:00 2001 From: cosminobol Date: Sat, 10 Aug 2024 17:51:47 +0300 Subject: [PATCH 15/17] patch: added OwnableRoles to 1155 --- src/owr/ObolErc1155Recipient.sol | 8 ++++++-- src/test/owr/ObolErc1155Recipient.t.sol | 22 ++++++++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/src/owr/ObolErc1155Recipient.sol b/src/owr/ObolErc1155Recipient.sol index 3658d1e..b3faa48 100644 --- a/src/owr/ObolErc1155Recipient.sol +++ b/src/owr/ObolErc1155Recipient.sol @@ -2,6 +2,7 @@ pragma solidity 0.8.19; import {ERC20} from "solmate/tokens/ERC20.sol"; +import {OwnableRoles} from "solady/auth/OwnableRoles.sol"; import {Ownable} from "solady/auth/Ownable.sol"; import {ERC1155} from "solady/tokens/ERC1155.sol"; import {LibString} from "solady/utils/LibString.sol"; @@ -19,7 +20,7 @@ import "forge-std/console.sol"; /// @notice OWR principal recipient /// @dev handles rewards and principal of OWR -contract ObolErc1155Recipient is ERC1155, Ownable, IERC1155Receiver { +contract ObolErc1155Recipient is ERC1155, Ownable, OwnableRoles, IERC1155Receiver { using SafeTransferLib for address; struct Partition { @@ -53,6 +54,8 @@ contract ObolErc1155Recipient is ERC1155, Ownable, IERC1155Receiver { uint256 private constant ETH_DEPOSIT_AMOUNT = 32 ether; uint256 private constant MIN_ETH_EXIT_AMOUNT = 16 ether; + uint256 private constant ADMIN_ROLE = 1000; + error OwrNotValid(); error ClaimFailed(); error InvalidOwner(); @@ -341,7 +344,8 @@ contract ObolErc1155Recipient is ERC1155, Ownable, IERC1155Receiver { } function _validateWithdrawalCredentials(bytes calldata _credentials, address _owr) private pure returns (bool) { address _address = address(uint160(bytes20(_credentials[12:32]))); - return _address == _owr; + bytes1 _firstByte = _credentials[0]; + return _address == _owr && _firstByte == 0x01; } /// ----------------------------------------------------------------------- diff --git a/src/test/owr/ObolErc1155Recipient.t.sol b/src/test/owr/ObolErc1155Recipient.t.sol index 34db455..a16ad2c 100644 --- a/src/test/owr/ObolErc1155Recipient.t.sol +++ b/src/test/owr/ObolErc1155Recipient.t.sol @@ -26,6 +26,9 @@ contract ObolErc1155RecipientTest is Test, IERC1155Receiver { ObolErc1155RecipientMock recipient; DepositContractMock depositContract; PullSplitMock pullSplitMock; + + uint256 private _counter; + string constant BASE_URI = "https://github.com"; uint256 internal constant ETH_STAKE = 32 ether; address internal constant ETH_ADDRESS = address(0); @@ -105,6 +108,15 @@ contract ObolErc1155RecipientTest is Test, IERC1155Receiver { assertEq(recipient.getPartitionTokensLength(0), 2); } + function testMintGas_owrErc1155() public { + bytes memory withdrawalCreds = _createBytesWithAddress(address(OWR_ADDRESS)); + recipient.createPartition(1000, OWR_ADDRESS); + + for(uint256 i; i < 10; i++) { + recipient.mint{value: 32 ether}(0, IObolErc1155Recipient.DepositInfo({pubkey: _generateRandomBytes(), withdrawal_credentials: withdrawalCreds, sig: "0x", root: bytes32(0)})); + } + } + function testRewards_owrErc1155() public { vm.mockCall( @@ -185,4 +197,14 @@ contract ObolErc1155RecipientTest is Test, IERC1155Receiver { return result; } + + function _generateRandomBytes() private returns (bytes memory) { + uint256 length = 20; + bytes memory randomBytes = new bytes(length); + for (uint256 i = 0; i < length; i++) { + _counter++; + randomBytes[i] = bytes1(uint8(uint256(keccak256(abi.encodePacked(_counter, msg.sender, i))) % 256)); + } + return randomBytes; + } } From d15a287adc4f8d1a6bff2190915c055a7e2acece Mon Sep 17 00:00:00 2001 From: cosminobol Date: Tue, 13 Aug 2024 08:49:13 +0300 Subject: [PATCH 16/17] patch: updated createPartition and mint to take ADMIN role into account --- src/owr/ObolErc1155Recipient.sol | 75 ++++++++++++------ src/test/owr/ObolErc1155Recipient.t.sol | 100 +++++++++++++----------- 2 files changed, 104 insertions(+), 71 deletions(-) diff --git a/src/owr/ObolErc1155Recipient.sol b/src/owr/ObolErc1155Recipient.sol index b3faa48..9e54321 100644 --- a/src/owr/ObolErc1155Recipient.sol +++ b/src/owr/ObolErc1155Recipient.sol @@ -3,7 +3,6 @@ pragma solidity 0.8.19; import {ERC20} from "solmate/tokens/ERC20.sol"; import {OwnableRoles} from "solady/auth/OwnableRoles.sol"; -import {Ownable} from "solady/auth/Ownable.sol"; import {ERC1155} from "solady/tokens/ERC1155.sol"; import {LibString} from "solady/utils/LibString.sol"; import {SafeTransferLib} from "solady/utils/SafeTransferLib.sol"; @@ -16,11 +15,9 @@ import {ISplitMain, SplitConfiguration} from "src/interfaces/external/splits/ISp import {IOptimisticPullWithdrawalRecipient} from "../interfaces/IOptimisticPullWithdrawalRecipient.sol"; import {IObolErc1155Recipient} from "../interfaces/IObolErc1155Recipient.sol"; -import "forge-std/console.sol"; - /// @notice OWR principal recipient /// @dev handles rewards and principal of OWR -contract ObolErc1155Recipient is ERC1155, Ownable, OwnableRoles, IERC1155Receiver { +contract ObolErc1155Recipient is ERC1155, OwnableRoles, IERC1155Receiver { using SafeTransferLib for address; struct Partition { @@ -29,12 +26,16 @@ contract ObolErc1155Recipient is ERC1155, Ownable, OwnableRoles, IERC1155Receive address operator; } - uint256 public tokenId; uint256 public partitionId; mapping(uint256 _partitionId => Partition) public partitions; - mapping(uint256 _partitionId => uint256[] _tokenIds) public partitionTokens; - mapping(address _owr => uint256 _partitionId) public owrsPartition; + mapping(uint256 _partitionId => uint256[] _tokenIds) public partitionTokens; // TODO: refactor by adding it to + // Partition struct + mapping(uint256 _partitionId => IObolErc1155Recipient.DepositInfo[] _depositInfos) public partitionDepositInfos; + mapping(uint256 _partitionId => uint256 depositInfoPointer) public depositInfoIndex; + mapping(address _owr => uint256 _partitionId) public owrsPartition; + + uint256 public tokenId; mapping(uint256 _tokenId => uint256 _partitionId) public tokensPartition; mapping(uint256 _tokenId => address _owner) public ownerOf; @@ -66,11 +67,14 @@ contract ObolErc1155Recipient is ERC1155, Ownable, OwnableRoles, IERC1155Receive error InvalidBurnAmount(uint256 necessary, uint256 received); error PubKeyUsed(); error WithdrawCredentialsNotValid(); + error AllDepositInfoConsumed(); event PartitionCreated(address indexed _owr, uint256 indexed _partitionId, uint256 indexed _maxSupply); event Minted(uint256 indexed _partitionId, uint256 indexed _mintedId, address indexed _sender); event Claimed(address indexed _account, address indexed _token, uint256 _amount); - event RewardsDistributed(address indexed _token, uint256 indexed _tokenId, address indexed _account, uint256 _amount, uint256 _totalRewards); + event RewardsDistributed( + address indexed _token, uint256 indexed _tokenId, address indexed _account, uint256 _amount, uint256 _totalRewards + ); constructor(string memory baseUri_, address _owner, address _depositContract) { _baseUri = baseUri_; @@ -117,8 +121,30 @@ contract ObolErc1155Recipient is ERC1155, Ownable, OwnableRoles, IERC1155Receive /// @notice creates a new partition /// @param maxSupply the maximum number of unique tokens /// @param owr the Optimistic Withdrawal Recipient address - function createPartition(uint256 maxSupply, address owr) external { + function createPartition(uint256 maxSupply, address owr, IObolErc1155Recipient.DepositInfo[] calldata depositInfos) + external + onlyOwnerOrRoles(ADMIN_ROLE) + { uint256 _id = partitionId; + if (depositInfos.length != maxSupply) revert PartitionNotValid(); + + for (uint256 i; i < maxSupply; i++) { + IObolErc1155Recipient.DepositInfo calldata _depositInfo = depositInfos[i]; + + _validateWithdrawalCredentials(_depositInfo.withdrawal_credentials, owr); + if (_usedPubKeys[_depositInfo.pubkey]) revert PubKeyUsed(); + _usedPubKeys[_depositInfo.pubkey] = true; + + partitionDepositInfos[_id].push( + IObolErc1155Recipient.DepositInfo({ + withdrawal_credentials: _depositInfo.withdrawal_credentials, + pubkey: _depositInfo.pubkey, + root: _depositInfo.root, + sig: _depositInfo.sig + }) + ); + } + partitions[_id] = Partition({maxSupply: maxSupply, owr: owr, operator: msg.sender}); owrsPartition[owr] = _id; @@ -128,18 +154,19 @@ contract ObolErc1155Recipient is ERC1155, Ownable, OwnableRoles, IERC1155Receive /// @notice mints a new token and deposits to ETH deposit contract /// @param _partitionId the partition to assign it to - /// @param depositInfo deposit data needed for `DepositContract` /// @return mintedId id of the minted NFT - function mint(uint256 _partitionId, IObolErc1155Recipient.DepositInfo calldata depositInfo) external payable returns (uint256 mintedId) { - + function mint(uint256 _partitionId) external payable returns (uint256 mintedId) { // validation + if (depositInfoIndex[_partitionId] == partitionDepositInfos[_partitionId].length - 1) { + revert AllDepositInfoConsumed(); + } if (partitions[_partitionId].owr == address(0)) revert PartitionNotValid(); if (partitionTokens[_partitionId].length + 1 > partitions[_partitionId].maxSupply) revert PartitionSupplyReached(); if (msg.value != ETH_DEPOSIT_AMOUNT) revert DepositAmountNotValid(); - if (_usedPubKeys[depositInfo.pubkey]) revert PubKeyUsed(); - if (!_validateWithdrawalCredentials(depositInfo.withdrawal_credentials, partitions[_partitionId].owr)) revert WithdrawCredentialsNotValid(); - _usedPubKeys[depositInfo.pubkey] = true; + IObolErc1155Recipient.DepositInfo memory depositInfo = + partitionDepositInfos[_partitionId][depositInfoIndex[_partitionId]]; + depositInfoIndex[_partitionId] += 1; // deposit first to ETH deposit contract depositContract.deposit{value: ETH_DEPOSIT_AMOUNT}( @@ -172,7 +199,8 @@ contract ObolErc1155Recipient is ERC1155, Ownable, OwnableRoles, IERC1155Receive if (!isOwnerOf(_tokenId)) revert InvalidOwner(); // retrieve OWR - IOptimisticPullWithdrawalRecipient _owr = IOptimisticPullWithdrawalRecipient(partitions[tokensPartition[_tokenId]].owr); + IOptimisticPullWithdrawalRecipient _owr = + IOptimisticPullWithdrawalRecipient(partitions[tokensPartition[_tokenId]].owr); if (address(_owr) == address(0)) revert OwrNotValid(); // retrieve ETH from the OWR @@ -195,7 +223,8 @@ contract ObolErc1155Recipient is ERC1155, Ownable, OwnableRoles, IERC1155Receive if (!isOwnerOf(_tokenId)) revert InvalidOwner(); // retrieve OWR - IOptimisticPullWithdrawalRecipient _owr = IOptimisticPullWithdrawalRecipient(partitions[tokensPartition[_tokenId]].owr); + IOptimisticPullWithdrawalRecipient _owr = + IOptimisticPullWithdrawalRecipient(partitions[tokensPartition[_tokenId]].owr); if (address(_owr) == address(0)) revert OwrNotValid(); // retrieve ETH from the OWR @@ -203,7 +232,7 @@ contract ObolErc1155Recipient is ERC1155, Ownable, OwnableRoles, IERC1155Receive // withdraw from the OWR uint256 pullBalance = _owr.getPullBalance(address(this)); - uint256 toWithdraw = pullBalance < ETH_DEPOSIT_AMOUNT ? pullBalance: ETH_DEPOSIT_AMOUNT; + uint256 toWithdraw = pullBalance < ETH_DEPOSIT_AMOUNT ? pullBalance : ETH_DEPOSIT_AMOUNT; _owr.withdraw(address(this), toWithdraw); _burn(msg.sender, _tokenId, 1); @@ -215,7 +244,6 @@ contract ObolErc1155Recipient is ERC1155, Ownable, OwnableRoles, IERC1155Receive if (!sent) revert TransferFailed(); } - /// @notice triggers `OWR.distributeFunds` and updates claimable balances for partition /// @param _tokenId token id /// @param _distributor `PullSplit` distributor address @@ -250,7 +278,7 @@ contract ObolErc1155Recipient is ERC1155, Ownable, OwnableRoles, IERC1155Receive } } } - + /// @notice claim rewards /// @dev for ETH, `_token` should be `address(0)` /// @param _user the account to claim for @@ -258,9 +286,7 @@ contract ObolErc1155Recipient is ERC1155, Ownable, OwnableRoles, IERC1155Receive uint256 _amount = claimable[_user]; // send `_token` to user - if (_amount > 0) { - _user.safeTransferETH(_amount); - } + if (_amount > 0) _user.safeTransferETH(_amount); // reset `claimable` for `_user` and `_token` claimable[_user] = 0; @@ -342,6 +368,7 @@ contract ObolErc1155Recipient is ERC1155, Ownable, OwnableRoles, IERC1155Receive function _useBeforeTokenTransfer() internal pure override returns (bool) { return true; } + function _validateWithdrawalCredentials(bytes calldata _credentials, address _owr) private pure returns (bool) { address _address = address(uint160(bytes20(_credentials[12:32]))); bytes1 _firstByte = _credentials[0]; @@ -367,4 +394,4 @@ contract ObolErc1155Recipient is ERC1155, Ownable, OwnableRoles, IERC1155Receive { return this.onERC1155BatchReceived.selector; } -} \ No newline at end of file +} diff --git a/src/test/owr/ObolErc1155Recipient.t.sol b/src/test/owr/ObolErc1155Recipient.t.sol index a16ad2c..ea6ffa9 100644 --- a/src/test/owr/ObolErc1155Recipient.t.sol +++ b/src/test/owr/ObolErc1155Recipient.t.sol @@ -15,7 +15,6 @@ import {IPullSplit} from "src/interfaces/external/splits/IPullSplit.sol"; import {IENSReverseRegistrar} from "../../interfaces/external/IENSReverseRegistrar.sol"; import {IObolErc1155Recipient} from "src/interfaces/IObolErc1155Recipient.sol"; - import {PullSplitMock} from "./mocks/PullSplitMock.sol"; import {DepositContractMock} from "./mocks/DepositContractMock.sol"; import {ObolErc1155ReceiverMock} from "./mocks/ObolErc1155ReceiverMock.sol"; @@ -71,7 +70,7 @@ contract ObolErc1155RecipientTest is Test, IERC1155Receiver { function testCreatePartition_owrErc1155() public { address owrAddress = makeAddr("owrAddress"); - recipient.createPartition(10, owrAddress); + recipient.createPartition(10, owrAddress, _generateDepositInfo(10, false, false)); (uint256 maxSupply, address owr, address operator) = recipient.partitions(0); assertEq(maxSupply, 10); assertEq(owr, owrAddress); @@ -81,26 +80,19 @@ contract ObolErc1155RecipientTest is Test, IERC1155Receiver { function testWithdrawCredentials_owrErc1155() public { address owrAddress = 0x747515655BaC1A8CcD1dA01ed0F9aeEac464c8B6; - recipient.createPartition(10, owrAddress); - vm.expectRevert(); - recipient.mint{value: 32 ether}(0, IObolErc1155Recipient.DepositInfo({pubkey: "0x", withdrawal_credentials: "0x", sig: "0x", root: bytes32(0)})); + recipient.createPartition(1, owrAddress, _generateDepositInfo(1, true, false)); } function testPubKey_owrErc1155() public { - bytes memory withdrawalCreds = _createBytesWithAddress(address(OWR_ADDRESS)); - recipient.createPartition(10, OWR_ADDRESS); - recipient.mint{value: 32 ether}(0, IObolErc1155Recipient.DepositInfo({pubkey: "0x", withdrawal_credentials: withdrawalCreds, sig: "0x", root: bytes32(0)})); vm.expectRevert(); - recipient.mint{value: 32 ether}(0, IObolErc1155Recipient.DepositInfo({pubkey: "0x", withdrawal_credentials: withdrawalCreds, sig: "0x", root: bytes32(0)})); - + recipient.createPartition(10, OWR_ADDRESS, _generateDepositInfo(10, false, true)); } function testMint_owrErc1155() public { - bytes memory withdrawalCreds = _createBytesWithAddress(address(OWR_ADDRESS)); - recipient.createPartition(10, OWR_ADDRESS); - recipient.mint{value: 32 ether}(0, IObolErc1155Recipient.DepositInfo({pubkey: "0x", withdrawal_credentials: withdrawalCreds, sig: "0x", root: bytes32(0)})); - recipient.mint{value: 32 ether}(0, IObolErc1155Recipient.DepositInfo({pubkey: "0x1", withdrawal_credentials: withdrawalCreds, sig: "0x", root: bytes32(0)})); + recipient.createPartition(10, OWR_ADDRESS, _generateDepositInfo(10, false, false)); + recipient.mint{value: 32 ether}(0); + recipient.mint{value: 32 ether}(0); uint256 firstToken = recipient.partitionTokens(0, 0); assertEq(recipient.ownerOf(firstToken), address(this)); @@ -109,15 +101,13 @@ contract ObolErc1155RecipientTest is Test, IERC1155Receiver { } function testMintGas_owrErc1155() public { - bytes memory withdrawalCreds = _createBytesWithAddress(address(OWR_ADDRESS)); - recipient.createPartition(1000, OWR_ADDRESS); + // 117707542 gas ( 0.1 ETH for 1 gwei price ) + recipient.createPartition(1000, OWR_ADDRESS, _generateDepositInfo(1000, false, false)); - for(uint256 i; i < 10; i++) { - recipient.mint{value: 32 ether}(0, IObolErc1155Recipient.DepositInfo({pubkey: _generateRandomBytes(), withdrawal_credentials: withdrawalCreds, sig: "0x", root: bytes32(0)})); - } + // 1193591940 gas ( 1.1 ETH for 1 gwei price ) + recipient.createPartition(10_000, OWR_ADDRESS, _generateDepositInfo(10_000, false, false)); } - function testRewards_owrErc1155() public { vm.mockCall( ENS_REVERSE_REGISTRAR_GOERLI, @@ -132,29 +122,31 @@ contract ObolErc1155RecipientTest is Test, IERC1155Receiver { OptimisticPullWithdrawalRecipientFactory owrFactory = new OptimisticPullWithdrawalRecipientFactory("demo.obol.eth", ENS_REVERSE_REGISTRAR_GOERLI, address(this)); - OptimisticPullWithdrawalRecipient owrETH = - owrFactory.createOWRecipient(ETH_ADDRESS, address(pullSplitMock), address(this), address(pullSplitMock), ETH_STAKE); - - bytes memory withdrawalCreds = _createBytesWithAddress(address(owrETH)); - recipient.createPartition(10, address(owrETH)); - recipient.mint{value: 32 ether}(0, IObolErc1155Recipient.DepositInfo({pubkey: "0x", withdrawal_credentials: withdrawalCreds, sig: "0x", root: bytes32(0)})); + OptimisticPullWithdrawalRecipient owrETH = owrFactory.createOWRecipient( + ETH_ADDRESS, address(pullSplitMock), address(this), address(pullSplitMock), ETH_STAKE + ); + + recipient.createPartition(10, address(owrETH), _generateDepositInfo(10, false, false)); + recipient.mint{value: 32 ether}(0); address(owrETH).safeTransferETH(1 ether); assertEq(address(owrETH).balance, 1 ether); - recipient.distributeRewards(0, address(this), IPullSplit.PullSplitConfiguration({ - recipients: new address[](0), - allocations: new uint256[](0), - totalAllocation: 0, - distributionIncentive: 0 - })); + recipient.distributeRewards( + 0, + address(this), + IPullSplit.PullSplitConfiguration({ + recipients: new address[](0), + allocations: new uint256[](0), + totalAllocation: 0, + distributionIncentive: 0 + }) + ); uint256 claimable = recipient.claimable(address(this)); assertEq(claimable, 1 ether); } - - function testBurn_owrErc1155() public { vm.mockCall( ENS_REVERSE_REGISTRAR_GOERLI, @@ -169,12 +161,12 @@ contract ObolErc1155RecipientTest is Test, IERC1155Receiver { OptimisticPullWithdrawalRecipientFactory owrFactory = new OptimisticPullWithdrawalRecipientFactory("demo.obol.eth", ENS_REVERSE_REGISTRAR_GOERLI, address(this)); - OptimisticPullWithdrawalRecipient owrETH = - owrFactory.createOWRecipient(ETH_ADDRESS, address(pullSplitMock), address(recipient), address(pullSplitMock), ETH_STAKE); + OptimisticPullWithdrawalRecipient owrETH = owrFactory.createOWRecipient( + ETH_ADDRESS, address(pullSplitMock), address(recipient), address(pullSplitMock), ETH_STAKE + ); - bytes memory withdrawalCreds = _createBytesWithAddress(address(owrETH)); - recipient.createPartition(2, address(owrETH)); - recipient.mint{value: 32 ether}(0, IObolErc1155Recipient.DepositInfo({pubkey: "0x", withdrawal_credentials: withdrawalCreds, sig: "0x", root: bytes32(0)})); + recipient.createPartition(2, address(owrETH), _generateDepositInfo(2, false, false)); + recipient.mint{value: 32 ether}(0); address(owrETH).safeTransferETH(32 ether); assertEq(address(owrETH).balance, 32 ether); @@ -185,17 +177,16 @@ contract ObolErc1155RecipientTest is Test, IERC1155Receiver { assertEq(balanceBefore + 32 ether, balanceAfter); } - function _createBytesWithAddress(address addr) private pure returns (bytes memory) { - bytes20 addrBytes = bytes20(addr); - bytes memory result = new bytes(32); - result[0] = 0x01; + bytes20 addrBytes = bytes20(addr); + bytes memory result = new bytes(32); + result[0] = 0x01; - for (uint256 i = 12; i < 32; i++) { - result[i] = addrBytes[i-12]; - } + for (uint256 i = 12; i < 32; i++) { + result[i] = addrBytes[i - 12]; + } - return result; + return result; } function _generateRandomBytes() private returns (bytes memory) { @@ -207,4 +198,19 @@ contract ObolErc1155RecipientTest is Test, IERC1155Receiver { } return randomBytes; } + + function _generateDepositInfo(uint256 count, bool wrongWithdrawalCred, bool wrongPubKey) + private + returns (IObolErc1155Recipient.DepositInfo[] memory depositInfos) + { + depositInfos = new IObolErc1155Recipient.DepositInfo[](count); + for (uint256 i; i < count; i++) { + depositInfos[i] = IObolErc1155Recipient.DepositInfo({ + pubkey: wrongPubKey ? bytes("0x1") : _generateRandomBytes(), + withdrawal_credentials: wrongWithdrawalCred ? bytes("0x1") : _createBytesWithAddress(address(OWR_ADDRESS)), + sig: "0x", + root: bytes32(0) + }); + } + } } From 8e6a2c8a1993db8e7c42405ba4f767ddd66db71b Mon Sep 17 00:00:00 2001 From: cosminobol Date: Sun, 18 Aug 2024 14:07:37 +0300 Subject: [PATCH 17/17] chore: removed memory intensive test --- src/test/owr/ObolErc1155Recipient.t.sol | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/test/owr/ObolErc1155Recipient.t.sol b/src/test/owr/ObolErc1155Recipient.t.sol index ea6ffa9..f5eba91 100644 --- a/src/test/owr/ObolErc1155Recipient.t.sol +++ b/src/test/owr/ObolErc1155Recipient.t.sol @@ -99,7 +99,7 @@ contract ObolErc1155RecipientTest is Test, IERC1155Receiver { assertEq(recipient.ownerOf(1), address(this)); assertEq(recipient.getPartitionTokensLength(0), 2); } - + /* function testMintGas_owrErc1155() public { // 117707542 gas ( 0.1 ETH for 1 gwei price ) recipient.createPartition(1000, OWR_ADDRESS, _generateDepositInfo(1000, false, false)); @@ -107,6 +107,7 @@ contract ObolErc1155RecipientTest is Test, IERC1155Receiver { // 1193591940 gas ( 1.1 ETH for 1 gwei price ) recipient.createPartition(10_000, OWR_ADDRESS, _generateDepositInfo(10_000, false, false)); } + */ function testRewards_owrErc1155() public { vm.mockCall(