diff --git a/.github/workflows/foundry.yml b/.github/workflows/foundry.yml index 78d16de5..b1903535 100644 --- a/.github/workflows/foundry.yml +++ b/.github/workflows/foundry.yml @@ -20,15 +20,18 @@ jobs: with: version: nightly - - name: Run tests + - name: Run local tests working-directory: . run: yarn test && forge coverage --no-match-test Mainnet --report lcov - name: Coverage uses: VeryGoodOpenSource/very_good_coverage@v2 with: - path: "./lcov.info" + path: './lcov.info' min_coverage: 95 - exclude: "test/* test/resources/* archive/* script/*" + exclude: 'test/* test/resources/* archive/* script/*' - name: Run snapshot working-directory: . run: forge snapshot --no-match-test Mainnet + - name: Run mainnet tests + working-directory: . + run: forge test --match-test Mainnet --fork-url ${{ secrets.MAINNET_RPC_URL }} diff --git a/.github/workflows/prettier.yml b/.github/workflows/prettier.yml index f9875609..60c5c13b 100644 --- a/.github/workflows/prettier.yml +++ b/.github/workflows/prettier.yml @@ -4,7 +4,7 @@ on: - main workflow_dispatch: branches: - - "**" + - '**' name: prettier @@ -25,3 +25,4 @@ jobs: # This part is also where you can pass other options, for example: prettier_options: --write **/*.{sol,md,js} prettier_plugins: prettier-plugin-solidity + prettier_version: 2.8.8 diff --git a/.gitignore b/.gitignore index 3adad405..eb65b25d 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,4 @@ broadcast/ !.env.example .npmignore .VSCodeCounter/ +*.code-workspace \ No newline at end of file diff --git a/archive/PersonalInviteOld.sol b/archive/PersonalInviteOld.sol index 0092cbab..9f5a90ce 100644 --- a/archive/PersonalInviteOld.sol +++ b/archive/PersonalInviteOld.sol @@ -70,15 +70,9 @@ contract PersonalInviteOld is ERC2771Context, Ownable, ReentrancyGuard { require(_buyer != address(0), "_buyer can not be zero address"); require(_receiver != address(0), "_receiver can not be zero address"); - require( - _minAmount <= _maxAmount, - "_minAmount needs to be smaller or equal to _maxAmount" - ); + require(_minAmount <= _maxAmount, "_minAmount needs to be smaller or equal to _maxAmount"); require(_tokenPrice != 0, "_tokenPrice can not be zero"); - require( - _expiration > block.timestamp, - "Expiration date needs to be in the future" - ); + require(_expiration > block.timestamp, "Expiration date needs to be in the future"); // after creating the contract, it needs to be set up as minter } @@ -88,10 +82,7 @@ contract PersonalInviteOld is ERC2771Context, Ownable, ReentrancyGuard { @param _tokenAmount Amount of tokens to buy, bits (bit = smallest subunit of token). [tok_bits] */ function deal(uint _tokenAmount) public nonReentrant { - require( - buyer == _msgSender(), - "Only the personally invited buyer can take this deal" - ); + require(buyer == _msgSender(), "Only the personally invited buyer can take this deal"); require( minAmount <= _tokenAmount && _tokenAmount <= maxAmount, "Amount needs to be inbetween minAmount and maxAmount" @@ -112,11 +103,7 @@ contract PersonalInviteOld is ERC2771Context, Ownable, ReentrancyGuard { (_tokenAmount * tokenPrice) % (10 ** token.decimals()) == 0, "Amount * tokenprice needs to be a multiple of 10**token.decimals()" ); - currency.safeTransferFrom( - buyer, - receiver, - (_tokenAmount * tokenPrice) / (10 ** token.decimals()) - ); + currency.safeTransferFrom(buyer, receiver, (_tokenAmount * tokenPrice) / (10 ** token.decimals())); require(token.mint(buyer, _tokenAmount), "Minting new tokens failed"); emit Deal(buyer, _tokenAmount, tokenPrice, currency, token); @@ -138,24 +125,14 @@ contract PersonalInviteOld is ERC2771Context, Ownable, ReentrancyGuard { /** * @dev both Ownable and ERC2771Context have a _msgSender() function, so we need to override and select which one to use. */ - function _msgSender() - internal - view - override(Context, ERC2771Context) - returns (address) - { + function _msgSender() internal view override(Context, ERC2771Context) returns (address) { return ERC2771Context._msgSender(); } /** * @dev both Ownable and ERC2771Context have a _msgData() function, so we need to override and select which one to use. */ - function _msgData() - internal - view - override(Context, ERC2771Context) - returns (bytes calldata) - { + function _msgData() internal view override(Context, ERC2771Context) returns (bytes calldata) { return ERC2771Context._msgData(); } diff --git a/archive/PersonalInviteOld.t.sol b/archive/PersonalInviteOld.t.sol index edc20417..64715d08 100644 --- a/archive/PersonalInviteOld.t.sol +++ b/archive/PersonalInviteOld.t.sol @@ -12,21 +12,16 @@ contract PersonalInviteOldOldTest is Test { CorpusToken currency; // todo: add different ERC20 token as currency! PersonalInviteOld invite; - uint256 MAX_INT = - 115792089237316195423570985008687907853269984665640564039457584007913129639935; + uint256 MAX_INT = 115792089237316195423570985008687907853269984665640564039457584007913129639935; address public constant admin = 0x0109709eCFa91a80626FF3989D68f67f5b1dD120; address public constant buyer = 0x1109709ecFA91a80626ff3989D68f67F5B1Dd121; - address public constant minterAdmin = - 0x2109709EcFa91a80626Ff3989d68F67F5B1Dd122; + address public constant minterAdmin = 0x2109709EcFa91a80626Ff3989d68F67F5B1Dd122; address public constant minter = 0x3109709ECfA91A80626fF3989D68f67F5B1Dd123; address public constant owner = 0x6109709EcFA91A80626FF3989d68f67F5b1dd126; - address public constant receiver = - 0x7109709eCfa91A80626Ff3989D68f67f5b1dD127; - address public constant paymentTokenProvider = - 0x8109709ecfa91a80626fF3989d68f67F5B1dD128; - address public constant trustedForwarder = - 0x9109709EcFA91A80626FF3989D68f67F5B1dD129; + address public constant receiver = 0x7109709eCfa91A80626Ff3989D68f67f5b1dD127; + address public constant paymentTokenProvider = 0x8109709ecfa91a80626fF3989d68f67F5B1dD128; + address public constant trustedForwarder = 0x9109709EcFA91A80626FF3989D68f67F5B1dD129; uint256 public constant price = 10000000; @@ -101,32 +96,18 @@ contract PersonalInviteOldOldTest is Test { assertTrue(currency.balanceOf(buyer) == 10000000000); vm.prank(buyer); invite.deal(100); - assertTrue( - currency.balanceOf(buyer) == - 10000000000 - (100 * 10000000) / (10 ** token.decimals()) - ); + assertTrue(currency.balanceOf(buyer) == 10000000000 - (100 * 10000000) / (10 ** token.decimals())); assertTrue(token.balanceOf(buyer) == 100); - assertTrue( - currency.balanceOf(receiver) == - (100 * 10000000) / (10 ** token.decimals()) - ); + assertTrue(currency.balanceOf(receiver) == (100 * 10000000) / (10 ** token.decimals())); } function testDealHappyCase2() public { assertTrue(currency.balanceOf(buyer) == 10000000000); vm.prank(buyer); invite.deal(100000000000000); - assertTrue( - currency.balanceOf(buyer) == - 10000000000 - - (100000000000000 * 10000000) / - (10 ** token.decimals()) - ); + assertTrue(currency.balanceOf(buyer) == 10000000000 - (100000000000000 * 10000000) / (10 ** token.decimals())); assertTrue(token.balanceOf(buyer) == 100000000000000); - assertTrue( - currency.balanceOf(receiver) == - (100000000000000 * 10000000) / (10 ** token.decimals()) - ); + assertTrue(currency.balanceOf(receiver) == (100000000000000 * 10000000) / (10 ** token.decimals())); } // todo: make sure a valid test case is found @@ -134,33 +115,24 @@ contract PersonalInviteOldOldTest is Test { // limit tokenSaleBits to to values [minAmount, maxAmount] vm.assume(tokenSaleBits >= invite.minAmount()); vm.assume(tokenSaleBits <= invite.maxAmount()); - if ( - (tokenSaleBits * invite.tokenPrice()) % (10 ** token.decimals()) == - 0 - ) { + if ((tokenSaleBits * invite.tokenPrice()) % (10 ** token.decimals()) == 0) { // test cases without rest must be successful uint256 buyerStartCurrencyBalance = currency.balanceOf(buyer); // uint256 tokenSaleBits = 2.7 * 10**14; - uint256 currencyCost = (tokenSaleBits * price) / - (10 ** token.decimals()); - uint256 expectedBuyerCurrencyBalance = buyerStartCurrencyBalance - - currencyCost; + uint256 currencyCost = (tokenSaleBits * price) / (10 ** token.decimals()); + uint256 expectedBuyerCurrencyBalance = buyerStartCurrencyBalance - currencyCost; assertTrue(currency.balanceOf(buyer) == buyerStartCurrencyBalance); // buyer owns 10**10 currency, so 10**10 * 10**currency.decimals() currency bits (bit = smallest subunit of token) vm.prank(buyer); invite.deal(tokenSaleBits); // buyer brings in their amount of payment currency in bits - assertTrue( - currency.balanceOf(buyer) == expectedBuyerCurrencyBalance - ); + assertTrue(currency.balanceOf(buyer) == expectedBuyerCurrencyBalance); assertTrue(token.balanceOf(buyer) == tokenSaleBits); assertTrue(currency.balanceOf(receiver) == currencyCost); } else { // test cases with rest must fail vm.prank(buyer); - vm.expectRevert( - "Amount * tokenprice needs to be a multiple of 10**token.decimals()" - ); + vm.expectRevert("Amount * tokenprice needs to be a multiple of 10**token.decimals()"); invite.deal(tokenSaleBits); } } @@ -172,11 +144,7 @@ contract PersonalInviteOldOldTest is Test { uint8 maxDecimals = 25; FakePaymentToken paymentToken; - for ( - uint8 paymentTokenDecimals = 1; - paymentTokenDecimals < maxDecimals; - paymentTokenDecimals++ - ) { + for (uint8 paymentTokenDecimals = 1; paymentTokenDecimals < maxDecimals; paymentTokenDecimals++) { //uint8 paymentTokenDecimals = 10; /* @@ -192,10 +160,7 @@ contract PersonalInviteOldOldTest is Test { list = new AllowList(); token = new CorpusToken(admin, list, 0x0, "TESTTOKEN", "TEST"); vm.prank(paymentTokenProvider); - paymentToken = new FakePaymentToken( - _paymentTokenAmount, - paymentTokenDecimals - ); + paymentToken = new FakePaymentToken(_paymentTokenAmount, paymentTokenDecimals); vm.prank(owner); invite = new PersonalInviteOld( @@ -235,16 +200,11 @@ contract PersonalInviteOldOldTest is Test { vm.prank(buyer); invite.deal(33 * 10 ** 18); // buyer should have 10 FPT left - assertTrue( - paymentToken.balanceOf(buyer) == 10 * 10 ** paymentTokenDecimals - ); + assertTrue(paymentToken.balanceOf(buyer) == 10 * 10 ** paymentTokenDecimals); // buyer should have the 33 CT they bought assertTrue(token.balanceOf(buyer) == 33 * 10 ** 18); // receiver should have the 990 FPT that were paid - assertTrue( - paymentToken.balanceOf(receiver) == - 990 * 10 ** paymentTokenDecimals - ); + assertTrue(paymentToken.balanceOf(receiver) == 990 * 10 ** paymentTokenDecimals); } } @@ -370,10 +330,7 @@ contract PersonalInviteOldOldTest is Test { // give invite contract allowance vm.prank(buyer); - currency.approve( - address(invite), - 200000000000000000000 * 2000000000000 - ); + currency.approve(address(invite), 200000000000000000000 * 2000000000000); vm.prank(buyer); invite.deal(10000000000000000000); } @@ -387,17 +344,9 @@ contract PersonalInviteOldOldTest is Test { assertTrue(currency.balanceOf(buyer) == 10000000000); vm.prank(buyer); invite.deal(100000000000000); - assertTrue( - currency.balanceOf(buyer) == - 10000000000 - - (100000000000000 * 10000000) / - (10 ** token.decimals()) - ); + assertTrue(currency.balanceOf(buyer) == 10000000000 - (100000000000000 * 10000000) / (10 ** token.decimals())); assertTrue(token.balanceOf(buyer) == 100000000000000); - assertTrue( - currency.balanceOf(receiver) == - (100000000000000 * 10000000) / (10 ** token.decimals()) - ); + assertTrue(currency.balanceOf(receiver) == (100000000000000 * 10000000) / (10 ** token.decimals())); // after deal, all state variables are deleted. Therefore, even buyer can not buy anymore, rendering the deal unusable. vm.expectRevert("Only the personally invited buyer can take this deal"); vm.prank(buyer); diff --git a/contracts/AllowList.sol b/contracts/AllowList.sol index 2ace35c4..4552964f 100644 --- a/contracts/AllowList.sol +++ b/contracts/AllowList.sol @@ -3,51 +3,61 @@ pragma solidity 0.8.17; import "@openzeppelin/contracts/access/Ownable2Step.sol"; -/* - The AllowList contract is used to manage a list of addresses and attest each address certain attributes. - Examples for possible attributes are: is KYCed, is american, is of age, etc. - One AllowList managed by one entity (e.g. tokenize.it) can manage up to 252 different attributes, and one tier with 5 levels, and can be used by an unlimited number of other Tokens. -*/ +/** + * @title AllowList + * @author malteish, cjentzsch + * @notice The AllowList contract is used to manage a list of addresses and attest each address certain attributes. + * Examples for possible attributes are: is KYCed, is american, is of age, etc. + * One AllowList managed by one entity (e.g. tokenize.it) can manage up to 252 different attributes, and one tier with 5 levels, and can be used by an unlimited number of other Tokens. + */ contract AllowList is Ownable2Step { /** - @dev Attributes are defined as bit mask, with the bit position encoding it's meaning and the bit's value whether this attribute is attested or not. - Example: - - position 0: 1 = has been KYCed (0 = not KYCed) - - position 1: 1 = is american citizen (0 = not american citizen) - - position 2: 1 = is a penguin (0 = not a penguin) - These meanings are not defined within code, neither in the token contract nor the allowList. Nevertheless, the definition used by the people responsible for both contracts MUST match, - or the token contract will not work as expected. E.g. if the allowList defines position 2 as "is a penguin", while the token contract uses position 2 as "is a hedgehog", then the tokens - might be sold to hedgehogs, which was never the intention. - Here some examples of how requirements can be used in practice: - value 0b0000000000000000000000000000000000000000000000000000000000000101, means "is KYCed and is a penguin" - value 0b0000000000000000000000000000000000000000000000000000000000000111, means "is KYCed, is american and is a penguin" - value 0b0000000000000000000000000000000000000000000000000000000000000000, means "has not proven any relevant attributes to the allowList operator" (default value) - - The highest four bits are defined as tiers as follows (depicted with less bits because 256 is a lot): - - 0b0000000000000000000000000000000000000000000000000000000000000000 = tier 0 - - 0b0001000000000000000000000000000000000000000000000000000000000000 = tier 1 - - 0b0011000000000000000000000000000000000000000000000000000000000000 = tier 2 (and 1) - - 0b0111000000000000000000000000000000000000000000000000000000000000 = tier 3 (and 2 and 1) - - 0b1111000000000000000000000000000000000000000000000000000000000000 = tier 4 (and 3 and 2 and 1) - This very simple definition allows for a maximum of 5 tiers, even though 4 bits are used for encoding. By sacrificing some space it can be implemented without code changes. - + * @notice Stores the attributes for each address. + * @dev Attributes are defined as bit mask, with the bit position encoding it's meaning and the bit's value whether this attribute is attested or not. + * Example: + * - position 0: 1 = has been KYCed (0 = not KYCed) + * - position 1: 1 = is american citizen (0 = not american citizen) + * - position 2: 1 = is a penguin (0 = not a penguin) + * These meanings are not defined within code, neither in the token contract nor the allowList. Nevertheless, the definition used by the people responsible for both contracts MUST match, + * or the token contract will not work as expected. E.g. if the allowList defines position 2 as "is a penguin", while the token contract uses position 2 as "is a hedgehog", then the tokens + * might be sold to hedgehogs, which was never the intention. + * Here some examples of how requirements can be used in practice: + * value 0b0000000000000000000000000000000000000000000000000000000000000101, means "is KYCed and is a penguin" + * value 0b0000000000000000000000000000000000000000000000000000000000000111, means "is KYCed, is american and is a penguin" + * value 0b0000000000000000000000000000000000000000000000000000000000000000, means "has not proven any relevant attributes to the allowList operator" (default value) + * + * The highest four bits are defined as tiers as follows (depicted with less bits because 256 is a lot): + * - 0b0000000000000000000000000000000000000000000000000000000000000000 = tier 0 + * - 0b0001000000000000000000000000000000000000000000000000000000000000 = tier 1 + * - 0b0011000000000000000000000000000000000000000000000000000000000000 = tier 2 (and 1) + * - 0b0111000000000000000000000000000000000000000000000000000000000000 = tier 3 (and 2 and 1) + * - 0b1111000000000000000000000000000000000000000000000000000000000000 = tier 4 (and 3 and 2 and 1) + * This very simple definition allows for a maximum of 5 tiers, even though 4 bits are used for encoding. By sacrificing some space it can be implemented without code changes. */ mapping(address => uint256) public map; - event Set(address indexed key, uint256 value); + /** + * @notice Attributes for `key` have been set to `value` + * @param _addr address the attributes are set for + * @param _attributes new attributes + */ + event Set(address indexed _addr, uint256 _attributes); /** - @notice sets (or updates) the attributes for an address - */ + * @notice sets (or updates) the attributes for an address + * @param _addr address to be set + * @param _attributes new attributes + */ function set(address _addr, uint256 _attributes) external onlyOwner { map[_addr] = _attributes; emit Set(_addr, _attributes); } /** - @notice purges an address from the allowList - @dev this is a convenience function, it is equivalent to calling set(_addr, 0) - */ + * @notice purges an address from the allowList + * @dev this is a convenience function, it is equivalent to calling set(_addr, 0) + * @param _addr address to be removed + */ function remove(address _addr) external onlyOwner { delete map[_addr]; emit Set(_addr, 0); diff --git a/contracts/ContinuousFundraising.sol b/contracts/ContinuousFundraising.sol index 906ba7e3..ebe35d26 100644 --- a/contracts/ContinuousFundraising.sol +++ b/contracts/ContinuousFundraising.sol @@ -8,70 +8,85 @@ import "@openzeppelin/contracts/security/Pausable.sol"; import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; import "@openzeppelin/contracts/metatx/ERC2771Context.sol"; import "@openzeppelin/contracts/utils/math/Math.sol"; +import "../contracts/MoneriumI.sol"; import "./Token.sol"; -/* -This contract represents the offer to buy an amount of tokens at a preset price. It can be used by anyone and there is no limit to the number of times it can be used. -The buyer can decide how many tokens to buy, but has to buy at least minAmount and can buy at most maxAmount. -The currency the offer is denominated in is set at creation time and can be updated later. -The contract can be paused at any time by the owner, which will prevent any new deals from being made. Then, changes to the contract can be made, like changing the currency, price or requirements. -The contract can be unpaused after "delay", which will allow new deals to be made again. - -A company will create only one ContinuousFundraising contract for their token (or one for each currency if they want to accept multiple currencies). - -The contract inherits from ERC2771Context in order to be usable with Gas Station Network (GSN) https://docs.opengsn.org/faq/troubleshooting.html#my-contract-is-using-openzeppelin-how-do-i-add-gsn-support - +/** + * @title ContinuousFundraising + * @author malteish, cjentzsch + * @notice This contract represents the offer to buy an amount of tokens at a preset price. It can be used by anyone and there is no limit to the number of times it can be used. + * The buyer can decide how many tokens to buy, but has to buy at least minAmount and can buy at most maxAmount. + * The currency the offer is denominated in is set at creation time and can be updated later. + * The contract can be paused at any time by the owner, which will prevent any new deals from being made. Then, changes to the contract can be made, like changing the currency, price or requirements. + * The contract can be unpaused after "delay", which will allow new deals to be made again. + * A company will create only one ContinuousFundraising contract for their token (or one for each currency if they want to accept multiple currencies). + * @dev The contract inherits from ERC2771Context in order to be usable with Gas Station Network (GSN) https://docs.opengsn.org/faq/troubleshooting.html#my-contract-is-using-openzeppelin-how-do-i-add-gsn-support */ -contract ContinuousFundraising is - ERC2771Context, - Ownable2Step, - Pausable, - ReentrancyGuard -{ +contract ContinuousFundraising is ERC2771Context, Ownable2Step, Pausable, ReentrancyGuard { using SafeERC20 for IERC20; - /// @notice address that receives the currency when tokens are bought + /// address that receives the currency when tokens are bought address public currencyReceiver; - /// @notice smallest amount of tokens that can be minted, in bits (bit = smallest subunit of token) + /// smallest amount of tokens that can be minted, in bits (bit = smallest subunit of token) uint256 public minAmountPerBuyer; - /// @notice largest amount of tokens that can be minted, in bits (bit = smallest subunit of token) + /// largest amount of tokens that can be minted, in bits (bit = smallest subunit of token) uint256 public maxAmountPerBuyer; - /** - @notice amount of bits of currency per main unit token (e.g.: 2 USDC (6 decimals) per TOK (18 decimals) => price = 2*10^6 ). - @dev units: [tokenPrice] = [currency_bits]/[token], so for above example: [tokenPrice] = [USDC_bits]/[TOK] - */ + /// The price of a token, expressed as amount of bits of currency per main unit token (e.g.: 2 USDC (6 decimals) per TOK (18 decimals) => price = 2*10^6 ). + /// @dev units: [tokenPrice] = [currency_bits]/[token], so for above example: [tokenPrice] = [USDC_bits]/[TOK] uint256 public tokenPrice; - /// @notice total amount of tokens that CAN BE minted through this contract, in bits (bit = smallest subunit of token) + /// total amount of tokens that CAN BE minted through this contract, in bits (bit = smallest subunit of token) uint256 public maxAmountOfTokenToBeSold; - /// @notice total amount of tokens that HAVE BEEN minted through this contract, in bits (bit = smallest subunit of token) + /// total amount of tokens that HAVE BEEN minted through this contract, in bits (bit = smallest subunit of token) uint256 public tokensSold; - /// @notice currency used to pay for the token mint. Must be ERC20, so ether can only be used as wrapped ether (WETH) + /// currency used to pay for the token mint. Must be ERC20, so ether can only be used as wrapped ether (WETH) IERC20 public currency; - /// @notice token to be minted + /// token to be minted Token public token; - // delay is calculated from pause or parameter change to unpause. + /// @notice Minimum waiting time between pause or parameter change and unpause. + /// @dev delay is calculated from pause or parameter change to unpause. uint256 public constant delay = 1 days; - // timestamp of the last time the contract was paused or a parameter was changed + /// timestamp of the last time the contract was paused or a parameter was changed uint256 public coolDownStart; - // keeps track of how much each buyer has bought, in order to enforce maxAmountPerBuyer + /// This mapping keeps track of how much each buyer has bought, in order to enforce maxAmountPerBuyer mapping(address => uint256) public tokensBought; - event CurrencyReceiverChanged(address indexed); - event MinAmountPerBuyerChanged(uint256); - event MaxAmountPerBuyerChanged(uint256); - event TokenPriceAndCurrencyChanged(uint256, IERC20 indexed); - event MaxAmountOfTokenToBeSoldChanged(uint256); - event TokensBought( - address indexed buyer, - uint256 tokenAmount, - uint256 currencyAmount - ); + /// @notice CurrencyReceiver has been changed to `newCurrencyReceiver` + /// @param newCurrencyReceiver address that receives the payment (in currency) when tokens are bought + event CurrencyReceiverChanged(address indexed newCurrencyReceiver); + /// @notice A buyer must at least own `newMinAmountPerBuyer` tokens after buying. If they already own more, they can buy smaller amounts than this, too. + /// @param newMinAmountPerBuyer smallest amount of tokens a buyer can buy is allowed to own after buying. + event MinAmountPerBuyerChanged(uint256 newMinAmountPerBuyer); + /// @notice A buyer can buy at most `newMaxAmountPerBuyer` tokens, from this contract, even if they split the buys into multiple transactions. + /// @param newMaxAmountPerBuyer largest amount of tokens a buyer can buy from this contract + event MaxAmountPerBuyerChanged(uint256 newMaxAmountPerBuyer); + /// @notice Price and currency changed. + /// @param newTokenPrice new price of a token, expressed as amount of bits of currency per main unit token (e.g.: 2 USDC (6 decimals) per TOK (18 decimals) => price = 2*10^6 ). + /// @param newCurrency new currency used to pay for the token purchase + event TokenPriceAndCurrencyChanged(uint256 newTokenPrice, IERC20 indexed newCurrency); + /// @param newMaxAmountOfTokenToBeSold new total amount of tokens that can be minted through this contract, in bits (bit = smallest subunit of token)ยด + event MaxAmountOfTokenToBeSoldChanged(uint256 newMaxAmountOfTokenToBeSold); + /** + * @notice `buyer` bought `tokenAmount` tokens for `currencyAmount` currency. + * @param buyer Address that bought the tokens + * @param tokenAmount Amount of tokens bought + * @param currencyAmount Amount of currency paid + */ + event TokensBought(address indexed buyer, uint256 tokenAmount, uint256 currencyAmount); /** + * @notice Sets up the ContinuousFundraising. The contract is usable immediately after deployment, but does need a minting allowance for the token. * @dev Constructor that passes the trusted forwarder to the ERC2771Context constructor + * @param _trustedForwarder This address can execute transactions in the name of any other address + * @param _currencyReceiver address that receives the payment (in currency) when tokens are bought + * @param _minAmountPerBuyer smallest amount of tokens a buyer is allowed to buy when buying for the first time + * @param _maxAmountPerBuyer largest amount of tokens a buyer can buy from this contract + * @param _tokenPrice price of a token, expressed as amount of bits of currency per main unit token (e.g.: 2 USDC (6 decimals) per TOK (18 decimals) => price = 2*10^6 ). + * @param _maxAmountOfTokenToBeSold total amount of tokens that can be minted through this contract + * @param _currency currency used to pay for the token mint. Must be ERC20, so ether can only be used as wrapped ether (WETH) + * @param _token token to be sold */ constructor( address _trustedForwarder, @@ -90,140 +105,104 @@ contract ContinuousFundraising is maxAmountOfTokenToBeSold = _maxAmountOfTokenToBeSold; currency = _currency; token = _token; - require( - _trustedForwarder != address(0), - "trustedForwarder can not be zero address" - ); - require( - _currencyReceiver != address(0), - "currencyReceiver can not be zero address" - ); - require( - address(_currency) != address(0), - "currency can not be zero address" - ); + require(_trustedForwarder != address(0), "trustedForwarder can not be zero address"); + require(_currencyReceiver != address(0), "currencyReceiver can not be zero address"); + require(address(_currency) != address(0), "currency can not be zero address"); require(address(_token) != address(0), "token can not be zero address"); require( _minAmountPerBuyer <= _maxAmountPerBuyer, "_minAmountPerBuyer needs to be smaller or equal to _maxAmountPerBuyer" ); require(_tokenPrice != 0, "_tokenPrice needs to be a non-zero amount"); - require( - _maxAmountOfTokenToBeSold != 0, - "_maxAmountOfTokenToBeSold needs to be larger than zero" - ); + require(_maxAmountOfTokenToBeSold != 0, "_maxAmountOfTokenToBeSold needs to be larger than zero"); // after creating the contract, it needs a minting allowance (in the token contract) } /** - @notice buy tokens - @param _amount amount of tokens to buy, in bits (smallest subunit of token) - @param _tokenReceiver address the tokens should be minted to + * @notice Buy `amount` tokens and mint them to `_tokenReceiver`. + * @param _amount amount of tokens to buy, in bits (smallest subunit of token) + * @param _tokenReceiver address the tokens should be minted to */ function buy( uint256 _amount, - address _tokenReceiver + address _tokenReceiver, + MoneriumI _moneriumAccount ) external whenNotPaused nonReentrant { - require( - tokensSold + _amount <= maxAmountOfTokenToBeSold, - "Not enough tokens to sell left" - ); - require( - tokensBought[_tokenReceiver] + _amount >= minAmountPerBuyer, - "Buyer needs to buy at least minAmount" - ); + require(tokensSold + _amount <= maxAmountOfTokenToBeSold, "Not enough tokens to sell left"); + require(tokensBought[_tokenReceiver] + _amount >= minAmountPerBuyer, "Buyer needs to buy at least minAmount"); require( tokensBought[_tokenReceiver] + _amount <= maxAmountPerBuyer, "Total amount of bought tokens needs to be lower than or equal to maxAmount" ); + address sender; + + if (_moneriumAccount == MoneriumI(address(0x00))) { + sender = _msgSender(); + } else { + address investor = _moneriumAccount.investor(); + require(_msgSender() == investor); + sender = investor; + } + tokensSold += _amount; tokensBought[_tokenReceiver] += _amount; // rounding up to the next whole number. Investor is charged up to one currency bit more in case of a fractional currency bit. - uint256 currencyAmount = Math.ceilDiv( - _amount * tokenPrice, - 10 ** token.decimals() - ); + uint256 currencyAmount = Math.ceilDiv(_amount * tokenPrice, 10 ** token.decimals()); IFeeSettingsV1 feeSettings = token.feeSettings(); uint256 fee = feeSettings.continuousFundraisingFee(currencyAmount); if (fee != 0) { - currency.safeTransferFrom( - _msgSender(), - feeSettings.feeCollector(), - fee - ); + currency.safeTransferFrom(sender, feeSettings.feeCollector(), fee); } - currency.safeTransferFrom( - _msgSender(), - currencyReceiver, - currencyAmount - fee - ); + currency.safeTransferFrom(sender, currencyReceiver, currencyAmount - fee); token.mint(_tokenReceiver, _amount); - emit TokensBought(_msgSender(), _amount, currencyAmount); + emit TokensBought(sender, _amount, currencyAmount); } /** - @notice change the currencyReceiver - @param _currencyReceiver new currencyReceiver + * @notice change the currencyReceiver to `_currencyReceiver` + * @param _currencyReceiver new currencyReceiver */ - function setCurrencyReceiver( - address _currencyReceiver - ) external onlyOwner whenPaused { - require( - _currencyReceiver != address(0), - "receiver can not be zero address" - ); + function setCurrencyReceiver(address _currencyReceiver) external onlyOwner whenPaused { + require(_currencyReceiver != address(0), "receiver can not be zero address"); currencyReceiver = _currencyReceiver; emit CurrencyReceiverChanged(_currencyReceiver); coolDownStart = block.timestamp; } /** - @notice change the minAmountPerBuyer - @param _minAmountPerBuyer new minAmountPerBuyer + * @notice change the minAmountPerBuyer to `_minAmountPerBuyer` + * @param _minAmountPerBuyer new minAmountPerBuyer */ - function setMinAmountPerBuyer( - uint256 _minAmountPerBuyer - ) external onlyOwner whenPaused { - require( - _minAmountPerBuyer <= maxAmountPerBuyer, - "_minAmount needs to be smaller or equal to maxAmount" - ); + function setMinAmountPerBuyer(uint256 _minAmountPerBuyer) external onlyOwner whenPaused { + require(_minAmountPerBuyer <= maxAmountPerBuyer, "_minAmount needs to be smaller or equal to maxAmount"); minAmountPerBuyer = _minAmountPerBuyer; emit MinAmountPerBuyerChanged(_minAmountPerBuyer); coolDownStart = block.timestamp; } /** - @notice change the maxAmountPerBuyer - @param _maxAmountPerBuyer new maxAmountPerBuyer + * @notice change the maxAmountPerBuyer to `_maxAmountPerBuyer` + * @param _maxAmountPerBuyer new maxAmountPerBuyer */ - function setMaxAmountPerBuyer( - uint256 _maxAmountPerBuyer - ) external onlyOwner whenPaused { - require( - minAmountPerBuyer <= _maxAmountPerBuyer, - "_maxAmount needs to be larger or equal to minAmount" - ); + function setMaxAmountPerBuyer(uint256 _maxAmountPerBuyer) external onlyOwner whenPaused { + require(minAmountPerBuyer <= _maxAmountPerBuyer, "_maxAmount needs to be larger or equal to minAmount"); maxAmountPerBuyer = _maxAmountPerBuyer; emit MaxAmountPerBuyerChanged(_maxAmountPerBuyer); coolDownStart = block.timestamp; } /** - @notice change currency and tokenPrice - @param _currency new currency - @param _tokenPrice new tokenPrice + * @notice change currency to `_currency` and tokenPrice to `_tokenPrice` + * @param _currency new currency + * @param _tokenPrice new tokenPrice */ - function setCurrencyAndTokenPrice( - IERC20 _currency, - uint256 _tokenPrice - ) external onlyOwner whenPaused { + function setCurrencyAndTokenPrice(IERC20 _currency, uint256 _tokenPrice) external onlyOwner whenPaused { require(_tokenPrice != 0, "_tokenPrice needs to be a non-zero amount"); tokenPrice = _tokenPrice; currency = _currency; @@ -232,23 +211,18 @@ contract ContinuousFundraising is } /** - @notice change the maxAmountOfTokenToBeSold - @param _maxAmountOfTokenToBeSold new maxAmountOfTokenToBeSold + * @notice change the maxAmountOfTokenToBeSold to `_maxAmountOfTokenToBeSold` + * @param _maxAmountOfTokenToBeSold new maxAmountOfTokenToBeSold */ - function setMaxAmountOfTokenToBeSold( - uint256 _maxAmountOfTokenToBeSold - ) external onlyOwner whenPaused { - require( - _maxAmountOfTokenToBeSold != 0, - "_maxAmountOfTokenToBeSold needs to be larger than zero" - ); + function setMaxAmountOfTokenToBeSold(uint256 _maxAmountOfTokenToBeSold) external onlyOwner whenPaused { + require(_maxAmountOfTokenToBeSold != 0, "_maxAmountOfTokenToBeSold needs to be larger than zero"); maxAmountOfTokenToBeSold = _maxAmountOfTokenToBeSold; emit MaxAmountOfTokenToBeSoldChanged(_maxAmountOfTokenToBeSold); coolDownStart = block.timestamp; } /** - @notice pause the contract + * @notice pause the contract */ function pause() external onlyOwner { _pause(); @@ -256,37 +230,24 @@ contract ContinuousFundraising is } /** - @notice unpause the contract + * @notice unpause the contract */ function unpause() external onlyOwner { - require( - block.timestamp > coolDownStart + delay, - "There needs to be at minimum one day to change parameters" - ); + require(block.timestamp > coolDownStart + delay, "There needs to be at minimum one day to change parameters"); _unpause(); } /** * @dev both Ownable and ERC2771Context have a _msgSender() function, so we need to override and select which one to use. */ - function _msgSender() - internal - view - override(Context, ERC2771Context) - returns (address) - { + function _msgSender() internal view override(Context, ERC2771Context) returns (address) { return ERC2771Context._msgSender(); } /** * @dev both Ownable and ERC2771Context have a _msgData() function, so we need to override and select which one to use. */ - function _msgData() - internal - view - override(Context, ERC2771Context) - returns (bytes calldata) - { + function _msgData() internal view override(Context, ERC2771Context) returns (bytes calldata) { return ERC2771Context._msgData(); } } diff --git a/contracts/FeeSettings.sol b/contracts/FeeSettings.sol index 8894fccb..a15e6818 100644 --- a/contracts/FeeSettings.sol +++ b/contracts/FeeSettings.sol @@ -5,146 +5,158 @@ import "@openzeppelin/contracts/access/Ownable2Step.sol"; import "@openzeppelin/contracts/utils/introspection/ERC165.sol"; import "./interfaces/IFeeSettings.sol"; -/* - This FeeSettings contract is used to manage fees paid to the tokenize.it platfom -*/ +/** + * @title FeeSettings + * @author malteish, cjentzsch + * @notice The FeeSettings contract is used to manage fees paid to the tokenize.it platfom + */ contract FeeSettings is Ownable2Step, ERC165, IFeeSettingsV1 { - /// @notice Denominator to calculate fees paid in Token.sol. UINT256_MAX means no fees. + /// Denominator to calculate fees paid in Token.sol. UINT256_MAX means no fees. uint256 public tokenFeeDenominator; - /// @notice Denominator to calculate fees paid in ContinuousFundraising.sol. UINT256_MAX means no fees. + /// Denominator to calculate fees paid in ContinuousFundraising.sol. UINT256_MAX means no fees. uint256 public continuousFundraisingFeeDenominator; - /// @notice Denominator to calculate fees paid in PersonalInvite.sol. UINT256_MAX means no fees. + /// Denominator to calculate fees paid in PersonalInvite.sol. UINT256_MAX means no fees. uint256 public personalInviteFeeDenominator; - /// @notice address the fees have to be paid to + /// address the fees have to be paid to address public feeCollector; - /// @notice new fee settings that can be activated (after a delay in case of fee increase) + /// new fee settings that can be activated (after a delay in case of fee increase) Fees public proposedFees; + /** + * @notice Fee denominators have been set to the following values: `tokenFeeDenominator`, `continuousFundraisingFeeDenominator`, `personalInviteFeeDenominator` + * @param tokenFeeDenominator Defines the fee paid in Token.sol. UINT256_MAX means no fees. + * @param continuousFundraisingFeeDenominator Defines the fee paid in ContinuousFundraising.sol. UINT256_MAX means no fees. + * @param personalInviteFeeDenominator Defines the fee paid in PersonalInvite.sol. UINT256_MAX means no fees. + */ event SetFeeDenominators( uint256 tokenFeeDenominator, uint256 continuousFundraisingFeeDenominator, uint256 personalInviteFeeDenominator ); + + /** + * @notice The fee collector has been changed to `newFeeCollector` + * @param newFeeCollector The new fee collector + */ event FeeCollectorChanged(address indexed newFeeCollector); + + /** + * @notice A fee change has been proposed + * @param proposal The new fee settings that have been proposed + */ event ChangeProposed(Fees proposal); + /** + * @notice Initializes the contract with the given fee denominators and fee collector + * @param _fees The initial fee denominators + * @param _feeCollector The initial fee collector + */ constructor(Fees memory _fees, address _feeCollector) { checkFeeLimits(_fees); tokenFeeDenominator = _fees.tokenFeeDenominator; - continuousFundraisingFeeDenominator = _fees - .continuousFundraisingFeeDenominator; + continuousFundraisingFeeDenominator = _fees.continuousFundraisingFeeDenominator; personalInviteFeeDenominator = _fees.personalInviteFeeDenominator; require(_feeCollector != address(0), "Fee collector cannot be 0x0"); feeCollector = _feeCollector; } + /** + * @notice Prepares a fee change. Fee increases are subject to a minimum delay of 12 weeks, while fee reductions can be executed immediately. + * @dev reducing fees = increasing the denominator + * @param _fees The new fee denominators + */ function planFeeChange(Fees memory _fees) external onlyOwner { checkFeeLimits(_fees); - // Reducing fees is possible immediately. Increasing fees can only be executed after a minimum of 12 weeks. - // Beware: reducing fees = increasing the denominator // if at least one fee increases, enforce minimum delay if ( _fees.tokenFeeDenominator < tokenFeeDenominator || - _fees.continuousFundraisingFeeDenominator < - continuousFundraisingFeeDenominator || + _fees.continuousFundraisingFeeDenominator < continuousFundraisingFeeDenominator || _fees.personalInviteFeeDenominator < personalInviteFeeDenominator ) { - require( - _fees.time > block.timestamp + 12 weeks, - "Fee change must be at least 12 weeks in the future" - ); + require(_fees.time > block.timestamp + 12 weeks, "Fee change must be at least 12 weeks in the future"); } proposedFees = _fees; emit ChangeProposed(_fees); } + /** + * @notice Executes a fee change that has been planned before + */ function executeFeeChange() external onlyOwner { - require( - block.timestamp >= proposedFees.time, - "Fee change must be executed after the change time" - ); + require(block.timestamp >= proposedFees.time, "Fee change must be executed after the change time"); tokenFeeDenominator = proposedFees.tokenFeeDenominator; - continuousFundraisingFeeDenominator = proposedFees - .continuousFundraisingFeeDenominator; - personalInviteFeeDenominator = proposedFees - .personalInviteFeeDenominator; - emit SetFeeDenominators( - tokenFeeDenominator, - continuousFundraisingFeeDenominator, - personalInviteFeeDenominator - ); + continuousFundraisingFeeDenominator = proposedFees.continuousFundraisingFeeDenominator; + personalInviteFeeDenominator = proposedFees.personalInviteFeeDenominator; + emit SetFeeDenominators(tokenFeeDenominator, continuousFundraisingFeeDenominator, personalInviteFeeDenominator); delete proposedFees; } + /** + * @notice Sets a new fee collector + * @param _feeCollector The new fee collector + */ function setFeeCollector(address _feeCollector) external onlyOwner { require(_feeCollector != address(0), "Fee collector cannot be 0x0"); feeCollector = _feeCollector; emit FeeCollectorChanged(_feeCollector); } + /** + * @notice Checks if the given fee settings are valid + * @param _fees The fees to check + */ function checkFeeLimits(Fees memory _fees) internal pure { - require( - _fees.tokenFeeDenominator >= 20, - "Fee must be equal or less 5% (denominator must be >= 20)" - ); + require(_fees.tokenFeeDenominator >= 20, "Fee must be equal or less 5% (denominator must be >= 20)"); require( _fees.continuousFundraisingFeeDenominator >= 20, "Fee must be equal or less 5% (denominator must be >= 20)" ); - require( - _fees.personalInviteFeeDenominator >= 20, - "Fee must be equal or less 5% (denominator must be >= 20)" - ); + require(_fees.personalInviteFeeDenominator >= 20, "Fee must be equal or less 5% (denominator must be >= 20)"); } /** - @notice Returns the fee for a given token amount - @dev will wrongly return 1 if denominator and amount are both uint256 max + * @notice Returns the fee for a given token amount + * @dev will wrongly return 1 if denominator and amount are both uint256 max */ function tokenFee(uint256 _tokenAmount) external view returns (uint256) { return _tokenAmount / tokenFeeDenominator; } /** - @notice Returns the fee for a given currency amount - @dev will wrongly return 1 if denominator and amount are both uint256 max + * @notice Calculates the fee for a given currency amount in ContinuousFundraising.sol + * @dev will wrongly return 1 if denominator and amount are both uint256 max + * @param _currencyAmount The amount of currency to calculate the fee for + * @return The fee */ - function continuousFundraisingFee( - uint256 _currencyAmount - ) external view returns (uint256) { + function continuousFundraisingFee(uint256 _currencyAmount) external view returns (uint256) { return _currencyAmount / continuousFundraisingFeeDenominator; } - /** - @notice Returns the fee for a given currency amount - @dev will wrongly return 1 if denominator and amount are both uint256 max + /** + * @notice Calculates the fee for a given currency amount in PersonalInvite.sol + * @dev will wrongly return 1 if denominator and amount are both uint256 max + * @param _currencyAmount The amount of currency to calculate the fee for + * @return The fee */ - function personalInviteFee( - uint256 _currencyAmount - ) external view returns (uint256) { + function personalInviteFee(uint256 _currencyAmount) external view returns (uint256) { return _currencyAmount / personalInviteFeeDenominator; } /** - * Specify where the implementation of owner() is located + * @dev Specify where the implementation of owner() is located + * @return The owner of the contract */ - function owner() - public - view - override(Ownable, IFeeSettingsV1) - returns (address) - { + function owner() public view override(Ownable, IFeeSettingsV1) returns (address) { return Ownable.owner(); } /** * @notice This contract implements the ERC165 interface in order to enable other contracts to query which interfaces this contract implements. * @dev See https://eips.ethereum.org/EIPS/eip-165 + * @return `true` for supported interfaces, otherwise `false` */ - function supportsInterface( - bytes4 interfaceId - ) public view virtual override(ERC165, IFeeSettingsV1) returns (bool) { + function supportsInterface(bytes4 interfaceId) public view virtual override(ERC165, IFeeSettingsV1) returns (bool) { return interfaceId == type(IFeeSettingsV1).interfaceId || // we implement IFeeSettingsV1 ERC165.supportsInterface(interfaceId); // default implementation that enables further querying diff --git a/contracts/MoneriumConstructorOnly.sol b/contracts/MoneriumConstructorOnly.sol new file mode 100644 index 00000000..03a868bd --- /dev/null +++ b/contracts/MoneriumConstructorOnly.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity 0.8.17; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "../contracts/MoneriumI.sol"; + +/* +The contract is deployed by the fundraising company and is getting an IBAN by Monerium. By sending EUR to this IBAN, the investors agrees to the investment. +The then minted EURe are approved for the corresponding investment contract. In case of an personal invite, it can be executed by the company. +In ces of a continuous fundraising, the investor can now call the `buy`function in order to make an investment. +This contract is inteneded to be used only once per investment per investor. +*/ + +contract MoneriumInterfacePersonalInvite is MoneriumI { + address public investor; + + constructor(address _investor, address _investment, uint256 _amount) { + investor = _investor; + IERC20(0x3231Cb76718CDeF2155FC47b5286d82e6eDA273f).approve(address(_investment), _amount); + } + + /* warning: In the case the investment contract does not work anymore (expired personal invites, paused fundraising, ...). All the EURe in this contract are techincally stuck. + But since this IBAN belongs to the company, they can make a IBAN transfer back to the investor using the Monerium WebApp. Monerium will then burn the tokens belonging to this address. + */ +} diff --git a/contracts/MoneriumI.sol b/contracts/MoneriumI.sol new file mode 100644 index 00000000..86422575 --- /dev/null +++ b/contracts/MoneriumI.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +interface MoneriumI { + function investor() external returns (address); +} diff --git a/contracts/MoneriumInterface.sol b/contracts/MoneriumInterface.sol new file mode 100644 index 00000000..2fadf729 --- /dev/null +++ b/contracts/MoneriumInterface.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity 0.8.17; + +import "@openzeppelin/contracts/access/Ownable2Step.sol"; +import "../contracts/PersonalInvite.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "../contracts/MoneriumI.sol"; + +/* +The contract is deployed by the fundraising company and is getting an IBAN by Monerium. By sending EUR to this IBAN, the investors is able to approve funds to investment contract which are added by the fundraising company. +In case of an personal invite, it can be executed by the company. +In ces of a continuous fundraising, the investor can now call the `buy`function in order to make an investment. +This contract is inteneded to be used for all investments into one specific fundraising company (`owner`) by a single investor. +*/ + +contract MoneriumInterface is MoneriumI, Ownable2Step { + mapping(address => bool) public map; + + event Set(address indexed key, bool value); + + IERC20 constant EURe = IERC20(0x3231Cb76718CDeF2155FC47b5286d82e6eDA273f); // ToDO get address from ENS + address public investor; + + constructor(address _investor) { + investor = _investor; + } + + function addInvestmentContract(address _addr, bool _active) external onlyOwner { + map[_addr] = _active; + emit Set(_addr, _active); + } + + function approve(address _investment, uint256 _amount) external { + require(msg.sender == investor); + require(map[_investment]); + EURe.approve(address(_investment), _amount); + } + + /** + @notice purges an address from the map + @dev this is a convenience function, it is equivalent to calling addInvestmentContract(_addr, 0) + */ + function remove(address _addr) external onlyOwner { + delete map[_addr]; + emit Set(_addr, false); + } + + // called in case money is stuck in the contract due to malfunction of investment contracts or missing approve calls // TODO do we really need this? + function emergency() external onlyOwner { + EURe.transfer(owner(), EURe.balanceOf(address(this))); + } +} diff --git a/contracts/PersonalInvite.sol b/contracts/PersonalInvite.sol index 9fd5f333..8aa41d40 100644 --- a/contracts/PersonalInvite.sol +++ b/contracts/PersonalInvite.sol @@ -6,27 +6,39 @@ import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "./Token.sol"; /** -@notice This contract represents the offer to buy an amount of tokens at a preset price. It is created for a specific buyer and can only be claimed once and only by that buyer. - All parameters of the invitation (currencyPayer, tokenReceiver, currencyReceiver, tokenAmount, tokenPrice, currency, token) are immutable (see description of CREATE2). - It is likely a company will create many PersonalInvites for specific investors to buy their one token. - The use of CREATE2 (https://docs.openzeppelin.com/cli/2.8/deploying-with-create2) enables this invitation to be privacy preserving until it is accepted through - granting of an allowance to the PersonalInvite's future address and deployment of the PersonalInvite. -@dev This contract is deployed using CREATE2 (https://docs.openzeppelin.com/cli/2.8/deploying-with-create2), using a deploy factory. That makes the future address of this contract - deterministic: it can be computed from the parameters of the invitation. This allows the company and buyer to grant allowances to the future address of this contract - before it is deployed. - The process of deploying this contract is as follows: - 1. Company and investor agree on the terms of the invitation (currencyPayer, tokenReceiver, currencyReceiver, tokenAmount, tokenPrice, currency, token) - and a salt (used for deployment only). - 2. With the help of a deploy factory, the company computes the future address of the PersonalInvite contract. - 3. The company grants a token minting allowance of amount to the future address of the PersonalInvite contract. - 4. The investor grants a currency allowance of amount*tokenPrice / 10**tokenDecimals to the future address of the PersonalInvite contract, using their currencyPayer address. - 5. Finally, company, buyer or anyone else deploys the PersonalInvite contract using the deploy factory. - Because all of the execution logic is in the constructor, the deployment of the PersonalInvite contract is the last step. During the deployment, the newly - minted tokens will be transferred to the buyer and the currency will be transferred to the company's receiver address. + * @title PersonalInvite + * @author malteish, cjentzsch + * @notice This contract represents the offer to buy an amount of tokens at a preset price. It is created for a specific buyer and can only be claimed once and only by that buyer. + * All parameters of the invitation (currencyPayer, tokenReceiver, currencyReceiver, tokenAmount, tokenPrice, currency, token) are immutable (see description of CREATE2). + * It is likely a company will create many PersonalInvites for specific investors to buy their one token. + * The use of CREATE2 (https://docs.openzeppelin.com/cli/2.8/deploying-with-create2) enables this invitation to be privacy preserving until it is accepted through + * granting of an allowance to the PersonalInvite's future address and deployment of the PersonalInvite. + * @dev This contract is deployed using CREATE2 (https://docs.openzeppelin.com/cli/2.8/deploying-with-create2), using a deploy factory. That makes the future address of this contract + * deterministic: it can be computed from the parameters of the invitation. This allows the company and buyer to grant allowances to the future address of this contract + * before it is deployed. + * The process of deploying this contract is as follows: + * 1. Company and investor agree on the terms of the invitation (currencyPayer, tokenReceiver, currencyReceiver, tokenAmount, tokenPrice, currency, token) + * and a salt (used for deployment only). + * 2. With the help of a deploy factory, the company computes the future address of the PersonalInvite contract. + * 3. The company grants a token minting allowance of amount to the future address of the PersonalInvite contract. + * 4. The investor grants a currency allowance of amount*tokenPrice / 10**tokenDecimals to the future address of the PersonalInvite contract, using their currencyPayer address. + * 5. Finally, company, buyer or anyone else deploys the PersonalInvite contract using the deploy factory. + * Because all of the execution logic is in the constructor, the deployment of the PersonalInvite contract is the last step. During the deployment, the newly + * minted tokens will be transferred to the buyer and the currency will be transferred to the company's receiver address. */ contract PersonalInvite { using SafeERC20 for IERC20; + /** + * @notice Emitted when a PersonalInvite is deployed. `currencyPayer` paid for `tokenAmount` tokens at `tokenPrice` per token. The tokens were minted to `tokenReceiver`. + * The token is deployed at `token` and the currency is `currency`. + * @param currencyPayer address that paid the currency + * @param tokenReceiver address that received the tokens + * @param tokenAmount amount of tokens that were bought + * @param tokenPrice price company and investor agreed on + * @param currency currency used for payment + * @param token contract of the token that was bought + */ event Deal( address indexed currencyPayer, address indexed tokenReceiver, @@ -57,51 +69,24 @@ contract PersonalInvite { IERC20 _currency, Token _token ) { - require( - _currencyPayer != address(0), - "_currencyPayer can not be zero address" - ); - require( - _tokenReceiver != address(0), - "_tokenReceiver can not be zero address" - ); - require( - _currencyReceiver != address(0), - "_currencyReceiver can not be zero address" - ); + require(_currencyPayer != address(0), "_currencyPayer can not be zero address"); + require(_tokenReceiver != address(0), "_tokenReceiver can not be zero address"); + require(_currencyReceiver != address(0), "_currencyReceiver can not be zero address"); require(_tokenPrice != 0, "_tokenPrice can not be zero"); require(block.timestamp <= _expiration, "Deal expired"); // rounding up to the next whole number. Investor is charged up to one currency bit more in case of a fractional currency bit. - uint256 currencyAmount = Math.ceilDiv( - _tokenAmount * _tokenPrice, - 10 ** _token.decimals() - ); + uint256 currencyAmount = Math.ceilDiv(_tokenAmount * _tokenPrice, 10 ** _token.decimals()); IFeeSettingsV1 feeSettings = _token.feeSettings(); uint256 fee = feeSettings.personalInviteFee(currencyAmount); if (fee != 0) { - _currency.safeTransferFrom( - _currencyPayer, - feeSettings.feeCollector(), - fee - ); + _currency.safeTransferFrom(_currencyPayer, feeSettings.feeCollector(), fee); } - _currency.safeTransferFrom( - _currencyPayer, - _currencyReceiver, - (currencyAmount - fee) - ); + _currency.safeTransferFrom(_currencyPayer, _currencyReceiver, (currencyAmount - fee)); _token.mint(_tokenReceiver, _tokenAmount); - emit Deal( - _currencyPayer, - _tokenReceiver, - _tokenAmount, - _tokenPrice, - _currency, - _token - ); + emit Deal(_currencyPayer, _tokenReceiver, _tokenAmount, _tokenPrice, _currency, _token); } } diff --git a/contracts/PersonalInviteFactory.sol b/contracts/PersonalInviteFactory.sol index 5e751b72..170a214f 100644 --- a/contracts/PersonalInviteFactory.sol +++ b/contracts/PersonalInviteFactory.sol @@ -7,14 +7,27 @@ import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; import "@openzeppelin/contracts/utils/Create2.sol"; import "../contracts/PersonalInvite.sol"; -/* - One deployment of this contract can be used for deployment of any number of PersonalInvites using create2. -*/ +/** + * @title PersonalInviteFactory + * @author malteish, cjentzsch + * @notice This contract deploys PersonalInvites using create2. It is used to deploy PersonalInvites with a deterministic address. + * @dev One deployment of this contract can be used for deployment of any number of PersonalInvites using create2. + */ contract PersonalInviteFactory { event Deploy(address indexed addr); /** - * @notice Deploys a contract using create2. + * @notice Deploys a contract using create2. During the deployment, `_currencyPayer` pays `_currencyReceiver` for the purchase of `_tokenAmount` tokens at `_tokenPrice` per token. + * The tokens are minted to `_tokenReceiver`. The token is deployed at `_token` and the currency is `_currency`. + * @param _salt salt used for privacy. Could be used for vanity addresses, too. + * @param _currencyPayer address holding the currency. Must have given sufficient allowance to this contract. + * @param _tokenReceiver address receiving the tokens + * @param _currencyReceiver address receiving the currency + * @param _tokenAmount amount of tokens to be minted + * @param _tokenPrice price of one token in currency + * @param _expiration timestamp after which the contract is no longer valid + * @param _currency address of the currency + * @param _token address of the token */ function deploy( bytes32 _salt, @@ -48,6 +61,16 @@ contract PersonalInviteFactory { /** * @notice Computes the address of a contract to be deployed using create2. + * @param _salt salt used for privacy. Could be used for vanity addresses, too. + * @param _currencyPayer address holding the currency. Must have given sufficient allowance to this contract. + * @param _tokenReceiver address receiving the tokens + * @param _currencyReceiver address receiving the currency + * @param _amount amount of tokens to be minted + * @param _tokenPrice price of one token in currency + * @param _expiration timestamp after which the contract is no longer valid + * @param _currency address of the currency + * @param _token address of the token + * @return address of the contract to be deployed */ function getAddress( bytes32 _salt, @@ -73,6 +96,10 @@ contract PersonalInviteFactory { return Create2.computeAddress(_salt, keccak256(bytecode)); } + /** + * @dev Generates the bytecode of the contract to be deployed, using the parameters. + * @return bytecode of the contract to be deployed. + */ function getBytecode( address _currencyPayer, address _tokenReceiver, diff --git a/contracts/Token.sol b/contracts/Token.sol index d416a6a4..71bde552 100644 --- a/contracts/Token.sol +++ b/contracts/Token.sol @@ -9,17 +9,17 @@ import "./AllowList.sol"; import "./interfaces/IFeeSettings.sol"; /** -@title tokenize.it Token -@notice This contract implements the token used to tokenize companies, which follows the ERC20 standard and adds the following features: - - pausing - - access control with dedicated roles - - burning (burner role can burn any token from any address) - - requirements for sending and receiving tokens - - allow list (documents which address satisfies which requirement) - Decimals is inherited as 18 from ERC20. This should be the standard to adhere by for all deployments of this token. - - The contract inherits from ERC2771Context in order to be usable with Gas Station Network (GSN) https://docs.opengsn.org/faq/troubleshooting.html#my-contract-is-using-openzeppelin-how-do-i-add-gsn-support and meta-transactions. - + * @title tokenize.it Token + * @author malteish, cjentzsch + * @notice This contract implements the token used to tokenize companies, which follows the ERC20 standard and adds the following features: + * - pausing + * - access control with dedicated roles + * - burning (burner role can burn any token from any address) + * - requirements for sending and receiving tokens + * - allow list (documents which address satisfies which requirement) + * Decimals is inherited as 18 from ERC20. This should be the standard to adhere by for all deployments of this token. + * + * @dev The contract inherits from ERC2771Context in order to be usable with Gas Station Network (GSN) https://docs.opengsn.org/faq/troubleshooting.html#my-contract-is-using-openzeppelin-how-do-i-add-gsn-support and meta-transactions. */ contract Token is ERC2771Context, ERC20Permit, Pausable, AccessControl { /// @notice The role that has the ability to define which requirements an address must satisfy to receive tokens @@ -29,8 +29,7 @@ contract Token is ERC2771Context, ERC20Permit, Pausable, AccessControl { /// @notice The role that has the ability to burn tokens from anywhere. Usage is planned for legal purposes and error recovery. bytes32 public constant BURNER_ROLE = keccak256("BURNER_ROLE"); /// @notice The role that has the ability to grant transfer rights to other addresses - bytes32 public constant TRANSFERERADMIN_ROLE = - keccak256("TRANSFERERADMIN_ROLE"); + bytes32 public constant TRANSFERERADMIN_ROLE = keccak256("TRANSFERERADMIN_ROLE"); /// @notice Addresses with this role do not need to satisfy any requirements to send or receive tokens bytes32 public constant TRANSFERER_ROLE = keccak256("TRANSFERER_ROLE"); /// @notice The role that has the ability to pause the token. Transferring, burning and minting will not be possible while the contract is paused. @@ -45,58 +44,63 @@ contract Token is ERC2771Context, ERC20Permit, Pausable, AccessControl { // Suggested new fee settings, which will be applied after admin approval IFeeSettingsV1 public suggestedFeeSettings; /** - @notice defines requirements to send or receive tokens for non-TRANSFERER_ROLE. If zero, everbody can transfer the token. If non-zero, then only those who have met the requirements can send or receive tokens. - Requirements can be defined by the REQUIREMENT_ROLE, and are validated against the allowList. They can include things like "must have a verified email address", "must have a verified phone number", "must have a verified identity", etc. - Also, tiers from 0 to four can be used. - @dev Requirements are defined as bit mask, with the bit position encoding it's meaning and the bit's value whether this requirement will be enforced. - Example: - - position 0: 1 = must be KYCed (0 = no KYC required) - - position 1: 1 = must be american citizen (0 = american citizenship not required) - - position 2: 1 = must be a penguin (0 = penguin status not required) - These meanings are not defined within code, neither in the token contract nor the allowList. Nevertheless, the definition used by the people responsible for both contracts MUST match, - or the token contract will not work as expected. E.g. if the allowList defines position 2 as "is a penguin", while the token contract uses position 2 as "is a hedgehog", then the tokens - might be sold to hedgehogs, which was never the intention. - Here some examples of how requirements can be used in practice: - With requirements 0b0000000000000000000000000000000000000000000000000000000000000101, only KYCed penguins will be allowed to send or receive tokens. - With requirements 0b0000000000000000000000000000000000000000000000000000000000000111, only KYCed american penguins will be allowed to send or receive tokens. - With requirements 0b0000000000000000000000000000000000000000000000000000000000000000, even french hedgehogs will be allowed to send or receive tokens. - - The highest four bits are defined as tiers as follows: - - 0b0000000000000000000000000000000000000000000000000000000000000000 = tier 0 is required - - 0b0001000000000000000000000000000000000000000000000000000000000000 = tier 1 is required - - 0b0010000000000000000000000000000000000000000000000000000000000000 = tier 2 is required - - 0b0100000000000000000000000000000000000000000000000000000000000000 = tier 3 is required - - 0b1000000000000000000000000000000000000000000000000000000000000000 = tier 4 is required - This very simple definition allows for a maximum of 5 tiers, even though 4 bits are used for encoding. By sacrificing some space it can be implemented without code changes. - - Keep in mind that addresses with the TRANSFERER_ROLE do not need to satisfy any requirements to send or receive tokens. - */ + * @notice defines requirements to send or receive tokens for non-TRANSFERER_ROLE. If zero, everbody can transfer the token. If non-zero, then only those who have met the requirements can send or receive tokens. + * Requirements can be defined by the REQUIREMENT_ROLE, and are validated against the allowList. They can include things like "must have a verified email address", "must have a verified phone number", "must have a verified identity", etc. + * Also, tiers from 0 to four can be used. + * @dev Requirements are defined as bit mask, with the bit position encoding it's meaning and the bit's value whether this requirement will be enforced. + * Example: + * - position 0: 1 = must be KYCed (0 = no KYC required) + * - position 1: 1 = must be american citizen (0 = american citizenship not required) + * - position 2: 1 = must be a penguin (0 = penguin status not required) + * These meanings are not defined within code, neither in the token contract nor the allowList. Nevertheless, the definition used by the people responsible for both contracts MUST match, + * or the token contract will not work as expected. E.g. if the allowList defines position 2 as "is a penguin", while the token contract uses position 2 as "is a hedgehog", then the tokens + * might be sold to hedgehogs, which was never the intention. + * Here some examples of how requirements can be used in practice: + * With requirements 0b0000000000000000000000000000000000000000000000000000000000000101, only KYCed penguins will be allowed to send or receive tokens. + * With requirements 0b0000000000000000000000000000000000000000000000000000000000000111, only KYCed american penguins will be allowed to send or receive tokens. + * With requirements 0b0000000000000000000000000000000000000000000000000000000000000000, even french hedgehogs will be allowed to send or receive tokens. + * + * The highest four bits are defined as tiers as follows: + * - 0b0000000000000000000000000000000000000000000000000000000000000000 = tier 0 is required + * - 0b0001000000000000000000000000000000000000000000000000000000000000 = tier 1 is required + * - 0b0010000000000000000000000000000000000000000000000000000000000000 = tier 2 is required + * - 0b0100000000000000000000000000000000000000000000000000000000000000 = tier 3 is required + * - 0b1000000000000000000000000000000000000000000000000000000000000000 = tier 4 is required + * + * Keep in mind that addresses with the TRANSFERER_ROLE do not need to satisfy any requirements to send or receive tokens. + */ uint256 public requirements; /** - @notice defines the maximum amount of tokens that can be minted by a specific address. If zero, no tokens can be minted. - Tokens paid as fees, as specified in the `feeSettings` contract, do not require an allowance. - Example: Fee is set to 1% and mintingAllowance is 100. When executing the `mint` function with 100 as `amount`, - 100 tokens will be minted to the `to` address, and 1 token to the feeCollector. - */ + * @notice defines the maximum amount of tokens that can be minted by a specific address. If zero, no tokens can be minted. + * Tokens paid as fees, as specified in the `feeSettings` contract, do not require an allowance. + * Example: Fee is set to 1% and mintingAllowance is 100. When executing the `mint` function with 100 as `amount`, + * 100 tokens will be minted to the `to` address, and 1 token to the feeCollector. + */ mapping(address => uint256) public mintingAllowance; // used for token generating events such as vesting or new financing rounds + /// @param newRequirements The new requirements that will be enforced from now on. event RequirementsChanged(uint newRequirements); + /// @param newAllowList The AllowList contract that is in use from now on. event AllowListChanged(AllowList indexed newAllowList); - event NewFeeSettingsSuggested(IFeeSettingsV1 indexed _feeSettings); + /// @param suggestedFeeSettings The FeeSettings contract that has been suggested, but not yet approved by the admin. + event NewFeeSettingsSuggested(IFeeSettingsV1 indexed suggestedFeeSettings); + /// @param newFeeSettings The FeeSettings contract that is in use from now on. event FeeSettingsChanged(IFeeSettingsV1 indexed newFeeSettings); + /// @param minter The address for which the minting allowance has been changed. + /// @param newAllowance The new minting allowance for the address (does not include fees). event MintingAllowanceChanged(address indexed minter, uint256 newAllowance); /** - @notice Constructor for the token - @param _trustedForwarder trusted forwarder for the ERC2771Context constructor - used for meta-transactions. OpenGSN v2 Forwarder should be used. - @param _feeSettings fee settings contract that determines the fee for minting tokens - @param _admin address of the admin. Admin will initially have all roles and can grant roles to other addresses. - @param _name name of the specific token, e.g. "MyGmbH Token" - @param _symbol symbol of the token, e.g. "MGT" - @param _allowList allowList contract that defines which addresses satisfy which requirements - @param _requirements requirements an address has to meet for sending or receiving tokens - */ + * @notice Constructor for the token. + * @param _trustedForwarder trusted forwarder for the ERC2771Context constructor - used for meta-transactions. OpenGSN v2 Forwarder should be used. + * @param _feeSettings fee settings contract that determines the fee for minting tokens + * @param _admin address of the admin. Admin will initially have all roles and can grant roles to other addresses. + * @param _name name of the specific token, e.g. "MyGmbH Token" + * @param _symbol symbol of the token, e.g. "MGT" + * @param _allowList allowList contract that defines which addresses satisfy which requirements + * @param _requirements requirements an address has to meet for sending or receiving tokens + */ constructor( address _trustedForwarder, IFeeSettingsV1 _feeSettings, @@ -105,11 +109,7 @@ contract Token is ERC2771Context, ERC20Permit, Pausable, AccessControl { uint256 _requirements, string memory _name, string memory _symbol - ) - ERC2771Context(_trustedForwarder) - ERC20Permit(_name) - ERC20(_name, _symbol) - { + ) ERC2771Context(_trustedForwarder) ERC20Permit(_name) ERC20(_name, _symbol) { // Grant admin roles _grantRole(DEFAULT_ADMIN_ROLE, _admin); // except for the Transferer role, the _admin is the roles admin for all other roles _setRoleAdmin(TRANSFERER_ROLE, TRANSFERERADMIN_ROLE); @@ -126,30 +126,29 @@ contract Token is ERC2771Context, ERC20Permit, Pausable, AccessControl { feeSettings = _feeSettings; // set up allowList - require( - address(_allowList) != address(0), - "AllowList must not be zero address" - ); + require(address(_allowList) != address(0), "AllowList must not be zero address"); allowList = _allowList; // set requirements (can be 0 to allow everyone to send and receive tokens) requirements = _requirements; } - function setAllowList( - AllowList _allowList - ) external onlyRole(DEFAULT_ADMIN_ROLE) { - require( - address(_allowList) != address(0), - "AllowList must not be zero address" - ); + /** + * @notice Change the AllowList that defines which addresses satisfy which requirements to `_allowList`. + * @dev An interface check is not necessary because AllowList can not brick the token like FeeSettings could. + * @param _allowList new AllowList contract + */ + function setAllowList(AllowList _allowList) external onlyRole(DEFAULT_ADMIN_ROLE) { + require(address(_allowList) != address(0), "AllowList must not be zero address"); allowList = _allowList; emit AllowListChanged(_allowList); } - function setRequirements( - uint256 _requirements - ) external onlyRole(REQUIREMENT_ROLE) { + /** + * @notice Change the requirements an address has to meet for sending or receiving tokens to `_requirements`. + * @param _requirements requirements an address has to meet for sending or receiving tokens + */ + function setRequirements(uint256 _requirements) external onlyRole(REQUIREMENT_ROLE) { requirements = _requirements; emit RequirementsChanged(_requirements); } @@ -158,63 +157,51 @@ contract Token is ERC2771Context, ERC20Permit, Pausable, AccessControl { * @notice This function can only be used by the feeSettings owner to suggest switching to a new feeSettings contract. * The new feeSettings contract will be applied immediately after admin approval. * @dev This is a possibility to change fees without honoring the delay enforced in the feeSettings contract. Therefore, approval of the admin is required. + * The feeSettings contract can brick the token, so an interface check is necessary. * @param _feeSettings the new feeSettings contract */ function suggestNewFeeSettings(IFeeSettingsV1 _feeSettings) external { - require( - _msgSender() == feeSettings.owner(), - "Only fee settings owner can suggest fee settings update" - ); + require(_msgSender() == feeSettings.owner(), "Only fee settings owner can suggest fee settings update"); _checkIfFeeSettingsImplementsInterface(_feeSettings); suggestedFeeSettings = _feeSettings; emit NewFeeSettingsSuggested(_feeSettings); } /** - * @notice This function can only be used by the admin to approve switching to the new feeSettings contract. + * @notice This function can only be used by the default admin to approve switching to the new feeSettings contract. * The new feeSettings contract will be applied immediately. - * @dev Enforcing the suggested and accepted new contract to be the same is not necessary, prevents frontrunning. - * Requiring not 0 prevent bricking the token. + * @dev Enforcing the suggested and accepted new contract to be the same is necessary to prevent frontrunning the acceptance with a new suggestion. + * Checking if the address implements the interface also prevents the 0 address from being accepted. * @param _feeSettings the new feeSettings contract */ - function acceptNewFeeSettings( - IFeeSettingsV1 _feeSettings - ) external onlyRole(DEFAULT_ADMIN_ROLE) { + function acceptNewFeeSettings(IFeeSettingsV1 _feeSettings) external onlyRole(DEFAULT_ADMIN_ROLE) { // after deployment, suggestedFeeSettings is 0x0. Therefore, this check is necessary, otherwise the admin could accept 0x0 as new feeSettings. // Checking that the suggestedFeeSettings is not 0x0 would work, too, but this check is used in other places, too. _checkIfFeeSettingsImplementsInterface(_feeSettings); - require( - _feeSettings == suggestedFeeSettings, - "Only suggested fee settings can be accepted" - ); + require(_feeSettings == suggestedFeeSettings, "Only suggested fee settings can be accepted"); feeSettings = suggestedFeeSettings; emit FeeSettingsChanged(_feeSettings); } - /** - @notice minting contracts such as personal investment invite, vesting, crowdfunding must be granted a minting allowance. - @notice the contract does not keep track of how many tokens a minter has minted over time - @param _minter address of the minter - @param _allowance how many tokens can be minted by this minter, in addition to their current allowance (excluding the tokens minted as a fee) - */ - function increaseMintingAllowance( - address _minter, - uint256 _allowance - ) external onlyRole(MINTALLOWER_ROLE) { + /** + * @notice Increase the amount of tokens `_minter` can mint by `_allowance`. Any address can be used, e.g. of an investment contract like PersonalInvite, a vesting contract, or an EOA. + * The contract does not keep track of how many tokens a minter has minted over time + * @param _minter address of the minter + * @param _allowance how many tokens can be minted by this minter, in addition to their current allowance (excluding the tokens minted as a fee) + */ + function increaseMintingAllowance(address _minter, uint256 _allowance) external onlyRole(MINTALLOWER_ROLE) { mintingAllowance[_minter] += _allowance; emit MintingAllowanceChanged(_minter, mintingAllowance[_minter]); } - /** - @dev underflow is cast to 0 in order to be able to use decreaseMintingAllowance(minter, UINT256_MAX) to reset the allowance to 0 - @param _minter address of the minter - @param _allowance how many tokens should be deducted from the current minting allowance (excluding the tokens minted as a fee) - */ - function decreaseMintingAllowance( - address _minter, - uint256 _allowance - ) external onlyRole(MINTALLOWER_ROLE) { + /** + * @notice Reduce the amount of tokens `_minter` can mint by `_allowance`. + * @dev Underflow is cast to 0 in order to be able to use decreaseMintingAllowance(minter, UINT256_MAX) to reset the allowance to 0. + * @param _minter address of the minter + * @param _allowance how many tokens should be deducted from the current minting allowance (excluding the tokens minted as a fee) + */ + function decreaseMintingAllowance(address _minter, uint256 _allowance) external onlyRole(MINTALLOWER_ROLE) { if (mintingAllowance[_minter] > _allowance) { mintingAllowance[_minter] -= _allowance; emit MintingAllowanceChanged(_minter, mintingAllowance[_minter]); @@ -224,12 +211,16 @@ contract Token is ERC2771Context, ERC20Permit, Pausable, AccessControl { } } + /** + * @notice Mint `_amount` tokens to `_to` and pay the fee to the fee collector + * @param _to address that receives the tokens + * @param _amount how many tokens to mint + */ function mint(address _to, uint256 _amount) external { - require( - mintingAllowance[_msgSender()] >= _amount, - "MintingAllowance too low" - ); - mintingAllowance[_msgSender()] -= _amount; + if (!hasRole(MINTALLOWER_ROLE, _msgSender())) { + require(mintingAllowance[_msgSender()] >= _amount, "MintingAllowance too low"); + mintingAllowance[_msgSender()] -= _amount; + } // this check is executed here, because later minting of the buy amount can not be differentiated from minting of the fee amount _checkIfAllowedToTransact(_to); _mint(_to, _amount); @@ -241,25 +232,23 @@ contract Token is ERC2771Context, ERC20Permit, Pausable, AccessControl { } } - function burn( - address _from, - uint256 _amount - ) external onlyRole(BURNER_ROLE) { + /** + * @notice Burn `_amount` tokens from `_from`. + * @param _from address that holds the tokens + * @param _amount how many tokens to burn + */ + function burn(address _from, uint256 _amount) external onlyRole(BURNER_ROLE) { _burn(_from, _amount); } /** - @notice There are 3 types of transfers: - 1. minting: transfers from the zero address to another address. Only minters can do this, which is checked in the mint function. The recipient must be allowed to transact. - 2. burning: transfers from an address to the zero address. Only burners can do this, which is checked in the burn function. - 3. transfers from one address to another. The sender and recipient must be allowed to transact. - @dev this hook is executed before the transfer function itself + * @notice There are 3 types of transfers: + * 1. minting: transfers from the zero address to another address. Only minters can do this, which is checked in the mint function. The recipient must be allowed to transact. + * 2. burning: transfers from an address to the zero address. Only burners can do this, which is checked in the burn function. + * 3. transfers from one address to another. The sender and recipient must be allowed to transact. + * @dev this hook is executed before the transfer function itself */ - function _beforeTokenTransfer( - address _from, - address _to, - uint256 _amount - ) internal virtual override { + function _beforeTokenTransfer(address _from, address _to, uint256 _amount) internal virtual override { super._beforeTokenTransfer(_from, _to, _amount); _requireNotPaused(); if (_from != address(0) && _to != address(0)) { @@ -277,34 +266,29 @@ contract Token is ERC2771Context, ERC20Permit, Pausable, AccessControl { } /** - * @notice checks if _address is a) a transferer or b) satisfies the requirements + * @notice checks if `_address` is either a transferer or satisfies the requirements. + * @param _address address to check */ function _checkIfAllowedToTransact(address _address) internal view { require( - hasRole(TRANSFERER_ROLE, _address) || + requirements == 0 || + hasRole(TRANSFERER_ROLE, _address) || allowList.map(_address) & requirements == requirements, "Sender or Receiver is not allowed to transact. Either locally issue the role as a TRANSFERER or they must meet requirements as defined in the allowList" ); } /** - * @notice Make sure the address posing as FeeSettings actually implements the interfaces that are needed. + * @notice Make sure `_feeSettings` actually implements the interfaces that are needed. * This is a sanity check to make sure that the FeeSettings contract is actually compatible with this token. * @dev This check uses EIP165, see https://eips.ethereum.org/EIPS/eip-165 + * @param _feeSettings address of the FeeSettings contract */ - function _checkIfFeeSettingsImplementsInterface( - IFeeSettingsV1 _feeSettings - ) internal view { + function _checkIfFeeSettingsImplementsInterface(IFeeSettingsV1 _feeSettings) internal view { // step 1: needs to return true if EIP165 is supported - require( - _feeSettings.supportsInterface(0x01ffc9a7) == true, - "FeeSettings must implement IFeeSettingsV1" - ); + require(_feeSettings.supportsInterface(0x01ffc9a7) == true, "FeeSettings must implement IFeeSettingsV1"); // step 2: needs to return false if EIP165 is supported - require( - _feeSettings.supportsInterface(0xffffffff) == false, - "FeeSettings must implement IFeeSettingsV1" - ); + require(_feeSettings.supportsInterface(0xffffffff) == false, "FeeSettings must implement IFeeSettingsV1"); // now we know EIP165 is supported // step 3: needs to return true if IFeeSettingsV1 is supported require( @@ -324,24 +308,14 @@ contract Token is ERC2771Context, ERC20Permit, Pausable, AccessControl { /** * @dev both ERC20Pausable and ERC2771Context have a _msgSender() function, so we need to override and select which one to use. */ - function _msgSender() - internal - view - override(Context, ERC2771Context) - returns (address) - { + function _msgSender() internal view override(Context, ERC2771Context) returns (address) { return ERC2771Context._msgSender(); } /** * @dev both ERC20Pausable and ERC2771Context have a _msgData() function, so we need to override and select which one to use. */ - function _msgData() - internal - view - override(Context, ERC2771Context) - returns (bytes calldata) - { + function _msgData() internal view override(Context, ERC2771Context) returns (bytes calldata) { return ERC2771Context._msgData(); } } diff --git a/contracts/VestingWalletFactory.sol b/contracts/VestingWalletFactory.sol new file mode 100644 index 00000000..e9f3d01b --- /dev/null +++ b/contracts/VestingWalletFactory.sol @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity 0.8.17; + +// taken from https://docs.alchemy.com/docs/create2-an-alternative-to-deriving-contract-addresses + +import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; +import "@openzeppelin/contracts/utils/Create2.sol"; +import "@openzeppelin/contracts/finance/VestingWallet.sol"; + +/** + * @title VestingWalletFactory + * @author malteish + * @notice This contract deploys VestingWallets using create2. + * @dev One deployment of this contract can be used for deployment of any number of VestingWallets using create2. + */ +contract VestingWalletFactory { + event Deploy(address indexed addr); + + /** + * @notice Deploys VestingWallet contract using create2. + * @param _salt salt used for privacy. Could be used for vanity addresses, too. + * @param beneficiaryAddress address receiving the tokens + * @param startTimestamp timestamp of when to start releasing tokens linearly + * @param durationSeconds duration of the vesting period in seconds + */ + function deploy( + bytes32 _salt, + address beneficiaryAddress, + uint64 startTimestamp, + uint64 durationSeconds + ) external returns (address) { + address actualAddress = Create2.deploy( + 0, + _salt, + getBytecode(beneficiaryAddress, startTimestamp, durationSeconds) + ); + + emit Deploy(actualAddress); + return actualAddress; + } + + /** + * @notice Computes the address of VestingWallet contract to be deployed using create2. + * @param _salt salt for vanity addresses + * @param beneficiaryAddress address receiving the tokens + * @param startTimestamp timestamp of when to start releasing tokens linearly + * @param durationSeconds duration of the vesting period in seconds + */ + function getAddress( + bytes32 _salt, + address beneficiaryAddress, + uint64 startTimestamp, + uint64 durationSeconds + ) external view returns (address) { + bytes memory bytecode = getBytecode(beneficiaryAddress, startTimestamp, durationSeconds); + return Create2.computeAddress(_salt, keccak256(bytecode)); + } + + /** + * @dev Generates the bytecode of the contract to be deployed, using the parameters. + * @param beneficiaryAddress address receiving the tokens + * @param startTimestamp timestamp of when to start releasing tokens linearly + * @param durationSeconds duration of the vesting period in seconds + * @return bytecode of the contract to be deployed. + */ + function getBytecode( + address beneficiaryAddress, + uint64 startTimestamp, + uint64 durationSeconds + ) private pure returns (bytes memory) { + return + abi.encodePacked( + type(VestingWallet).creationCode, + abi.encode(beneficiaryAddress, startTimestamp, durationSeconds) + ); + } +} diff --git a/docs/deployment.md b/docs/deployment.md index 81a12bbd..aabe4acd 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -26,6 +26,7 @@ Note: ```bash forge create --rpc-url $GOERLI_RPC_URL --private-key $PRIVATE_KEY --verify --etherscan-api-key=$ETHERSCAN_API_KEY contracts/AllowList.sol:AllowList forge create --rpc-url $GOERLI_RPC_URL --private-key $PRIVATE_KEY --verify --etherscan-api-key=$ETHERSCAN_API_KEY contracts/PersonalInviteFactory.sol:PersonalInviteFactory + forge create --rpc-url $GOERLI_RPC_URL --private-key $PRIVATE_KEY --verify --etherscan-api-key=$ETHERSCAN_API_KEY contracts/VestingWalletFactory.sol:VestingWalletFactory ``` **After the contracts have been deployed like this, they are still owned by the wallet used for deployment. Don't forget to transfer ownership to a safer address, like a multisig.** diff --git a/docs/npm_publishing.md b/docs/npm_publishing.md index aeb31dd9..c13af749 100644 --- a/docs/npm_publishing.md +++ b/docs/npm_publishing.md @@ -4,16 +4,24 @@ The smart contracts are published to npm as a package. The package name is `@tok Currently, no automated publishing is set up. Publishing is done manually. To publish a new version, follow these steps: -1. First test without publishing: +1. Update version in package.json and create git tag: ```bash - npm publish --access public --dry-run + npm version ``` -2. Check if all necessary files are contained and no secrets are leaked. -3. If everything is fine, publish: + The version number must be a valid semver version. The version number must be higher than the current version number. + +2. Test without publishing: + + ```bash + npm publish [--tag ] --dry-run + ``` + +3. Check if all necessary files are contained and no secrets are leaked. +4. If everything is fine, publish: ```bash - npm publish --access public + npm publish [--tag ] ``` Sadly, `yarn publish` did not work at the time of writing. It appeared to have trouble with the 2FA. diff --git a/foundry.toml b/foundry.toml index 2bc811e7..b8878bcf 100644 --- a/foundry.toml +++ b/foundry.toml @@ -2,9 +2,15 @@ fuzz-runs = 10_000 [profile.default] -# via-ir = true # Enable the IR-based optimizer (yul) +via-ir = true # Enable the IR-based optimizer (yul) +optimizer = true +bytecode_hash = "none" # prevents compiler from attaching bytecode hash to contract metadata + +[profile.fastDev] +via-ir = false # Enable the IR-based optimizer (yul) optimizer = true optimizer_runs = 10_000 + gas_reports = ["AllowList", "ContinuousFundraising", "FeeSettings", "PersonalInvite", "PersonalInviteFactory", "Token"] [rpc_endpoints] diff --git a/hardhat.config.ts b/hardhat.config.ts index ae6be276..92bdad19 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -1,11 +1,11 @@ -import { HardhatUserConfig, task } from "hardhat/config"; +import { HardhatUserConfig, task } from 'hardhat/config'; -import "dotenv/config"; -import "hardhat-gas-reporter"; -import "@nomiclabs/hardhat-etherscan"; -import "@typechain/hardhat"; -import "@nomiclabs/hardhat-ethers"; -import "@nomiclabs/hardhat-waffle"; +import 'dotenv/config'; +import 'hardhat-gas-reporter'; +import '@nomiclabs/hardhat-etherscan'; +import '@typechain/hardhat'; +import '@nomiclabs/hardhat-ethers'; +import '@nomiclabs/hardhat-waffle'; // require("@nomiclabs/hardhat-waffle"); // require("hardhat-gas-reporter"); @@ -14,7 +14,7 @@ import "@nomiclabs/hardhat-waffle"; // This is a sample Hardhat task. To learn how to create your own go to // https://hardhat.org/guides/create-task.html -task("accounts", "Prints the list of accounts", async (taskArgs, hre) => { +task('accounts', 'Prints the list of accounts', async (taskArgs, hre) => { const accounts = await hre.ethers.getSigners(); for (const account of accounts) { @@ -30,43 +30,59 @@ task("accounts", "Prints the list of accounts", async (taskArgs, hre) => { */ const config: HardhatUserConfig = { solidity: { - version: "0.8.17", + version: '0.8.17', settings: { + // optimizer: { + // enabled: true, + // runs: 10000, + // }, + metadata: { + bytecodeHash: 'none', + }, + viaIR: true, optimizer: { enabled: true, - runs: 10000, + details: { + yulDetails: { + //optimizerSteps: 'u', // recommended by hh, but yields longer bytecode + }, + }, }, - // viaIR: true, // outputSelection: { "*": { "*": ["storageLayout"] } }, }, }, networks: { localhost: { - url: "http://localhost:8545", + url: 'http://localhost:8545', }, ropsten: { - url: process.env.ROPSTEN_URL || "", + url: process.env.ROPSTEN_URL || '', accounts: process.env.PRIVATE_KEY !== undefined ? [process.env.PRIVATE_KEY] : [], }, goerli: { - url: process.env.GOERLI_RPC_URL || "", + url: process.env.GOERLI_RPC_URL || '', + accounts: + process.env.PRIVATE_KEY !== undefined ? [process.env.PRIVATE_KEY] : [], + }, + mainnet: { + url: process.env.MAINNET_RPC_URL || '', accounts: process.env.PRIVATE_KEY !== undefined ? [process.env.PRIVATE_KEY] : [], }, }, gasReporter: { enabled: process.env.REPORT_GAS !== undefined, - currency: "USD", + currency: 'USD', }, etherscan: { apiKey: process.env.ETHERSCAN_API_KEY, }, typechain: { - outDir: "types", - target: "ethers-v5", + outDir: 'types', + target: 'ethers-v5', alwaysGenerateOverloads: false, // should overloads with full signatures like deposit(uint256) be generated always, even if there are no overloads? - externalArtifacts: ["externalArtifacts/*.json"], // optional array of glob patterns with external artifacts to process (for example external libs from node_modules) + externalArtifacts: ['externalArtifacts/*.json'], // optional array of glob patterns with external artifacts to process (for example external libs from node_modules) dontOverrideCompile: false, // defaults to false }, }; diff --git a/lib/forge-std b/lib/forge-std index 066ff16c..e8a047e3 160000 --- a/lib/forge-std +++ b/lib/forge-std @@ -1 +1 @@ -Subproject commit 066ff16c5c03e6f931cd041fd366bc4be1fae82a +Subproject commit e8a047e3f40f13fa37af6fe14e6e06283d9a060e diff --git a/package.json b/package.json index 85972ad3..72f1b70c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@tokenize.it/contracts", - "version": "4.0.1", + "version": "4.1.0-alpha.0", "description": "Tokenize.it smart contracts for company tokenization, public fundraising, private offers and employee participation", "keywords": [ "evm", @@ -72,7 +72,7 @@ "hardhat-gas-reporter": "^1.0.8", "npmignore": "^0.3.0", "prettier": "^2.8.0", - "prettier-plugin-solidity": "^1.0.0", + "prettier-plugin-solidity": "^1.1.3", "solhint": "^3.3.7", "solidity-coverage": "^0.7.21", "ts-node": "^10.9.1", @@ -81,7 +81,7 @@ }, "dependencies": { "@opengsn/contracts": "2.2.5", - "@openzeppelin/contracts": "4.8.0" + "@openzeppelin/contracts": "4.9.1" }, "scripts": { "prepack": "yarn npmignore --auto && yarn test && yarn build ", @@ -90,15 +90,14 @@ "coverage": "forge coverage --report lcov && genhtml lcov.info --branch-coverage --output-directory=./coverage" }, "prettier": { + "singleQuote": true, + "trailingComma": "all", "overrides": [ { "files": "*.sol", "options": { - "printWidth": 80, - "tabWidth": 4, - "useTabs": false, "singleQuote": false, - "bracketSpacing": false + "printWidth": 120 } } ] diff --git a/script/CheckToken.sol b/script/CheckToken.sol index f26e4fed..15f2ef45 100644 --- a/script/CheckToken.sol +++ b/script/CheckToken.sol @@ -29,27 +29,16 @@ contract CheckToken is Script { function run() public view { Token token = Token(address(TOKEN_ADDRESS)); - console.log( - "Remember to update addresses in this script in order to check other deployments." - ); + console.log("Remember to update addresses in this script in order to check other deployments."); console.log("Token name: ", token.name()); console.log("Token symbol: ", token.symbol()); - console.log( - "Token fee settings matches: ", - address(token.feeSettings()) == FEE_SETTINGS - ); + console.log("Token fee settings matches: ", address(token.feeSettings()) == FEE_SETTINGS); console.log( "Token trusted forwarder matches: ", token.isTrustedForwarder(0x994257AcCF99E5995F011AB2A3025063e5367629) ); - console.log( - "Token allow list matches: ", - address(token.allowList()) == ALLOW_LIST - ); - console.log( - "Token admin matches: ", - token.hasRole(DEFAULT_ADMIN_ROLE, ADMIN) - ); + console.log("Token allow list matches: ", address(token.allowList()) == ALLOW_LIST); + console.log("Token admin matches: ", token.hasRole(DEFAULT_ADMIN_ROLE, ADMIN)); } } diff --git a/script/DeployCompany.sol b/script/DeployCompany.sol index 56e48b4f..cac066ab 100644 --- a/script/DeployCompany.sol +++ b/script/DeployCompany.sol @@ -17,12 +17,8 @@ contract DeployCompany is Script { function run() public { uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); address deployerAddress = vm.addr(deployerPrivateKey); - AllowList allowList = AllowList( - 0x47EE5950B9a790A292B731789a35CcCB7381667E - ); - FeeSettings feeSettings = FeeSettings( - 0x147addF9C8E4030F8104c713Dad2A1d76E6c85a1 - ); + AllowList allowList = AllowList(0x47EE5950B9a790A292B731789a35CcCB7381667E); + FeeSettings feeSettings = FeeSettings(0x147addF9C8E4030F8104c713Dad2A1d76E6c85a1); vm.startBroadcast(deployerPrivateKey); console.log("Deployer address: ", deployerAddress); @@ -33,7 +29,7 @@ contract DeployCompany is Script { address companyAdmin = 0x6CcD9E07b035f9E6e7f086f3EaCf940187d03A29; // testing founder address forwarder = 0x0445d09A1917196E1DC12EdB7334C70c1FfB1623; - address investor = 0x35bb2Ded62588f7fb3771658dbE699826Cd1041A; + // address investor = 0x35bb2Ded62588f7fb3771658dbE699826Cd1041A; // string memory name = "MyTasticToken"; // string memory symbol = "MTT"; diff --git a/script/DeployPlatform.s.sol b/script/DeployPlatform.s.sol index 83e1a8c5..9ab324c9 100644 --- a/script/DeployPlatform.s.sol +++ b/script/DeployPlatform.s.sol @@ -7,6 +7,7 @@ import "../lib/forge-std/src/Script.sol"; import "../contracts/FeeSettings.sol"; import "../contracts/AllowList.sol"; import "../contracts/PersonalInviteFactory.sol"; +import "../contracts/VestingWalletFactory.sol"; contract DeployPlatform is Script { function setUp() public {} @@ -42,15 +43,14 @@ contract DeployPlatform is Script { console.log("Deploying PersonalInviteFactory contract..."); PersonalInviteFactory personalInviteFactory = new PersonalInviteFactory(); - console.log( - "PersonalInviteFactory deployed at: ", - address(personalInviteFactory) - ); + console.log("PersonalInviteFactory deployed at: ", address(personalInviteFactory)); + + console.log("Deploying VestingWalletFactory contract..."); + VestingWalletFactory vestingWalletFactory = new VestingWalletFactory(); + console.log("VestingWalletFactory deployed at: ", address(vestingWalletFactory)); vm.stopBroadcast(); - console.log( - "Don't forget to check and finalize ownership transfers for all contracts!" - ); + console.log("Don't forget to check and finalize ownership transfers for all contracts!"); } } diff --git a/script/deploy.ts b/script/deploy.ts index 197643a9..71006784 100644 --- a/script/deploy.ts +++ b/script/deploy.ts @@ -3,7 +3,7 @@ // // When running the script with `npx hardhat run