From f244b4cc38543b67dbb00583502ccaab4cec6efe Mon Sep 17 00:00:00 2001 From: nour-karoui Date: Tue, 17 Oct 2023 11:49:41 +0100 Subject: [PATCH] update the crowdsale to accept fees and update tests --- .env.example | 6 +- foundry.toml | 2 +- script/DeployTokenizer.s.sol | 2 +- script/dev/CrowdSale.s.sol | 38 +++- script/dev/CrowdSaleWithFees.s.sol | 121 ------------ script/prod/RolloutV23Sale.sol | 2 +- setupLocal.sh | 7 +- src/crowdsale/CrowdSale.sol | 43 +++- src/crowdsale/CrowdSaleWithFees.sol | 65 ------ src/crowdsale/LockingCrowdSale.sol | 1 + src/crowdsale/StakedLockingCrowdSale.sol | 4 +- test/CrowdSale.t.sol | 80 +++++++- test/CrowdSaleFuzz.t.sol | 2 +- test/CrowdSaleLockedStakedTest.t.sol | 2 +- test/CrowdSaleLockedTest.t.sol | 2 +- test/CrowdSalePermissioned.t.sol | 2 +- test/CrowdSaleWithFees.t.sol | 196 +++++++++---------- test/CrowdSaleWithNonStandardERC20Test.t.sol | 2 +- test/SynthesizerUpgrade.t.sol | 2 +- test/helpers/CrowdSaleHelpers.sol | 16 ++ 20 files changed, 279 insertions(+), 316 deletions(-) delete mode 100644 script/dev/CrowdSaleWithFees.s.sol delete mode 100644 src/crowdsale/CrowdSaleWithFees.sol diff --git a/.env.example b/.env.example index aefff312..79772158 100644 --- a/.env.example +++ b/.env.example @@ -25,10 +25,10 @@ TERMS_ACCEPTED_PERMISSIONER_ADDRESS=0x610178dA211FEF7D417bC0e6FeD39F05609AD788 TOKENIZER_ADDRESS=0xA51c1fc2f0D1a1b8494Ed1FE312d7C3a78Ed91C0 STAKED_LOCKING_CROWDSALE_ADDRESS=0x9A676e781A523b5d0C0e43731313A708CB607508 -CROWDSALE_WITH_FEES_ADDRESS=0x4A679253410272dd5232B3Ff7cF5dbB88f295319 +CROWDSALE_WITH_FEES_ADDRESS=0x0B306BF915C4d645ff596e518fAf3F9669b97016 -USDC6_ADDRESS=0x9A9f2CCfdE556A7E9Ff0848998Aa4a0CFD8863AE -WETH_ADDRESS=0x59b670e9fA9D0A427751Af201D676719a970857b +USDC6_ADDRESS=0x68B1D87F95878fE05B998F19b66F4baba5De1aed +WETH_ADDRESS=0x4ed7c70F96B99c776995fB64377f0d4aB3B0e1C1 #these are generated when running the fixture scripts IPTS_ADDRESS=0x1F708C24a0D3A740cD47cC0444E9480899f3dA7D diff --git a/foundry.toml b/foundry.toml index 4f360277..f376ea14 100644 --- a/foundry.toml +++ b/foundry.toml @@ -6,7 +6,7 @@ test = 'test' cache_path = 'cache_forge' solc_version = "0.8.18" gas_reports = ["IPNFT", "IPNFTV2", "SchmackoSwap", "Tokenizer", "IPToken", "CrowdSale", "LockingCrowdSale", "StakedLockingCrowdSale", "TimelockedToken", "TermsAcceptedPermissioner", "SignedMintAuthorizer"] -fs_permissions = [{ access = "read-write", path = "./SALEID.txt"}] +fs_permissions = [{ access = "read-write", path = "./SALEID.txt"}, { access = "read-write", path = "./SALEWITHFEESID.txt"}] [fmt] bracket_spacing = true diff --git a/script/DeployTokenizer.s.sol b/script/DeployTokenizer.s.sol index d2f12104..3bbcab9a 100644 --- a/script/DeployTokenizer.s.sol +++ b/script/DeployTokenizer.s.sol @@ -26,7 +26,7 @@ contract DeployTokenizerInfrastructure is Script { ); tokenizer.initialize(IPNFT(ipnftAddress), p); - StakedLockingCrowdSale stakedLockingCrowdSale = new StakedLockingCrowdSale(); + StakedLockingCrowdSale stakedLockingCrowdSale = new StakedLockingCrowdSale(0); vm.stopBroadcast(); diff --git a/script/dev/CrowdSale.s.sol b/script/dev/CrowdSale.s.sol index 15441728..83f24586 100644 --- a/script/dev/CrowdSale.s.sol +++ b/script/dev/CrowdSale.s.sol @@ -28,7 +28,8 @@ contract DeployCrowdSale is CommonScript { function run() public { prepareAddresses(); vm.startBroadcast(deployer); - StakedLockingCrowdSale stakedLockingCrowdSale = new StakedLockingCrowdSale(); + StakedLockingCrowdSale stakedLockingCrowdSale = new StakedLockingCrowdSale(0); + CrowdSale crowdSaleWithFees = new CrowdSale(10); TokenVesting vestedDaoToken = TokenVesting(vm.envAddress("VDAO_TOKEN_ADDRESS")); vestedDaoToken.grantRole(vestedDaoToken.ROLE_CREATE_SCHEDULE(), address(stakedLockingCrowdSale)); stakedLockingCrowdSale.trustVestingContract(vestedDaoToken); @@ -36,6 +37,7 @@ contract DeployCrowdSale is CommonScript { //console.log("vested molecules Token %s", address(vestedMolToken)); console.log("STAKED_LOCKING_CROWDSALE_ADDRESS=%s", address(stakedLockingCrowdSale)); + console.log("CROWDSALE_WITH_FEES=%s", address(crowdSaleWithFees)); } } @@ -52,6 +54,7 @@ contract FixtureCrowdSale is CommonScript { IPToken internal auctionToken; StakedLockingCrowdSale stakedLockingCrowdSale; + CrowdSale crowdSaleWithFees; TermsAcceptedPermissioner permissioner; function prepareAddresses() internal override { @@ -64,6 +67,7 @@ contract FixtureCrowdSale is CommonScript { auctionToken = IPToken(vm.envAddress("IPTS_ADDRESS")); stakedLockingCrowdSale = StakedLockingCrowdSale(vm.envAddress("STAKED_LOCKING_CROWDSALE_ADDRESS")); + crowdSaleWithFees = CrowdSale(vm.envAddress("CROWDSALE_WITH_FEES_ADDRESS")); permissioner = TermsAcceptedPermissioner(vm.envAddress("TERMS_ACCEPTED_PERMISSIONER_ADDRESS")); } @@ -83,6 +87,14 @@ contract FixtureCrowdSale is CommonScript { vm.stopBroadcast(); } + function placeBidCrowdSaleWithFees(address bidder, uint256 amount, uint256 saleId, bytes memory permission) internal { + vm.startBroadcast(bidder); + usdc.approve(address(crowdSaleWithFees), amount); + daoToken.approve(address(crowdSaleWithFees), amount); + crowdSaleWithFees.placeBid(saleId, amount, permission); + vm.stopBroadcast(); + } + function run() public virtual { prepareAddresses(); @@ -109,6 +121,8 @@ contract FixtureCrowdSale is CommonScript { vm.startBroadcast(bob); auctionToken.approve(address(stakedLockingCrowdSale), 400 ether); + auctionToken.approve(address(crowdSaleWithFees), 400 ether); + uint256 saleWithFeesId = crowdSaleWithFees.startSale(_sale); uint256 saleId = stakedLockingCrowdSale.startSale(_sale, daoToken, vestedDaoToken, 1e18, 7 days); TimelockedToken lockedIpt = stakedLockingCrowdSale.lockingContracts(address(auctionToken)); vm.stopBroadcast(); @@ -117,10 +131,13 @@ contract FixtureCrowdSale is CommonScript { (uint8 v, bytes32 r, bytes32 s) = vm.sign(alicePk, ECDSA.toEthSignedMessageHash(abi.encodePacked(terms))); placeBid(alice, 600 ether, saleId, abi.encodePacked(r, s, v)); + placeBidCrowdSaleWithFees(alice, 600 ether, saleWithFeesId, abi.encodePacked(r, s, v)); (v, r, s) = vm.sign(charliePk, ECDSA.toEthSignedMessageHash(abi.encodePacked(terms))); placeBid(charlie, 200 ether, saleId, abi.encodePacked(r, s, v)); console.log("LOCKED_IPTS_ADDRESS=%s", address(lockedIpt)); console.log("SALE_ID=%s", saleId); + console.log("SALE_ID_WITH_FEES=%s", saleWithFeesId); + vm.writeFile("SALEWITHFEESID.txt", Strings.toString(saleWithFeesId)); vm.writeFile("SALEID.txt", Strings.toString(saleId)); } } @@ -130,13 +147,18 @@ contract ClaimSale is CommonScript { prepareAddresses(); TermsAcceptedPermissioner permissioner = TermsAcceptedPermissioner(vm.envAddress("TERMS_ACCEPTED_PERMISSIONER_ADDRESS")); StakedLockingCrowdSale stakedLockingCrowdSale = StakedLockingCrowdSale(vm.envAddress("STAKED_LOCKING_CROWDSALE_ADDRESS")); + CrowdSale crowdSaleWithFees = CrowdSale(vm.envAddress("CROWDSALE_WITH_FEES_ADDRESS")); IPToken auctionToken = IPToken(vm.envAddress("IPTS_ADDRESS")); uint256 saleId = SLib.stringToUint(vm.readFile("SALEID.txt")); + uint256 saleWithFeesId = SLib.stringToUint(vm.readFile("SALEWITHFEESID.txt")); vm.removeFile("SALEID.txt"); + vm.removeFile("SALEWITHFEESID.txt"); vm.startBroadcast(anyone); stakedLockingCrowdSale.settle(saleId); + crowdSaleWithFees.settle(saleWithFeesId); stakedLockingCrowdSale.claimResults(saleId); + crowdSaleWithFees.claimResults(saleWithFeesId); vm.stopBroadcast(); string memory terms = permissioner.specificTermsV1(auctionToken); @@ -153,3 +175,17 @@ contract ClaimSale is CommonScript { // vm.stopBroadcast(); } } + +contract ClaimSaleWithFees is CommonScript { + function run() public { + prepareAddresses(); + CrowdSale crowdSaleWithFees = CrowdSale(vm.envAddress("CROWDSALE_WITH_FEES_ADDRESS")); + uint256 saleId = SLib.stringToUint(vm.readFile("SALEID.txt")); + vm.removeFile("SALEID.txt"); + + vm.startBroadcast(anyone); + crowdSaleWithFees.settle(saleId); + crowdSaleWithFees.claimResults(saleId); + vm.stopBroadcast(); + } +} diff --git a/script/dev/CrowdSaleWithFees.s.sol b/script/dev/CrowdSaleWithFees.s.sol deleted file mode 100644 index 96261797..00000000 --- a/script/dev/CrowdSaleWithFees.s.sol +++ /dev/null @@ -1,121 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.18; - -import "forge-std/Test.sol"; -import "forge-std/console.sol"; -import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; -import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import { IERC20Metadata } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; -import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; -import { ECDSA } from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; - -import { TokenVesting } from "@moleculeprotocol/token-vesting/TokenVesting.sol"; -import { TimelockedToken } from "../../src/TimelockedToken.sol"; -import { IPermissioner, TermsAcceptedPermissioner } from "../../src/Permissioner.sol"; -import { CrowdSale, Sale, SaleInfo } from "../../src/crowdsale/CrowdSale.sol"; -import { CrowdSaleWithFees } from "../../src/crowdsale/CrowdSaleWithFees.sol"; -import { FakeERC20 } from "../../src/helpers/FakeERC20.sol"; -import { Strings as SLib } from "../../src/helpers/Strings.sol"; -import { IPToken } from "../../src/IPToken.sol"; - -import { CommonScript } from "./Common.sol"; - -/** - * @title deploy crowdSale - * @author - */ -contract DeployCrowdSale is CommonScript { - function run() public { - prepareAddresses(); - vm.startBroadcast(deployer); - CrowdSaleWithFees crowdSaleWithFees = new CrowdSaleWithFees(10); - vm.stopBroadcast(); - - console.log("CROWDSALE_WITH_FEES_ADDRESS=%s", address(crowdSaleWithFees)); - } -} - -/** - * @notice execute Ipnft.s.sol && Fixture.s.sol && Tokenizer.s.sol first - * @notice assumes that bob (hh1) owns IPNFT#1 and has synthesized it - */ -contract FixtureCrowdSale is CommonScript { - FakeERC20 internal usdc; - - FakeERC20 daoToken; - - IPToken internal auctionToken; - - CrowdSaleWithFees crowdSaleWithFees; - TermsAcceptedPermissioner permissioner; - - function prepareAddresses() internal override { - super.prepareAddresses(); - - usdc = FakeERC20(vm.envAddress("USDC_ADDRESS")); - - daoToken = FakeERC20(vm.envAddress("DAO_TOKEN_ADDRESS")); - auctionToken = IPToken(vm.envAddress("IPTS_ADDRESS")); - crowdSaleWithFees = CrowdSaleWithFees(vm.envAddress("CROWDSALE_WITH_FEES_ADDRESS")); - permissioner = TermsAcceptedPermissioner(vm.envAddress("TERMS_ACCEPTED_PERMISSIONER_ADDRESS")); - } - - function placeBid(address bidder, uint256 amount, uint256 saleId, bytes memory permission) internal { - vm.startBroadcast(bidder); - usdc.approve(address(crowdSaleWithFees), amount); - daoToken.approve(address(crowdSaleWithFees), amount); - crowdSaleWithFees.placeBid(saleId, amount, permission); - vm.stopBroadcast(); - } - - function run() public virtual { - prepareAddresses(); - - // Deal Charlie ERC20 tokens to bid in crowdsale - dealERC20(alice, 1200 ether, usdc); - dealERC20(charlie, 400 ether, usdc); - - // Deal Alice and Charlie DAO tokens to stake in crowdsale - dealERC20(alice, 1200 ether, daoToken); - dealERC20(charlie, 400 ether, daoToken); - - Sale memory _sale = Sale({ - auctionToken: IERC20Metadata(address(auctionToken)), - biddingToken: IERC20Metadata(address(usdc)), - beneficiary: bob, - fundingGoal: 200 ether, - salesAmount: 400 ether, - closingTime: uint64(block.timestamp + 15), - permissioner: permissioner - }); - - vm.startBroadcast(bob); - - auctionToken.approve(address(crowdSaleWithFees), 400 ether); - uint256 saleId = crowdSaleWithFees.startSale(_sale); - vm.stopBroadcast(); - - string memory terms = permissioner.specificTermsV1(auctionToken); - - (uint8 v, bytes32 r, bytes32 s) = vm.sign(alicePk, ECDSA.toEthSignedMessageHash(abi.encodePacked(terms))); - placeBid(alice, 600 ether, saleId, abi.encodePacked(r, s, v)); - (v, r, s) = vm.sign(charliePk, ECDSA.toEthSignedMessageHash(abi.encodePacked(terms))); - placeBid(charlie, 200 ether, saleId, abi.encodePacked(r, s, v)); - console.log("SALE_ID=%s", saleId); - vm.writeFile("SALEID.txt", Strings.toString(saleId)); - } -} - -contract ClaimSale is CommonScript { - function run() public { - prepareAddresses(); - CrowdSaleWithFees crowdSaleWithFees = CrowdSaleWithFees(vm.envAddress("CROWDSALE_WITH_FEES_ADDRESS")); - uint256 saleId = SLib.stringToUint(vm.readFile("SALEID.txt")); - vm.removeFile("SALEID.txt"); - - vm.startBroadcast(anyone); - crowdSaleWithFees.settle(saleId); - crowdSaleWithFees.claimResults(saleId); - vm.stopBroadcast(); - } -} diff --git a/script/prod/RolloutV23Sale.sol b/script/prod/RolloutV23Sale.sol index 8e4b4403..cf824f8b 100644 --- a/script/prod/RolloutV23Sale.sol +++ b/script/prod/RolloutV23Sale.sol @@ -13,7 +13,7 @@ contract RolloutV23Sale is Script { address moleculeDevMultisig = 0xCfA0F84660fB33bFd07C369E5491Ab02C449f71B; vm.startBroadcast(); - StakedLockingCrowdSale stakedLockingCrowdSale = new StakedLockingCrowdSale(); + StakedLockingCrowdSale stakedLockingCrowdSale = new StakedLockingCrowdSale(0); stakedLockingCrowdSale.transferOwnership(moleculeDevMultisig); vm.stopBroadcast(); diff --git a/setupLocal.sh b/setupLocal.sh index f6d0a176..f0af6567 100755 --- a/setupLocal.sh +++ b/setupLocal.sh @@ -31,7 +31,6 @@ $FSC script/dev/Periphery.s.sol $FSC script/dev/Tokenizer.s.sol:DeployTokenizer $FSC script/dev/CrowdSale.s.sol:DeployCrowdSale $FSC script/dev/Tokens.s.sol:DeployFakeTokens -$FSC script/dev/CrowdSaleWithFees.s.sol:DeployCrowdSale # optionally: fixtures if [ "$fixture" -eq "1" ]; then @@ -39,13 +38,11 @@ if [ "$fixture" -eq "1" ]; then $FSC script/dev/Ipnft.s.sol:FixtureIpnft $FSC script/dev/Tokenizer.s.sol:FixtureTokenizer - # $FSC script/dev/CrowdSale.s.sol:FixtureCrowdSale - $FSC script/dev/CrowdSaleWithFees.s.sol:FixtureCrowdSale + $FSC script/dev/CrowdSale.s.sol:FixtureCrowdSale echo "Waiting 15 seconds until claiming sale..." sleep 16 cast rpc evm_mine - # $FSC script/dev/CrowdSale.s.sol:ClaimSale - $FSC script/dev/CrowdSaleWithFees.s.sol:ClaimSale + $FSC script/dev/CrowdSale.s.sol:ClaimSale fi diff --git a/src/crowdsale/CrowdSale.sol b/src/crowdsale/CrowdSale.sol index 53f9c8ec..3800e727 100644 --- a/src/crowdsale/CrowdSale.sol +++ b/src/crowdsale/CrowdSale.sol @@ -7,6 +7,7 @@ import { ReentrancyGuard } from "@openzeppelin/contracts/security/ReentrancyGuar import { FixedPointMathLib } from "solmate/utils/FixedPointMathLib.sol"; import { IPermissioner } from "../Permissioner.sol"; import { IPToken } from "../IPToken.sol"; +import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; enum SaleState { UNKNOWN, @@ -34,6 +35,7 @@ struct SaleInfo { uint256 total; uint256 surplus; bool claimed; + uint16 percentageFee; } error BadDecimals(); @@ -47,13 +49,14 @@ error SaleNotFund(uint256); error SaleNotConcluded(); error BadSaleState(SaleState expected, SaleState actual); error AlreadyClaimed(); +error InsufficientFunds(); /** * @title CrowdSale * @author molecule.to * @notice a fixed price sales base contract */ -contract CrowdSale is ReentrancyGuard { +contract CrowdSale is ReentrancyGuard, Ownable { using SafeERC20 for IERC20Metadata; using FixedPointMathLib for uint256; @@ -62,7 +65,9 @@ contract CrowdSale is ReentrancyGuard { mapping(uint256 => mapping(address => uint256)) internal _contributions; - event Started(uint256 indexed saleId, address indexed issuer, Sale sale); + uint16 public percentageFee; + + event Started(uint256 indexed saleId, address indexed issuer, Sale sale, uint16 percentageFee); event Settled(uint256 indexed saleId, uint256 totalBids, uint256 surplus); /// @notice emitted when participants of the sale claim their tokens event Claimed(uint256 indexed saleId, address indexed claimer, uint256 claimed, uint256 refunded); @@ -75,6 +80,23 @@ contract CrowdSale is ReentrancyGuard { /// @notice emitted when sales owner / beneficiary claims `salesAmount` `auctionTokens` after a non successful sale event ClaimedAuctionTokens(uint256 indexed saleId); + /** + * @notice is called when we deploy this smart contract, + * we need to instantiate the initialFees percentage and the contract owner to send the fees to at the end of each successful auction + * @param _percentageFee A basis point (1/10_000) representing the percentageFee to cut to each auction + */ + constructor(uint16 _percentageFee) { + percentageFee = _percentageFee; + } + + /** + * @notice updates the currentPercentageFee that will be applied to auctions created in the future + * @param newFee the new feePercentage + */ + function updateCrowdSaleFees(uint16 newFee) public onlyOwner { + percentageFee = newFee; + } + /** * @notice bidding tokens can have arbitrary decimals, auctionTokens must be 18 decimals * if no beneficiary is provided, the beneficiary will be set to msg.sender @@ -103,7 +125,7 @@ contract CrowdSale is ReentrancyGuard { } _sales[saleId] = sale; - _saleInfo[saleId] = SaleInfo(SaleState.RUNNING, 0, 0, false); + _saleInfo[saleId] = SaleInfo(SaleState.RUNNING, 0, 0, false, percentageFee); sale.auctionToken.safeTransferFrom(msg.sender, address(this), sale.salesAmount); _afterSaleStarted(saleId); @@ -197,10 +219,21 @@ contract CrowdSale is ReentrancyGuard { saleInfo.claimed = true; Sale storage sale = _sales[saleId]; + uint256 claimableAmount = sale.fundingGoal; if (saleInfo.state == SaleState.SETTLED) { + if (saleInfo.percentageFee != 0) { + // this condition can never be met as the lowest fundingGoal is 10000 and the lowest percentageFee is 1 + if (saleInfo.percentageFee * sale.fundingGoal < 10000) { + revert InsufficientFunds(); + } + uint256 saleFees = (saleInfo.percentageFee * sale.fundingGoal) / 10000; + claimableAmount -= saleFees; + sale.biddingToken.safeTransfer(owner(), saleFees); + } + //transfer funds to issuer / beneficiary emit ClaimedFundingGoal(saleId); - sale.biddingToken.safeTransfer(sale.beneficiary, sale.fundingGoal); + sale.biddingToken.safeTransfer(sale.beneficiary, claimableAmount); } else if (saleInfo.state == SaleState.FAILED) { //return auction tokens emit ClaimedAuctionTokens(saleId); @@ -319,6 +352,6 @@ contract CrowdSale is ReentrancyGuard { * @dev allows us to emit different events per derived contract */ function _afterSaleStarted(uint256 saleId) internal virtual { - emit Started(saleId, msg.sender, _sales[saleId]); + emit Started(saleId, msg.sender, _sales[saleId], _saleInfo[saleId].percentageFee); } } diff --git a/src/crowdsale/CrowdSaleWithFees.sol b/src/crowdsale/CrowdSaleWithFees.sol deleted file mode 100644 index 1966dbc5..00000000 --- a/src/crowdsale/CrowdSaleWithFees.sol +++ /dev/null @@ -1,65 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.18; - -import { IERC20Metadata } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; -import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import { CrowdSale, Sale, SaleInfo, AlreadyClaimed, SaleState, BadSaleState } from "./CrowdSale.sol"; -import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; - -/** - * @title CrowdSaleWithFees - * @author molecule.to - * @notice a plain crowdsale that takes 0.5% fees of the funding goal upon sale settlement - */ -contract CrowdSaleWithFees is CrowdSale, Ownable { - using SafeERC20 for IERC20Metadata; - - mapping(uint256 => uint256) crowdSaleFees; - uint256 public feesPercentage; - - event Started(uint256 indexed saleId, address indexed issuer, Sale sale, uint256 feesPercentage); - /** - * is called when we deploy this smart contract, - * we need to instantiate the initialFees percentage and the contract owner to send the fees to at the end of each successful auction - * @param _feesPercentage the percentage of fees to cut of each auction - */ - - constructor(uint256 _feesPercentage) { - feesPercentage = _feesPercentage; - } - - function getFees() public view returns (uint256) { - return feesPercentage; - } - - function getCrowdSaleFees(uint256 saleId) public view returns (uint256) { - return crowdSaleFees[saleId]; - } - - function updateCrowdSaleFees(uint256 newFee) public onlyOwner { - feesPercentage = newFee; - } - - /** - * @notice will instantiate a new crowdsale with fees when none exists yet - * - * @param sale sale configuration - * @return saleId the newly created sale's id - */ - function startSale(Sale calldata sale) public override returns (uint256 saleId) { - saleId = super.startSale(sale); - crowdSaleFees[saleId] = feesPercentage; - } - - function _afterSaleStarted(uint256 saleId) internal override { - emit Started(saleId, msg.sender, _sales[saleId], crowdSaleFees[saleId]); - } - - function settle(uint256 saleId) public override { - Sale storage sale = _sales[saleId]; - super.settle(saleId); - uint256 saleFees = sale.fundingGoal * feesPercentage / 1000; - sale.fundingGoal = sale.fundingGoal - saleFees; - sale.biddingToken.safeTransfer(owner(), saleFees); - } -} diff --git a/src/crowdsale/LockingCrowdSale.sol b/src/crowdsale/LockingCrowdSale.sol index 6e335277..80f3c82d 100644 --- a/src/crowdsale/LockingCrowdSale.sol +++ b/src/crowdsale/LockingCrowdSale.sol @@ -30,6 +30,7 @@ contract LockingCrowdSale is CrowdSale { event Started(uint256 indexed saleId, address indexed issuer, Sale sale, TimelockedToken lockingToken, uint256 lockingDuration); event LockingContractCreated(TimelockedToken indexed lockingContract, IERC20Metadata indexed underlyingToken); + constructor(uint16 percentageFee) CrowdSale(percentageFee) { } /// @dev disable parent sale starting functions function startSale(Sale calldata) public pure override returns (uint256) { revert UnsupportedInitializer(); diff --git a/src/crowdsale/StakedLockingCrowdSale.sol b/src/crowdsale/StakedLockingCrowdSale.sol index 13c91e1f..ca6c566c 100644 --- a/src/crowdsale/StakedLockingCrowdSale.sol +++ b/src/crowdsale/StakedLockingCrowdSale.sol @@ -30,7 +30,7 @@ error BadPrice(); * @notice a fixed price sales contract that locks the sold tokens in a configured locking contract and requires vesting another ("dao") token for a certain period of time to participate * @dev see https://github.com/moleculeprotocol/IPNFT */ -contract StakedLockingCrowdSale is LockingCrowdSale, Ownable { +contract StakedLockingCrowdSale is LockingCrowdSale { using SafeERC20 for IERC20Metadata; using FixedPointMathLib for uint256; @@ -56,7 +56,7 @@ contract StakedLockingCrowdSale is LockingCrowdSale, Ownable { revert UnsupportedInitializer(); } - constructor() Ownable() { } + constructor(uint16 percentageFee) LockingCrowdSale(percentageFee) { } /** * [H-01] diff --git a/test/CrowdSale.t.sol b/test/CrowdSale.t.sol index 19de98b7..86664d8f 100644 --- a/test/CrowdSale.t.sol +++ b/test/CrowdSale.t.sol @@ -18,7 +18,8 @@ import { BidTooLow, SaleNotFund, SaleNotConcluded, - BadSaleState + BadSaleState, + InsufficientFunds } from "../src/crowdsale/CrowdSale.sol"; import { IPermissioner } from "../src/Permissioner.sol"; import { FakeERC20 } from "../src/helpers/FakeERC20.sol"; @@ -26,26 +27,39 @@ import { CrowdSaleHelpers } from "./helpers/CrowdSaleHelpers.sol"; contract CrowdSaleTest is Test { address emitter = makeAddr("emitter"); + address highFeesCrowdSaleEmitter = makeAddr("highFeesEmitter"); address bidder = makeAddr("bidder"); address bidder2 = makeAddr("bidder2"); address anyone = makeAddr("anyone"); + address crowdSalesOwner = makeAddr("crowdSalesOwner"); + uint16 percentageFees = 10; FakeERC20 internal auctionToken; FakeERC20 internal biddingToken; + FakeERC20 internal biddingToken6Decimals; CrowdSale internal crowdSale; + CrowdSale internal lowFeesCrowdSale; function setUp() public { - crowdSale = new CrowdSale(); + vm.startPrank(crowdSalesOwner); + crowdSale = new CrowdSale(percentageFees); + lowFeesCrowdSale = new CrowdSale(1); + vm.stopPrank(); auctionToken = new FakeERC20("IPTOKENS","IPT"); biddingToken = new FakeERC20("USD token", "USDC"); + biddingToken6Decimals = new FakeERC20("6DECIMALS", "6DCLS"); + biddingToken6Decimals.setDecimals(6); auctionToken.mint(emitter, 500_000 ether); + auctionToken.mint(highFeesCrowdSaleEmitter, 500_000 ether); biddingToken.mint(bidder, 1_000_000 ether); + biddingToken6Decimals.mint(bidder, 1_000_000 ether); biddingToken.mint(bidder2, 1_000_000 ether); vm.startPrank(bidder); biddingToken.approve(address(crowdSale), 1_000_000 ether); + biddingToken6Decimals.approve(address(lowFeesCrowdSale), 1_000_000 ether); vm.stopPrank(); vm.startPrank(bidder2); @@ -53,12 +67,17 @@ contract CrowdSaleTest is Test { vm.stopPrank(); } + function testSetUp() public { + assertEq(crowdSale.percentageFee(), 10); + assertEq(crowdSale.owner(), crowdSalesOwner); + } + function testCreateSale() public { Sale memory _sale = CrowdSaleHelpers.makeSale(emitter, auctionToken, biddingToken); vm.startPrank(emitter); auctionToken.approve(address(crowdSale), 400_000 ether); - crowdSale.startSale(_sale); + uint256 saleId = crowdSale.startSale(_sale); vm.stopPrank(); //cant create the same sale twice @@ -67,6 +86,7 @@ contract CrowdSaleTest is Test { auctionToken.approve(address(crowdSale), 400_000 ether); vm.expectRevert(SaleAlreadyActive.selector); crowdSale.startSale(_sale); + assertEq(crowdSale.getSaleInfo(saleId).percentageFee, 10); vm.stopPrank(); } @@ -199,7 +219,9 @@ contract CrowdSaleTest is Test { vm.expectRevert(AlreadyClaimed.selector); crowdSale.claimResults(saleId); vm.stopPrank(); - assertEq(biddingToken.balanceOf(emitter), _sale.fundingGoal); + SaleInfo memory saleInfo = crowdSale.getSaleInfo(saleId); + assertEq(biddingToken.balanceOf(emitter), _sale.fundingGoal - (saleInfo.percentageFee * _sale.fundingGoal / 10000)); + assertEq(biddingToken.balanceOf(crowdSalesOwner), (saleInfo.percentageFee * _sale.fundingGoal / 10000)); SaleInfo memory info = crowdSale.getSaleInfo(saleId); assertEq(info.surplus, 0); @@ -243,7 +265,51 @@ contract CrowdSaleTest is Test { assertEq(auctionToken.balanceOf(bidder), 0); } - //todo test bidders that bit 1 wei + // This test is for insufficientFunds error which can never be reached as fundingGoal * percentageFee is always higher or equal to 10000 + // function testInsufficientFundsSettleRevertCrowdSale() public { + // vm.startPrank(highFeesCrowdSaleEmitter); + // Sale memory _sale = CrowdSaleHelpers.makeLowFundingGoalSale(highFeesCrowdSaleEmitter, auctionToken, biddingToken6Decimals); + // auctionToken.approve(address(lowFeesCrowdSale), 400_000 ether); + // uint256 saleId = lowFeesCrowdSale.startSale(_sale); + // vm.stopPrank(); + + // vm.startPrank(bidder); + // lowFeesCrowdSale.placeBid(saleId, 400_000 ether, ""); + // vm.stopPrank(); + + // vm.startPrank(anyone); + // vm.warp(block.timestamp + 3 hours); + // lowFeesCrowdSale.settle(saleId); + // vm.expectRevert(abi.encodeWithSelector(InsufficientFunds.selector)); + // lowFeesCrowdSale.claimResults(saleId); + // vm.stopPrank(); + + // // assertEq(biddingToken.balanceOf(emitter), 0); + // // assertEq(auctionToken.balanceOf(emitter), 500_000 ether); + // // SaleInfo memory info = crowdSale.getSaleInfo(saleId); + // // assertEq(info.surplus, 0); + // // assertEq(uint256(info.state), uint256(SaleState.FAILED)); + // } + + function testUpdatePercentage() public { + vm.startPrank(emitter); + Sale memory _sale = CrowdSaleHelpers.makeSale(emitter, auctionToken, biddingToken); + auctionToken.approve(address(crowdSale), 400_000 ether); + uint256 saleId = crowdSale.startSale(_sale); + vm.stopPrank(); + vm.startPrank(anyone); + vm.expectRevert("Ownable: caller is not the owner"); + crowdSale.updateCrowdSaleFees(25); + + vm.stopPrank(); + vm.startPrank(crowdSalesOwner); + crowdSale.updateCrowdSaleFees(25); + assertEq(crowdSale.percentageFee(), 25); + assertEq(crowdSale.getSaleInfo(saleId).percentageFee, 10); + vm.stopPrank(); + } + + // //todo test bidders that bit 1 wei function testTwoBiddersMeetExactly() public { vm.startPrank(emitter); @@ -445,8 +511,8 @@ contract CrowdSaleTest is Test { assertEq(biddingToken.balanceOf(bidder2), 915094339622641508720000); crowdSale.claimResults(saleId); - - assertEq(biddingToken.balanceOf(emitter), 200_000 ether); + SaleInfo memory saleInfo = crowdSale.getSaleInfo(saleId); + assertEq(biddingToken.balanceOf(emitter), 200_000 ether - (saleInfo.percentageFee * _sale.fundingGoal / 10000)); //some dust is left on the table //these are 0.0000000000004 tokens at 18 decimals diff --git a/test/CrowdSaleFuzz.t.sol b/test/CrowdSaleFuzz.t.sol index e866da36..f78d0cc0 100644 --- a/test/CrowdSaleFuzz.t.sol +++ b/test/CrowdSaleFuzz.t.sol @@ -21,7 +21,7 @@ contract CrowdSaleFuzzTest is Test { CrowdSale internal crowdSale; function setUp() public { - crowdSale = new CrowdSale(); + crowdSale = new CrowdSale(0); auctionToken = new FakeERC20("IPTOKENS","IPT"); biddingToken = new FakeERC20("USD token", "USDC"); } diff --git a/test/CrowdSaleLockedStakedTest.t.sol b/test/CrowdSaleLockedStakedTest.t.sol index 250128c8..a67cfd8b 100644 --- a/test/CrowdSaleLockedStakedTest.t.sol +++ b/test/CrowdSaleLockedStakedTest.t.sol @@ -55,7 +55,7 @@ contract CrowdSaleLockedStakedTest is Test { // // 1=1 is the trivial case // priceFeed.signal(address(biddingToken), address(daoToken), 1e18); - crowdSale = new StakedLockingCrowdSale(); + crowdSale = new StakedLockingCrowdSale(0); vestedDao = new TokenVesting( daoToken, diff --git a/test/CrowdSaleLockedTest.t.sol b/test/CrowdSaleLockedTest.t.sol index 0207ea9f..126c9cae 100644 --- a/test/CrowdSaleLockedTest.t.sol +++ b/test/CrowdSaleLockedTest.t.sol @@ -25,7 +25,7 @@ contract CrowdSaleLockedTest is Test { LockingCrowdSale internal crowdSale; function setUp() public { - crowdSale = new LockingCrowdSale(); + crowdSale = new LockingCrowdSale(0); auctionToken = new FakeERC20("IPTOKENS","IPT"); biddingToken = new FakeERC20("USD token", "USDC"); diff --git a/test/CrowdSalePermissioned.t.sol b/test/CrowdSalePermissioned.t.sol index df884f0a..4b1297d2 100644 --- a/test/CrowdSalePermissioned.t.sol +++ b/test/CrowdSalePermissioned.t.sol @@ -49,7 +49,7 @@ contract CrowdSalePermissionedTest is Test { biddingToken = new FakeERC20("USD token", "USDC"); daoToken = new FakeERC20("DAO token", "DAO"); - crowdSale = new StakedLockingCrowdSale(); + crowdSale = new StakedLockingCrowdSale(0); auctionToken.issue(emitter, 500_000 ether); vestedDao = new TokenVesting( diff --git a/test/CrowdSaleWithFees.t.sol b/test/CrowdSaleWithFees.t.sol index 4262c0b0..88e2f7e1 100644 --- a/test/CrowdSaleWithFees.t.sol +++ b/test/CrowdSaleWithFees.t.sol @@ -1,98 +1,98 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.18; - -import "forge-std/Test.sol"; -import { CrowdSaleWithFees } from "../src/crowdsale/CrowdSaleWithFees.sol"; -import { Sale, SaleInfo } from "../src/crowdsale/CrowdSale.sol"; -import { FakeERC20 } from "../src/helpers/FakeERC20.sol"; -import { CrowdSaleHelpers } from "./helpers/CrowdSaleHelpers.sol"; - -contract CrowdSaleWithFeesTest is Test { - CrowdSaleWithFees crowdSaleWithFees; - FakeERC20 internal auctionToken; - FakeERC20 internal biddingToken; - uint256 percentageFees = 10; - - address emitter = makeAddr("emitter"); - address bidder = makeAddr("bidder"); - address anyone = makeAddr("anyone"); - address crowdSalesOwner = makeAddr("crowdSalesOwner"); - - // TEST HAPPY PATHS NOW - - function setUp() public { - vm.startPrank(crowdSalesOwner); - crowdSaleWithFees = new CrowdSaleWithFees(percentageFees); - vm.stopPrank(); - auctionToken = new FakeERC20("IPTOKENS","IPT"); - biddingToken = new FakeERC20("USD token", "USDC"); - - auctionToken.mint(emitter, 500_000 ether); - biddingToken.mint(bidder, 1_000_000 ether); - - vm.startPrank(bidder); - biddingToken.approve(address(crowdSaleWithFees), 1_000_000 ether); - vm.stopPrank(); - } - - function testSetUp() public { - assertEq(crowdSaleWithFees.getFees(), 10); - assertEq(crowdSaleWithFees.owner(), crowdSalesOwner); - } - - function testStartSale() public { - Sale memory _sale = CrowdSaleHelpers.makeSale(emitter, auctionToken, biddingToken); - vm.startPrank(emitter); - auctionToken.approve(address(crowdSaleWithFees), 400_000 ether); - uint256 saleId = crowdSaleWithFees.startSale(_sale); - assertEq(crowdSaleWithFees.getCrowdSaleFees(saleId), 10); - vm.stopPrank(); - } - - function testUpdateFees() public { - Sale memory _sale = CrowdSaleHelpers.makeSale(emitter, auctionToken, biddingToken); - vm.startPrank(emitter); - auctionToken.approve(address(crowdSaleWithFees), 400_000 ether); - uint256 saleId = crowdSaleWithFees.startSale(_sale); - assertEq(crowdSaleWithFees.getCrowdSaleFees(saleId), 10); - vm.stopPrank(); - vm.startPrank(anyone); - vm.expectRevert("Ownable: caller is not the owner"); - crowdSaleWithFees.updateCrowdSaleFees(20); - vm.stopPrank(); - - vm.startPrank(crowdSalesOwner); - crowdSaleWithFees.updateCrowdSaleFees(20); - vm.stopPrank(); - - assertEq(crowdSaleWithFees.getCrowdSaleFees(saleId), 10); - assertEq(crowdSaleWithFees.getFees(), 20); - } - - function testClaim() public { - vm.startPrank(emitter); - Sale memory _sale = CrowdSaleHelpers.makeSale(emitter, auctionToken, biddingToken); - assertEq(_sale.beneficiary, emitter); - auctionToken.approve(address(crowdSaleWithFees), 400_000 ether); - uint256 saleId = crowdSaleWithFees.startSale(_sale); - vm.stopPrank(); - - vm.startPrank(bidder); - crowdSaleWithFees.placeBid(saleId, 200_000 ether, ""); - vm.stopPrank(); - - vm.warp(block.timestamp + 2 hours + 1); - - vm.startPrank(anyone); - crowdSaleWithFees.settle(saleId); - vm.stopPrank(); - - assertEq(biddingToken.balanceOf(emitter), 0); - vm.startPrank(anyone); - crowdSaleWithFees.claimResults(saleId); - vm.stopPrank(); - - assertEq(biddingToken.balanceOf(emitter), _sale.fundingGoal - _sale.fundingGoal * crowdSaleWithFees.getCrowdSaleFees(saleId) / 1000); - assertEq(biddingToken.balanceOf(crowdSalesOwner), _sale.fundingGoal * crowdSaleWithFees.getCrowdSaleFees(saleId) / 1000); - } -} +// // SPDX-License-Identifier: MIT +// pragma solidity ^0.8.18; + +// import "forge-std/Test.sol"; +// import { CrowdSaleWithFees } from "../src/crowdsale/CrowdSaleWithFees.sol"; +// import { Sale, SaleInfo } from "../src/crowdsale/CrowdSale.sol"; +// import { FakeERC20 } from "../src/helpers/FakeERC20.sol"; +// import { CrowdSaleHelpers } from "./helpers/CrowdSaleHelpers.sol"; + +// contract CrowdSaleWithFeesTest is Test { +// CrowdSaleWithFees crowdSaleWithFees; +// FakeERC20 internal auctionToken; +// FakeERC20 internal biddingToken; +// uint256 percentageFees = 10; + +// address emitter = makeAddr("emitter"); +// address bidder = makeAddr("bidder"); +// address anyone = makeAddr("anyone"); +// address crowdSalesOwner = makeAddr("crowdSalesOwner"); + +// // TEST HAPPY PATHS NOW + +// function setUp() public { +// vm.startPrank(crowdSalesOwner); +// crowdSaleWithFees = new CrowdSaleWithFees(percentageFees); +// vm.stopPrank(); +// auctionToken = new FakeERC20("IPTOKENS","IPT"); +// biddingToken = new FakeERC20("USD token", "USDC"); + +// auctionToken.mint(emitter, 500_000 ether); +// biddingToken.mint(bidder, 1_000_000 ether); + +// vm.startPrank(bidder); +// biddingToken.approve(address(crowdSaleWithFees), 1_000_000 ether); +// vm.stopPrank(); +// } + +// function testSetUp() public { +// assertEq(crowdSaleWithFees.getFees(), 10); +// assertEq(crowdSaleWithFees.owner(), crowdSalesOwner); +// } + +// function testStartSale() public { +// Sale memory _sale = CrowdSaleHelpers.makeSale(emitter, auctionToken, biddingToken); +// vm.startPrank(emitter); +// auctionToken.approve(address(crowdSaleWithFees), 400_000 ether); +// uint256 saleId = crowdSaleWithFees.startSale(_sale); +// assertEq(crowdSaleWithFees.getCrowdSaleFees(saleId), 10); +// vm.stopPrank(); +// } + +// function testUpdateFees() public { +// Sale memory _sale = CrowdSaleHelpers.makeSale(emitter, auctionToken, biddingToken); +// vm.startPrank(emitter); +// auctionToken.approve(address(crowdSaleWithFees), 400_000 ether); +// uint256 saleId = crowdSaleWithFees.startSale(_sale); +// assertEq(crowdSaleWithFees.getCrowdSaleFees(saleId), 10); +// vm.stopPrank(); +// vm.startPrank(anyone); +// vm.expectRevert("Ownable: caller is not the owner"); +// crowdSaleWithFees.updateCrowdSaleFees(20); +// vm.stopPrank(); + +// vm.startPrank(crowdSalesOwner); +// crowdSaleWithFees.updateCrowdSaleFees(20); +// vm.stopPrank(); + +// assertEq(crowdSaleWithFees.getCrowdSaleFees(saleId), 10); +// assertEq(crowdSaleWithFees.getFees(), 20); +// } + +// function testClaim() public { +// vm.startPrank(emitter); +// Sale memory _sale = CrowdSaleHelpers.makeSale(emitter, auctionToken, biddingToken); +// assertEq(_sale.beneficiary, emitter); +// auctionToken.approve(address(crowdSaleWithFees), 400_000 ether); +// uint256 saleId = crowdSaleWithFees.startSale(_sale); +// vm.stopPrank(); + +// vm.startPrank(bidder); +// crowdSaleWithFees.placeBid(saleId, 200_000 ether, ""); +// vm.stopPrank(); + +// vm.warp(block.timestamp + 2 hours + 1); + +// vm.startPrank(anyone); +// crowdSaleWithFees.settle(saleId); +// vm.stopPrank(); + +// assertEq(biddingToken.balanceOf(emitter), 0); +// vm.startPrank(anyone); +// crowdSaleWithFees.claimResults(saleId); +// vm.stopPrank(); + +// assertEq(biddingToken.balanceOf(emitter), _sale.fundingGoal - _sale.fundingGoal * crowdSaleWithFees.getCrowdSaleFees(saleId) / 1000); +// assertEq(biddingToken.balanceOf(crowdSalesOwner), _sale.fundingGoal * crowdSaleWithFees.getCrowdSaleFees(saleId) / 1000); +// } +// } diff --git a/test/CrowdSaleWithNonStandardERC20Test.t.sol b/test/CrowdSaleWithNonStandardERC20Test.t.sol index 0d6873dd..cc5833a6 100644 --- a/test/CrowdSaleWithNonStandardERC20Test.t.sol +++ b/test/CrowdSaleWithNonStandardERC20Test.t.sol @@ -50,7 +50,7 @@ contract CrowdSaleWithNonStandardERC20Test is Test { // // 1=1 is the trivial case // priceFeed.signal(address(biddingToken), address(daoToken), 1e18); - crowdSale = new StakedLockingCrowdSale(); + crowdSale = new StakedLockingCrowdSale(0); auctionToken.mint(emitter, 500_000 ether); diff --git a/test/SynthesizerUpgrade.t.sol b/test/SynthesizerUpgrade.t.sol index 65a6d30b..6dca0ec3 100644 --- a/test/SynthesizerUpgrade.t.sol +++ b/test/SynthesizerUpgrade.t.sol @@ -181,7 +181,7 @@ contract SynthesizerUpgradeTest is Test { Molecules tokenContractOld = synthesizer.synthesizeIpnft(1, 500_000 ether, "MOLE", agreementCid, xsignature); - LockingCrowdSale crowdSale = new LockingCrowdSale(); + LockingCrowdSale crowdSale = new LockingCrowdSale(0); Sale memory _sale = CrowdSaleHelpers.makeSale(originalOwner, IERC20Metadata(address(tokenContractOld)), erc20); //todo: in reality the sale has been initialized with the old interface that used the `Molecule` type _sale.permissioner = IPermissioner(address(oldTermsPermissioner)); diff --git a/test/helpers/CrowdSaleHelpers.sol b/test/helpers/CrowdSaleHelpers.sol index 3678cd2d..72cddf72 100644 --- a/test/helpers/CrowdSaleHelpers.sol +++ b/test/helpers/CrowdSaleHelpers.sol @@ -19,4 +19,20 @@ library CrowdSaleHelpers { permissioner: IPermissioner(address(0x0)) }); } + + function makeLowFundingGoalSale(address beneficiary, IERC20Metadata auctionToken, IERC20Metadata biddingToken) + internal + view + returns (Sale memory sale) + { + return Sale({ + auctionToken: auctionToken, + biddingToken: biddingToken, + beneficiary: beneficiary, + fundingGoal: 0.00000000000001 ether, + salesAmount: 400_000 ether, + closingTime: uint64(block.timestamp + 2 hours), + permissioner: IPermissioner(address(0x0)) + }); + } }