diff --git a/.env.example b/.env.example index fe01d51f..d1622223 100644 --- a/.env.example +++ b/.env.example @@ -29,8 +29,8 @@ STAKED_LOCKING_CROWDSALE_ADDRESS=0x9A676e781A523b5d0C0e43731313A708CB607508 USDC6_ADDRESS=0x9A9f2CCfdE556A7E9Ff0848998Aa4a0CFD8863AE WETH_ADDRESS=0x59b670e9fA9D0A427751Af201D676719a970857b +PLAIN_CROWDSALE_ADDRESS=0x4A679253410272dd5232B3Ff7cF5dbB88f295319 + #these are generated when running the fixture scripts IPTS_ADDRESS=0x1F708C24a0D3A740cD47cC0444E9480899f3dA7D -LOCKED_IPTS_ADDRESS=0x06cd7788D77332cF1156f1E327eBC090B5FF16a3 - - +LOCKED_IPTS_ADDRESS=0x06cd7788D77332cF1156f1E327eBC090B5FF16a3 \ No newline at end of file diff --git a/README.md b/README.md index 6ba87347..77711306 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,7 @@ Defender Relayer that signs off minting requests from our side: | SchmackoSwap | [0x67D8ed102E2168A46FA342e39A5f7D16c103Bd0d](https://goerli.etherscan.io/address/0x67D8ed102E2168A46FA342e39A5f7D16c103Bd0d#code) | View contract | | Tokenizer | [0xb12494eeA6B992d0A1Db3C5423BE7a2d2337F58c](https://goerli.etherscan.io/address/0xb12494eeA6B992d0A1Db3C5423BE7a2d2337F58c#code) | View contract | | Permissioner | [0xd735d9504cce32F2cd665b779D699B4157686fcd](https://goerli.etherscan.io/address/0xd735d9504cce32F2cd665b779D699B4157686fcd#code) | View contract | +| Crowdsale | [0x8c83DA72b4591bE526ca8C7cb848bC89c0e23373](https://goerli.etherscan.io/address/0x8c83DA72b4591bE526ca8C7cb848bC89c0e23373#code>) | View contract | | StakedLockingCrowdSale | [0x46c3369dece07176ad7164906d3593aa4c126d35](https://goerli.etherscan.io/address/0x46c3369dece07176ad7164906d3593aa4c126d35#code) | View contract | | SignedMintAuthorizer | [0x5e555eE24DB66825171Ac63EA614864987CEf1Af](https://goerli.etherscan.io/address/0x5e555eE24DB66825171Ac63EA614864987CEf1Af#code) | View contract | diff --git a/script/dev/CrowdSale.s.sol b/script/dev/CrowdSale.s.sol index 15441728..b1344ea2 100644 --- a/script/dev/CrowdSale.s.sol +++ b/script/dev/CrowdSale.s.sol @@ -20,98 +20,103 @@ 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(); + 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)); } } -/** - * @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 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")); - 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 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); - 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); @@ -119,31 +124,68 @@ contract FixtureCrowdSale is CommonScript { 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)); + + 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); - 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")); + IPToken auctionToken = IPToken(vm.envAddress("IPTS_ADDRESS")); uint256 saleId = SLib.stringToUint(vm.readFile("SALEID.txt")); vm.removeFile("SALEID.txt"); vm.startBroadcast(anyone); - stakedLockingCrowdSale.settle(saleId); - stakedLockingCrowdSale.claimResults(saleId); + 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 diff --git a/setupLocal.sh b/setupLocal.sh index aed1912f..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:DeployStakedCrowdSale +$FSC script/dev/Tokens.s.sol:DeployFakeTokens $FSC script/dev/CrowdSale.s.sol:DeployCrowdSale -$FSC script/dev/Tokens.s.sol:DeployFakeTokens # 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..." + 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 53f9c8ec..d6e8edd2 100644 --- a/src/crowdsale/CrowdSale.sol +++ b/src/crowdsale/CrowdSale.sol @@ -4,6 +4,7 @@ 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"; @@ -34,6 +35,7 @@ struct SaleInfo { uint256 total; uint256 surplus; bool claimed; + uint16 feeBp; } error BadDecimals(); @@ -47,13 +49,14 @@ error SaleNotFund(uint256); error SaleNotConcluded(); error BadSaleState(SaleState expected, SaleState actual); error AlreadyClaimed(); +error FeesTooHigh(); /** * @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,12 @@ contract CrowdSale is ReentrancyGuard { mapping(uint256 => mapping(address => uint256)) internal _contributions; - event Started(uint256 indexed saleId, address indexed issuer, Sale sale); + /** + * @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); /// @notice emitted when participants of the sale claim their tokens event Claimed(uint256 indexed saleId, address indexed claimer, uint256 claimed, uint256 refunded); @@ -75,6 +83,22 @@ contract CrowdSale is ReentrancyGuard { /// @notice emitted when sales owner / beneficiary claims `salesAmount` `auctionTokens` after a non successful sale event ClaimedAuctionTokens(uint256 indexed saleId); + event FeesUpdated(uint256 feeBp); + + constructor() Ownable() { } + + /** + * @notice This will only affect future auctions + * @param newFeeBp uint16 the new fee in basis points. Must be <= 50% + */ + function setCurrentFeesBp(uint16 newFeeBp) public onlyOwner { + if (newFeeBp > 5000) { + revert FeesTooHigh(); + } + emit FeesUpdated(newFeeBp); + currentFeeBp = newFeeBp; + } + /** * @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 +127,7 @@ contract CrowdSale is ReentrancyGuard { } _sales[saleId] = sale; - _saleInfo[saleId] = SaleInfo(SaleState.RUNNING, 0, 0, false); + _saleInfo[saleId] = SaleInfo(SaleState.RUNNING, 0, 0, false, currentFeeBp); sale.auctionToken.safeTransferFrom(msg.sender, address(this), sale.salesAmount); _afterSaleStarted(saleId); @@ -189,7 +213,7 @@ contract CrowdSale is ReentrancyGuard { * 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(); @@ -198,9 +222,16 @@ contract CrowdSale is ReentrancyGuard { Sale storage sale = _sales[saleId]; if (saleInfo.state == SaleState.SETTLED) { + uint256 claimableAmount = sale.fundingGoal; + if (saleInfo.feeBp > 0) { + uint256 saleFees = (saleInfo.feeBp * sale.fundingGoal) / 10_000; + 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 +350,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].feeBp); } } diff --git a/src/crowdsale/StakedLockingCrowdSale.sol b/src/crowdsale/StakedLockingCrowdSale.sol index 13c91e1f..b8747882 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,8 +56,6 @@ contract StakedLockingCrowdSale is LockingCrowdSale, Ownable { revert UnsupportedInitializer(); } - constructor() Ownable() { } - /** * [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/subgraph/config/goerli.js b/subgraph/config/goerli.js index f09fedbd..9116650e 100644 --- a/subgraph/config/goerli.js +++ b/subgraph/config/goerli.js @@ -18,6 +18,10 @@ module.exports = { address: '0xb12494eeA6B992d0A1Db3C5423BE7a2d2337F58c', startBlock: 9142681 }, + crowdSale: { + address: '0x8c83DA72b4591bE526ca8C7cb848bC89c0e23373', + startBlock: 9933419 + }, stakedLockingCrowdSale: { address: '0x46c3369dece07176ad7164906d3593aa4c126d35', startBlock: 9168705 diff --git a/subgraph/config/local.js b/subgraph/config/local.js index 550ba5dc..4997923c 100644 --- a/subgraph/config/local.js +++ b/subgraph/config/local.js @@ -18,6 +18,10 @@ module.exports = { address: process.env.TOKENIZER_ADDRESS, startBlock: 0 }, + crowdSale: { + address: process.env.PLAIN_CROWDSALE_ADDRESS, + startBlock: 0 + }, stakedLockingCrowdSale: { address: process.env.STAKED_LOCKING_CROWDSALE_ADDRESS, startBlock: 0 diff --git a/subgraph/config/mainnet.js b/subgraph/config/mainnet.js index 3fd5557d..e0fe1f71 100644 --- a/subgraph/config/mainnet.js +++ b/subgraph/config/mainnet.js @@ -12,6 +12,11 @@ module.exports = { address: '0x58EB89C69CB389DBef0c130C6296ee271b82f436', startBlock: 17464363 }, + // TO DO: add the correct values + // crowdSale: { + // address: '0x8c83DA72b4591bE526ca8C7cb848bC89c0e23373', + // startBlock: 9933419 + // }, stakedLockingCrowdSale: { address: '0x35Bce29F52f51f547998717CD598068Afa2B29B7', startBlock: 17481804 diff --git a/test/CrowdSale.t.sol b/test/CrowdSale.t.sol index 19de98b7..59282ad8 100644 --- a/test/CrowdSale.t.sol +++ b/test/CrowdSale.t.sol @@ -18,13 +18,15 @@ import { BidTooLow, SaleNotFund, SaleNotConcluded, - BadSaleState + BadSaleState, + 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 bidder = makeAddr("bidder"); address bidder2 = makeAddr("bidder2"); @@ -36,7 +38,9 @@ contract CrowdSaleTest is Test { CrowdSale internal crowdSale; function setUp() public { + vm.startPrank(deployer); crowdSale = new CrowdSale(); + vm.stopPrank(); auctionToken = new FakeERC20("IPTOKENS","IPT"); biddingToken = new FakeERC20("USD token", "USDC"); @@ -53,6 +57,25 @@ contract CrowdSaleTest is Test { vm.stopPrank(); } + 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 { Sale memory _sale = CrowdSaleHelpers.makeSale(emitter, auctionToken, biddingToken); @@ -243,7 +266,20 @@ contract CrowdSaleTest is Test { assertEq(auctionToken.balanceOf(bidder), 0); } - //todo test bidders that bit 1 wei + 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(); + + assertEq(crowdSale.getSaleInfo(saleId).feeBp, 0); + + vm.startPrank(deployer); + crowdSale.setCurrentFeesBp(2500); + assertEq(crowdSale.getSaleInfo(saleId).feeBp, 0); + vm.stopPrank(); + } function testTwoBiddersMeetExactly() public { vm.startPrank(emitter); @@ -446,11 +482,101 @@ contract CrowdSaleTest is Test { crowdSale.claimResults(saleId); - assertEq(biddingToken.balanceOf(emitter), 200_000 ether); - //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); + } }