Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add crowdsale with fees contract and unit tests for it #141

Merged
merged 9 commits into from
Oct 26, 2023
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ TERMS_ACCEPTED_PERMISSIONER_ADDRESS=0x610178dA211FEF7D417bC0e6FeD39F05609AD788

TOKENIZER_ADDRESS=0xA51c1fc2f0D1a1b8494Ed1FE312d7C3a78Ed91C0
STAKED_LOCKING_CROWDSALE_ADDRESS=0x9A676e781A523b5d0C0e43731313A708CB607508
CROWDSALE_WITH_FEES_ADDRESS=0x4A679253410272dd5232B3Ff7cF5dbB88f295319

USDC6_ADDRESS=0x9A9f2CCfdE556A7E9Ff0848998Aa4a0CFD8863AE
WETH_ADDRESS=0x59b670e9fA9D0A427751Af201D676719a970857b
Expand Down
121 changes: 121 additions & 0 deletions script/dev/CrowdSaleWithFees.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
// 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();
}
}
9 changes: 6 additions & 3 deletions setupLocal.sh
Original file line number Diff line number Diff line change
Expand Up @@ -30,19 +30,22 @@ $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/Tokens.s.sol:DeployFakeTokens
$FSC script/dev/Tokens.s.sol:DeployFakeTokens
$FSC script/dev/CrowdSaleWithFees.s.sol:DeployCrowdSale

# optionally: fixtures
if [ "$fixture" -eq "1" ]; then
echo "Running fixture scripts."

$FSC script/dev/Ipnft.s.sol:FixtureIpnft
$FSC script/dev/Tokenizer.s.sol:FixtureTokenizer
$FSC script/dev/CrowdSale.s.sol:FixtureCrowdSale
# $FSC script/dev/CrowdSale.s.sol:FixtureCrowdSale
$FSC script/dev/CrowdSaleWithFees.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/CrowdSale.s.sol:ClaimSale
$FSC script/dev/CrowdSaleWithFees.s.sol:ClaimSale
fi
65 changes: 65 additions & 0 deletions src/crowdsale/CrowdSaleWithFees.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// 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;
elmariachi111 marked this conversation as resolved.
Show resolved Hide resolved

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) {
elmariachi111 marked this conversation as resolved.
Show resolved Hide resolved
return feesPercentage;
}

function getCrowdSaleFees(uint256 saleId) public view returns (uint256) {
return crowdSaleFees[saleId];
}

function updateCrowdSaleFees(uint256 newFee) public onlyOwner {
elmariachi111 marked this conversation as resolved.
Show resolved Hide resolved
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);
elmariachi111 marked this conversation as resolved.
Show resolved Hide resolved
uint256 saleFees = sale.fundingGoal * feesPercentage / 1000;
elmariachi111 marked this conversation as resolved.
Show resolved Hide resolved
sale.fundingGoal = sale.fundingGoal - saleFees;
sale.biddingToken.safeTransfer(owner(), saleFees);
elmariachi111 marked this conversation as resolved.
Show resolved Hide resolved
}
}
98 changes: 98 additions & 0 deletions test/CrowdSaleWithFees.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +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);
}
}