diff --git a/contracts/PublicFundraising.sol b/contracts/PublicFundraising.sol index 815755c3..cf2deb1d 100644 --- a/contracts/PublicFundraising.sol +++ b/contracts/PublicFundraising.sol @@ -171,12 +171,7 @@ contract PublicFundraising is return priceBase; } - /** - * @notice Buy `amount` tokens and mint them to `_tokenReceiver`. - * @param _amount amount of tokens to buy, in bits (smallest subunit of token) - * @param _tokenReceiver address the tokens should be minted to - */ - function buy(uint256 _amount, address _tokenReceiver) external whenNotPaused nonReentrant { + function _checkAndDeliver(uint256 _amount, address _tokenReceiver) internal { require(tokensSold + _amount <= maxAmountOfTokenToBeSold, "Not enough tokens to sell left"); require(tokensBought[_tokenReceiver] + _amount >= minAmountPerBuyer, "Buyer needs to buy at least minAmount"); require( @@ -187,9 +182,22 @@ contract PublicFundraising is tokensSold += _amount; tokensBought[_tokenReceiver] += _amount; - // rounding up to the next whole number. Investor is charged up to one currency bit more in case of a fractional currency bit. - uint256 currencyAmount = Math.ceilDiv(_amount * getPrice(), 10 ** token.decimals()); + token.mint(_tokenReceiver, _amount); + } + function _getFeeAndFeeReceiver(uint256 _currencyAmount) internal view returns (uint256, address) { + IFeeSettingsV2 feeSettings = token.feeSettings(); + return (feeSettings.publicFundraisingFee(_currencyAmount), feeSettings.publicFundraisingFeeCollector()); + } + + /** + * @notice Buy `amount` tokens and mint them to `_tokenReceiver`. + * @param _amount amount of tokens to buy, in bits (smallest subunit of token) + * @param _tokenReceiver address the tokens should be minted to + */ + function buy(uint256 _amount, address _tokenReceiver) public whenNotPaused nonReentrant { + // rounding up to the next whole number. Investor is charged up to one currency bit more in case of a fractional currency bit. + uint256 currencyAmount = calculateCurrencyAmountFromTokenAmount(_amount); IFeeSettingsV2 feeSettings = token.feeSettings(); uint256 fee = feeSettings.publicFundraisingFee(currencyAmount); if (fee != 0) { @@ -197,11 +205,59 @@ contract PublicFundraising is } currency.safeTransferFrom(_msgSender(), currencyReceiver, currencyAmount - fee); + _checkAndDeliver(_amount, _tokenReceiver); - token.mint(_tokenReceiver, _amount); emit TokensBought(_msgSender(), _amount, currencyAmount); } + /// calculate token amount from currency amount and price. Must be rounded down anyway, so the normal integer math is fine. + /// This calculation often results in a larger amount of tokens + function calculateTokenAmountFromCurrencyAmount(uint256 _currencyAmount) public view returns (uint256) { + return (_currencyAmount * 10 ** token.decimals()) / getPrice(); + } + + function calculateCurrencyAmountFromTokenAmount(uint256 _tokenAmount) public view returns (uint256) { + return Math.ceilDiv(_tokenAmount * getPrice(), 10 ** token.decimals()); + } + + function findMaxAmount(uint256 _minAmount) external view returns (uint256) { + uint256 currencyAmount = calculateCurrencyAmountFromTokenAmount(_minAmount); + return (calculateTokenAmountFromCurrencyAmount(currencyAmount)); + } + + function onTokenTransfer( + address _from, + uint256 _currencyAmount, + bytes calldata data + ) external whenNotPaused nonReentrant returns (bool) { + require(_msgSender() == address(currency), "only the currency contract can call this function"); + + // if a recipient address was provided in data, use it as receiver. Otherwise, use _from as receiver. + address tokenReceiver; + if (data.length == 32) { + tokenReceiver = abi.decode(data, (address)); + } else { + tokenReceiver = _from; + } + + // address tokenReceiver = abi.decode(data, (address)); + // tokenReceiver = tokenReceiver == address(0) ? _from : tokenReceiver; + + uint256 amount = calculateTokenAmountFromCurrencyAmount(_currencyAmount); + + // move payment to currencyReceiver and feeCollector + (uint256 fee, address feeCollector) = _getFeeAndFeeReceiver(_currencyAmount); + currency.safeTransfer(feeCollector, fee); + currency.safeTransfer(currencyReceiver, _currencyAmount - fee); + + _checkAndDeliver(amount, tokenReceiver); + + emit TokensBought(_from, amount, _currencyAmount); + + // return true is an antipattern, but required by the interface + return true; + } + /** * @notice change the currencyReceiver to `_currencyReceiver` * @param _currencyReceiver new currencyReceiver diff --git a/test/PublicFundraising.t.sol b/test/PublicFundraising.t.sol index 8ab3600d..eeb136c6 100644 --- a/test/PublicFundraising.t.sol +++ b/test/PublicFundraising.t.sol @@ -340,11 +340,6 @@ contract PublicFundraisingTest is Test { } function testBuyTooMuch() public { - uint256 tokenBuyAmount = 5 * 10 ** token.decimals(); - uint256 costInPaymentToken = (tokenBuyAmount * price) / 10 ** 18; - - assert(costInPaymentToken == 35 * 10 ** paymentTokenDecimals); // 35 payment tokens, manually calculated - uint256 paymentTokenBalanceBefore = paymentToken.balanceOf(buyer); vm.prank(buyer); @@ -401,9 +396,9 @@ contract PublicFundraisingTest is Test { uint256 availableBalance = paymentToken.balanceOf(buyer); vm.prank(buyer); - paymentToken.transfer(person1, availableBalance / 2); + paymentToken.transfer(person1, availableBalance / 3); vm.prank(buyer); - paymentToken.transfer(person2, 10 ** 6); + paymentToken.transfer(person2, availableBalance / 3); vm.prank(person1); paymentToken.approve(address(raise), paymentTokenAmount); @@ -1212,4 +1207,88 @@ contract PublicFundraisingTest is Test { raise.acceptOwnership(); assertTrue(raise.owner() == newOwner); } + + function testMaxAmountFixed() public { + uint256 _price = 7 * 10 ** paymentTokenDecimals; // 7 payment tokens per token + PublicFundraising _raise = PublicFundraising( + factory.createPublicFundraisingClone( + bytes32("a"), + trustedForwarder, + owner, + payable(receiver), + minAmountPerBuyer, + maxAmountPerBuyer, + _price, + maxAmountOfTokenToBeSold, + paymentToken, + token + ) + ); + + // If I want to buy 1 tokens bit, I need to pay 1 payment token bit, even though + // the "real" cost would only be 7/(10^12) payment tokens + // Therefore, it is cleverer if I buy as much as possible for that 1 payment token bit, which is 1/7 tokens + uint256 _amount = 1; // token bit + uint256 _currencyAmount = _raise.calculateCurrencyAmountFromTokenAmount(_amount); // 1 payment token bit + uint256 _maxAmountManual = _raise.calculateTokenAmountFromCurrencyAmount(_currencyAmount); // 1/7 * 10^12 tokens + uint256 _maxAmount = _raise.findMaxAmount(_amount); + uint256 _effectivePrice = (_currencyAmount * 10 ** token.decimals()) / _maxAmount; + + // log all 3 values + console.log("amount", _amount); + console.log("currencyAmount", _currencyAmount); + console.log("maxAmount", _maxAmount); + // difference between amount and maxAmount + console.log("difference", _maxAmount - _amount); + // price calculated from _maxAmount and _currencyAmount + console.log("price", (_currencyAmount * 10 ** token.decimals()) / _maxAmount); + + assertTrue(_effectivePrice == _price, "Prices don't match"); + assertTrue(_maxAmount == uint256(10 ** 12) / 7, "Max amount is wrong"); + assertTrue(_maxAmount == _maxAmountManual, "Max amounts don't match"); + } + + function testMaxAmountVariable(uint256 _price, uint256 _amount) public { + vm.assume(_price > 0); + vm.assume(_amount > 0); + vm.assume(_amount < type(uint256).max / _price); + + PublicFundraising _raise = PublicFundraising( + factory.createPublicFundraisingClone( + bytes32("a"), + trustedForwarder, + owner, + payable(receiver), + minAmountPerBuyer, + maxAmountPerBuyer, + _price, + maxAmountOfTokenToBeSold, + paymentToken, + token + ) + ); + + uint256 _currencyAmount = _raise.calculateCurrencyAmountFromTokenAmount(_amount); + + vm.assume(_currencyAmount < type(uint256).max / 10 ** token.decimals()); // otherwise an overflow will occur + uint256 _maxAmountManual = _raise.calculateTokenAmountFromCurrencyAmount(_currencyAmount); + uint256 _maxAmount = _raise.findMaxAmount(_amount); + uint256 _effectivePriceForMax = (_currencyAmount * 10 ** token.decimals()) / _maxAmount; + uint256 _effectivePriceForInput = (_currencyAmount * 10 ** token.decimals()) / _amount; + + // log all 3 values + console.log("amount", _amount); + console.log("currencyAmount", _currencyAmount); + console.log("maxAmount", _maxAmount); + // difference between amount and maxAmount + console.log("difference", _maxAmount - _amount); + // price calculated from _maxAmount and _currencyAmount + console.log("_effectivePriceForMax", _effectivePriceForMax); + console.log("price", _price); + + assertTrue(_effectivePriceForInput >= _price, "Effective price lower than nominal price!"); + assertTrue(_effectivePriceForMax >= _price, "Effective price for max amount lower than nominal price"); + assertTrue(_maxAmount >= _amount, "Max amount is wrong"); + assertTrue(_maxAmount == _maxAmountManual, "Max amounts don't match"); + } } diff --git a/test/PublicFundraisingERC677.t.sol b/test/PublicFundraisingERC677.t.sol new file mode 100644 index 00000000..a101f05c --- /dev/null +++ b/test/PublicFundraisingERC677.t.sol @@ -0,0 +1,389 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity ^0.8.13; + +import "../lib/forge-std/src/Test.sol"; +import "../contracts/TokenCloneFactory.sol"; +import "../contracts/FeeSettings.sol"; +import "../contracts/PublicFundraisingCloneFactory.sol"; +import "./resources/FakePaymentToken.sol"; +import "./resources/MaliciousPaymentToken.sol"; + +contract PublicFundraisingTest is Test { + event CurrencyReceiverChanged(address indexed); + event MinAmountPerBuyerChanged(uint256); + event MaxAmountPerBuyerChanged(uint256); + event TokenPriceAndCurrencyChanged(uint256, IERC20 indexed); + event MaxAmountOfTokenToBeSoldChanged(uint256); + event TokensBought(address indexed buyer, uint256 tokenAmount, uint256 currencyAmount); + + PublicFundraisingCloneFactory factory; + PublicFundraising raise; + AllowList list; + IFeeSettingsV2 feeSettings; + + address wrongFeeReceiver = address(5); + + TokenCloneFactory tokenCloneFactory; + Token token; + FakePaymentToken paymentToken; + + MaliciousPaymentToken maliciousPaymentToken; + + address public constant admin = 0x0109709eCFa91a80626FF3989D68f67f5b1dD120; + address public constant buyer = 0x1109709ecFA91a80626ff3989D68f67F5B1Dd121; + address public constant mintAllower = 0x2109709EcFa91a80626Ff3989d68F67F5B1Dd122; + address public constant minter = 0x3109709ECfA91A80626fF3989D68f67F5B1Dd123; + address public constant owner = 0x6109709EcFA91A80626FF3989d68f67F5b1dd126; + address public constant receiver = 0x7109709eCfa91A80626Ff3989D68f67f5b1dD127; + address public constant paymentTokenProvider = 0x8109709ecfa91a80626fF3989d68f67F5B1dD128; + address public constant trustedForwarder = 0x9109709EcFA91A80626FF3989D68f67F5B1dD129; + + uint8 public constant paymentTokenDecimals = 6; + uint256 public constant paymentTokenAmount = 1000 * 10 ** paymentTokenDecimals; + + uint256 public constant price = 7 * 10 ** paymentTokenDecimals; // 7 payment tokens per token + + uint256 public constant maxAmountOfTokenToBeSold = 20 * 10 ** 18; // 20 token + uint256 public constant maxAmountPerBuyer = maxAmountOfTokenToBeSold / 2; // 10 token + uint256 public constant minAmountPerBuyer = maxAmountOfTokenToBeSold / 200; // 0.1 token + + function setUp() public { + list = new AllowList(); + Fees memory fees = Fees(1, 100, 1, 100, 1, 100, 100); + feeSettings = new FeeSettings(fees, wrongFeeReceiver, admin, wrongFeeReceiver); + + // create token + address tokenLogicContract = address(new Token(trustedForwarder)); + tokenCloneFactory = new TokenCloneFactory(tokenLogicContract); + token = Token( + tokenCloneFactory.createTokenClone(0, trustedForwarder, feeSettings, admin, list, 0x0, "TESTTOKEN", "TEST") + ); + + // set up currency + vm.prank(paymentTokenProvider); + paymentToken = new FakePaymentToken(paymentTokenAmount, paymentTokenDecimals); // 1000 tokens with 6 decimals + // transfer currency to buyer + vm.prank(paymentTokenProvider); + paymentToken.transfer(buyer, paymentTokenAmount); + assertTrue(paymentToken.balanceOf(buyer) == paymentTokenAmount); + + vm.prank(owner); + factory = new PublicFundraisingCloneFactory(address(new PublicFundraising(trustedForwarder))); + + raise = PublicFundraising( + factory.createPublicFundraisingClone( + 0, + trustedForwarder, + owner, + payable(receiver), + minAmountPerBuyer, + maxAmountPerBuyer, + price, + maxAmountOfTokenToBeSold, + paymentToken, + token + ) + ); + + // allow raise contract to mint + bytes32 roleMintAllower = token.MINTALLOWER_ROLE(); + + vm.prank(admin); + token.grantRole(roleMintAllower, mintAllower); + vm.prank(mintAllower); + token.increaseMintingAllowance(address(raise), maxAmountOfTokenToBeSold); + + // give raise contract allowance + vm.prank(buyer); + paymentToken.approve(address(raise), paymentTokenAmount); + } + + /* + set up with MaliciousPaymentToken which tries to reenter the buy function + */ + function testReentrancy() public { + uint8 _paymentTokenDecimals = 18; + + /* + _paymentToken: 1 FPT = 10**_paymentTokenDecimals FPTbits (bit = smallest subunit of token) + Token: 1 CT = 10**18 CTbits + price definition: 30FPT buy 1CT, but must be expressed in FPTbits/CT + price = 30 * 10**_paymentTokenDecimals + */ + + uint256 _price = 7 * 10 ** _paymentTokenDecimals; + uint256 _maxMintAmount = 1000 * 10 ** 18; // 2**256 - 1; // need maximum possible value because we are using a fake token with variable decimals + uint256 _paymentTokenAmount = 100000 * 10 ** _paymentTokenDecimals; + + list = new AllowList(); + Token _token = Token( + tokenCloneFactory.createTokenClone( + 0, + trustedForwarder, + feeSettings, + admin, + list, + 0x0, + "REENTRANCYTOKEN", + "TEST" + ) + ); + vm.prank(paymentTokenProvider); + maliciousPaymentToken = new MaliciousPaymentToken(_paymentTokenAmount); + vm.prank(owner); + + PublicFundraising _raise = PublicFundraising( + factory.createPublicFundraisingClone( + 0, + trustedForwarder, + address(this), + payable(receiver), + 1, + _maxMintAmount / 100, + _price, + _maxMintAmount, + maliciousPaymentToken, + _token + ) + ); + + // allow invite contract to mint + bytes32 roleMintAllower = token.MINTALLOWER_ROLE(); + + vm.prank(admin); + _token.grantRole(roleMintAllower, mintAllower); + vm.startPrank(mintAllower); + _token.increaseMintingAllowance(address(_raise), _maxMintAmount - token.mintingAllowance(address(_raise))); + vm.stopPrank(); + + // mint _paymentToken for buyer + vm.prank(paymentTokenProvider); + maliciousPaymentToken.transfer(buyer, _paymentTokenAmount); + assertTrue(maliciousPaymentToken.balanceOf(buyer) == _paymentTokenAmount); + + // set exploitTarget + maliciousPaymentToken.setExploitTarget(address(_raise), 3, _maxMintAmount / 200000); + + // give invite contract allowance + vm.prank(buyer); + maliciousPaymentToken.approve(address(_raise), _paymentTokenAmount); + + // run actual test + assertTrue(maliciousPaymentToken.balanceOf(buyer) == _paymentTokenAmount); + uint256 buyAmount = _maxMintAmount / 100000; + vm.prank(buyer); + vm.expectRevert("ReentrancyGuard: reentrant call"); + _raise.buy(buyAmount, buyer); + } + + function testERC677BuyHappyCase(uint256 tokenBuyAmount) public { + // uint256 tokenBuyAmount = 10 ** token.decimals(); // buy one token + vm.assume(tokenBuyAmount >= raise.minAmountPerBuyer()); + vm.assume(tokenBuyAmount <= raise.maxAmountPerBuyer()); + uint256 costInPaymentToken = Math.ceilDiv(tokenBuyAmount * raise.priceBase(), 10 ** 18); + vm.assume(costInPaymentToken <= paymentToken.balanceOf(buyer)); + + uint256 realTokenBuyAmount = (costInPaymentToken * 10 ** token.decimals()) / raise.getPrice(); + + // log tokenBuyAmount and costInPaymentToken and price, realTokenBuyAmount + console.log("tokenBuyAmount: ", tokenBuyAmount); + console.log("costInPaymentToken: ", costInPaymentToken); + console.log("tokenPrice: ", raise.getPrice()); + console.log("realTokenBuyAmount: ", realTokenBuyAmount); + // log price from realTokenBuyAmount and costInPaymentToken + console.log( + "price from realTokenBuyAmount and costInPaymentToken: ", + (costInPaymentToken * 10 ** 18) / realTokenBuyAmount + ); + + uint256 paymentTokenBalanceBefore = paymentToken.balanceOf(buyer); + + FeeSettings localFeeSettings = FeeSettings(address(token.feeSettings())); + + vm.prank(buyer); + // vm.expectEmit(true, true, true, true, address(raise)); + // emit TokensBought(buyer, tokenBuyAmount, costInPaymentToken); + paymentToken.transferAndCall(address(raise), costInPaymentToken, new bytes(0)); + + // log token holdings of buyer + console.log("buyer token balance: ", token.balanceOf(buyer)); + // log token buy amount + console.log("tokenBuyAmount: ", tokenBuyAmount); + + assertTrue(paymentToken.balanceOf(buyer) == paymentTokenBalanceBefore - costInPaymentToken, "buyer has paid"); + assertTrue(token.balanceOf(buyer) == realTokenBuyAmount, "buyer has wrong token amount"); + assertTrue( + paymentToken.balanceOf(receiver) == + costInPaymentToken - localFeeSettings.publicFundraisingFee(costInPaymentToken), + "receiver has payment tokens" + ); + assertTrue( + paymentToken.balanceOf(token.feeSettings().publicFundraisingFeeCollector()) == + localFeeSettings.publicFundraisingFee(costInPaymentToken), + "fee collector has collected fee in payment tokens" + ); + assertTrue( + token.balanceOf(token.feeSettings().tokenFeeCollector()) >= localFeeSettings.tokenFee(tokenBuyAmount), + "fee collector has collected fee in tokens" + ); + assertTrue(raise.tokensSold() == realTokenBuyAmount, "raise has sold wrong amount of tokens"); + assertTrue( + raise.tokensBought(buyer) == realTokenBuyAmount, + "raise has stored wrong amount of tokens for buyer" + ); + } + + function testBuyTooMuch() public { + uint256 tokenBuyAmount = maxAmountPerBuyer + 1; + uint256 costInPaymentToken = raise.calculateCurrencyAmountFromTokenAmount(tokenBuyAmount); + + uint256 paymentTokenBalanceBefore = paymentToken.balanceOf(buyer); + + vm.prank(buyer); + vm.expectRevert("Total amount of bought tokens needs to be lower than or equal to maxAmount"); + paymentToken.transferAndCall(address(raise), costInPaymentToken, new bytes(0)); + assertTrue(paymentToken.balanceOf(buyer) == paymentTokenBalanceBefore); + assertTrue(token.balanceOf(buyer) == 0); + assertTrue(paymentToken.balanceOf(receiver) == 0); + assertTrue(raise.tokensSold() == 0); + assertTrue(raise.tokensBought(buyer) == 0); + } + + function testBuyAndMintToDifferentAddress() public { + address addressWithFunds = address(1); + address addressForTokens = address(2); + + uint256 currencyAmount = price; // buy one token + uint256 tokenBuyAmount = raise.calculateTokenAmountFromCurrencyAmount(currencyAmount); + + vm.prank(buyer); + paymentToken.transfer(addressWithFunds, currencyAmount); + + vm.prank(addressWithFunds); + paymentToken.approve(address(raise), paymentTokenAmount); + + // check state before + assertTrue(paymentToken.balanceOf(addressWithFunds) == currencyAmount, "addressWithFunds has no funds"); + assertTrue(paymentToken.balanceOf(addressForTokens) == 0, "addressForTokens has funds"); + assertTrue(token.balanceOf(addressForTokens) == 0, "addressForTokens has tokens before buy"); + assertTrue(token.balanceOf(addressWithFunds) == 0, "addressWithFunds has tokens before buy"); + + // execute buy, with addressForTokens as recipient + bytes memory data = abi.encode(addressForTokens); + + console.log("bytes lenght: ", data.length); + + vm.startPrank(addressWithFunds); + paymentToken.transferAndCall(address(raise), currencyAmount, data); + vm.stopPrank(); + + // log token holdings of addressForTokens + console.log("addressForTokens token balance: ", token.balanceOf(addressForTokens)); + + // check state after + console.log("addressWithFunds balance: ", paymentToken.balanceOf(addressWithFunds)); + assertTrue(paymentToken.balanceOf(addressWithFunds) == 0, "addressWithFunds has funds after buy"); + assertTrue(paymentToken.balanceOf(addressForTokens) == 0, "addressForTokens has funds after buy"); + assertTrue( + token.balanceOf(addressForTokens) == tokenBuyAmount, + "addressForTokens has wrong amount of tokens after buy" + ); + } + + function testMultiplePeopleBuyTooMuch() public { + address person1 = address(1); + address person2 = address(2); + + uint256 amountToSpend = raise.calculateCurrencyAmountFromTokenAmount(raise.maxAmountOfTokenToBeSold()) / 2; + + vm.prank(buyer); + paymentToken.transfer(person1, amountToSpend); + vm.prank(buyer); + paymentToken.transfer(person2, amountToSpend); + + vm.prank(buyer); + paymentToken.transferAndCall(address(raise), amountToSpend, new bytes(0)); + vm.prank(person1); + paymentToken.transferAndCall(address(raise), amountToSpend, new bytes(0)); + vm.prank(person2); + vm.expectRevert("Not enough tokens to sell left"); + paymentToken.transferAndCall(address(raise), amountToSpend, new bytes(0)); + } + + function testMultipleAddressesBuyForOneReceiver() public { + address person1 = vm.addr(1); + address person2 = vm.addr(2); + + uint256 availableBalance = paymentToken.balanceOf(buyer); + + vm.prank(buyer); + paymentToken.transfer(person1, availableBalance / 2); + vm.prank(buyer); + paymentToken.transfer(person2, 10 ** 6); + + uint256 amountToPay = raise.calculateCurrencyAmountFromTokenAmount(maxAmountPerBuyer / 2) + 1; + bytes memory data = abi.encode(buyer); + + console.log("Buying first batch of tokens"); + + vm.startPrank(buyer); + paymentToken.transferAndCall(address(raise), amountToPay, data); + vm.stopPrank(); + + console.log("Buying second batch of tokens"); + + vm.startPrank(person1); + vm.expectRevert("Total amount of bought tokens needs to be lower than or equal to maxAmount"); + paymentToken.transferAndCall(address(raise), amountToPay, data); + vm.stopPrank(); + } + + function testCorrectAccounting() public { + address person1 = address(1); + + uint256 availableBalance = paymentToken.balanceOf(buyer); + + vm.prank(buyer); + paymentToken.transfer(person1, availableBalance / 2); + + uint256 tokenAmount1 = raise.findMaxAmount(maxAmountOfTokenToBeSold / 2); + uint256 tokenAmount2 = raise.findMaxAmount(maxAmountOfTokenToBeSold / 4); + + // check all entries are 0 before + assertTrue(raise.tokensSold() == 0, "raise has sold tokens"); + assertTrue(raise.tokensBought(buyer) == 0, "buyer has bought tokens"); + assertTrue(raise.tokensBought(person1) == 0, "person1 has bought tokens"); + + vm.prank(buyer); + raise.buy(tokenAmount1, buyer); + vm.prank(buyer); + raise.buy(tokenAmount2, person1); + + // check all entries are correct after + assertTrue(raise.tokensSold() == tokenAmount1 + tokenAmount2, "raise has sold wrong amount of tokens"); + assertTrue(raise.tokensBought(buyer) == tokenAmount1); + assertTrue(raise.tokensBought(person1) == tokenAmount2); + assertTrue(token.balanceOf(buyer) == tokenAmount1); + assertTrue(token.balanceOf(person1) == tokenAmount2); + } + + function testBuyTooLittle() public { + uint256 tokenBuyAmount = 5 * 10 ** token.decimals(); + uint256 costInPaymentToken = (tokenBuyAmount * price) / 10 ** 18; + + assert(costInPaymentToken == 35 * 10 ** paymentTokenDecimals); // 35 payment tokens, manually calculated + + uint256 paymentTokenBalanceBefore = paymentToken.balanceOf(buyer); + + uint256 currencyAmount = raise.calculateCurrencyAmountFromTokenAmount(minAmountPerBuyer / 2); + + vm.startPrank(buyer); + vm.expectRevert("Buyer needs to buy at least minAmount"); + paymentToken.transferAndCall(address(raise), currencyAmount, new bytes(0)); + assertTrue(paymentToken.balanceOf(buyer) == paymentTokenBalanceBefore); + assertTrue(token.balanceOf(buyer) == 0); + assertTrue(paymentToken.balanceOf(receiver) == 0); + assertTrue(raise.tokensSold() == 0); + assertTrue(raise.tokensBought(buyer) == 0); + } +} diff --git a/test/PublicFundraisingPrice.t.sol b/test/PublicFundraisingPrice.t.sol new file mode 100644 index 00000000..c0f89f81 --- /dev/null +++ b/test/PublicFundraisingPrice.t.sol @@ -0,0 +1,100 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity ^0.8.13; + +import "../lib/forge-std/src/Test.sol"; +import "../contracts/TokenCloneFactory.sol"; +import "../contracts/FeeSettings.sol"; +import "../contracts/PublicFundraisingCloneFactory.sol"; +import "./resources/FakePaymentToken.sol"; +import "./resources/MaliciousPaymentToken.sol"; + +contract PublicFundraisingTest is Test { + event CurrencyReceiverChanged(address indexed); + event MinAmountPerBuyerChanged(uint256); + event MaxAmountPerBuyerChanged(uint256); + event TokenPriceAndCurrencyChanged(uint256, IERC20 indexed); + event MaxAmountOfTokenToBeSoldChanged(uint256); + event TokensBought(address indexed buyer, uint256 tokenAmount, uint256 currencyAmount); + + PublicFundraisingCloneFactory factory; + PublicFundraising raise; + AllowList list; + IFeeSettingsV2 feeSettings; + + address wrongFeeReceiver = address(5); + + TokenCloneFactory tokenCloneFactory; + Token token; + FakePaymentToken paymentToken; + + MaliciousPaymentToken maliciousPaymentToken; + + address public constant admin = 0x0109709eCFa91a80626FF3989D68f67f5b1dD120; + address public constant buyer = 0x1109709ecFA91a80626ff3989D68f67F5B1Dd121; + address public constant mintAllower = 0x2109709EcFa91a80626Ff3989d68F67F5B1Dd122; + address public constant minter = 0x3109709ECfA91A80626fF3989D68f67F5B1Dd123; + address public constant owner = 0x6109709EcFA91A80626FF3989d68f67F5b1dd126; + address public constant receiver = 0x7109709eCfa91A80626Ff3989D68f67f5b1dD127; + address public constant paymentTokenProvider = 0x8109709ecfa91a80626fF3989d68f67F5B1dD128; + address public constant trustedForwarder = 0x9109709EcFA91A80626FF3989D68f67F5B1dD129; + + uint8 public constant paymentTokenDecimals = 6; + uint256 public constant paymentTokenAmount = 1000 * 10 ** paymentTokenDecimals; + + uint256 public constant price = 7 * 10 ** paymentTokenDecimals; // 7 payment tokens per token + + uint256 public constant maxAmountOfTokenToBeSold = 20 * 10 ** 18; // 20 token + uint256 public constant maxAmountPerBuyer = maxAmountOfTokenToBeSold / 2; // 10 token + uint256 public constant minAmountPerBuyer = maxAmountOfTokenToBeSold / 200; // 0.1 token + + function setUp() public { + list = new AllowList(); + Fees memory fees = Fees(1, 100, 1, 100, 1, 100, 100); + feeSettings = new FeeSettings(fees, wrongFeeReceiver, admin, wrongFeeReceiver); + + // create token + address tokenLogicContract = address(new Token(trustedForwarder)); + tokenCloneFactory = new TokenCloneFactory(tokenLogicContract); + token = Token( + tokenCloneFactory.createTokenClone(0, trustedForwarder, feeSettings, admin, list, 0x0, "TESTTOKEN", "TEST") + ); + + // set up currency + vm.prank(paymentTokenProvider); + paymentToken = new FakePaymentToken(paymentTokenAmount, paymentTokenDecimals); // 1000 tokens with 6 decimals + // transfer currency to buyer + vm.prank(paymentTokenProvider); + paymentToken.transfer(buyer, paymentTokenAmount); + assertTrue(paymentToken.balanceOf(buyer) == paymentTokenAmount); + + vm.prank(owner); + factory = new PublicFundraisingCloneFactory(address(new PublicFundraising(trustedForwarder))); + + raise = PublicFundraising( + factory.createPublicFundraisingClone( + 0, + trustedForwarder, + owner, + payable(receiver), + minAmountPerBuyer, + maxAmountPerBuyer, + price, + maxAmountOfTokenToBeSold, + paymentToken, + token + ) + ); + + // allow raise contract to mint + bytes32 roleMintAllower = token.MINTALLOWER_ROLE(); + + vm.prank(admin); + token.grantRole(roleMintAllower, mintAllower); + vm.prank(mintAllower); + token.increaseMintingAllowance(address(raise), maxAmountOfTokenToBeSold); + + // give raise contract allowance + vm.prank(buyer); + paymentToken.approve(address(raise), paymentTokenAmount); + } +} diff --git a/test/TokenSnapshots.t.sol b/test/TokenSnapshots.t.sol index d26acf6f..43e4a97c 100644 --- a/test/TokenSnapshots.t.sol +++ b/test/TokenSnapshots.t.sol @@ -98,6 +98,8 @@ contract tokenTest is Test { vm.assume(amount1 < (type(uint256).max - amount2) - 1000); vm.assume(rando1 != address(0)); vm.assume(rando2 != address(0)); + vm.assume(token.balanceOf(rando1) == 0); + vm.assume(token.balanceOf(rando2) == 0); uint256 snapshotId; diff --git a/test/resources/FakePaymentToken.sol b/test/resources/FakePaymentToken.sol index 0c4dc2eb..84751237 100644 --- a/test/resources/FakePaymentToken.sol +++ b/test/resources/FakePaymentToken.sol @@ -5,6 +5,12 @@ import "../../lib/forge-std/src/Test.sol"; import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol"; import "../../contracts/Token.sol"; +interface IERC677Receiver { + event TokenReceived(address from, uint256 amount, bytes data); + + function onTokenTransfer(address from, uint256 amount, bytes calldata data) external returns (bool success); +} + /* fake currency to test the main contract with */ @@ -27,4 +33,10 @@ contract FakePaymentToken is ERC20Permit { function mint(address _to, uint256 _amount) external { _mint(_to, _amount); } + + function transferAndCall(address receiver, uint amount, bytes calldata data) public returns (bool success) { + transfer(receiver, amount); + bool callSuccess = IERC677Receiver(receiver).onTokenTransfer(msg.sender, amount, data); + return callSuccess; + } }