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) | |
| Tokenizer | [0xb12494eeA6B992d0A1Db3C5423BE7a2d2337F58c](https://goerli.etherscan.io/address/0xb12494eeA6B992d0A1Db3C5423BE7a2d2337F58c#code) | |
| Permissioner | [0xd735d9504cce32F2cd665b779D699B4157686fcd](https://goerli.etherscan.io/address/0xd735d9504cce32F2cd665b779D699B4157686fcd#code) | |
+| Crowdsale | [0x8c83DA72b4591bE526ca8C7cb848bC89c0e23373](https://goerli.etherscan.io/address/0x8c83DA72b4591bE526ca8C7cb848bC89c0e23373#code>) | |
| StakedLockingCrowdSale | [0x46c3369dece07176ad7164906d3593aa4c126d35](https://goerli.etherscan.io/address/0x46c3369dece07176ad7164906d3593aa4c126d35#code) | |
| SignedMintAuthorizer | [0x5e555eE24DB66825171Ac63EA614864987CEf1Af](https://goerli.etherscan.io/address/0x5e555eE24DB66825171Ac63EA614864987CEf1Af#code) | |
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);
+ }
}