Skip to content

Commit

Permalink
add bounty capabilities and some tests
Browse files Browse the repository at this point in the history
  • Loading branch information
burnto committed Jan 22, 2024
1 parent 62aa529 commit d5557e4
Show file tree
Hide file tree
Showing 5 changed files with 252 additions and 53 deletions.
10 changes: 9 additions & 1 deletion src/IPizzaInitializer.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
206 changes: 164 additions & 42 deletions src/Pizza.sol
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@ contract Pizza is Initializable, Context, Multicall, ReentrancyGuard {
*/
error NoPayees();

/**
* @dev Error thrown when the bounty is invalid.
*/
error InvalidBounty();

/* ////////////////////////////////////////////////////////////////////////
Events
//////////////////////////////////////////////////////////////////////// */
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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));
}
}
}
}

Expand All @@ -163,44 +203,35 @@ 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();
}

/**
* @notice Releases available ERC20 token balance.
* @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);
}

/* ////////////////////////////////////////////////////////////////////////
Expand Down Expand Up @@ -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);
}
}
38 changes: 29 additions & 9 deletions src/PizzaFactory.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
49 changes: 49 additions & 0 deletions test/Pizza.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
2 changes: 1 addition & 1 deletion test/PizzaFactory.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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))));
Expand Down

0 comments on commit d5557e4

Please sign in to comment.