From d5557e47f0319329d66a526b61082ec870ac19e1 Mon Sep 17 00:00:00 2001 From: Brent Fitzgerald Date: Sun, 21 Jan 2024 18:58:12 -0800 Subject: [PATCH] add bounty capabilities and some tests --- src/IPizzaInitializer.sol | 10 +- src/Pizza.sol | 206 ++++++++++++++++++++++++++++++-------- src/PizzaFactory.sol | 38 +++++-- test/Pizza.t.sol | 49 +++++++++ test/PizzaFactory.t.sol | 2 +- 5 files changed, 252 insertions(+), 53 deletions(-) diff --git a/src/IPizzaInitializer.sol b/src/IPizzaInitializer.sol index 700e4bf..ee437ca 100644 --- a/src/IPizzaInitializer.sol +++ b/src/IPizzaInitializer.sol @@ -6,5 +6,13 @@ pragma solidity 0.8.23; * @dev Interface for initializing a pizza contract. */ interface IPizzaInitializer { - function initialize(address[] memory _payees, uint256[] memory _shares) external; + function initialize(address[] memory _payees, uint256[] memory _shares, uint256 _bounty) external; + + function initializeWithBountyRelease( + address[] calldata _payees, + uint256[] calldata _shares, + uint256 _bounty, + address[] calldata _bountyTokens, + address _bountyReceiver + ) external; } diff --git a/src/Pizza.sol b/src/Pizza.sol index e1cd749..15e0da2 100644 --- a/src/Pizza.sol +++ b/src/Pizza.sol @@ -52,6 +52,11 @@ contract Pizza is Initializable, Context, Multicall, ReentrancyGuard { */ error NoPayees(); + /** + * @dev Error thrown when the bounty is invalid. + */ + error InvalidBounty(); + /* //////////////////////////////////////////////////////////////////////// Events //////////////////////////////////////////////////////////////////////// */ @@ -76,11 +81,26 @@ contract Pizza is Initializable, Context, Multicall, ReentrancyGuard { */ event ERC20Release(IERC20 indexed token, uint256 amount); + /** + * @notice Emitted when the bounty is released. + * @param receiver The address of the bounty receiver. + * @param amount The amount of ETH released. + */ + event PayBounty(address indexed receiver, uint256 amount); + + /** + * @notice Emitted when the bounty is released. + * @param token The ERC20 token being released. + * @param receiver The address of the bounty receiver. + * @param amount The amount of ERC20 tokens released. + */ + event PayERC20Bounty(IERC20 indexed token, address indexed receiver, uint256 amount); + /* //////////////////////////////////////////////////////////////////////// Constants //////////////////////////////////////////////////////////////////////// */ - uint256 private constant BIPS_PRECISION = 10000; + uint256 public constant BOUNTY_PRECISION = 1e6; /* //////////////////////////////////////////////////////////////////////// Storage @@ -114,7 +134,7 @@ contract Pizza is Initializable, Context, Multicall, ReentrancyGuard { /** * @notice The amount of funds released to a payee. */ - uint32 public releaseBountyBIPS; + uint256 public bounty; /* //////////////////////////////////////////////////////////////////////// Construction + Initialization @@ -129,16 +149,36 @@ contract Pizza is Initializable, Context, Multicall, ReentrancyGuard { * @param _payees The addresses of the payees. * @param _shares The corresponding shares of each payee. */ - function initialize(address[] memory _payees, uint256[] memory _shares) external initializer { - if (_payees.length != _shares.length) { - revert PayeeShareLengthMismatch(); - } - if (_payees.length == 0) { - revert NoPayees(); - } + function initialize(address[] memory _payees, uint256[] memory _shares, uint256 _bounty) external initializer { + _init(_payees, _shares, _bounty); + } - for (uint256 i = 0; i < _payees.length; i++) { - _addPayee(_payees[i], _shares[i]); + /** + * @notice Initializes the contract with the specified payees and shares. + * @param _payees The addresses of the payees. + * @param _shares The corresponding shares of each payee. + */ + function initializeWithBountyRelease( + address[] calldata _payees, + uint256[] calldata _shares, + uint256 _bounty, + address[] calldata _bountyTokens, + address _bountyReceiver + ) external initializer nonReentrant { + _init(_payees, _shares, _bounty); + + bounty = _bounty; + if (_bounty > 0 && _bountyReceiver != address(0)) { + for (uint256 i = 0; i < _bountyTokens.length; i++) { + address token = _bountyTokens[i]; + if (token == address(0)) { + _payBounty(_bountyReceiver); + _release(); + } else { + _payERC20Bounty(IERC20(token), _bountyReceiver); + _erc20Release(IERC20(token)); + } + } } } @@ -163,22 +203,17 @@ contract Pizza is Initializable, Context, Multicall, ReentrancyGuard { /** * @notice Releases available ETH balance. */ - function release() external { - uint256 totalReleasable = address(this).balance; - address account; - uint256 amountToPay; - uint256 released; - for (uint256 i = 0; i < payee.length; ++i) { - account = payee[i]; - amountToPay = totalReleasable * shares[account] / totalShares; - released += amountToPay; - Address.sendValue(payable(account), amountToPay); - } - if (released == 0) { - revert NoPaymentDue(); - } - totalReleased += released; - emit Release(released); + function release() external nonReentrant { + _release(); + } + + /** + * @dev Releases the bounty to the specified receiver. + * @param _bountyReceiver The address of the receiver of the bounty. + */ + function release(address _bountyReceiver) public nonReentrant { + _payBounty(_bountyReceiver); + _release(); } /** @@ -186,21 +221,17 @@ contract Pizza is Initializable, Context, Multicall, ReentrancyGuard { * @param token The ERC20 token to be released. */ function erc20Release(IERC20 token) external nonReentrant { - uint256 erc20TotalReleasable = token.balanceOf(address(this)); - address account; - uint256 amountToPay; - uint256 released; - for (uint256 i = 0; i < payee.length; ++i) { - account = payee[i]; - amountToPay = (erc20TotalReleasable * shares[account]) / totalShares; - released += amountToPay; - SafeERC20.safeTransfer(token, payable(account), amountToPay); - } - if (released == 0) { - revert NoPaymentDue(); - } - erc20TotalReleased[token] += released; - emit ERC20Release(token, released); + _erc20Release(token); + } + + /** + * @dev Releases ERC20 tokens to a specified bounty receiver. + * @param token The ERC20 token contract address. + * @param _bountyReceiver The address of the bounty receiver. + */ + function erc20Release(IERC20 token, address _bountyReceiver) external nonReentrant { + _payERC20Bounty(token, _bountyReceiver); + _erc20Release(token); } /* //////////////////////////////////////////////////////////////////////// @@ -247,4 +278,95 @@ contract Pizza is Initializable, Context, Multicall, ReentrancyGuard { shares[account] = _shares; totalShares = totalShares + _shares; } + + /** + * @notice Releases the ETH bounty to the bounty receiver. + * @param _bountyReceiver The address of the bounty receiver. + */ + + function _payBounty(address _bountyReceiver) private { + uint256 bountyAmount = address(this).balance * bounty / BOUNTY_PRECISION; + if (bountyAmount > 0) { + Address.sendValue(payable(_bountyReceiver), bountyAmount); + emit PayBounty(_bountyReceiver, bountyAmount); + } + } + + /** + * @notice Releases the bounty to the bounty receiver. + * @param _bountyToken The ERC20 tokens to be released. + * @param _bountyReceiver The address of the bounty receiver. + */ + function _payERC20Bounty(IERC20 _bountyToken, address _bountyReceiver) private { + uint256 bountyAmount = _bountyToken.balanceOf(address(this)) * bounty / BOUNTY_PRECISION; + if (bountyAmount > 0) { + SafeERC20.safeTransfer(_bountyToken, payable(_bountyReceiver), bountyAmount); + emit PayERC20Bounty(_bountyToken, _bountyReceiver, bountyAmount); + } + } + + /** + * @dev Initializes the contract with the given payees, shares, and bounty. + * @param _payees The addresses of the payees. + * @param _shares The corresponding shares of each payee. + * @param _bounty The bounty amount to be distributed among the payees. + */ + function _init(address[] memory _payees, uint256[] memory _shares, uint256 _bounty) internal { + if (_payees.length != _shares.length) { + revert PayeeShareLengthMismatch(); + } + if (_payees.length == 0) { + revert NoPayees(); + } + if (_bounty > BOUNTY_PRECISION) { + revert InvalidBounty(); + } + + for (uint256 i = 0; i < _payees.length; i++) { + _addPayee(_payees[i], _shares[i]); + } + } + + /** + * @notice Releases available ETH balance. + */ + function _release() internal { + uint256 totalReleasable = address(this).balance; + address account; + uint256 amountToPay; + uint256 released; + for (uint256 i = 0; i < payee.length; ++i) { + account = payee[i]; + amountToPay = totalReleasable * shares[account] / totalShares; + released += amountToPay; + Address.sendValue(payable(account), amountToPay); + } + if (released == 0) { + revert NoPaymentDue(); + } + totalReleased += released; + emit Release(released); + } + + /** + * @dev Releases ERC20 tokens. + * @param token The ERC20 token to be released. + */ + function _erc20Release(IERC20 token) internal { + uint256 erc20TotalReleasable = token.balanceOf(address(this)); + address account; + uint256 amountToPay; + uint256 released; + for (uint256 i = 0; i < payee.length; ++i) { + account = payee[i]; + amountToPay = (erc20TotalReleasable * shares[account]) / totalShares; + released += amountToPay; + SafeERC20.safeTransfer(token, payable(account), amountToPay); + } + if (released == 0) { + revert NoPaymentDue(); + } + erc20TotalReleased[token] += released; + emit ERC20Release(token, released); + } } diff --git a/src/PizzaFactory.sol b/src/PizzaFactory.sol index 1b8d7c5..fe8f124 100644 --- a/src/PizzaFactory.sol +++ b/src/PizzaFactory.sol @@ -41,33 +41,53 @@ contract PizzaFactory is Context { function create(address[] memory _payees, uint256[] memory _shares) external returns (address pizza) { pizza = address(Clones.clone(implementation)); - _initPizza(pizza, _payees, _shares); + _initFreePizza(pizza, _payees, _shares); } function createDeterministic(address[] memory _payees, uint256[] memory _shares, uint256 _salt) external returns (address pizza) { - pizza = address(Clones.cloneDeterministic(implementation, keccak256(abi.encode(_payees, _shares, _salt)))); - _initPizza(pizza, _payees, _shares); + pizza = address(Clones.cloneDeterministic(implementation, salt(_payees, _shares, 0, _salt))); + _initFreePizza(pizza, _payees, _shares); } - function predict(address[] memory _payees, uint256[] memory _shares, uint256 _salt) + function createDeterministicAndRelease( + address[] memory _payees, + uint256[] memory _shares, + uint256 _salt, + uint256 _bounty, + address[] memory _bountyTokens, + address _bountyReceiver + ) external returns (address pizza) { + pizza = address(Clones.cloneDeterministic(implementation, salt(_payees, _shares, _bounty, _salt))); + IPizzaInitializer(pizza).initializeWithBountyRelease(_payees, _shares, _bounty, _bountyTokens, _bountyReceiver); + pizzas[pizza] = _msgSender(); + emit PizzaCreated(pizza); + } + + function predict(address[] memory _payees, uint256[] memory _shares, uint256 _bounty, uint256 _salt) external view returns (address) { - return Clones.predictDeterministicAddress( - implementation, keccak256(abi.encode(_payees, _shares, _salt)), address(this) - ); + return Clones.predictDeterministicAddress(implementation, salt(_payees, _shares, _bounty, _salt), address(this)); } /* //////////////////////////////////////////////////////////////////////// Private //////////////////////////////////////////////////////////////////////// */ - function _initPizza(address _pizza, address[] memory _payees, uint256[] memory _shares) private { - IPizzaInitializer(_pizza).initialize(_payees, _shares); + function salt(address[] memory _payees, uint256[] memory _shares, uint256 _bounty, uint256 _salt) + private + pure + returns (bytes32) + { + return keccak256(abi.encode(_payees, _shares, _bounty, _salt)); + } + + function _initFreePizza(address _pizza, address[] memory _payees, uint256[] memory _shares) private { + IPizzaInitializer(_pizza).initialize(_payees, _shares, 0); pizzas[_pizza] = _msgSender(); emit PizzaCreated(_pizza); } diff --git a/test/Pizza.t.sol b/test/Pizza.t.sol index ffe6762..e71a3af 100644 --- a/test/Pizza.t.sol +++ b/test/Pizza.t.sol @@ -91,4 +91,53 @@ contract PizzaTest is Test { vm.expectRevert(abi.encodeWithSelector(Pizza.NoPaymentDue.selector)); pizza.erc20Release(token); } + + function test_bountyCreateInvalidBounty(uint256 salt, uint256 bounty) public { + vm.assume(bounty > pizza.BOUNTY_PRECISION()); + address predicted = f.predict(payees, shares, bounty, salt); + address[] memory bountyTokens = new address[](2); + bountyTokens[0] = address(token); + bountyTokens[1] = address(0); + address bountyDeployer = address(0x3); + address bountyReceiver = address(0x4); + token.transfer(address(predicted), 1e18); + vm.deal(payable(predicted), 2e18); + + vm.expectRevert(abi.encodeWithSelector(Pizza.InvalidBounty.selector)); + f.createDeterministicAndRelease(payees, shares, salt, bounty, bountyTokens, bountyReceiver); + } + + function test_bountyCreate(uint256 salt) public { + uint256 bounty = 1e4; // 0.01 aka 1% + address predicted = f.predict(payees, shares, bounty, salt); + + // Now some balances accumulate on the undeployed address + + token.transfer(address(predicted), 1e18); + vm.deal(payable(predicted), 2e18); + + // Now we a deployer/releaser comes along and is willing to pay to release + // the funds. Their designated receiver will get the bounty. + + address[] memory bountyTokens = new address[](2); + bountyTokens[0] = address(token); + bountyTokens[1] = address(0); + address bountyDeployer = address(0x3); + address bountyReceiver = address(0x4); + + vm.prank(bountyDeployer); + f.createDeterministicAndRelease(payees, shares, salt, bounty, bountyTokens, bountyReceiver); + + assertEq(token.balanceOf(bountyDeployer), 0); + assertEq(token.balanceOf(bountyReceiver), 1e16); + assertEq(token.balanceOf(predicted), 0); + assertEq(token.balanceOf(payees[0]), 396e15); + assertEq(token.balanceOf(payees[1]), 594e15); + + assertEq(bountyDeployer.balance, 0); + assertEq(bountyReceiver.balance, 2e16); + assertEq(predicted.balance, 0); + assertEq(payees[0].balance, 792e15); + assertEq(payees[1].balance, 1188e15); + } } diff --git a/test/PizzaFactory.t.sol b/test/PizzaFactory.t.sol index 6f8cca9..4c75f39 100644 --- a/test/PizzaFactory.t.sol +++ b/test/PizzaFactory.t.sol @@ -27,7 +27,7 @@ contract PizzaFactoryTest is Test { } function test_createDeterministic(uint256 nonce) public { - address predicted = f.predict(payees, shares, nonce); + address predicted = f.predict(payees, shares, 0, nonce); vm.expectEmit(true, true, true, true); emit PizzaCreated(predicted); Pizza p = Pizza(payable(address(f.createDeterministic(payees, shares, nonce))));