diff --git a/.gitmodules b/.gitmodules index 690924b..9296efd 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,3 +4,6 @@ [submodule "lib/openzeppelin-contracts"] path = lib/openzeppelin-contracts url = https://github.com/OpenZeppelin/openzeppelin-contracts +[submodule "lib/openzeppelin-contracts-upgradeable"] + path = lib/openzeppelin-contracts-upgradeable + url = https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable diff --git a/Makefile b/Makefile index f35d5cf..6beb9f5 100644 --- a/Makefile +++ b/Makefile @@ -4,9 +4,11 @@ # .SILENT: +LIVENET_DEPLOY_COMMAND = forge script script/PizzaFactory.s.sol:DeployPizzaFactory --private-key ${PRIVATE_KEY} -vvvv + .PHONY: all test clean -all: clean update build +all:; forge test -vvv -w # Clean the repo clean :; forge clean @@ -34,13 +36,11 @@ abi: # solhint should be installed globally lint :; solhint src/**/*.sol && solhint src/*.sol -anvil :; anvil -m 'test test test test test test test test test test test junk' - -# use the "@" to hide the command from your shell -deploy-mainnet :; @forge script script/PizzaFactory.s.sol:DeployPizzaFactory --rpc-url mainnet --private-key ${PRIVATE_KEY} -vvvv +deploy-mainnet-dryrun :; @${LIVENET_DEPLOY_COMMAND} --rpc-url mainnet +deploy-sepolia-dryrun :; @${LIVENET_DEPLOY_COMMAND} --rpc-url sepolia -# use the "@" to hide the command from your shell -deploy-sepolia :; @forge script script/PizzaFactory.s.sol:DeployPizzaFactory --rpc-url sepolia --private-key ${PRIVATE_KEY} -vvvv +deploy-mainnet :; @${LIVENET_DEPLOY_COMMAND} --rpc-url mainnet --broadcast --verify +deploy-sepolia :; @${LIVENET_DEPLOY_COMMAND} --rpc-url sepolia --broadcast --verify # anvil deploy with the default user deploy-anvil :; @forge script script/PizzaFactory.s.sol:DeployPizzaFactory --rpc-url http://localhost:8545 --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 --broadcast diff --git a/README.md b/README.md index 6b50d1e..ab407bb 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,8 @@ Copy the `.env.example` file to `.env` and fill in your own values. ## Testing +For this project I've gone ahead and just created a Makefile for common build targets. You can run tests with the bare `make` command: + ``` make ``` @@ -29,5 +31,12 @@ See Foundry docs for more testing options. Check out the [Makefile](./Makefile) for build/deploy targets. Example: ``` +make deploy-sepolia-dryrun make deploy-sepolia ``` + +## Private key management + +The private key is only used for deployment. + +Depending on your development needs and risk tolerance, your key can be managed any way you like. My recommended approach is to use some kind of encrypted key storage. Check out [Foundry's encrypted keystore](https://book.getfoundry.sh/reference/cast/cast-wallet-import), or use something like [1Password's `op` CLI](https://developer.1password.com/docs/cli/get-started/). diff --git a/foundry.toml b/foundry.toml index 2773dc8..d9519bf 100644 --- a/foundry.toml +++ b/foundry.toml @@ -2,8 +2,8 @@ src = "src" out = "out" libs = ["lib"] - -# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options +via_ir = true +solc = "0.8.23" [profile.forked] fork_block_number = 18814000 @@ -15,4 +15,4 @@ sepolia = "${SEPOLIA_RPC_URL}" [etherscan] mainnet = { key = "${ETHERSCAN_API_KEY}" } -goerli = { key = "${ETHERSCAN_API_KEY}", chain = "goerli"} \ No newline at end of file +sepolia = { key = "${ETHERSCAN_API_KEY}", chain = "sepolia"} \ No newline at end of file diff --git a/lib/forge-std b/lib/forge-std index ae570fe..4513bc2 160000 --- a/lib/forge-std +++ b/lib/forge-std @@ -1 +1 @@ -Subproject commit ae570fec082bfe1c1f45b0acca4a2b4f84d345ce +Subproject commit 4513bc2063f23c57bee6558799584b518d387a39 diff --git a/lib/openzeppelin-contracts b/lib/openzeppelin-contracts index 72c642e..a51f1e1 160000 --- a/lib/openzeppelin-contracts +++ b/lib/openzeppelin-contracts @@ -1 +1 @@ -Subproject commit 72c642e13e4e8c36da56ebeeecf2ee3e4abe9781 +Subproject commit a51f1e1354dbae62cec024d2590de5cab249a27c diff --git a/lib/openzeppelin-contracts-upgradeable b/lib/openzeppelin-contracts-upgradeable new file mode 160000 index 0000000..92b7b57 --- /dev/null +++ b/lib/openzeppelin-contracts-upgradeable @@ -0,0 +1 @@ +Subproject commit 92b7b57c8c8aad89403900842167fca9f2315756 diff --git a/script/PizzaFactory.s.sol b/script/PizzaFactory.s.sol index eb70a6c..02d5da1 100644 --- a/script/PizzaFactory.s.sol +++ b/script/PizzaFactory.s.sol @@ -5,6 +5,10 @@ import {Script} from "forge-std/Script.sol"; import {Pizza} from "../src/Pizza.sol"; import {PizzaFactory} from "../src/PizzaFactory.sol"; +/** + * @title DeployPizzaFactory + * @dev A script contract for deploying the PizzaFactory contract. + */ contract DeployPizzaFactory is Script { function setUp() public {} diff --git a/script/SamplePizza.s.sol b/script/SamplePizza.s.sol index d23a69f..222f9d1 100644 --- a/script/SamplePizza.s.sol +++ b/script/SamplePizza.s.sol @@ -4,6 +4,10 @@ pragma solidity ^0.8.13; import {Script} from "forge-std/Script.sol"; import {PizzaFactory} from "../src/PizzaFactory.sol"; +/** + * @title DeploySamplePizza + * @dev A contract for deploying a sample pizza contract. + */ contract DeploySamplePizza is Script { function setUp() public {} @@ -17,6 +21,6 @@ contract DeploySamplePizza is Script { uint256[] memory shares = new uint256[](2); shares[0] = 100; shares[1] = 100; - return address(f.create(payees, shares)); + return address(f.create(payees, shares, 0)); } } diff --git a/src/IPizzaInitializer.sol b/src/IPizzaInitializer.sol index 700e4bf..57848f4 100644 --- a/src/IPizzaInitializer.sol +++ b/src/IPizzaInitializer.sol @@ -6,5 +6,27 @@ pragma solidity 0.8.23; * @dev Interface for initializing a pizza contract. */ interface IPizzaInitializer { - function initialize(address[] memory _payees, uint256[] memory _shares) external; + /** + * @dev Initializes the contract. + * @param _payees The addresses of the payees. + * @param _shares The shares of each payee. + * @param _bounty The bounty amount. + */ + function initialize(address[] memory _payees, uint256[] memory _shares, uint256 _bounty) external; + + /** + * @dev Initializes the contract. + * @param _payees The addresses of the payees. + * @param _shares The shares of each payee. + * @param _bounty The bounty amount. + * @param _bountyTokens The tokens to be used for the bounty. + * @param _bountyReceiver The address of the bounty receiver. + */ + 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..dc1139f 100644 --- a/src/Pizza.sol +++ b/src/Pizza.sol @@ -1,13 +1,11 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.23; -import {Initializable} from "openzeppelin-contracts/proxy/utils/Initializable.sol"; import {SafeERC20} from "openzeppelin-contracts/token/ERC20/utils/SafeERC20.sol"; import {IERC20} from "openzeppelin-contracts/token/ERC20/IERC20.sol"; -import {Context} from "openzeppelin-contracts/utils/Context.sol"; import {Address} from "openzeppelin-contracts/utils/Address.sol"; import {Multicall} from "openzeppelin-contracts/utils/Multicall.sol"; -import {ReentrancyGuard} from "openzeppelin-contracts/utils/ReentrancyGuard.sol"; +import {ReentrancyGuardUpgradeable} from "openzeppelin-contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; /** * @title Pizza @@ -15,7 +13,7 @@ import {ReentrancyGuard} from "openzeppelin-contracts/utils/ReentrancyGuard.sol" * It allows for the release of ERC20 tokens as well as Ether. * Releases are modified to be only callable in a batch, rather than individually. */ -contract Pizza is Initializable, Context, Multicall, ReentrancyGuard { +contract Pizza is Multicall, ReentrancyGuardUpgradeable { using SafeERC20 for IERC20; /* //////////////////////////////////////////////////////////////////////// @@ -52,6 +50,11 @@ contract Pizza is Initializable, Context, Multicall, ReentrancyGuard { */ error NoPayees(); + /** + * @dev Error thrown when the bounty is invalid. + */ + error InvalidBounty(); + /* //////////////////////////////////////////////////////////////////////// Events //////////////////////////////////////////////////////////////////////// */ @@ -76,11 +79,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 +132,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 +147,33 @@ 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; + 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)); + } } } @@ -153,7 +188,7 @@ contract Pizza is Initializable, Context, Multicall, ReentrancyGuard { * balance is increased. */ receive() external payable virtual { - emit PaymentReceived(_msgSender(), msg.value); + emit PaymentReceived(msg.sender, msg.value); } /* //////////////////////////////////////////////////////////////////////// @@ -163,22 +198,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 +216,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); } /* //////////////////////////////////////////////////////////////////////// @@ -232,7 +258,7 @@ contract Pizza is Initializable, Context, Multicall, ReentrancyGuard { * @param account The address of the payee to add. * @param _shares The number of shares owned by the payee. */ - function _addPayee(address account, uint256 _shares) private { + function _addPayee(address account, uint256 _shares) internal { if (account == address(0)) { revert NullPayee(account); } @@ -247,4 +273,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) internal { + uint256 bountyAmount = (bounty * address(this).balance) / 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) internal { + uint256 bountyAmount = (bounty * _bountyToken.balanceOf(address(this))) / 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 { + __ReentrancyGuard_init(); + 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..2cb85a5 100644 --- a/src/PizzaFactory.sol +++ b/src/PizzaFactory.sol @@ -3,15 +3,17 @@ pragma solidity 0.8.23; import {Clones} from "openzeppelin-contracts/proxy/Clones.sol"; import {IPizzaInitializer} from "./IPizzaInitializer.sol"; -import {Context} from "openzeppelin-contracts/utils/Context.sol"; - -event PizzaCreated(address indexed pizza); /** * @title PizzaFactory * @dev A contract for creating {IPizzaInitializer} splitter contracts. */ -contract PizzaFactory is Context { +contract PizzaFactory { + /* //////////////////////////////////////////////////////////////////////// + Events + //////////////////////////////////////////////////////////////////////// */ + event PizzaCreated(address indexed pizza, address indexed creator); + /* //////////////////////////////////////////////////////////////////////// Storage //////////////////////////////////////////////////////////////////////// */ @@ -21,12 +23,6 @@ contract PizzaFactory is Context { */ address public implementation; - /** - * @dev A mapping that stores pizzas created. - * The key is the address of the pizza owner, and the value is a boolean indicating creator. - */ - mapping(address => address) public pizzas; - /* //////////////////////////////////////////////////////////////////////// Constructor //////////////////////////////////////////////////////////////////////// */ @@ -39,36 +35,84 @@ contract PizzaFactory is Context { Factory //////////////////////////////////////////////////////////////////////// */ + /** + * @dev Creates a new Pizza contract with the given payees and shares. + * @param _payees The addresses of the payees. + * @param _shares The corresponding shares of each payee. + * @return pizza address of the newly created Pizza contract. + */ + function create(address[] memory _payees, uint256[] memory _shares) external returns (address pizza) { pizza = address(Clones.clone(implementation)); - _initPizza(pizza, _payees, _shares); + IPizzaInitializer(pizza).initialize(_payees, _shares, 0); + emit PizzaCreated(pizza, msg.sender); } - function createDeterministic(address[] memory _payees, uint256[] memory _shares, uint256 _salt) + /** + * @dev Creates a new Pizza contract with the given payees, shares, and salt. + * @param _payees The addresses of the payees. + * @param _shares The corresponding shares of each payee. + * @param _salt The salt value used for deterministic cloning. + * @return pizza address of the newly created Pizza contract. + */ + function create(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))); + IPizzaInitializer(pizza).initialize(_payees, _shares, 0); + emit PizzaCreated(pizza, msg.sender); } - function predict(address[] memory _payees, uint256[] memory _shares, uint256 _salt) + /** + * @dev Creates a new Pizza contract with the given payees, shares, salt, bounty, bounty tokens, and bounty receiver. + * @param _payees The addresses of the payees. + * @param _shares The corresponding shares of each payee. + * @param _salt The salt value used for deterministic cloning. + * @param _bounty The bounty amount to be released. + * @param _bountyTokens The addresses of the bounty tokens. + * @param _bountyReceiver The address of the bounty receiver. + * @return pizza address of the newly created Pizza contract. + */ + function createAndRelease( + 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); + emit PizzaCreated(pizza, msg.sender); + } + + /** + * @dev Predicts the address of the pizza contract with the given params + * + * @param _payees The addresses of the payees who will receive a share of the pizza order. + * @param _shares The corresponding shares of each payee. + * @param _bounty The bounty amount to be awarded to the successful predictor. + * @param _salt A random value used in case multiple contracts are created with the same params. + * @return The predicted address of the pizza contract. + */ + 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); - pizzas[_pizza] = _msgSender(); - emit PizzaCreated(_pizza); + function salt(address[] memory _payees, uint256[] memory _shares, uint256 _bounty, uint256 _salt) + private + pure + returns (bytes32) + { + return keccak256(abi.encode(_payees, _shares, _bounty, _salt)); } } diff --git a/test/Pizza.t.sol b/test/Pizza.t.sol index ffe6762..2bbbc96 100644 --- a/test/Pizza.t.sol +++ b/test/Pizza.t.sol @@ -2,7 +2,7 @@ pragma solidity 0.8.23; import {Test, console2} from "forge-std/Test.sol"; -import {PizzaFactory, PizzaCreated} from "../src/PizzaFactory.sol"; +import {PizzaFactory} from "../src/PizzaFactory.sol"; import {Pizza} from "../src/Pizza.sol"; import {Address} from "openzeppelin-contracts/utils/Address.sol"; import {IERC20} from "openzeppelin-contracts/token/ERC20/IERC20.sol"; @@ -26,7 +26,7 @@ contract PizzaTest is Test { payees.push(address(0x2)); shares.push(2); shares.push(3); - pizza = Pizza(payable(address(f.create(payees, shares)))); + pizza = Pizza(payable(address(f.create(payees, shares, 0)))); } function test_emitPaymentReceived() public { @@ -91,4 +91,52 @@ 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 bountyReceiver = address(0x4); + token.transfer(address(predicted), 1e18); + vm.deal(payable(predicted), 2e18); + + vm.expectRevert(abi.encodeWithSelector(Pizza.InvalidBounty.selector)); + f.createAndRelease(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.createAndRelease(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.fork.t.sol b/test/PizzaFactory.fork.t.sol index 131c0ca..2e4a85e 100644 --- a/test/PizzaFactory.fork.t.sol +++ b/test/PizzaFactory.fork.t.sol @@ -2,7 +2,7 @@ pragma solidity 0.8.23; import {Test, console2} from "forge-std/Test.sol"; -import {PizzaFactory, PizzaCreated} from "../src/PizzaFactory.sol"; +import {PizzaFactory} from "../src/PizzaFactory.sol"; import {Pizza} from "../src/Pizza.sol"; import {Address} from "openzeppelin-contracts/utils/Address.sol"; import {IERC20} from "openzeppelin-contracts/token/ERC20/IERC20.sol"; @@ -28,7 +28,7 @@ contract PizzaFactoryForkTest is Test { } function test_cloneReceive() public { - Pizza p = Pizza(payable(address(f.create(payees, shares)))); + Pizza p = Pizza(payable(address(f.create(payees, shares, 0)))); address sender = address(0x3); vm.deal(sender, 3 ether); @@ -51,7 +51,7 @@ contract PizzaFactoryForkTest is Test { } function test_release() public { - Pizza p = Pizza(payable(address(f.create(payees, shares)))); + Pizza p = Pizza(payable(address(f.create(payees, shares, 0)))); address sender = address(0x3); vm.deal(sender, 1 ether); diff --git a/test/PizzaFactory.t.sol b/test/PizzaFactory.t.sol index 6f8cca9..c6abc3e 100644 --- a/test/PizzaFactory.t.sol +++ b/test/PizzaFactory.t.sol @@ -2,7 +2,7 @@ pragma solidity 0.8.23; import {Test, console2} from "forge-std/Test.sol"; -import {PizzaFactory, PizzaCreated} from "../src/PizzaFactory.sol"; +import {PizzaFactory} from "../src/PizzaFactory.sol"; import {Pizza} from "../src/Pizza.sol"; import {Address} from "openzeppelin-contracts/utils/Address.sol"; import {IERC20} from "openzeppelin-contracts/token/ERC20/IERC20.sol"; @@ -26,11 +26,11 @@ contract PizzaFactoryTest is Test { shares.push(3); } - function test_createDeterministic(uint256 nonce) public { - address predicted = f.predict(payees, shares, nonce); + function test_create(uint256 nonce) public { + 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)))); + emit PizzaFactory.PizzaCreated(predicted, address(this)); + Pizza p = Pizza(payable(address(f.create(payees, shares, nonce)))); assertEq(address(p), predicted); assertNotEq(address(p), address(0)); @@ -48,14 +48,14 @@ contract PizzaFactoryTest is Test { assertEq(p.erc20TotalReleased(IERC20(address(0))), 0); } - function test_createDeterministicOnce(uint256 nonce) public { - Pizza(payable(address(f.createDeterministic(payees, shares, nonce)))); + function test_createOnce(uint256 nonce) public { + Pizza(payable(address(f.create(payees, shares, nonce)))); vm.expectRevert(abi.encodeWithSelector(Clones.ERC1167FailedCreateClone.selector)); - f.createDeterministic(payees, shares, nonce); + f.create(payees, shares, nonce); } function test_cloneReceive() public { - Pizza p = Pizza(payable(address(f.create(payees, shares)))); + Pizza p = Pizza(payable(address(f.create(payees, shares, 0)))); address sender = address(0x3); vm.deal(sender, 3 ether);