diff --git a/.env.example b/.env.example index 46a29ec6..d1622223 100644 --- a/.env.example +++ b/.env.example @@ -25,10 +25,11 @@ TERMS_ACCEPTED_PERMISSIONER_ADDRESS=0x610178dA211FEF7D417bC0e6FeD39F05609AD788 TOKENIZER_ADDRESS=0xA51c1fc2f0D1a1b8494Ed1FE312d7C3a78Ed91C0 STAKED_LOCKING_CROWDSALE_ADDRESS=0x9A676e781A523b5d0C0e43731313A708CB607508 -CROWDSALE_WITH_FEES_ADDRESS=0x0B306BF915C4d645ff596e518fAf3F9669b97016 -USDC6_ADDRESS=0x68B1D87F95878fE05B998F19b66F4baba5De1aed -WETH_ADDRESS=0x4ed7c70F96B99c776995fB64377f0d4aB3B0e1C1 +USDC6_ADDRESS=0x9A9f2CCfdE556A7E9Ff0848998Aa4a0CFD8863AE +WETH_ADDRESS=0x59b670e9fA9D0A427751Af201D676719a970857b + +PLAIN_CROWDSALE_ADDRESS=0x4A679253410272dd5232B3Ff7cF5dbB88f295319 #these are generated when running the fixture scripts IPTS_ADDRESS=0x1F708C24a0D3A740cD47cC0444E9480899f3dA7D diff --git a/script/DeployTokenizer.s.sol b/script/DeployTokenizer.s.sol index 3bbcab9a..d2f12104 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(0); + StakedLockingCrowdSale stakedLockingCrowdSale = new StakedLockingCrowdSale(); vm.stopBroadcast(); diff --git a/script/dev/CrowdSale.s.sol b/script/dev/CrowdSale.s.sol index 8e7fa4ff..b1344ea2 100644 --- a/script/dev/CrowdSale.s.sol +++ b/script/dev/CrowdSale.s.sol @@ -20,152 +20,172 @@ import { IPToken } from "../../src/IPToken.sol"; import { CommonScript } from "./Common.sol"; +contract DeployCrowdSale is CommonScript { + function run() public { + prepareAddresses(); + vm.startBroadcast(deployer); + CrowdSale crowdSale = new CrowdSale(); + crowdSale.setCurrentFeesBp(1000); + + console.log("PLAIN_CROWDSALE_ADDRESS=%s", address(crowdSale)); + } +} + /** * @title deploy crowdSale * @author */ -contract DeployCrowdSale is CommonScript { +contract DeployStakedCrowdSale is CommonScript { function run() public { prepareAddresses(); vm.startBroadcast(deployer); - StakedLockingCrowdSale stakedLockingCrowdSale = new StakedLockingCrowdSale(0); - CrowdSale crowdSaleWithFees = new CrowdSale(10); + StakedLockingCrowdSale stakedLockingCrowdSale = new StakedLockingCrowdSale(); + TokenVesting vestedDaoToken = TokenVesting(vm.envAddress("VDAO_TOKEN_ADDRESS")); vestedDaoToken.grantRole(vestedDaoToken.ROLE_CREATE_SCHEDULE(), address(stakedLockingCrowdSale)); stakedLockingCrowdSale.trustVestingContract(vestedDaoToken); vm.stopBroadcast(); - //console.log("vested molecules Token %s", address(vestedMolToken)); console.log("STAKED_LOCKING_CROWDSALE_ADDRESS=%s", address(stakedLockingCrowdSale)); - 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; - TokenVesting vestedDaoToken; IPToken internal auctionToken; - StakedLockingCrowdSale stakedLockingCrowdSale; - CrowdSale crowdSaleWithFees; + CrowdSale crowdSale; TermsAcceptedPermissioner permissioner; - function prepareAddresses() internal override { + function prepareAddresses() internal virtual override { super.prepareAddresses(); usdc = FakeERC20(vm.envAddress("USDC_ADDRESS")); daoToken = FakeERC20(vm.envAddress("DAO_TOKEN_ADDRESS")); - vestedDaoToken = TokenVesting(vm.envAddress("VDAO_TOKEN_ADDRESS")); - 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")); - } - - function setupVestedMolToken() internal { - vm.startBroadcast(deployer); auctionToken = IPToken(vm.envAddress("IPTS_ADDRESS")); - vestedDaoToken.grantRole(vestedDaoToken.ROLE_CREATE_SCHEDULE(), address(stakedLockingCrowdSale)); - vm.stopBroadcast(); + crowdSale = CrowdSale(vm.envAddress("PLAIN_CROWDSALE_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(stakedLockingCrowdSale), amount); - daoToken.approve(address(stakedLockingCrowdSale), amount); - stakedLockingCrowdSale.placeBid(saleId, amount, permission); + usdc.approve(address(crowdSale), amount); + daoToken.approve(address(crowdSale), amount); + crowdSale.placeBid(saleId, amount, permission); 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(); - - setupVestedMolToken(); - + function prepareRun() internal virtual returns (Sale memory _sale) { // 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({ + _sale = Sale({ auctionToken: IERC20Metadata(address(auctionToken)), biddingToken: IERC20Metadata(address(usdc)), beneficiary: bob, fundingGoal: 200 ether, salesAmount: 400 ether, - closingTime: uint64(block.timestamp + 15), + closingTime: uint64(block.timestamp + 10), permissioner: permissioner }); vm.startBroadcast(bob); + auctionToken.approve(address(crowdSale), 400 ether); + vm.stopBroadcast(); + } - 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)); + function startSale() internal virtual returns (uint256 saleId) { + Sale memory _sale = prepareRun(); + vm.startBroadcast(bob); + saleId = crowdSale.startSale(_sale); vm.stopBroadcast(); + } + + function afterRun(uint256 saleId) internal virtual { + console.log("SALE_ID=%s", saleId); + vm.writeFile("SALEID.txt", Strings.toString(saleId)); + } + + function run() public virtual { + prepareAddresses(); + + uint256 saleId = startSale(); 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)); - 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)); + + afterRun(saleId); + } +} +/** + * @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 FixtureStakedCrowdSale is FixtureCrowdSale { + StakedLockingCrowdSale _slCrowdSale; + TokenVesting vestedDaoToken; + + function prepareAddresses() internal override { + super.prepareAddresses(); + vestedDaoToken = TokenVesting(vm.envAddress("VDAO_TOKEN_ADDRESS")); + + _slCrowdSale = StakedLockingCrowdSale(vm.envAddress("STAKED_LOCKING_CROWDSALE_ADDRESS")); + crowdSale = _slCrowdSale; + } + + function prepareRun() internal virtual override returns (Sale memory _sale) { + _sale = super.prepareRun(); + dealERC20(alice, 1200 ether, daoToken); + dealERC20(charlie, 400 ether, daoToken); + } + + function startSale() internal override returns (uint256 saleId) { + Sale memory _sale = prepareRun(); + vm.startBroadcast(bob); + saleId = _slCrowdSale.startSale(_sale, daoToken, vestedDaoToken, 1e18, 7 days); + vm.stopBroadcast(); + } + + function afterRun(uint256 saleId) internal virtual override { + super.afterRun(saleId); + + TimelockedToken lockedIpt = _slCrowdSale.lockingContracts(address(auctionToken)); 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)); } } contract ClaimSale is CommonScript { function run() public { prepareAddresses(); + CrowdSale crowdSale = CrowdSale(vm.envAddress("CROWDSALE")); 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); + crowdSale.settle(saleId); + crowdSale.claimResults(saleId); vm.stopBroadcast(); string memory terms = permissioner.specificTermsV1(auctionToken); (uint8 v, bytes32 r, bytes32 s) = vm.sign(alicePk, ECDSA.toEthSignedMessageHash(abi.encodePacked(terms))); vm.startBroadcast(alice); - stakedLockingCrowdSale.claim(saleId, abi.encodePacked(r, s, v)); + crowdSale.claim(saleId, abi.encodePacked(r, s, v)); vm.stopBroadcast(); //we don't let charlie claim so we can test upgrades @@ -175,17 +195,3 @@ 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/prod/RolloutV23Sale.sol b/script/prod/RolloutV23Sale.sol index cf824f8b..8e4b4403 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(0); + StakedLockingCrowdSale stakedLockingCrowdSale = new StakedLockingCrowdSale(); stakedLockingCrowdSale.transferOwnership(moleculeDevMultisig); vm.stopBroadcast(); diff --git a/setupLocal.sh b/setupLocal.sh index f0af6567..c1798edd 100755 --- a/setupLocal.sh +++ b/setupLocal.sh @@ -29,8 +29,9 @@ $FSC script/dev/Ipnft.s.sol:DeployIpnftSuite $FSC script/dev/Tokens.s.sol:DeployTokens $FSC script/dev/Periphery.s.sol $FSC script/dev/Tokenizer.s.sol:DeployTokenizer -$FSC script/dev/CrowdSale.s.sol:DeployCrowdSale +$FSC script/dev/CrowdSale.s.sol:DeployStakedCrowdSale $FSC script/dev/Tokens.s.sol:DeployFakeTokens +$FSC script/dev/CrowdSale.s.sol:DeployCrowdSale # optionally: fixtures if [ "$fixture" -eq "1" ]; then @@ -38,11 +39,16 @@ 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 - - echo "Waiting 15 seconds until claiming sale..." + + $FSC script/dev/CrowdSale.s.sol:FixtureCrowdSale + echo "Waiting 15 seconds until claiming plain sale..." sleep 16 cast rpc evm_mine + CROWDSALE=$PLAIN_CROWDSALE_ADDRESS $FSC script/dev/CrowdSale.s.sol:ClaimSale - $FSC script/dev/CrowdSale.s.sol:ClaimSale + $FSC script/dev/CrowdSale.s.sol:FixtureStakedCrowdSale + echo "Waiting 15 seconds until claiming staked sale..." + sleep 16 + cast rpc evm_mine + CROWDSALE=$STAKED_LOCKING_CROWDSALE_ADDRESS $FSC script/dev/CrowdSale.s.sol:ClaimSale fi diff --git a/src/crowdsale/CrowdSale.sol b/src/crowdsale/CrowdSale.sol index 3800e727..d6e8edd2 100644 --- a/src/crowdsale/CrowdSale.sol +++ b/src/crowdsale/CrowdSale.sol @@ -4,10 +4,10 @@ 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 { ReentrancyGuard } from "@openzeppelin/contracts/security/ReentrancyGuard.sol"; +import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; 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, @@ -35,7 +35,7 @@ struct SaleInfo { uint256 total; uint256 surplus; bool claimed; - uint16 percentageFee; + uint16 feeBp; } error BadDecimals(); @@ -49,7 +49,7 @@ error SaleNotFund(uint256); error SaleNotConcluded(); error BadSaleState(SaleState expected, SaleState actual); error AlreadyClaimed(); -error InsufficientFunds(); +error FeesTooHigh(); /** * @title CrowdSale @@ -65,7 +65,10 @@ contract CrowdSale is ReentrancyGuard, Ownable { mapping(uint256 => mapping(address => uint256)) internal _contributions; - uint16 public percentageFee; + /** + * @notice currently configured fee cut expressed in basis points (1/10_000) + */ + uint16 public currentFeeBp = 0; event Started(uint256 indexed saleId, address indexed issuer, Sale sale, uint16 percentageFee); event Settled(uint256 indexed saleId, uint256 totalBids, uint256 surplus); @@ -80,21 +83,20 @@ contract CrowdSale is ReentrancyGuard, Ownable { /// @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; - } + event FeesUpdated(uint256 feeBp); + + constructor() Ownable() { } /** - * @notice updates the currentPercentageFee that will be applied to auctions created in the future - * @param newFee the new feePercentage + * @notice This will only affect future auctions + * @param newFeeBp uint16 the new fee in basis points. Must be <= 50% */ - function updateCrowdSaleFees(uint16 newFee) public onlyOwner { - percentageFee = newFee; + function setCurrentFeesBp(uint16 newFeeBp) public onlyOwner { + if (newFeeBp > 5000) { + revert FeesTooHigh(); + } + emit FeesUpdated(newFeeBp); + currentFeeBp = newFeeBp; } /** @@ -125,7 +127,7 @@ contract CrowdSale is ReentrancyGuard, Ownable { } _sales[saleId] = sale; - _saleInfo[saleId] = SaleInfo(SaleState.RUNNING, 0, 0, false, percentageFee); + _saleInfo[saleId] = SaleInfo(SaleState.RUNNING, 0, 0, false, currentFeeBp); sale.auctionToken.safeTransferFrom(msg.sender, address(this), sale.salesAmount); _afterSaleStarted(saleId); @@ -211,7 +213,7 @@ contract CrowdSale is ReentrancyGuard, Ownable { * this is callable by anonye * @param saleId the sale id */ - function claimResults(uint256 saleId) external virtual { + function claimResults(uint256 saleId) external { SaleInfo storage saleInfo = _saleInfo[saleId]; if (saleInfo.claimed) { revert AlreadyClaimed(); @@ -219,14 +221,10 @@ contract CrowdSale is ReentrancyGuard, Ownable { 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; + uint256 claimableAmount = sale.fundingGoal; + if (saleInfo.feeBp > 0) { + uint256 saleFees = (saleInfo.feeBp * sale.fundingGoal) / 10_000; claimableAmount -= saleFees; sale.biddingToken.safeTransfer(owner(), saleFees); } @@ -352,6 +350,6 @@ contract CrowdSale is ReentrancyGuard, Ownable { * @dev allows us to emit different events per derived contract */ function _afterSaleStarted(uint256 saleId) internal virtual { - emit Started(saleId, msg.sender, _sales[saleId], _saleInfo[saleId].percentageFee); + emit Started(saleId, msg.sender, _sales[saleId], _saleInfo[saleId].feeBp); } } diff --git a/src/crowdsale/LockingCrowdSale.sol b/src/crowdsale/LockingCrowdSale.sol index 80f3c82d..6e335277 100644 --- a/src/crowdsale/LockingCrowdSale.sol +++ b/src/crowdsale/LockingCrowdSale.sol @@ -30,7 +30,6 @@ 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 ca6c566c..b8747882 100644 --- a/src/crowdsale/StakedLockingCrowdSale.sol +++ b/src/crowdsale/StakedLockingCrowdSale.sol @@ -56,8 +56,6 @@ contract StakedLockingCrowdSale is LockingCrowdSale { revert UnsupportedInitializer(); } - constructor(uint16 percentageFee) LockingCrowdSale(percentageFee) { } - /** * [H-01] * @notice this contract can only vest stakes for contracts that it knows so unknown actors cannot start crowdsales with malicious contracts diff --git a/test/CrowdSale.t.sol b/test/CrowdSale.t.sol index 86664d8f..59282ad8 100644 --- a/test/CrowdSale.t.sol +++ b/test/CrowdSale.t.sol @@ -19,47 +19,37 @@ import { SaleNotFund, SaleNotConcluded, BadSaleState, - InsufficientFunds + FeesTooHigh } from "../src/crowdsale/CrowdSale.sol"; import { IPermissioner } from "../src/Permissioner.sol"; import { FakeERC20 } from "../src/helpers/FakeERC20.sol"; import { CrowdSaleHelpers } from "./helpers/CrowdSaleHelpers.sol"; contract CrowdSaleTest is Test { + address deployer = makeAddr("chucknorris"); 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 { - vm.startPrank(crowdSalesOwner); - crowdSale = new CrowdSale(percentageFees); - lowFeesCrowdSale = new CrowdSale(1); + vm.startPrank(deployer); + crowdSale = new CrowdSale(); 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); @@ -67,9 +57,23 @@ contract CrowdSaleTest is Test { vm.stopPrank(); } - function testSetUp() public { - assertEq(crowdSale.percentageFee(), 10); - assertEq(crowdSale.owner(), crowdSalesOwner); + function testOwnerCanControlFees() public { + assertEq(crowdSale.currentFeeBp(), 0); + assertEq(crowdSale.owner(), deployer); + + vm.startPrank(anyone); + vm.expectRevert("Ownable: caller is not the owner"); + crowdSale.setCurrentFeesBp(2500); + vm.stopPrank(); + + vm.startPrank(deployer); + vm.expectRevert(FeesTooHigh.selector); + crowdSale.setCurrentFeesBp(5001); + + //10% + crowdSale.setCurrentFeesBp(1000); + assertEq(crowdSale.currentFeeBp(), 1000); + vm.stopPrank(); } function testCreateSale() public { @@ -77,7 +81,7 @@ contract CrowdSaleTest is Test { vm.startPrank(emitter); auctionToken.approve(address(crowdSale), 400_000 ether); - uint256 saleId = crowdSale.startSale(_sale); + crowdSale.startSale(_sale); vm.stopPrank(); //cant create the same sale twice @@ -86,7 +90,6 @@ 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(); } @@ -219,9 +222,7 @@ contract CrowdSaleTest is Test { vm.expectRevert(AlreadyClaimed.selector); crowdSale.claimResults(saleId); vm.stopPrank(); - 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)); + assertEq(biddingToken.balanceOf(emitter), _sale.fundingGoal); SaleInfo memory info = crowdSale.getSaleInfo(saleId); assertEq(info.surplus, 0); @@ -265,52 +266,21 @@ contract CrowdSaleTest is Test { assertEq(auctionToken.balanceOf(bidder), 0); } - // 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 { + function testFeesDontAffectExistingCrowdSale() 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); + assertEq(crowdSale.getSaleInfo(saleId).feeBp, 0); + + vm.startPrank(deployer); + crowdSale.setCurrentFeesBp(2500); + assertEq(crowdSale.getSaleInfo(saleId).feeBp, 0); vm.stopPrank(); } - // //todo test bidders that bit 1 wei - function testTwoBiddersMeetExactly() public { vm.startPrank(emitter); Sale memory _sale = CrowdSaleHelpers.makeSale(emitter, auctionToken, biddingToken); @@ -511,12 +481,102 @@ contract CrowdSaleTest is Test { assertEq(biddingToken.balanceOf(bidder2), 915094339622641508720000); crowdSale.claimResults(saleId); - 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 assertEq(auctionToken.balanceOf(address(crowdSale)), 400_000); assertEq(biddingToken.balanceOf(address(crowdSale)), 860_000); } + + function testFeesAreTakenOnSettlement() public { + vm.startPrank(deployer); + crowdSale.setCurrentFeesBp(1000); + vm.stopPrank(); + + 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(bidder); + crowdSale.placeBid(saleId, 150_000 ether, ""); + vm.stopPrank(); + + vm.startPrank(bidder2); + crowdSale.placeBid(saleId, 150_000 ether, ""); + vm.stopPrank(); + + vm.warp(block.timestamp + 2 hours + 1); + + vm.startPrank(anyone); + crowdSale.settle(saleId); + assertEq(biddingToken.balanceOf(emitter), 0); + crowdSale.claimResults(saleId); + + //fees were taken + assertEq(biddingToken.balanceOf(emitter), 180_000 ether); + assertEq(biddingToken.balanceOf(deployer), 20_000 ether); + + vm.startPrank(bidder); + crowdSale.claim(saleId, ""); + vm.stopPrank(); + + vm.startPrank(bidder2); + crowdSale.claim(saleId, ""); + vm.stopPrank(); + + assertEq(auctionToken.balanceOf(bidder), 200_000 ether); + assertEq(auctionToken.balanceOf(bidder2), 200_000 ether); + + //fees don't affect refunds + assertEq(biddingToken.balanceOf(bidder), 900_000 ether); + assertEq(biddingToken.balanceOf(bidder2), 900_000 ether); + } + + //todo check how dangerous this is + function testTinyBidsDustEffect() public { + vm.startPrank(deployer); + crowdSale.setCurrentFeesBp(1000); + vm.stopPrank(); + + 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(bidder); + crowdSale.placeBid(saleId, 200_000 ether - 1, ""); + vm.stopPrank(); + + vm.startPrank(bidder2); + crowdSale.placeBid(saleId, 2, ""); + vm.stopPrank(); + + vm.warp(block.timestamp + 2 hours + 1); + + vm.startPrank(anyone); + crowdSale.settle(saleId); + crowdSale.claimResults(saleId); + assertEq(biddingToken.balanceOf(emitter), 180_000 ether); + + vm.startPrank(bidder); + crowdSale.claim(saleId, ""); + vm.stopPrank(); + + vm.startPrank(bidder2); + crowdSale.claim(saleId, ""); + vm.stopPrank(); + + assertEq(auctionToken.balanceOf(bidder), 399999999999999999600000); + assertEq(auctionToken.balanceOf(bidder2), 0); + + assertEq(biddingToken.balanceOf(bidder), 800000000000000000000001); + assertEq(biddingToken.balanceOf(bidder2), 999999999999999999999998); + + //dust stays on the contract + assertEq(auctionToken.balanceOf(address(crowdSale)), 400_000); + assertEq(biddingToken.balanceOf(address(crowdSale)), 1); + } } diff --git a/test/CrowdSaleFuzz.t.sol b/test/CrowdSaleFuzz.t.sol index f78d0cc0..e866da36 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(0); + crowdSale = new CrowdSale(); auctionToken = new FakeERC20("IPTOKENS","IPT"); biddingToken = new FakeERC20("USD token", "USDC"); } diff --git a/test/CrowdSaleLockedStakedTest.t.sol b/test/CrowdSaleLockedStakedTest.t.sol index a67cfd8b..250128c8 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(0); + crowdSale = new StakedLockingCrowdSale(); vestedDao = new TokenVesting( daoToken, diff --git a/test/CrowdSaleLockedTest.t.sol b/test/CrowdSaleLockedTest.t.sol index 126c9cae..0207ea9f 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(0); + crowdSale = new LockingCrowdSale(); auctionToken = new FakeERC20("IPTOKENS","IPT"); biddingToken = new FakeERC20("USD token", "USDC"); diff --git a/test/CrowdSalePermissioned.t.sol b/test/CrowdSalePermissioned.t.sol index 4b1297d2..df884f0a 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(0); + crowdSale = new StakedLockingCrowdSale(); auctionToken.issue(emitter, 500_000 ether); vestedDao = new TokenVesting( diff --git a/test/CrowdSaleWithNonStandardERC20Test.t.sol b/test/CrowdSaleWithNonStandardERC20Test.t.sol index cc5833a6..0d6873dd 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(0); + crowdSale = new StakedLockingCrowdSale(); auctionToken.mint(emitter, 500_000 ether); diff --git a/test/SynthesizerUpgrade.t.sol b/test/SynthesizerUpgrade.t.sol index 6dca0ec3..65a6d30b 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(0); + LockingCrowdSale crowdSale = new LockingCrowdSale(); 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 72cddf72..3678cd2d 100644 --- a/test/helpers/CrowdSaleHelpers.sol +++ b/test/helpers/CrowdSaleHelpers.sol @@ -19,20 +19,4 @@ 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)) - }); - } }