diff --git a/src/owr/OwnableRecipient.sol b/src/owr/OwnableRecipient.sol new file mode 100644 index 0000000..5110e71 --- /dev/null +++ b/src/owr/OwnableRecipient.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.19; + +import {Ownable} from "solady/auth/Ownable.sol"; +import {SafeTransferLib} from "solady/utils/SafeTransferLib.sol"; + +import {ERC20} from "solmate/tokens/ERC20.sol"; + + +/// @title OwnableRecipient +/// @author Obol +/// @notice OWR recipient +/// @dev This contract uses token = address(0) to refer to ETH. +contract OwnableRecipient is Ownable { + using SafeTransferLib for address; + + /// ----------------------------------------------------------------------- + /// errors + /// ----------------------------------------------------------------------- + + /// @dev thrown if ETH claim fails + error ClaimFailed(uint256 amount); + + constructor() { + _initializeOwner(msg.sender); + } + + receive() payable external {} + + /// @notice Claims tokens from contract + /// @dev uses token = address(0) to refer to ETH. + /// @param withdrawalRecipient Account to receive tokens + function claim(address token, address withdrawalRecipient) onlyOwner external { + if (token == address(0)) { + uint256 amount = address(this).balance; + (bool sent,) = withdrawalRecipient.call{value: amount}(""); + if (!sent) revert ClaimFailed(amount); + } else { + uint256 balance = ERC20(token).balanceOf(address(this)); + token.safeTransfer(withdrawalRecipient, balance); + } + } +} \ No newline at end of file diff --git a/src/test/owr/OptimisticWithdrawalRecipient.t.sol b/src/test/owr/OptimisticWithdrawalRecipient.t.sol index c30d1d5..b303a7a 100644 --- a/src/test/owr/OptimisticWithdrawalRecipient.t.sol +++ b/src/test/owr/OptimisticWithdrawalRecipient.t.sol @@ -2,6 +2,7 @@ pragma solidity ^0.8.19; import "forge-std/Test.sol"; +import {OwnableRecipient} from "src/owr/OwnableRecipient.sol"; import {OptimisticWithdrawalRecipient} from "src/owr/OptimisticWithdrawalRecipient.sol"; import {OptimisticWithdrawalRecipientFactory} from "src/owr/OptimisticWithdrawalRecipientFactory.sol"; import {MockERC20} from "../utils/mocks/MockERC20.sol"; @@ -33,6 +34,8 @@ contract OptimisticWithdrawalRecipientTest is OWRTestHelper, Test { address public rewardRecipient; uint256 internal trancheThreshold; + receive() payable external {} + function setUp() public { mERC20 = new MockERC20("demo", "DMT", 18); mERC20.mint(type(uint256).max); @@ -71,6 +74,39 @@ contract OptimisticWithdrawalRecipientTest is OWRTestHelper, Test { owrFactory.createOWRecipient(address(mERC20), address(0), principalRecipient, rewardRecipient, trancheThreshold); } + function testOwnableRecipient() public { + OwnableRecipient ownableRecipient = new OwnableRecipient(); + OptimisticWithdrawalRecipient owrOwnableRecipient = + owrFactory.createOWRecipient(ETH_ADDRESS, recoveryAddress, address(ownableRecipient), rewardRecipient, trancheThreshold); + + + address(owrOwnableRecipient).safeTransferETH(36 ether); + + uint256 principalPayout = 32 ether; + uint256 rewardPayout = 4 ether; + + vm.expectEmit(true, true, true, true); + emit DistributeFunds(principalPayout, rewardPayout, 0); + owrOwnableRecipient.distributeFunds(); + assertEq(address(owrOwnableRecipient).balance, 0 ether); + assertEq(address(ownableRecipient).balance, 32 ether); + assertEq(rewardRecipient.balance, 4 ether); + + ownableRecipient.claim(address(0), address(this)); + + assertEq(ownableRecipient.owner(), address(this)); + ownableRecipient.transferOwnership(principalRecipient); + assertEq(ownableRecipient.owner(), principalRecipient); + + ownableRecipient.requestOwnershipHandover(); + vm.prank(rewardRecipient); + ownableRecipient.requestOwnershipHandover(); + + vm.prank(principalRecipient); + ownableRecipient.completeOwnershipHandover(address(this)); + assertEq(ownableRecipient.owner(), address(this)); + } + function testGetTranches() public { // eth (address _principalRecipient, address _rewardRecipient, uint256 wtrancheThreshold) = owrETH.getTranches();