From cd1dd44bc5bb1fcff7b299ae635cdf93a10a207a Mon Sep 17 00:00:00 2001 From: malteish Date: Fri, 28 Apr 2023 14:59:33 +0200 Subject: [PATCH 01/75] add first demo --- contracts/Wallet.sol | 71 +++++++++++++++++ contracts/interfaces/ERC1363.sol | 9 +++ test/BuyWithMintAndCall.t.sol | 115 ++++++++++++++++++++++++++++ test/resources/MintAndCallToken.sol | 40 ++++++++++ 4 files changed, 235 insertions(+) create mode 100644 contracts/Wallet.sol create mode 100644 contracts/interfaces/ERC1363.sol create mode 100644 test/BuyWithMintAndCall.t.sol create mode 100644 test/resources/MintAndCallToken.sol diff --git a/contracts/Wallet.sol b/contracts/Wallet.sol new file mode 100644 index 00000000..c7effc1f --- /dev/null +++ b/contracts/Wallet.sol @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity 0.8.17; + +import "@openzeppelin/contracts/access/Ownable2Step.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import "./interfaces/ERC1363.sol"; +import "./ContinuousFundraising.sol"; + +/** + + */ + +contract Wallet is Ownable2Step, ERC1363Receiver { + using SafeERC20 for IERC20; + /** + @notice stores the receiving address for each IBAN hash + */ + mapping(bytes32 => address) public receiverAddress; + + ContinuousFundraising public fundraising; + + event Set(bytes32 indexed ibanHash, address tokenReceiver); + + constructor(ContinuousFundraising _fundraising) Ownable2Step() { + fundraising = _fundraising; + } + + /** + @notice sets (or updates) the receiving address for an IBAN hash + */ + function set(bytes32 _ibanHash, address _tokenReceiver) external onlyOwner { + receiverAddress[_ibanHash] = _tokenReceiver; + emit Set(_ibanHash, _tokenReceiver); + } + + /** + * @notice ERC1363 callback + * @dev to support the mintAndCall standard, this function MUST NOT revert! + */ + function onTransferReceived( + address operator, + address from, + uint256 value, + bytes memory data + ) external override returns (bytes4) { + bytes32 ibanHash = abi.decode(data, (bytes32)); + if (receiverAddress[ibanHash] == address(0)) { + return bytes4(0xDEADD00D); // ERC1363ReceiverNotRegistered + } + address tokenReceiver = receiverAddress[ibanHash]; + // todo: calculate amount + uint256 amount = 100; + // grant allowance to fundraising + IERC20(fundraising.currency()).approve(address(fundraising), amount); + // todo: add try catch https://solidity-by-example.org/try-catch/ + fundraising.buy(amount, tokenReceiver); + return 0x600D600D; // ERC1363ReceiverSuccess + } + + /* + @notice withdraws tokens to a given address + */ + function withdraw( + address token, + address to, + uint256 amount + ) external onlyOwner { + IERC20(token).safeTransferFrom(address(this), to, amount); + } +} diff --git a/contracts/interfaces/ERC1363.sol b/contracts/interfaces/ERC1363.sol new file mode 100644 index 00000000..ad304ade --- /dev/null +++ b/contracts/interfaces/ERC1363.sol @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity 0.8.17; + +/** + @notice ERC1363 interface, see https://eips.ethereum.org/EIPS/eip-1363 + */ +interface ERC1363Receiver { + function onTransferReceived(address operator, address from, uint256 value, bytes memory data) external returns (bytes4); +} \ No newline at end of file diff --git a/test/BuyWithMintAndCall.t.sol b/test/BuyWithMintAndCall.t.sol new file mode 100644 index 00000000..c50c1f09 --- /dev/null +++ b/test/BuyWithMintAndCall.t.sol @@ -0,0 +1,115 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity ^0.8.13; + +import "../lib/forge-std/src/Test.sol"; +import "../contracts/Token.sol"; +import "../contracts/FeeSettings.sol"; +import "../contracts/ContinuousFundraising.sol"; +import "./resources/MintAndCallToken.sol"; +import "./resources/MaliciousPaymentToken.sol"; +import "../contracts/Wallet.sol"; + +contract BuyWithMintAndCall is Test { + ContinuousFundraising raise; + AllowList list; + IFeeSettingsV1 feeSettings; + + Token token; + MintAndCallToken paymentToken; + Wallet wallet; + + address public constant admin = 0x0109709eCFa91a80626FF3989D68f67f5b1dD120; + address public constant buyer = 0x1109709ecFA91a80626ff3989D68f67F5B1Dd121; + address public constant mintAllower = + 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; + + uint8 public constant paymentTokenDecimals = 6; + uint256 public constant paymentTokenAmount = + 1000 * 10 ** paymentTokenDecimals; + + uint256 public constant price = 7 * 10 ** paymentTokenDecimals; // 7 payment tokens per token + + uint256 public constant maxAmountOfTokenToBeSold = 20 * 10 ** 18; // 20 token + uint256 public constant maxAmountPerBuyer = maxAmountOfTokenToBeSold / 2; // 10 token + uint256 public constant minAmountPerBuyer = 1; + + function setUp() public { + list = new AllowList(); + Fees memory fees = Fees(100, 100, 100, 100); + feeSettings = new FeeSettings(fees, admin); + + token = new Token( + trustedForwarder, + feeSettings, + admin, + list, + 0x0, + "TESTTOKEN", + "TEST" + ); + + // set up currency + vm.startPrank(paymentTokenProvider); + paymentToken = new MintAndCallToken(paymentTokenDecimals); + vm.stopPrank(); + + vm.prank(owner); + raise = new ContinuousFundraising( + trustedForwarder, + payable(receiver), + minAmountPerBuyer, + maxAmountPerBuyer, + price, + maxAmountOfTokenToBeSold, + paymentToken, + token + ); + + // allow raise contract to mint + bytes32 roleMintAllower = token.MINTALLOWER_ROLE(); + + vm.prank(admin); + token.grantRole(roleMintAllower, mintAllower); + vm.prank(mintAllower); + token.increaseMintingAllowance( + address(raise), + maxAmountOfTokenToBeSold + ); + + // give raise contract allowance + vm.prank(buyer); + paymentToken.approve(address(raise), paymentTokenAmount); + + // create wallet + vm.prank(owner); + wallet = new Wallet(raise); + } + + function testBuyHappyCase() public { + bytes32 buyersIbanHash = keccak256(abi.encodePacked("DE1234567890")); + // add buyers address to wallet + vm.prank(owner); + wallet.set(buyersIbanHash, buyer); + + uint currencyMintAmount = 1e20; + + // make sure buyer has no tokens before + assertTrue(token.balanceOf(buyer) == 0); + + // mint currency + bytes memory data = abi.encode(buyersIbanHash, 0xDEADBEEF); + vm.prank(paymentTokenProvider); + paymentToken.mintAndCall(address(wallet), currencyMintAmount, data); + + // make sure buyer has tokens after + assertTrue(token.balanceOf(buyer) > 0); + } +} diff --git a/test/resources/MintAndCallToken.sol b/test/resources/MintAndCallToken.sol new file mode 100644 index 00000000..978ede24 --- /dev/null +++ b/test/resources/MintAndCallToken.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity ^0.8.13; + +import "../../node_modules/@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import "../../node_modules/@openzeppelin/contracts/access/Ownable.sol"; +import "../../contracts/interfaces/ERC1363.sol"; + +/* + fake currency to test the mint and call feature +*/ +contract MintAndCallToken is ERC20, Ownable { + uint8 decimalPlaces; + + constructor(uint8 _decimals) ERC20("MintAndCallToken", "MACT") Ownable() { + decimalPlaces = _decimals; + } + + /// @dev price definition and deal() function rely on proper handling of decimalPlaces. Therefore we need to test if decimalPlaces other than 18 work fine, too. + function decimals() public view override returns (uint8) { + return decimalPlaces; + } + + function mint(address _to, uint256 _amount) external onlyOwner { + _mint(_to, _amount); + } + + function mintAndCall( + address _to, + uint256 _amount, + bytes memory _data + ) external onlyOwner { + _mint(_to, _amount); + ERC1363Receiver(_to).onTransferReceived( + msg.sender, + address(0), + _amount, + _data + ); + } +} From aae4eed70c792525b2ff85f08eac9d9b397c1c87 Mon Sep 17 00:00:00 2001 From: malteish Date: Fri, 28 Apr 2023 12:59:54 +0000 Subject: [PATCH 02/75] Prettified Code! --- contracts/interfaces/ERC1363.sol | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/contracts/interfaces/ERC1363.sol b/contracts/interfaces/ERC1363.sol index ad304ade..9ee50617 100644 --- a/contracts/interfaces/ERC1363.sol +++ b/contracts/interfaces/ERC1363.sol @@ -5,5 +5,10 @@ pragma solidity 0.8.17; @notice ERC1363 interface, see https://eips.ethereum.org/EIPS/eip-1363 */ interface ERC1363Receiver { - function onTransferReceived(address operator, address from, uint256 value, bytes memory data) external returns (bytes4); -} \ No newline at end of file + function onTransferReceived( + address operator, + address from, + uint256 value, + bytes memory data + ) external returns (bytes4); +} From f3ef09939e4fba014b359602dd235762bf9651fd Mon Sep 17 00:00:00 2001 From: malteish Date: Tue, 2 May 2023 10:15:05 +0200 Subject: [PATCH 03/75] transfer instead of transferFrom, add try catch --- contracts/Wallet.sol | 15 +++++++++------ test/BuyWithMintAndCall.t.sol | 21 +++++++++++++++++++++ 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/contracts/Wallet.sol b/contracts/Wallet.sol index c7effc1f..4eddc062 100644 --- a/contracts/Wallet.sol +++ b/contracts/Wallet.sol @@ -46,16 +46,19 @@ contract Wallet is Ownable2Step, ERC1363Receiver { ) external override returns (bytes4) { bytes32 ibanHash = abi.decode(data, (bytes32)); if (receiverAddress[ibanHash] == address(0)) { - return bytes4(0xDEADD00D); // ERC1363ReceiverNotRegistered + return bytes4(0xDEADD00D); // ReceiverNotRegistered } address tokenReceiver = receiverAddress[ibanHash]; // todo: calculate amount - uint256 amount = 100; + uint256 amount = 1e15; // grant allowance to fundraising IERC20(fundraising.currency()).approve(address(fundraising), amount); - // todo: add try catch https://solidity-by-example.org/try-catch/ - fundraising.buy(amount, tokenReceiver); - return 0x600D600D; // ERC1363ReceiverSuccess + // try buying tokens https://solidity-by-example.org/try-catch/ + try fundraising.buy(amount, tokenReceiver) { + return 0x600D600D; // ReceiverSuccess + } catch { + return 0x0BAD0BAD; // ReceiverFailure + } } /* @@ -66,6 +69,6 @@ contract Wallet is Ownable2Step, ERC1363Receiver { address to, uint256 amount ) external onlyOwner { - IERC20(token).safeTransferFrom(address(this), to, amount); + IERC20(token).safeTransfer(to, amount); } } diff --git a/test/BuyWithMintAndCall.t.sol b/test/BuyWithMintAndCall.t.sol index c50c1f09..ebda8414 100644 --- a/test/BuyWithMintAndCall.t.sol +++ b/test/BuyWithMintAndCall.t.sol @@ -112,4 +112,25 @@ contract BuyWithMintAndCall is Test { // make sure buyer has tokens after assertTrue(token.balanceOf(buyer) > 0); } + + function testBuyReverts() public { + bytes32 buyersIbanHash = keccak256(abi.encodePacked("DE1234567890")); + // add buyers address to wallet + vm.prank(owner); + wallet.set(buyersIbanHash, buyer); + + uint currencyMintAmount = 1; + + // make sure buyer has no tokens before + assertTrue(token.balanceOf(buyer) == 0); + + // mint currency (not enough for the buy) + bytes memory data = abi.encode(buyersIbanHash, 0xDEADBEEF); + + vm.prank(paymentTokenProvider); + paymentToken.mintAndCall(address(wallet), currencyMintAmount, data); + + // make sure buyer has no tokens after + assertTrue(token.balanceOf(buyer) == 0); + } } From a4041dc7c90e574489791c6433c290c66ff5be77 Mon Sep 17 00:00:00 2001 From: malteish Date: Tue, 2 May 2023 11:59:59 +0200 Subject: [PATCH 04/75] fix test --- test/PersonalInvite.t.sol | 1 - 1 file changed, 1 deletion(-) diff --git a/test/PersonalInvite.t.sol b/test/PersonalInvite.t.sol index 540c71eb..c67ea403 100644 --- a/test/PersonalInvite.t.sol +++ b/test/PersonalInvite.t.sol @@ -246,7 +246,6 @@ contract PersonalInviteTest is Test { vm.prank(paymentTokenProvider); currency.mint(currencyPayer, maxCurrencyAmount); - vm.stopPrank(); vm.prank(currencyPayer); currency.approve(expectedAddress, maxCurrencyAmount); From 994b76740a4647a9ce4c81cba214e5bbf9c827fa Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 May 2023 08:44:40 +0000 Subject: [PATCH 05/75] Bump decode-uri-component from 0.2.0 to 0.2.2 Bumps [decode-uri-component](https://github.com/SamVerschueren/decode-uri-component) from 0.2.0 to 0.2.2. - [Release notes](https://github.com/SamVerschueren/decode-uri-component/releases) - [Commits](https://github.com/SamVerschueren/decode-uri-component/compare/v0.2.0...v0.2.2) --- updated-dependencies: - dependency-name: decode-uri-component dependency-type: indirect ... Signed-off-by: dependabot[bot] --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index a46087c5..3871d120 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3281,9 +3281,9 @@ decamelize@^4.0.0: integrity sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ== decode-uri-component@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" - integrity sha512-hjf+xovcEn31w/EUYdTXQh/8smFL/dzYjohQGEIgjyNavaJfBY2p5F527Bo1VPATxv0VYTUC2bOcXvqFwk78Og== + version "0.2.2" + resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.2.tgz#e69dbe25d37941171dd540e024c444cd5188e1e9" + integrity sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ== decompress-response@^3.3.0: version "3.3.0" From 477b3dcf00dc929ec656ca27ea98193fd479848b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 May 2023 08:44:39 +0000 Subject: [PATCH 06/75] Bump cookiejar from 2.1.3 to 2.1.4 Bumps [cookiejar](https://github.com/bmeck/node-cookiejar) from 2.1.3 to 2.1.4. - [Commits](https://github.com/bmeck/node-cookiejar/commits) --- updated-dependencies: - dependency-name: cookiejar dependency-type: indirect ... Signed-off-by: dependabot[bot] --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 3871d120..c0c14a7c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3084,9 +3084,9 @@ cookie@^0.4.1: integrity sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA== cookiejar@^2.1.1: - version "2.1.3" - resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.3.tgz#fc7a6216e408e74414b90230050842dacda75acc" - integrity sha512-JxbCBUdrfr6AQjOXrxoTvAMJO4HBTUIlBzslcJPAz+/KT8yk53fXun51u+RenNYvad/+Vc2DIz5o9UxlCDymFQ== + version "2.1.4" + resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.4.tgz#ee669c1fea2cf42dc31585469d193fef0d65771b" + integrity sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw== copy-descriptor@^0.1.0: version "0.1.1" From eb193a86a5d7dbc3ba70bb12105c8b6414b8e7d3 Mon Sep 17 00:00:00 2001 From: malteish Date: Tue, 2 May 2023 14:03:22 +0200 Subject: [PATCH 07/75] natSpec for Token --- contracts/Token.sol | 174 +++++++++++++++++++++++---------------- script/DeployCompany.sol | 2 +- 2 files changed, 102 insertions(+), 74 deletions(-) diff --git a/contracts/Token.sol b/contracts/Token.sol index d416a6a4..2efa2d3a 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 + * @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. + * */ 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 @@ -45,58 +45,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, @@ -136,6 +141,11 @@ contract Token is ERC2771Context, ERC20Permit, Pausable, AccessControl { requirements = _requirements; } + /** + * @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) { @@ -147,6 +157,10 @@ contract Token is ERC2771Context, ERC20Permit, Pausable, AccessControl { emit AllowListChanged(_allowList); } + /** + * @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) { @@ -158,6 +172,7 @@ 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 { @@ -171,10 +186,10 @@ contract Token is ERC2771Context, ERC20Permit, Pausable, AccessControl { } /** - * @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 it the address implements the interface also prevents the 0 address from being accepted. * @param _feeSettings the new feeSettings contract */ function acceptNewFeeSettings( @@ -192,12 +207,12 @@ contract Token is ERC2771Context, ERC20Permit, Pausable, AccessControl { 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) - */ + /** + * @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 @@ -206,11 +221,12 @@ contract Token is ERC2771Context, ERC20Permit, Pausable, AccessControl { 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) - */ + /** + * @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 @@ -224,6 +240,11 @@ 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, @@ -241,6 +262,11 @@ contract Token is ERC2771Context, ERC20Permit, Pausable, AccessControl { } } + /** + * @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 @@ -249,11 +275,11 @@ contract Token is ERC2771Context, ERC20Permit, Pausable, AccessControl { } /** - @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, @@ -277,7 +303,8 @@ 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( @@ -288,9 +315,10 @@ contract Token is ERC2771Context, ERC20Permit, Pausable, AccessControl { } /** - * @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 diff --git a/script/DeployCompany.sol b/script/DeployCompany.sol index 56e48b4f..4d260138 100644 --- a/script/DeployCompany.sol +++ b/script/DeployCompany.sol @@ -33,7 +33,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"; From 9be8005869cfda86ba64bc656dcc891982982733 Mon Sep 17 00:00:00 2001 From: malteish Date: Tue, 2 May 2023 14:18:28 +0200 Subject: [PATCH 08/75] natSpec for ContinuousFundraising --- contracts/ContinuousFundraising.sol | 93 +++++++++++++++++------------ 1 file changed, 55 insertions(+), 38 deletions(-) diff --git a/contracts/ContinuousFundraising.sol b/contracts/ContinuousFundraising.sol index 906ba7e3..571ac3cc 100644 --- a/contracts/ContinuousFundraising.sol +++ b/contracts/ContinuousFundraising.sol @@ -12,16 +12,16 @@ import "@openzeppelin/contracts/utils/math/Math.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 - + * 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 + * */ contract ContinuousFundraising is ERC2771Context, @@ -37,10 +37,8 @@ contract ContinuousFundraising is uint256 public minAmountPerBuyer; /// @notice 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] - */ + /// @notice 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) uint256 public maxAmountOfTokenToBeSold; @@ -51,19 +49,38 @@ contract ContinuousFundraising is /// @notice 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 + /// @notice 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 + /// @notice 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); + /// @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, @@ -117,9 +134,9 @@ contract ContinuousFundraising is } /** - @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, @@ -168,8 +185,8 @@ contract ContinuousFundraising is } /** - @notice change the currencyReceiver - @param _currencyReceiver new currencyReceiver + * @notice change the currencyReceiver to `_currencyReceiver` + * @param _currencyReceiver new currencyReceiver */ function setCurrencyReceiver( address _currencyReceiver @@ -184,8 +201,8 @@ contract ContinuousFundraising is } /** - @notice change the minAmountPerBuyer - @param _minAmountPerBuyer new minAmountPerBuyer + * @notice change the minAmountPerBuyer to `_minAmountPerBuyer` + * @param _minAmountPerBuyer new minAmountPerBuyer */ function setMinAmountPerBuyer( uint256 _minAmountPerBuyer @@ -200,8 +217,8 @@ contract ContinuousFundraising is } /** - @notice change the maxAmountPerBuyer - @param _maxAmountPerBuyer new maxAmountPerBuyer + * @notice change the maxAmountPerBuyer to `_maxAmountPerBuyer` + * @param _maxAmountPerBuyer new maxAmountPerBuyer */ function setMaxAmountPerBuyer( uint256 _maxAmountPerBuyer @@ -216,9 +233,9 @@ contract ContinuousFundraising is } /** - @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, @@ -232,8 +249,8 @@ 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 @@ -248,7 +265,7 @@ contract ContinuousFundraising is } /** - @notice pause the contract + * @notice pause the contract */ function pause() external onlyOwner { _pause(); @@ -256,7 +273,7 @@ contract ContinuousFundraising is } /** - @notice unpause the contract + * @notice unpause the contract */ function unpause() external onlyOwner { require( From 2cf81d93adadde98a8814e391a797753a62d27f3 Mon Sep 17 00:00:00 2001 From: malteish Date: Tue, 2 May 2023 14:37:30 +0200 Subject: [PATCH 09/75] natSpec for PersonalInvite --- contracts/PersonalInvite.sol | 43 ++++++++++++++++++++++-------------- 1 file changed, 26 insertions(+), 17 deletions(-) diff --git a/contracts/PersonalInvite.sol b/contracts/PersonalInvite.sol index 9fd5f333..de1b20d2 100644 --- a/contracts/PersonalInvite.sol +++ b/contracts/PersonalInvite.sol @@ -6,27 +6,36 @@ 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. + * @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. + * @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, From 21411f8c7766336e662b57a185aa7d3e5963c27c Mon Sep 17 00:00:00 2001 From: malteish Date: Tue, 2 May 2023 14:39:56 +0200 Subject: [PATCH 10/75] more natSpec for PersonalInvite --- contracts/PersonalInvite.sol | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/contracts/PersonalInvite.sol b/contracts/PersonalInvite.sol index de1b20d2..aeba6450 100644 --- a/contracts/PersonalInvite.sol +++ b/contracts/PersonalInvite.sol @@ -28,7 +28,8 @@ contract PersonalInvite { using SafeERC20 for IERC20; /** - * @notice Emitted when a PersonalInvite is deployed. + * @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 From ada3c80bee52ff824d8194cb9eb008529e7e610c Mon Sep 17 00:00:00 2001 From: malteish Date: Tue, 2 May 2023 14:47:31 +0200 Subject: [PATCH 11/75] natSpec for PersonalInviteFactory --- contracts/PersonalInviteFactory.sol | 33 +++++++++++++++++++++++++---- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/contracts/PersonalInviteFactory.sol b/contracts/PersonalInviteFactory.sol index 5e751b72..97690968 100644 --- a/contracts/PersonalInviteFactory.sol +++ b/contracts/PersonalInviteFactory.sol @@ -7,14 +7,25 @@ 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. -*/ +/** + * @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 +59,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 +94,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, From 27e060a9ebd2882a8fba6f94e50a7fe1a49f6e6a Mon Sep 17 00:00:00 2001 From: malteish Date: Wed, 3 May 2023 09:02:07 +0200 Subject: [PATCH 12/75] natSpec for AllowList --- contracts/AllowList.sol | 74 +++++++++++++++++++++++------------------ 1 file changed, 42 insertions(+), 32 deletions(-) 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); From 36a966263f23005c87d53baaa1cf64a75979e477 Mon Sep 17 00:00:00 2001 From: malteish Date: Wed, 3 May 2023 09:15:03 +0200 Subject: [PATCH 13/75] natSpec for FeeSettings --- contracts/FeeSettings.sol | 79 ++++++++++++++++++++++++++++++--------- 1 file changed, 61 insertions(+), 18 deletions(-) diff --git a/contracts/FeeSettings.sol b/contracts/FeeSettings.sol index 8894fccb..6af22408 100644 --- a/contracts/FeeSettings.sol +++ b/contracts/FeeSettings.sol @@ -5,29 +5,52 @@ 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; @@ -38,10 +61,13 @@ contract FeeSettings is Ownable2Step, ERC165, IFeeSettingsV1 { 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 ( @@ -59,6 +85,9 @@ contract FeeSettings is Ownable2Step, ERC165, IFeeSettingsV1 { emit ChangeProposed(_fees); } + /** + * @notice Executes a fee change that has been planned before + */ function executeFeeChange() external onlyOwner { require( block.timestamp >= proposedFees.time, @@ -77,12 +106,20 @@ contract FeeSettings is Ownable2Step, ERC165, IFeeSettingsV1 { 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, @@ -99,16 +136,18 @@ contract FeeSettings is Ownable2Step, ERC165, IFeeSettingsV1 { } /** - @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 @@ -116,9 +155,11 @@ contract FeeSettings is Ownable2Step, ERC165, IFeeSettingsV1 { 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 @@ -127,7 +168,8 @@ contract FeeSettings is Ownable2Step, ERC165, IFeeSettingsV1 { } /** - * 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 @@ -141,6 +183,7 @@ contract FeeSettings is Ownable2Step, ERC165, IFeeSettingsV1 { /** * @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 From ff48a5d69e264fe244b296a3ad5a675139765966 Mon Sep 17 00:00:00 2001 From: malteish Date: Wed, 3 May 2023 09:24:06 +0200 Subject: [PATCH 14/75] more natSpec --- contracts/ContinuousFundraising.sol | 51 +++++++++++++++++------------ contracts/PersonalInvite.sol | 2 ++ contracts/PersonalInviteFactory.sol | 2 ++ contracts/Token.sol | 4 +-- 4 files changed, 36 insertions(+), 23 deletions(-) diff --git a/contracts/ContinuousFundraising.sol b/contracts/ContinuousFundraising.sol index 571ac3cc..cc0d0916 100644 --- a/contracts/ContinuousFundraising.sol +++ b/contracts/ContinuousFundraising.sol @@ -11,17 +11,16 @@ import "@openzeppelin/contracts/utils/math/Math.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, @@ -31,33 +30,34 @@ contract ContinuousFundraising is { 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 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 ). + /// 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; /// @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; - /// @notice 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; - /// @notice This mapping 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; + /// @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. @@ -88,7 +88,16 @@ contract ContinuousFundraising is ); /** + * @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, diff --git a/contracts/PersonalInvite.sol b/contracts/PersonalInvite.sol index aeba6450..3d6a3984 100644 --- a/contracts/PersonalInvite.sol +++ b/contracts/PersonalInvite.sol @@ -6,6 +6,8 @@ import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "./Token.sol"; /** + * @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. diff --git a/contracts/PersonalInviteFactory.sol b/contracts/PersonalInviteFactory.sol index 97690968..170a214f 100644 --- a/contracts/PersonalInviteFactory.sol +++ b/contracts/PersonalInviteFactory.sol @@ -8,6 +8,8 @@ import "@openzeppelin/contracts/utils/Create2.sol"; import "../contracts/PersonalInvite.sol"; /** + * @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. */ diff --git a/contracts/Token.sol b/contracts/Token.sol index 2efa2d3a..0761f625 100644 --- a/contracts/Token.sol +++ b/contracts/Token.sol @@ -10,6 +10,7 @@ import "./interfaces/IFeeSettings.sol"; /** * @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 @@ -18,8 +19,7 @@ import "./interfaces/IFeeSettings.sol"; * - 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. - * + * @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 From cc31cc5b0967e1ae65d0749b5d4e2a4a7a39ee8b Mon Sep 17 00:00:00 2001 From: malteish Date: Wed, 3 May 2023 10:18:00 +0200 Subject: [PATCH 15/75] typo --- contracts/Token.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/Token.sol b/contracts/Token.sol index 0761f625..3a929cd1 100644 --- a/contracts/Token.sol +++ b/contracts/Token.sol @@ -189,7 +189,7 @@ contract Token is ERC2771Context, ERC20Permit, Pausable, AccessControl { * @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 necessary to prevent frontrunning the acceptance with a new suggestion. - * Checking it the address implements the interface also prevents the 0 address from being accepted. + * Checking if the address implements the interface also prevents the 0 address from being accepted. * @param _feeSettings the new feeSettings contract */ function acceptNewFeeSettings( From e2350dafe4f97476687240c8310edb2f7d22972a Mon Sep 17 00:00:00 2001 From: malteish Date: Wed, 3 May 2023 11:50:52 +0200 Subject: [PATCH 16/75] refactor ERC20 helper --- test/MainnetCurrenciesInvest.t.sol | 317 +++++++++++++++++++++++++++++ test/resources/ERC20Helper.sol | 24 +++ 2 files changed, 341 insertions(+) create mode 100644 test/MainnetCurrenciesInvest.t.sol create mode 100644 test/resources/ERC20Helper.sol diff --git a/test/MainnetCurrenciesInvest.t.sol b/test/MainnetCurrenciesInvest.t.sol new file mode 100644 index 00000000..63c713e0 --- /dev/null +++ b/test/MainnetCurrenciesInvest.t.sol @@ -0,0 +1,317 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity ^0.8.13; + +import "../lib/forge-std/src/Test.sol"; +//import "../lib/forge-std/stdlib.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import "../contracts/Token.sol"; +import "../contracts/ContinuousFundraising.sol"; +import "../contracts/PersonalInvite.sol"; +import "../contracts/PersonalInviteFactory.sol"; +import "../contracts/FeeSettings.sol"; +import "./resources/ERC20Helper.sol"; + +/** + * @dev These tests need a mainnet fork of the blockchain, as they access contracts deployed on mainnet. Take a look at docs/testing.md for more information. + */ + +contract MainnetCurrencies is Test { + using SafeERC20 for IERC20; + using stdStorage for StdStorage; // for stdStorage.set() + + ERC20Helper helper = new ERC20Helper(); + + AllowList list; + FeeSettings feeSettings; + + Token token; + PersonalInviteFactory factory; + + address public constant admin = 0x0109709eCFa91a80626FF3989D68f67f5b1dD120; + address public constant buyer = 0x1109709ecFA91a80626ff3989D68f67F5B1Dd121; + address public constant mintAllower = + 0x2109709EcFa91a80626Ff3989d68F67F5B1Dd122; + address public constant minter = 0x3109709ECfA91A80626fF3989D68f67F5B1Dd123; + address public constant owner = 0x6109709EcFA91A80626FF3989d68f67F5b1dd126; + address public constant receiver = + 0x7109709eCfa91A80626Ff3989D68f67f5b1dD127; + address public constant paymentTokenProvider = + 0x8109709ecfa91a80626fF3989d68f67F5B1dD128; + + // use opengsn forwarder https://etherscan.io/address/0xAa3E82b4c4093b4bA13Cb5714382C99ADBf750cA + address public constant trustedForwarder = + 0xAa3E82b4c4093b4bA13Cb5714382C99ADBf750cA; + + uint256 public constant maxAmountOfTokenToBeSold = 20 * 10 ** 18; // 20 token + uint256 public constant maxAmountPerBuyer = maxAmountOfTokenToBeSold / 2; // 10 token + uint256 public constant minAmountPerBuyer = maxAmountOfTokenToBeSold / 200; // 0.1 token + uint256 public constant amountOfTokenToBuy = maxAmountPerBuyer; + + // some math + uint256 public constant price = 7 * 10 ** 18; + uint256 public currencyCost; + uint256 public currencyAmount; + + // generate address of invite + bytes32 salt = bytes32(0); + + // test currencies + IERC20 USDC = IERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48); + IERC20 WETH = IERC20(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); + IERC20 WBTC = IERC20(0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599); + IERC20 EUROC = IERC20(0x1aBaEA1f7C830bD89Acc67eC4af516284b1bC33c); + IERC20 DAI = IERC20(0x6B175474E89094C44Da98b954EedeAC495271d0F); + + function setUp() public { + list = new AllowList(); + Fees memory fees = Fees(100, 100, 100, 0); + feeSettings = new FeeSettings(fees, admin); + + token = new Token( + trustedForwarder, + feeSettings, + admin, + list, + 0x0, + "TESTTOKEN", + "TEST" + ); + factory = new PersonalInviteFactory(); + currencyCost = (amountOfTokenToBuy * price) / 10 ** token.decimals(); + currencyAmount = currencyCost * 2; + } + + /** + @notice sets the balance of who to amount + taken from here: https://mirror.xyz/brocke.eth/PnX7oAcU4LJCxcoICiaDhq_MUUu9euaM8Y5r465Rd2U + */ + // function writeERC20Balance( + // address who, + // address _token, + // uint256 amount + // ) internal { + // stdstore + // .target(_token) + // .sig(IERC20(_token).balanceOf.selector) + // .with_key(who) + // .checked_write(amount); + // } + + function continuousFundraisingWithIERC20Currency(IERC20 _currency) public { + // some math + //uint _decimals = _currency.decimals(); // can't get decimals from IERC20 + //uint _price = 7 * 10**_decimals; // 7 payment tokens per token + uint256 _price = 7 * 10 ** 18; + uint256 _currencyCost = (amountOfTokenToBuy * _price) / + 10 ** token.decimals(); + uint256 _currencyAmount = _currencyCost * 2; + + // set up fundraise with _currency + vm.prank(owner); + ContinuousFundraising _raise = new ContinuousFundraising( + trustedForwarder, + payable(receiver), + minAmountPerBuyer, + maxAmountPerBuyer, + _price, + maxAmountOfTokenToBeSold, + _currency, + token + ); + + // allow raise contract to mint + bytes32 roleMintAllower = token.MINTALLOWER_ROLE(); + vm.prank(admin); + token.grantRole(roleMintAllower, mintAllower); + vm.prank(mintAllower); + token.increaseMintingAllowance( + address(_raise), + maxAmountOfTokenToBeSold + ); + + // give the buyer funds + //console.log("buyer's balance: ", _currency.balanceOf(buyer)); + helper.writeERC20Balance(buyer, address(_currency), _currencyAmount); + //console.log("buyer's balance: ", _currency.balanceOf(buyer)); + + // give raise contract a currency allowance + vm.prank(buyer); + _currency.approve(address(_raise), _currencyCost); + + // make sure buyer has no tokens before and receiver has no _currency before + assertEq(token.balanceOf(buyer), 0); + assertEq(token.balanceOf(receiver), 0); + assertEq(_currency.balanceOf(receiver), 0); + assertEq(_currency.balanceOf(buyer), _currencyAmount); + + // buy tokens + vm.prank(buyer); + _raise.buy(maxAmountPerBuyer, buyer); + + // check buyer has tokens and receiver has _currency afterwards + assertEq( + token.balanceOf(buyer), + amountOfTokenToBuy, + "buyer has tokens" + ); + assertEq(token.balanceOf(receiver), 0, "receiver has no tokens"); + assertEq( + _currency.balanceOf(receiver), + _currencyCost - + _currencyCost / + FeeSettings(address(token.feeSettings())) + .continuousFundraisingFeeDenominator(), + "receiver should have received currency" + ); + assertEq( + _currency.balanceOf( + FeeSettings(address(token.feeSettings())).feeCollector() + ), + _currencyCost / + FeeSettings(address(token.feeSettings())) + .continuousFundraisingFeeDenominator(), + "fee receiver should have received currency" + ); + assertEq( + token.balanceOf( + FeeSettings(address(token.feeSettings())).feeCollector() + ), + amountOfTokenToBuy / + FeeSettings(address(token.feeSettings())) + .continuousFundraisingFeeDenominator(), + "fee receiver should have received tokens" + ); + assertEq( + _currency.balanceOf(buyer), + _currencyAmount - _currencyCost, + "buyer should have paid currency" + ); + } + + function testContinuousFundraisingWithMainnetUSDC() public { + continuousFundraisingWithIERC20Currency(USDC); + } + + function testContinuousFundraisingWithMainnetWETH() public { + continuousFundraisingWithIERC20Currency(WETH); + } + + function testContinuousFundraisingWithMainnetWBTC() public { + continuousFundraisingWithIERC20Currency(WBTC); + } + + function testContinuousFundraisingWithMainnetEUROC() public { + continuousFundraisingWithIERC20Currency(EUROC); + } + + function testContinuousFundraisingWithMainnetDAI() public { + continuousFundraisingWithIERC20Currency(DAI); + } + + function personalInviteWithIERC20Currency(IERC20 _currency) public { + //bytes memory creationCode = type(PersonalInvite).creationCode; + uint256 expiration = block.timestamp + 1000; + + address expectedAddress = factory.getAddress( + salt, + buyer, + buyer, + receiver, + amountOfTokenToBuy, + price, + expiration, + _currency, + token + ); + + // grant mint allowance to invite + vm.prank(admin); + token.increaseMintingAllowance(expectedAddress, amountOfTokenToBuy); + + // give the buyer funds and approve invite + helper.writeERC20Balance(buyer, address(_currency), currencyAmount); + vm.prank(buyer); + _currency.approve(address(expectedAddress), currencyCost); + + // make sure balances are as expected before deployment + assertEq(_currency.balanceOf(buyer), currencyAmount); + assertEq(_currency.balanceOf(receiver), 0); + assertEq(token.balanceOf(buyer), 0); + assertEq(token.balanceOf(receiver), 0); + + // deploy invite + address inviteAddress = factory.deploy( + salt, + buyer, + buyer, + receiver, + amountOfTokenToBuy, + price, + expiration, + _currency, + token + ); + + // check situation after deployment + assertEq( + inviteAddress, + expectedAddress, + "deployed contract address is not correct" + ); + // check buyer has tokens and receiver has _currency afterwards + assertEq( + token.balanceOf(buyer), + amountOfTokenToBuy, + "buyer has tokens" + ); + assertEq(token.balanceOf(receiver), 0, "receiver has no tokens"); + assertEq( + _currency.balanceOf(receiver), + currencyCost - + token.feeSettings().continuousFundraisingFee(currencyCost), + "receiver should have received currency" + ); + assertEq( + _currency.balanceOf(token.feeSettings().feeCollector()), + token.feeSettings().continuousFundraisingFee(currencyCost), + "fee receiver should have received currency" + ); + assertEq( + token.balanceOf( + FeeSettings(address(token.feeSettings())).feeCollector() + ), + FeeSettings(address(token.feeSettings())).tokenFee( + amountOfTokenToBuy + ), + "fee receiver should have received tokens" + ); + assertEq( + _currency.balanceOf(buyer), + currencyAmount - currencyCost, + "buyer should have paid currency" + ); + + // log buyers token balance + console.log("buyer's token balance: ", token.balanceOf(buyer)); + } + + function testPersonalInviteWithMainnetUSDC() public { + personalInviteWithIERC20Currency(USDC); + } + + function testPersonalInviteWithMainnetWETH() public { + personalInviteWithIERC20Currency(WETH); + } + + function testPersonalInviteWithMainnetWBTC() public { + personalInviteWithIERC20Currency(WBTC); + } + + function testPersonalInviteWithMainnetEUROC() public { + personalInviteWithIERC20Currency(EUROC); + } + + function testPersonalInviteWithMainnetDAI() public { + personalInviteWithIERC20Currency(DAI); + } +} diff --git a/test/resources/ERC20Helper.sol b/test/resources/ERC20Helper.sol new file mode 100644 index 00000000..455de3cd --- /dev/null +++ b/test/resources/ERC20Helper.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity ^0.8.13; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "../../lib/forge-std/src/Test.sol"; + +contract ERC20Helper is Test { + using stdStorage for StdStorage; + /** + @notice sets the balance of who to amount. This is only possible in a test environment. + taken from here: https://mirror.xyz/brocke.eth/PnX7oAcU4LJCxcoICiaDhq_MUUu9euaM8Y5r465Rd2U + */ + function writeERC20Balance( + address who, + address _token, + uint256 amount + ) public { + stdstore + .target(_token) + .sig(IERC20(_token).balanceOf.selector) + .with_key(who) + .checked_write(amount); + } +} From 0a81777a71f60fe7fcdc37d6f2a56186fe982b53 Mon Sep 17 00:00:00 2001 From: malteish Date: Wed, 3 May 2023 12:33:58 +0200 Subject: [PATCH 17/75] test permit for EUROC --- test/MainnetCurrencies.t.sol | 321 ----------------------------- test/MainnetCurrenciesInvest.t.sol | 13 +- test/MainnetCurrenciesPermit.t.sol | 147 +++++++++++++ test/resources/ERC20Helper.sol | 13 ++ test/sample-test.js | 19 -- 5 files changed, 166 insertions(+), 347 deletions(-) delete mode 100644 test/MainnetCurrencies.t.sol create mode 100644 test/MainnetCurrenciesPermit.t.sol delete mode 100644 test/sample-test.js diff --git a/test/MainnetCurrencies.t.sol b/test/MainnetCurrencies.t.sol deleted file mode 100644 index 40588787..00000000 --- a/test/MainnetCurrencies.t.sol +++ /dev/null @@ -1,321 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-only -pragma solidity ^0.8.13; - -import "../lib/forge-std/src/Test.sol"; -//import "../lib/forge-std/stdlib.sol"; -import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import "../contracts/Token.sol"; -import "../contracts/ContinuousFundraising.sol"; -import "../contracts/PersonalInvite.sol"; -import "../contracts/PersonalInviteFactory.sol"; -import "../contracts/FeeSettings.sol"; - -/** - * @dev These tests need a mainnet fork of the blockchain, as they access contracts deployed on mainnet. Take a look at docs/testing.md for more information. - */ - -contract MainnetCurrencies is Test { - using SafeERC20 for IERC20; - using stdStorage for StdStorage; // for stdStorage.set() - - AllowList list; - FeeSettings feeSettings; - - Token token; - PersonalInviteFactory factory; - - address public constant admin = 0x0109709eCFa91a80626FF3989D68f67f5b1dD120; - address public constant buyer = 0x1109709ecFA91a80626ff3989D68f67F5B1Dd121; - address public constant mintAllower = - 0x2109709EcFa91a80626Ff3989d68F67F5B1Dd122; - address public constant minter = 0x3109709ECfA91A80626fF3989D68f67F5B1Dd123; - address public constant owner = 0x6109709EcFA91A80626FF3989d68f67F5b1dd126; - address public constant receiver = - 0x7109709eCfa91A80626Ff3989D68f67f5b1dD127; - address public constant paymentTokenProvider = - 0x8109709ecfa91a80626fF3989d68f67F5B1dD128; - - // use opengsn forwarder https://etherscan.io/address/0xAa3E82b4c4093b4bA13Cb5714382C99ADBf750cA - address public constant trustedForwarder = - 0xAa3E82b4c4093b4bA13Cb5714382C99ADBf750cA; - - uint256 public constant maxAmountOfTokenToBeSold = 20 * 10 ** 18; // 20 token - uint256 public constant maxAmountPerBuyer = maxAmountOfTokenToBeSold / 2; // 10 token - uint256 public constant minAmountPerBuyer = maxAmountOfTokenToBeSold / 200; // 0.1 token - uint256 public constant amountOfTokenToBuy = maxAmountPerBuyer; - - // some math - uint256 public constant price = 7 * 10 ** 18; - uint256 public currencyCost; - uint256 public currencyAmount; - - // generate address of invite - bytes32 salt = bytes32(0); - - // test currencies - IERC20 USDC = IERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48); - IERC20 WETH = IERC20(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); - IERC20 WBTC = IERC20(0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599); - IERC20 EUROC = IERC20(0x1aBaEA1f7C830bD89Acc67eC4af516284b1bC33c); - IERC20 DAI = IERC20(0x6B175474E89094C44Da98b954EedeAC495271d0F); - - function setUp() public { - list = new AllowList(); - Fees memory fees = Fees(100, 100, 100, 0); - feeSettings = new FeeSettings(fees, admin); - - token = new Token( - trustedForwarder, - feeSettings, - admin, - list, - 0x0, - "TESTTOKEN", - "TEST" - ); - factory = new PersonalInviteFactory(); - currencyCost = (amountOfTokenToBuy * price) / 10 ** token.decimals(); - currencyAmount = currencyCost * 2; - } - - // function testUSDCBalance() public { - // uint balance1 = usdc.balanceOf(buyer); - // console.log("buyer's balance: ", balance1); - // uint balance2 = usdc.balanceOf(address(0x55FE002aefF02F77364de339a1292923A15844B8)); - // console.log("circle's balance: ", balance2); - // } - - /** - @notice sets the balance of who to amount - taken from here: https://mirror.xyz/brocke.eth/PnX7oAcU4LJCxcoICiaDhq_MUUu9euaM8Y5r465Rd2U - */ - function writeERC20Balance( - address who, - address _token, - uint256 amount - ) internal { - stdstore - .target(_token) - .sig(IERC20(_token).balanceOf.selector) - .with_key(who) - .checked_write(amount); - } - - function continuousFundraisingWithIERC20Currency(IERC20 _currency) public { - // some math - //uint _decimals = _currency.decimals(); // can't get decimals from IERC20 - //uint _price = 7 * 10**_decimals; // 7 payment tokens per token - uint256 _price = 7 * 10 ** 18; - uint256 _currencyCost = (amountOfTokenToBuy * _price) / - 10 ** token.decimals(); - uint256 _currencyAmount = _currencyCost * 2; - - // set up fundraise with _currency - vm.prank(owner); - ContinuousFundraising _raise = new ContinuousFundraising( - trustedForwarder, - payable(receiver), - minAmountPerBuyer, - maxAmountPerBuyer, - _price, - maxAmountOfTokenToBeSold, - _currency, - token - ); - - // allow raise contract to mint - bytes32 roleMintAllower = token.MINTALLOWER_ROLE(); - vm.prank(admin); - token.grantRole(roleMintAllower, mintAllower); - vm.prank(mintAllower); - token.increaseMintingAllowance( - address(_raise), - maxAmountOfTokenToBeSold - ); - - // give the buyer funds - //console.log("buyer's balance: ", _currency.balanceOf(buyer)); - writeERC20Balance(buyer, address(_currency), _currencyAmount); - //console.log("buyer's balance: ", _currency.balanceOf(buyer)); - - // give raise contract a currency allowance - vm.prank(buyer); - _currency.approve(address(_raise), _currencyCost); - - // make sure buyer has no tokens before and receiver has no _currency before - assertEq(token.balanceOf(buyer), 0); - assertEq(token.balanceOf(receiver), 0); - assertEq(_currency.balanceOf(receiver), 0); - assertEq(_currency.balanceOf(buyer), _currencyAmount); - - // buy tokens - vm.prank(buyer); - _raise.buy(maxAmountPerBuyer, buyer); - - // check buyer has tokens and receiver has _currency afterwards - assertEq( - token.balanceOf(buyer), - amountOfTokenToBuy, - "buyer has tokens" - ); - assertEq(token.balanceOf(receiver), 0, "receiver has no tokens"); - assertEq( - _currency.balanceOf(receiver), - _currencyCost - - _currencyCost / - FeeSettings(address(token.feeSettings())) - .continuousFundraisingFeeDenominator(), - "receiver should have received currency" - ); - assertEq( - _currency.balanceOf( - FeeSettings(address(token.feeSettings())).feeCollector() - ), - _currencyCost / - FeeSettings(address(token.feeSettings())) - .continuousFundraisingFeeDenominator(), - "fee receiver should have received currency" - ); - assertEq( - token.balanceOf( - FeeSettings(address(token.feeSettings())).feeCollector() - ), - amountOfTokenToBuy / - FeeSettings(address(token.feeSettings())) - .continuousFundraisingFeeDenominator(), - "fee receiver should have received tokens" - ); - assertEq( - _currency.balanceOf(buyer), - _currencyAmount - _currencyCost, - "buyer should have paid currency" - ); - } - - function testContinuousFundraisingWithMainnetUSDC() public { - continuousFundraisingWithIERC20Currency(USDC); - } - - function testContinuousFundraisingWithMainnetWETH() public { - continuousFundraisingWithIERC20Currency(WETH); - } - - function testContinuousFundraisingWithMainnetWBTC() public { - continuousFundraisingWithIERC20Currency(WBTC); - } - - function testContinuousFundraisingWithMainnetEUROC() public { - continuousFundraisingWithIERC20Currency(EUROC); - } - - function testContinuousFundraisingWithMainnetDAI() public { - continuousFundraisingWithIERC20Currency(DAI); - } - - function personalInviteWithIERC20Currency(IERC20 _currency) public { - //bytes memory creationCode = type(PersonalInvite).creationCode; - uint256 expiration = block.timestamp + 1000; - - address expectedAddress = factory.getAddress( - salt, - buyer, - buyer, - receiver, - amountOfTokenToBuy, - price, - expiration, - _currency, - token - ); - - // grant mint allowance to invite - vm.prank(admin); - token.increaseMintingAllowance(expectedAddress, amountOfTokenToBuy); - - // give the buyer funds and approve invite - writeERC20Balance(buyer, address(_currency), currencyAmount); - vm.prank(buyer); - _currency.approve(address(expectedAddress), currencyCost); - - // make sure balances are as expected before deployment - assertEq(_currency.balanceOf(buyer), currencyAmount); - assertEq(_currency.balanceOf(receiver), 0); - assertEq(token.balanceOf(buyer), 0); - assertEq(token.balanceOf(receiver), 0); - - // deploy invite - address inviteAddress = factory.deploy( - salt, - buyer, - buyer, - receiver, - amountOfTokenToBuy, - price, - expiration, - _currency, - token - ); - - // check situation after deployment - assertEq( - inviteAddress, - expectedAddress, - "deployed contract address is not correct" - ); - // check buyer has tokens and receiver has _currency afterwards - assertEq( - token.balanceOf(buyer), - amountOfTokenToBuy, - "buyer has tokens" - ); - assertEq(token.balanceOf(receiver), 0, "receiver has no tokens"); - assertEq( - _currency.balanceOf(receiver), - currencyCost - - token.feeSettings().continuousFundraisingFee(currencyCost), - "receiver should have received currency" - ); - assertEq( - _currency.balanceOf(token.feeSettings().feeCollector()), - token.feeSettings().continuousFundraisingFee(currencyCost), - "fee receiver should have received currency" - ); - assertEq( - token.balanceOf( - FeeSettings(address(token.feeSettings())).feeCollector() - ), - FeeSettings(address(token.feeSettings())).tokenFee( - amountOfTokenToBuy - ), - "fee receiver should have received tokens" - ); - assertEq( - _currency.balanceOf(buyer), - currencyAmount - currencyCost, - "buyer should have paid currency" - ); - - // log buyers token balance - console.log("buyer's token balance: ", token.balanceOf(buyer)); - } - - function testPersonalInviteWithMainnetUSDC() public { - personalInviteWithIERC20Currency(USDC); - } - - function testPersonalInviteWithMainnetWETH() public { - personalInviteWithIERC20Currency(WETH); - } - - function testPersonalInviteWithMainnetWBTC() public { - personalInviteWithIERC20Currency(WBTC); - } - - function testPersonalInviteWithMainnetEUROC() public { - personalInviteWithIERC20Currency(EUROC); - } - - function testPersonalInviteWithMainnetDAI() public { - personalInviteWithIERC20Currency(DAI); - } -} diff --git a/test/MainnetCurrenciesInvest.t.sol b/test/MainnetCurrenciesInvest.t.sol index 63c713e0..222dec31 100644 --- a/test/MainnetCurrenciesInvest.t.sol +++ b/test/MainnetCurrenciesInvest.t.sol @@ -17,7 +17,6 @@ import "./resources/ERC20Helper.sol"; contract MainnetCurrencies is Test { using SafeERC20 for IERC20; - using stdStorage for StdStorage; // for stdStorage.set() ERC20Helper helper = new ERC20Helper(); @@ -55,12 +54,12 @@ contract MainnetCurrencies is Test { // generate address of invite bytes32 salt = bytes32(0); - // test currencies - IERC20 USDC = IERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48); - IERC20 WETH = IERC20(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); - IERC20 WBTC = IERC20(0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599); - IERC20 EUROC = IERC20(0x1aBaEA1f7C830bD89Acc67eC4af516284b1bC33c); - IERC20 DAI = IERC20(0x6B175474E89094C44Da98b954EedeAC495271d0F); + // // test currencies + // IERC20 USDC = IERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48); + // IERC20 WETH = IERC20(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); + // IERC20 WBTC = IERC20(0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599); + // IERC20 EUROC = IERC20(0x1aBaEA1f7C830bD89Acc67eC4af516284b1bC33c); + // IERC20 DAI = IERC20(0x6B175474E89094C44Da98b954EedeAC495271d0F); function setUp() public { list = new AllowList(); diff --git a/test/MainnetCurrenciesPermit.t.sol b/test/MainnetCurrenciesPermit.t.sol new file mode 100644 index 00000000..f4837512 --- /dev/null +++ b/test/MainnetCurrenciesPermit.t.sol @@ -0,0 +1,147 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity ^0.8.13; + +import "../lib/forge-std/src/Test.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/extensions/draft-ERC20Permit.sol"; +import "../contracts/Token.sol"; +import "../contracts/ContinuousFundraising.sol"; +import "../contracts/PersonalInvite.sol"; +import "../contracts/PersonalInviteFactory.sol"; +import "../contracts/FeeSettings.sol"; +import "./resources/ERC20Helper.sol"; + + +/** + * @dev These tests need a mainnet fork of the blockchain, as they access contracts deployed on mainnet. Take a look at docs/testing.md for more information. + */ + +contract MainnetCurrencies is Test { + using SafeERC20 for IERC20; + + ERC20Helper helper = new ERC20Helper(); + + + uint256 public constant tokenOwnerPrivateKey = + 0x3c69254ad72222e3ddf37667b8173dd773bdbdfd93d4af1d192815ff0662de5f; + address public tokenOwner = vm.addr(tokenOwnerPrivateKey); // = 0x7109709eCfa91A80626Ff3989D68f67f5b1dD127; + + address public constant admin = 0x0109709eCFa91a80626FF3989D68f67f5b1dD120; + address public constant buyer = 0x1109709ecFA91a80626ff3989D68f67F5B1Dd121; + address public constant mintAllower = + 0x2109709EcFa91a80626Ff3989d68F67F5B1Dd122; + address public constant minter = 0x3109709ECfA91A80626fF3989D68f67F5B1Dd123; + address public constant owner = 0x6109709EcFA91A80626FF3989d68f67F5b1dd126; + address public constant receiver = + 0x7109709eCfa91A80626Ff3989D68f67f5b1dD127; + address public constant paymentTokenProvider = + 0x8109709ecfa91a80626fF3989d68f67F5B1dD128; + + // use opengsn forwarder https://etherscan.io/address/0xAa3E82b4c4093b4bA13Cb5714382C99ADBf750cA + address public constant trustedForwarder = + 0xAa3E82b4c4093b4bA13Cb5714382C99ADBf750cA; + + uint256 public constant maxAmountOfTokenToBeSold = 20 * 10 ** 18; // 20 token + uint256 public constant maxAmountPerBuyer = maxAmountOfTokenToBeSold / 2; // 10 token + uint256 public constant minAmountPerBuyer = maxAmountOfTokenToBeSold / 200; // 0.1 token + uint256 public constant amountOfTokenToBuy = maxAmountPerBuyer; + + // some math + uint256 public constant price = 7 * 10 ** 18; + uint256 public currencyCost; + uint256 public currencyAmount; + + // global variable because I am running out of local ones + uint256 nonce; + uint256 deadline; + bytes32 permitTypehash; + bytes32 DOMAIN_SEPARATOR; + bytes32 structHash; + + function setUp() public { + + } + + function permitERC2612( + ERC20Permit token, + uint256 _tokenPermitAmount, + uint256 _tokenTransferAmount, + uint256 _tokenOwnerPrivateKey, + address tokenSpender + ) public { + vm.assume(_tokenTransferAmount <= _tokenPermitAmount); + tokenOwner = vm.addr(_tokenOwnerPrivateKey); + helper.writeERC20Balance(tokenOwner, address(token), _tokenPermitAmount); + + // permit spender to spend holder's tokens + nonce = token.nonces(tokenOwner); + deadline = block.timestamp + 1000; + permitTypehash = keccak256( + "Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)" + ); + DOMAIN_SEPARATOR = token.DOMAIN_SEPARATOR(); + structHash = keccak256( + abi.encode( + permitTypehash, + tokenOwner, + tokenSpender, + _tokenPermitAmount, + nonce, + deadline + ) + ); + + bytes32 hash = ECDSA.toTypedDataHash(DOMAIN_SEPARATOR, structHash); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(_tokenOwnerPrivateKey, hash); + + // verify signature + require( + tokenOwner == ECDSA.recover(hash, v, r, s), + "invalid signature" + ); + + // check allowance + assertEq(token.allowance(tokenOwner, tokenSpender), 0, "allowance should be 0"); + + // call permit with a wallet that is not tokenOwner + token.permit( + tokenOwner, + tokenSpender, + _tokenPermitAmount, + deadline, + v, + r, + s + ); + + // check allowance + assertEq(token.allowance(tokenOwner, tokenSpender), _tokenPermitAmount, "allowance should be _tokenPermitAmount"); + + // check token balance of tokenSpender + assertEq(token.balanceOf(tokenOwner), _tokenPermitAmount, "token balance of tokenOwner should be _tokenPermitAmount"); + assertEq(token.balanceOf(tokenSpender), 0, "token balance of tokenSpender should be 0"); + + console.log("Tranfering %s tokens from %s to %s", _tokenPermitAmount, tokenOwner, tokenSpender); + // spend tokens + vm.prank(tokenSpender); + token.transferFrom(tokenOwner, tokenSpender, _tokenTransferAmount); + + // check token balance of tokenSpender + assertEq( + token.balanceOf(tokenOwner), + _tokenPermitAmount - _tokenTransferAmount, + "token balance of tokenOwner should be _tokenPermitAmount - _tokenTransferAmount" + ); + assertEq(token.balanceOf(tokenSpender), _tokenTransferAmount, "token balance of tokenSpender should be _tokenTransferAmount"); + } + + function testPermitEUROC() public { + permitERC2612(ERC20Permit(address(EUROC)), 200, 100, tokenOwnerPrivateKey, address(2)); + } + + // still fails for some reason + // function testPermitUSDC() public { + // permitERC2612(ERC20Permit(address(USDC)), 200, 100, tokenOwnerPrivateKey, address(2)); + // } +} diff --git a/test/resources/ERC20Helper.sol b/test/resources/ERC20Helper.sol index 455de3cd..508faf12 100644 --- a/test/resources/ERC20Helper.sol +++ b/test/resources/ERC20Helper.sol @@ -4,7 +4,15 @@ pragma solidity ^0.8.13; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "../../lib/forge-std/src/Test.sol"; +// test currencies +IERC20 constant USDC = IERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48); +IERC20 constant WETH = IERC20(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); +IERC20 constant WBTC = IERC20(0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599); +IERC20 constant EUROC = IERC20(0x1aBaEA1f7C830bD89Acc67eC4af516284b1bC33c); +IERC20 constant DAI = IERC20(0x6B175474E89094C44Da98b954EedeAC495271d0F); + contract ERC20Helper is Test { + using stdStorage for StdStorage; /** @notice sets the balance of who to amount. This is only possible in a test environment. @@ -20,5 +28,10 @@ contract ERC20Helper is Test { .sig(IERC20(_token).balanceOf.selector) .with_key(who) .checked_write(amount); + + require( + IERC20(_token).balanceOf(who) == amount, + "ERC20Helper: balance not set" + ); } } diff --git a/test/sample-test.js b/test/sample-test.js deleted file mode 100644 index 44e0fcb9..00000000 --- a/test/sample-test.js +++ /dev/null @@ -1,19 +0,0 @@ -const { expect } = require("chai"); -const { ethers } = require("hardhat"); - -describe("Greeter", function () { - it("Should return the new greeting once it's changed", async function () { - const Greeter = await ethers.getContractFactory("Greeter"); - const greeter = await Greeter.deploy("Hello, world!"); - await greeter.deployed(); - - expect(await greeter.greet()).to.equal("Hello, world!"); - - const setGreetingTx = await greeter.setGreeting("Hola, mundo!"); - - // wait until the transaction is mined - await setGreetingTx.wait(); - - expect(await greeter.greet()).to.equal("Hola, mundo!"); - }); -}); From 0e6d24de092880576f559e019ab879bf426d4337 Mon Sep 17 00:00:00 2001 From: malteish Date: Wed, 3 May 2023 10:34:26 +0000 Subject: [PATCH 18/75] Prettified Code! --- test/MainnetCurrenciesPermit.t.sol | 57 +++++++++++++++++++++++------- test/resources/ERC20Helper.sol | 2 +- 2 files changed, 45 insertions(+), 14 deletions(-) diff --git a/test/MainnetCurrenciesPermit.t.sol b/test/MainnetCurrenciesPermit.t.sol index f4837512..6b5ea09e 100644 --- a/test/MainnetCurrenciesPermit.t.sol +++ b/test/MainnetCurrenciesPermit.t.sol @@ -11,7 +11,6 @@ import "../contracts/PersonalInviteFactory.sol"; import "../contracts/FeeSettings.sol"; import "./resources/ERC20Helper.sol"; - /** * @dev These tests need a mainnet fork of the blockchain, as they access contracts deployed on mainnet. Take a look at docs/testing.md for more information. */ @@ -21,7 +20,6 @@ contract MainnetCurrencies is Test { ERC20Helper helper = new ERC20Helper(); - uint256 public constant tokenOwnerPrivateKey = 0x3c69254ad72222e3ddf37667b8173dd773bdbdfd93d4af1d192815ff0662de5f; address public tokenOwner = vm.addr(tokenOwnerPrivateKey); // = 0x7109709eCfa91A80626Ff3989D68f67f5b1dD127; @@ -58,9 +56,7 @@ contract MainnetCurrencies is Test { bytes32 DOMAIN_SEPARATOR; bytes32 structHash; - function setUp() public { - - } + function setUp() public {} function permitERC2612( ERC20Permit token, @@ -71,7 +67,11 @@ contract MainnetCurrencies is Test { ) public { vm.assume(_tokenTransferAmount <= _tokenPermitAmount); tokenOwner = vm.addr(_tokenOwnerPrivateKey); - helper.writeERC20Balance(tokenOwner, address(token), _tokenPermitAmount); + helper.writeERC20Balance( + tokenOwner, + address(token), + _tokenPermitAmount + ); // permit spender to spend holder's tokens nonce = token.nonces(tokenOwner); @@ -102,7 +102,11 @@ contract MainnetCurrencies is Test { ); // check allowance - assertEq(token.allowance(tokenOwner, tokenSpender), 0, "allowance should be 0"); + assertEq( + token.allowance(tokenOwner, tokenSpender), + 0, + "allowance should be 0" + ); // call permit with a wallet that is not tokenOwner token.permit( @@ -116,13 +120,30 @@ contract MainnetCurrencies is Test { ); // check allowance - assertEq(token.allowance(tokenOwner, tokenSpender), _tokenPermitAmount, "allowance should be _tokenPermitAmount"); + assertEq( + token.allowance(tokenOwner, tokenSpender), + _tokenPermitAmount, + "allowance should be _tokenPermitAmount" + ); // check token balance of tokenSpender - assertEq(token.balanceOf(tokenOwner), _tokenPermitAmount, "token balance of tokenOwner should be _tokenPermitAmount"); - assertEq(token.balanceOf(tokenSpender), 0, "token balance of tokenSpender should be 0"); + assertEq( + token.balanceOf(tokenOwner), + _tokenPermitAmount, + "token balance of tokenOwner should be _tokenPermitAmount" + ); + assertEq( + token.balanceOf(tokenSpender), + 0, + "token balance of tokenSpender should be 0" + ); - console.log("Tranfering %s tokens from %s to %s", _tokenPermitAmount, tokenOwner, tokenSpender); + console.log( + "Tranfering %s tokens from %s to %s", + _tokenPermitAmount, + tokenOwner, + tokenSpender + ); // spend tokens vm.prank(tokenSpender); token.transferFrom(tokenOwner, tokenSpender, _tokenTransferAmount); @@ -133,11 +154,21 @@ contract MainnetCurrencies is Test { _tokenPermitAmount - _tokenTransferAmount, "token balance of tokenOwner should be _tokenPermitAmount - _tokenTransferAmount" ); - assertEq(token.balanceOf(tokenSpender), _tokenTransferAmount, "token balance of tokenSpender should be _tokenTransferAmount"); + assertEq( + token.balanceOf(tokenSpender), + _tokenTransferAmount, + "token balance of tokenSpender should be _tokenTransferAmount" + ); } function testPermitEUROC() public { - permitERC2612(ERC20Permit(address(EUROC)), 200, 100, tokenOwnerPrivateKey, address(2)); + permitERC2612( + ERC20Permit(address(EUROC)), + 200, + 100, + tokenOwnerPrivateKey, + address(2) + ); } // still fails for some reason diff --git a/test/resources/ERC20Helper.sol b/test/resources/ERC20Helper.sol index 508faf12..1c4d6b60 100644 --- a/test/resources/ERC20Helper.sol +++ b/test/resources/ERC20Helper.sol @@ -12,8 +12,8 @@ IERC20 constant EUROC = IERC20(0x1aBaEA1f7C830bD89Acc67eC4af516284b1bC33c); IERC20 constant DAI = IERC20(0x6B175474E89094C44Da98b954EedeAC495271d0F); contract ERC20Helper is Test { - using stdStorage for StdStorage; + /** @notice sets the balance of who to amount. This is only possible in a test environment. taken from here: https://mirror.xyz/brocke.eth/PnX7oAcU4LJCxcoICiaDhq_MUUu9euaM8Y5r465Rd2U From 448304a61269fcefb861251dd6c25cef858314cf Mon Sep 17 00:00:00 2001 From: malteish Date: Wed, 3 May 2023 13:55:10 +0200 Subject: [PATCH 19/75] test USDC --- test/MainnetCurrenciesPermit.t.sol | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/test/MainnetCurrenciesPermit.t.sol b/test/MainnetCurrenciesPermit.t.sol index 6b5ea09e..b597a6ad 100644 --- a/test/MainnetCurrenciesPermit.t.sol +++ b/test/MainnetCurrenciesPermit.t.sol @@ -126,17 +126,18 @@ contract MainnetCurrencies is Test { "allowance should be _tokenPermitAmount" ); - // check token balance of tokenSpender - assertEq( + assertEq( token.balanceOf(tokenOwner), _tokenPermitAmount, "token balance of tokenOwner should be _tokenPermitAmount" ); - assertEq( - token.balanceOf(tokenSpender), - 0, - "token balance of tokenSpender should be 0" - ); + // store token balance of tokenSpender + uint tokenSpenderBalanceBefore = token.balanceOf(tokenSpender); + // assertEq( + // token.balanceOf(tokenSpender), + // 0, + // "token balance of tokenSpender should be 0" + // ); console.log( "Tranfering %s tokens from %s to %s", @@ -156,7 +157,7 @@ contract MainnetCurrencies is Test { ); assertEq( token.balanceOf(tokenSpender), - _tokenTransferAmount, + _tokenTransferAmount + tokenSpenderBalanceBefore, "token balance of tokenSpender should be _tokenTransferAmount" ); } @@ -171,8 +172,7 @@ contract MainnetCurrencies is Test { ); } - // still fails for some reason - // function testPermitUSDC() public { - // permitERC2612(ERC20Permit(address(USDC)), 200, 100, tokenOwnerPrivateKey, address(2)); - // } + function testPermitUSDC() public { + permitERC2612(ERC20Permit(address(USDC)), 200, 100, tokenOwnerPrivateKey, address(2)); + } } From 8facdd07bb434c405998d33ddd48d21a9e02f483 Mon Sep 17 00:00:00 2001 From: malteish Date: Wed, 3 May 2023 11:55:34 +0000 Subject: [PATCH 20/75] Prettified Code! --- test/MainnetCurrenciesPermit.t.sol | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/test/MainnetCurrenciesPermit.t.sol b/test/MainnetCurrenciesPermit.t.sol index b597a6ad..0f4882b4 100644 --- a/test/MainnetCurrenciesPermit.t.sol +++ b/test/MainnetCurrenciesPermit.t.sol @@ -126,7 +126,7 @@ contract MainnetCurrencies is Test { "allowance should be _tokenPermitAmount" ); - assertEq( + assertEq( token.balanceOf(tokenOwner), _tokenPermitAmount, "token balance of tokenOwner should be _tokenPermitAmount" @@ -173,6 +173,12 @@ contract MainnetCurrencies is Test { } function testPermitUSDC() public { - permitERC2612(ERC20Permit(address(USDC)), 200, 100, tokenOwnerPrivateKey, address(2)); + permitERC2612( + ERC20Permit(address(USDC)), + 200, + 100, + tokenOwnerPrivateKey, + address(2) + ); } } From b83f5e38d7b7d286b23a36ac8dd3b6a9ef76bbf8 Mon Sep 17 00:00:00 2001 From: malteish Date: Wed, 3 May 2023 14:32:02 +0200 Subject: [PATCH 21/75] add permit for DAI --- test/MainnetCurrenciesPermit.t.sol | 128 +++++++++++++++++++++++++++-- 1 file changed, 121 insertions(+), 7 deletions(-) diff --git a/test/MainnetCurrenciesPermit.t.sol b/test/MainnetCurrenciesPermit.t.sol index 0f4882b4..1eb7dace 100644 --- a/test/MainnetCurrenciesPermit.t.sol +++ b/test/MainnetCurrenciesPermit.t.sol @@ -11,10 +11,25 @@ import "../contracts/PersonalInviteFactory.sol"; import "../contracts/FeeSettings.sol"; import "./resources/ERC20Helper.sol"; + +interface DaiLike { + function PERMIT_TYPEHASH() external view returns (bytes32); + function permit( + address holder, + address spender, + uint256 nonce, + uint256 expiry, + bool allowed, + uint8 v, + bytes32 r, + bytes32 s + ) external; +} + + /** * @dev These tests need a mainnet fork of the blockchain, as they access contracts deployed on mainnet. Take a look at docs/testing.md for more information. */ - contract MainnetCurrencies is Test { using SafeERC20 for IERC20; @@ -108,7 +123,8 @@ contract MainnetCurrencies is Test { "allowance should be 0" ); - // call permit with a wallet that is not tokenOwner + // call permit as and address a that is not tokenOwner + assertTrue(address(this) != tokenOwner, "address(this) must not be tokenOwner"); token.permit( tokenOwner, tokenSpender, @@ -133,11 +149,6 @@ contract MainnetCurrencies is Test { ); // store token balance of tokenSpender uint tokenSpenderBalanceBefore = token.balanceOf(tokenSpender); - // assertEq( - // token.balanceOf(tokenSpender), - // 0, - // "token balance of tokenSpender should be 0" - // ); console.log( "Tranfering %s tokens from %s to %s", @@ -181,4 +192,107 @@ contract MainnetCurrencies is Test { address(2) ); } + + function permitDAI( + ERC20Permit token, + uint256 _tokenPermitAmount, + uint256 _tokenTransferAmount, + uint256 _tokenOwnerPrivateKey, + address tokenSpender + ) public { + vm.assume(_tokenTransferAmount <= _tokenPermitAmount); + tokenOwner = vm.addr(_tokenOwnerPrivateKey); + helper.writeERC20Balance( + tokenOwner, + address(token), + _tokenPermitAmount + ); + + // permit spender to spend holder's tokens + bool allowed = true; + nonce = token.nonces(tokenOwner); + deadline = block.timestamp + 1000; + DOMAIN_SEPARATOR = token.DOMAIN_SEPARATOR(); + structHash = keccak256( + abi.encode( + DaiLike(address(token)).PERMIT_TYPEHASH(), + tokenOwner, + tokenSpender, + nonce, + deadline, + allowed + ) + ); + + bytes32 hash = ECDSA.toTypedDataHash(DOMAIN_SEPARATOR, structHash); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(_tokenOwnerPrivateKey, hash); + + // verify signature + require( + tokenOwner == ECDSA.recover(hash, v, r, s), + "invalid signature" + ); + + // check allowance + assertEq( + token.allowance(tokenOwner, tokenSpender), + 0, + "allowance should be 0" + ); + + // call permit as and address a that is not tokenOwner + assertTrue(address(this) != tokenOwner, "address(this) must not be tokenOwner"); + DaiLike(address(token)).permit( + tokenOwner, + tokenSpender, + nonce, + deadline, + true, + v, + r, + s + ); + + // check allowance + assertEq( + token.allowance(tokenOwner, tokenSpender), + UINT256_MAX, + "allowance should be UINT256_MAX" + ); + + assertEq( + token.balanceOf(tokenOwner), + _tokenPermitAmount, + "token balance of tokenOwner should be _tokenPermitAmount" + ); + // store token balance of tokenSpender + uint tokenSpenderBalanceBefore = token.balanceOf(tokenSpender); + + console.log( + "Tranfering %s tokens from %s to %s", + _tokenPermitAmount, + tokenOwner, + tokenSpender + ); + // spend tokens + vm.prank(tokenSpender); + token.transferFrom(tokenOwner, tokenSpender, _tokenTransferAmount); + + // check token balance of tokenSpender + assertEq( + token.balanceOf(tokenOwner), + _tokenPermitAmount - _tokenTransferAmount, + "token balance of tokenOwner should be _tokenPermitAmount - _tokenTransferAmount" + ); + assertEq( + token.balanceOf(tokenSpender), + _tokenTransferAmount + tokenSpenderBalanceBefore, + "token balance of tokenSpender should be _tokenTransferAmount" + ); + } + + function testPermitDAI() public { + permitDAI(ERC20Permit(address(DAI)), 200, 100, tokenOwnerPrivateKey, address(2)); + } } From 636aae8f9c3e5c17e013721e500202d53b219b81 Mon Sep 17 00:00:00 2001 From: malteish Date: Wed, 3 May 2023 12:32:30 +0000 Subject: [PATCH 22/75] Prettified Code! --- test/MainnetCurrenciesPermit.t.sol | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/test/MainnetCurrenciesPermit.t.sol b/test/MainnetCurrenciesPermit.t.sol index 1eb7dace..71234650 100644 --- a/test/MainnetCurrenciesPermit.t.sol +++ b/test/MainnetCurrenciesPermit.t.sol @@ -11,9 +11,9 @@ import "../contracts/PersonalInviteFactory.sol"; import "../contracts/FeeSettings.sol"; import "./resources/ERC20Helper.sol"; - interface DaiLike { function PERMIT_TYPEHASH() external view returns (bytes32); + function permit( address holder, address spender, @@ -26,7 +26,6 @@ interface DaiLike { ) external; } - /** * @dev These tests need a mainnet fork of the blockchain, as they access contracts deployed on mainnet. Take a look at docs/testing.md for more information. */ @@ -124,7 +123,10 @@ contract MainnetCurrencies is Test { ); // call permit as and address a that is not tokenOwner - assertTrue(address(this) != tokenOwner, "address(this) must not be tokenOwner"); + assertTrue( + address(this) != tokenOwner, + "address(this) must not be tokenOwner" + ); token.permit( tokenOwner, tokenSpender, @@ -242,7 +244,10 @@ contract MainnetCurrencies is Test { ); // call permit as and address a that is not tokenOwner - assertTrue(address(this) != tokenOwner, "address(this) must not be tokenOwner"); + assertTrue( + address(this) != tokenOwner, + "address(this) must not be tokenOwner" + ); DaiLike(address(token)).permit( tokenOwner, tokenSpender, @@ -261,7 +266,7 @@ contract MainnetCurrencies is Test { "allowance should be UINT256_MAX" ); - assertEq( + assertEq( token.balanceOf(tokenOwner), _tokenPermitAmount, "token balance of tokenOwner should be _tokenPermitAmount" @@ -293,6 +298,12 @@ contract MainnetCurrencies is Test { } function testPermitDAI() public { - permitDAI(ERC20Permit(address(DAI)), 200, 100, tokenOwnerPrivateKey, address(2)); + permitDAI( + ERC20Permit(address(DAI)), + 200, + 100, + tokenOwnerPrivateKey, + address(2) + ); } } From 945835dfc76fc0bf4e585ebc8e78b7996ae18b42 Mon Sep 17 00:00:00 2001 From: malteish Date: Wed, 3 May 2023 15:36:26 +0200 Subject: [PATCH 23/75] cleanup --- .gitignore | 1 + test/MainnetCurrenciesPermit.t.sol | 81 +++++++++++------------------- 2 files changed, 31 insertions(+), 51 deletions(-) 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/test/MainnetCurrenciesPermit.t.sol b/test/MainnetCurrenciesPermit.t.sol index 71234650..ec6220a2 100644 --- a/test/MainnetCurrenciesPermit.t.sol +++ b/test/MainnetCurrenciesPermit.t.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.13; import "../lib/forge-std/src/Test.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "@openzeppelin/contracts/token/ERC20/extensions/draft-ERC20Permit.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "../contracts/Token.sol"; import "../contracts/ContinuousFundraising.sol"; import "../contracts/PersonalInvite.sol"; @@ -11,9 +12,13 @@ import "../contracts/PersonalInviteFactory.sol"; import "../contracts/FeeSettings.sol"; import "./resources/ERC20Helper.sol"; -interface DaiLike { +interface DaiLike is IERC20 { function PERMIT_TYPEHASH() external view returns (bytes32); + function DOMAIN_SEPARATOR() external view returns (bytes32); + + function nonces(address owner) external view returns (uint256); + function permit( address holder, address spender, @@ -38,35 +43,13 @@ contract MainnetCurrencies is Test { 0x3c69254ad72222e3ddf37667b8173dd773bdbdfd93d4af1d192815ff0662de5f; address public tokenOwner = vm.addr(tokenOwnerPrivateKey); // = 0x7109709eCfa91A80626Ff3989D68f67f5b1dD127; - address public constant admin = 0x0109709eCFa91a80626FF3989D68f67f5b1dD120; - address public constant buyer = 0x1109709ecFA91a80626ff3989D68f67F5B1Dd121; - address public constant mintAllower = - 0x2109709EcFa91a80626Ff3989d68F67F5B1Dd122; - address public constant minter = 0x3109709ECfA91A80626fF3989D68f67F5B1Dd123; - address public constant owner = 0x6109709EcFA91A80626FF3989d68f67F5b1dd126; address public constant receiver = 0x7109709eCfa91A80626Ff3989D68f67f5b1dD127; - address public constant paymentTokenProvider = - 0x8109709ecfa91a80626fF3989d68f67F5B1dD128; - - // use opengsn forwarder https://etherscan.io/address/0xAa3E82b4c4093b4bA13Cb5714382C99ADBf750cA - address public constant trustedForwarder = - 0xAa3E82b4c4093b4bA13Cb5714382C99ADBf750cA; - - uint256 public constant maxAmountOfTokenToBeSold = 20 * 10 ** 18; // 20 token - uint256 public constant maxAmountPerBuyer = maxAmountOfTokenToBeSold / 2; // 10 token - uint256 public constant minAmountPerBuyer = maxAmountOfTokenToBeSold / 200; // 0.1 token - uint256 public constant amountOfTokenToBuy = maxAmountPerBuyer; - - // some math - uint256 public constant price = 7 * 10 ** 18; - uint256 public currencyCost; - uint256 public currencyAmount; // global variable because I am running out of local ones uint256 nonce; uint256 deadline; - bytes32 permitTypehash; + bytes32 PERMIT_TYPE_HASH; bytes32 DOMAIN_SEPARATOR; bytes32 structHash; @@ -90,13 +73,13 @@ contract MainnetCurrencies is Test { // permit spender to spend holder's tokens nonce = token.nonces(tokenOwner); deadline = block.timestamp + 1000; - permitTypehash = keccak256( + PERMIT_TYPE_HASH = keccak256( "Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)" ); DOMAIN_SEPARATOR = token.DOMAIN_SEPARATOR(); structHash = keccak256( abi.encode( - permitTypehash, + PERMIT_TYPE_HASH, tokenOwner, tokenSpender, _tokenPermitAmount, @@ -175,42 +158,45 @@ contract MainnetCurrencies is Test { ); } - function testPermitEUROC() public { + function testPermitMainnetEUROC() public { permitERC2612( ERC20Permit(address(EUROC)), 200, - 100, + 123, tokenOwnerPrivateKey, - address(2) + receiver ); } - function testPermitUSDC() public { + function testPermitMainnetUSDC() public { permitERC2612( ERC20Permit(address(USDC)), 200, - 100, + 190, tokenOwnerPrivateKey, - address(2) + receiver ); } - function permitDAI( - ERC20Permit token, - uint256 _tokenPermitAmount, - uint256 _tokenTransferAmount, - uint256 _tokenOwnerPrivateKey, - address tokenSpender - ) public { + /** + * @dev This test takes into account the special permit implementation of DAI + */ + function testPermitMainnetDAI() public { + DaiLike token = DaiLike(address(DAI)); + uint256 _tokenPermitAmount = 200; + uint256 _tokenTransferAmount = 70; + tokenOwner = vm.addr(tokenOwnerPrivateKey); + address tokenSpender = receiver; + vm.assume(_tokenTransferAmount <= _tokenPermitAmount); - tokenOwner = vm.addr(_tokenOwnerPrivateKey); + tokenOwner = vm.addr(tokenOwnerPrivateKey); helper.writeERC20Balance( tokenOwner, address(token), _tokenPermitAmount ); - // permit spender to spend holder's tokens + // permit spender to spend ALL OF the owner's tokens. This is the special case for DAI. bool allowed = true; nonce = token.nonces(tokenOwner); deadline = block.timestamp + 1000; @@ -228,7 +214,7 @@ contract MainnetCurrencies is Test { bytes32 hash = ECDSA.toTypedDataHash(DOMAIN_SEPARATOR, structHash); - (uint8 v, bytes32 r, bytes32 s) = vm.sign(_tokenOwnerPrivateKey, hash); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(tokenOwnerPrivateKey, hash); // verify signature require( @@ -248,6 +234,7 @@ contract MainnetCurrencies is Test { address(this) != tokenOwner, "address(this) must not be tokenOwner" ); + DaiLike(address(token)).permit( tokenOwner, tokenSpender, @@ -297,13 +284,5 @@ contract MainnetCurrencies is Test { ); } - function testPermitDAI() public { - permitDAI( - ERC20Permit(address(DAI)), - 200, - 100, - tokenOwnerPrivateKey, - address(2) - ); - } + // sadly, WETH and WBTC seem not to support permit or an equivalent meta transaction } From 3b8f8b62528d0ecd6b234746d464263ceeb609fc Mon Sep 17 00:00:00 2001 From: malteish Date: Fri, 5 May 2023 15:38:02 +0200 Subject: [PATCH 24/75] add basic test for VestingWallet --- test/PersonalInviteTimeLock.t.sol | 224 ++++++++++++++++++ ...tokenize.it-smart-contracts.code-workspace | 8 + 2 files changed, 232 insertions(+) create mode 100644 test/PersonalInviteTimeLock.t.sol create mode 100644 test/tokenize.it-smart-contracts.code-workspace diff --git a/test/PersonalInviteTimeLock.t.sol b/test/PersonalInviteTimeLock.t.sol new file mode 100644 index 00000000..93bd5269 --- /dev/null +++ b/test/PersonalInviteTimeLock.t.sol @@ -0,0 +1,224 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity ^0.8.13; + +import "../lib/forge-std/src/Test.sol"; +import "../contracts/Token.sol"; +import "../contracts/PersonalInvite.sol"; +import "../contracts/PersonalInviteFactory.sol"; +import "../contracts/FeeSettings.sol"; +import "./resources/FakePaymentToken.sol"; +import "../node_modules/@openzeppelin/contracts/finance/VestingWallet.sol"; + +contract PersonalInviteTimeLockTest is Test { + PersonalInviteFactory factory; + + AllowList list; + FeeSettings feeSettings; + Token token; + FakePaymentToken currency; + + uint256 MAX_INT = + 115792089237316195423570985008687907853269984665640564039457584007913129639935; + + address public constant admin = 0x0109709eCFa91a80626FF3989D68f67f5b1dD120; + address public constant tokenReceiver = + 0x1109709ecFA91a80626ff3989D68f67F5B1Dd121; + address public constant mintAllower = + 0x2109709EcFa91a80626Ff3989d68F67F5B1Dd122; + address public constant currencyPayer = + 0x3109709ECfA91A80626fF3989D68f67F5B1Dd123; + address public constant owner = 0x6109709EcFA91A80626FF3989d68f67F5b1dd126; + address public constant currencyReceiver = + 0x7109709eCfa91A80626Ff3989D68f67f5b1dD127; + address public constant paymentTokenProvider = + 0x8109709ecfa91a80626fF3989d68f67F5B1dD128; + address public constant trustedForwarder = + 0x9109709EcFA91A80626FF3989D68f67F5B1dD129; + + uint256 public constant price = 10000000; + + uint256 requirements = 92785934; + + function setUp() public { + factory = new PersonalInviteFactory(); + list = new AllowList(); + + list.set(tokenReceiver, requirements); + + Fees memory fees = Fees(100, 100, 100, 0); + feeSettings = new FeeSettings(fees, admin); + + token = new Token( + trustedForwarder, + feeSettings, + admin, + list, + requirements, + "token", + "TOK" + ); + vm.prank(paymentTokenProvider); + currency = new FakePaymentToken(0, 18); + } + + /** + * @notice Test that the timeLock works as expected + */ + function testTimeLock() public { + uint lockDuration = 1000; + uint tokenAmount = 300; + + // create the time lock + VestingWallet timeLock = new VestingWallet( + tokenReceiver, + uint64(block.timestamp + lockDuration), + 1 // all tokens are released at once + ); + + // add time lock and token receiver to the allow list + list.set(address(timeLock), requirements); + list.set(tokenReceiver, requirements); + + // transfer some ERC20 tokens to the time lock + vm.startPrank(admin); + token.increaseMintingAllowance(admin, tokenAmount); + token.mint(address(timeLock), tokenAmount); + vm.stopPrank(); + + // try releasing tokens before the lock expires + timeLock.release(address(token)); + + // check that no tokens were released + assertEq(token.balanceOf(tokenReceiver), 0); + + // wait for the lock to expire + vm.warp(lockDuration + 2); + + // release tokens + timeLock.release(address(token)); + + // check that tokens were released + assertEq( + token.balanceOf(tokenReceiver), + tokenAmount, + "wrong token amount released" + ); + } + + function testAcceptWithDifferentTokenReceiver(uint256 rawSalt) public { + console.log( + "feeCollector currency balance: %s", + currency.balanceOf(token.feeSettings().feeCollector()) + ); + + //uint rawSalt = 0; + bytes32 salt = bytes32(rawSalt); + + //bytes memory creationCode = type(PersonalInvite).creationCode; + uint256 tokenAmount = 20000000000000; + uint256 expiration = block.timestamp + 1000; + uint256 tokenDecimals = token.decimals(); + uint256 currencyAmount = (tokenAmount * price) / 10 ** tokenDecimals; + + address expectedAddress = factory.getAddress( + salt, + currencyPayer, + tokenReceiver, + currencyReceiver, + tokenAmount, + price, + expiration, + currency, + token + ); + + vm.prank(admin); + token.increaseMintingAllowance(expectedAddress, tokenAmount); + + vm.prank(paymentTokenProvider); + currency.mint(currencyPayer, currencyAmount); + + vm.prank(currencyPayer); + currency.approve(expectedAddress, currencyAmount); + + // make sure balances are as expected before deployment + + console.log( + "feeCollector currency balance: %s", + currency.balanceOf(token.feeSettings().feeCollector()) + ); + + assertEq(currency.balanceOf(currencyPayer), currencyAmount); + assertEq(currency.balanceOf(currencyReceiver), 0); + assertEq(currency.balanceOf(tokenReceiver), 0); + assertEq(token.balanceOf(tokenReceiver), 0); + + console.log( + "feeCollector currency balance before deployment: %s", + currency.balanceOf(token.feeSettings().feeCollector()) + ); + + address inviteAddress = factory.deploy( + salt, + currencyPayer, + tokenReceiver, + currencyReceiver, + tokenAmount, + price, + expiration, + currency, + token + ); + + console.log( + "feeCollector currency balance after deployment: %s", + currency.balanceOf(token.feeSettings().feeCollector()) + ); + + assertEq( + inviteAddress, + expectedAddress, + "deployed contract address is not correct" + ); + + console.log("payer balance: %s", currency.balanceOf(currencyPayer)); + console.log( + "receiver balance: %s", + currency.balanceOf(currencyReceiver) + ); + console.log( + "tokenReceiver token balance: %s", + token.balanceOf(tokenReceiver) + ); + uint256 len; + assembly { + len := extcodesize(expectedAddress) + } + console.log("Deployed contract size: %s", len); + assertEq(currency.balanceOf(currencyPayer), 0); + + assertEq( + currency.balanceOf(currencyReceiver), + currencyAmount - + token.feeSettings().personalInviteFee(currencyAmount) + ); + + console.log( + "feeCollector currency balance: %s", + currency.balanceOf(token.feeSettings().feeCollector()) + ); + + assertEq( + currency.balanceOf(token.feeSettings().feeCollector()), + token.feeSettings().personalInviteFee(currencyAmount), + "feeCollector currency balance is not correct" + ); + + assertEq(token.balanceOf(tokenReceiver), tokenAmount); + + assertEq( + token.balanceOf(token.feeSettings().feeCollector()), + token.feeSettings().tokenFee(tokenAmount) + ); + } +} diff --git a/test/tokenize.it-smart-contracts.code-workspace b/test/tokenize.it-smart-contracts.code-workspace new file mode 100644 index 00000000..86ca7499 --- /dev/null +++ b/test/tokenize.it-smart-contracts.code-workspace @@ -0,0 +1,8 @@ +{ + "folders": [ + { + "path": "../../../../../home/malteish/projects/tokenize.it-smart-contracts" + } + ], + "settings": {} +} From ad7d5dfd4b96607af3ea9f68ec4cca19e71de0d2 Mon Sep 17 00:00:00 2001 From: malteish Date: Fri, 5 May 2023 16:12:11 +0200 Subject: [PATCH 25/75] add test of timeLock with PersonalInvite --- test/PersonalInviteTimeLock.t.sol | 94 +++++++++++++++++++++---------- 1 file changed, 65 insertions(+), 29 deletions(-) diff --git a/test/PersonalInviteTimeLock.t.sol b/test/PersonalInviteTimeLock.t.sol index 93bd5269..9bdbef9b 100644 --- a/test/PersonalInviteTimeLock.t.sol +++ b/test/PersonalInviteTimeLock.t.sol @@ -72,7 +72,7 @@ contract PersonalInviteTimeLockTest is Test { VestingWallet timeLock = new VestingWallet( tokenReceiver, uint64(block.timestamp + lockDuration), - 1 // all tokens are released at once + 0 // all tokens are released at once ); // add time lock and token receiver to the allow list @@ -92,7 +92,7 @@ contract PersonalInviteTimeLockTest is Test { assertEq(token.balanceOf(tokenReceiver), 0); // wait for the lock to expire - vm.warp(lockDuration + 2); + vm.warp(lockDuration + 1); // release tokens timeLock.release(address(token)); @@ -105,12 +105,28 @@ contract PersonalInviteTimeLockTest is Test { ); } - function testAcceptWithDifferentTokenReceiver(uint256 rawSalt) public { - console.log( - "feeCollector currency balance: %s", - currency.balanceOf(token.feeSettings().feeCollector()) + function testPersonalInviteWithTimeLock( + uint256 rawSalt, + uint64 lockDuration, + uint64 attemptDuration + ) public { + vm.assume(lockDuration > attemptDuration); + vm.assume(attemptDuration > 0); + vm.assume(lockDuration < type(uint64).max / 2); + + // check current time is 0 -> makes math easier further down + uint256 startTime = block.timestamp; + + // create vesting wallet as token receiver + VestingWallet timeLock = new VestingWallet( + tokenReceiver, + uint64(startTime + lockDuration), + 0 // all tokens are released at once ); + // add time lock and token receiver to the allow list + list.set(address(timeLock), requirements); + //uint rawSalt = 0; bytes32 salt = bytes32(rawSalt); @@ -123,7 +139,7 @@ contract PersonalInviteTimeLockTest is Test { address expectedAddress = factory.getAddress( salt, currencyPayer, - tokenReceiver, + address(timeLock), currencyReceiver, tokenAmount, price, @@ -150,8 +166,8 @@ contract PersonalInviteTimeLockTest is Test { assertEq(currency.balanceOf(currencyPayer), currencyAmount); assertEq(currency.balanceOf(currencyReceiver), 0); - assertEq(currency.balanceOf(tokenReceiver), 0); - assertEq(token.balanceOf(tokenReceiver), 0); + assertEq(currency.balanceOf(address(timeLock)), 0); + assertEq(token.balanceOf(address(timeLock)), 0); console.log( "feeCollector currency balance before deployment: %s", @@ -161,7 +177,7 @@ contract PersonalInviteTimeLockTest is Test { address inviteAddress = factory.deploy( salt, currencyPayer, - tokenReceiver, + address(timeLock), currencyReceiver, tokenAmount, price, @@ -170,11 +186,6 @@ contract PersonalInviteTimeLockTest is Test { token ); - console.log( - "feeCollector currency balance after deployment: %s", - currency.balanceOf(token.feeSettings().feeCollector()) - ); - assertEq( inviteAddress, expectedAddress, @@ -187,14 +198,10 @@ contract PersonalInviteTimeLockTest is Test { currency.balanceOf(currencyReceiver) ); console.log( - "tokenReceiver token balance: %s", - token.balanceOf(tokenReceiver) - ); - uint256 len; - assembly { - len := extcodesize(expectedAddress) - } - console.log("Deployed contract size: %s", len); + "timeLock token balance: %s", + token.balanceOf(address(timeLock)) + ); + assertEq(currency.balanceOf(currencyPayer), 0); assertEq( @@ -203,22 +210,51 @@ contract PersonalInviteTimeLockTest is Test { token.feeSettings().personalInviteFee(currencyAmount) ); - console.log( - "feeCollector currency balance: %s", - currency.balanceOf(token.feeSettings().feeCollector()) - ); - assertEq( currency.balanceOf(token.feeSettings().feeCollector()), token.feeSettings().personalInviteFee(currencyAmount), "feeCollector currency balance is not correct" ); - assertEq(token.balanceOf(tokenReceiver), tokenAmount); + assertEq(token.balanceOf(address(timeLock)), tokenAmount); assertEq( token.balanceOf(token.feeSettings().feeCollector()), token.feeSettings().tokenFee(tokenAmount) ); + + /* + * PersonalInvite worked properly, now test the time lock + */ + // immediate release should not work + assertEq( + token.balanceOf(tokenReceiver), + 0, + "investor vault should have no tokens" + ); + timeLock.release(address(token)); + assertEq( + token.balanceOf(tokenReceiver), + 0, + "investor vault should still have no tokens" + ); + + // too early release should not work + vm.warp(startTime + attemptDuration); + timeLock.release(address(token)); + assertEq( + token.balanceOf(tokenReceiver), + 0, + "investor vault should still be empty" + ); + + // release should work from 1 second after lock expires + vm.warp(startTime + lockDuration + 2); + timeLock.release(address(token)); + assertEq( + token.balanceOf(tokenReceiver), + tokenAmount, + "investor vault should have tokens" + ); } } From d0860ff77f4ba53061e548a3d99b537f0f2bc7ae Mon Sep 17 00:00:00 2001 From: malteish Date: Fri, 5 May 2023 16:15:14 +0200 Subject: [PATCH 26/75] fix test --- test/PersonalInviteTimeLock.t.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/PersonalInviteTimeLock.t.sol b/test/PersonalInviteTimeLock.t.sol index 9bdbef9b..ce487231 100644 --- a/test/PersonalInviteTimeLock.t.sol +++ b/test/PersonalInviteTimeLock.t.sol @@ -92,7 +92,7 @@ contract PersonalInviteTimeLockTest is Test { assertEq(token.balanceOf(tokenReceiver), 0); // wait for the lock to expire - vm.warp(lockDuration + 1); + vm.warp(lockDuration + 2); // +2 to account for block time, which starts at 1 in these tests // release tokens timeLock.release(address(token)); From feb1a8b771cdf19ed1f34dfe5a1360991c3b10af Mon Sep 17 00:00:00 2001 From: malteish Date: Fri, 5 May 2023 16:18:17 +0200 Subject: [PATCH 27/75] remove workspace file --- test/tokenize.it-smart-contracts.code-workspace | 8 -------- 1 file changed, 8 deletions(-) delete mode 100644 test/tokenize.it-smart-contracts.code-workspace diff --git a/test/tokenize.it-smart-contracts.code-workspace b/test/tokenize.it-smart-contracts.code-workspace deleted file mode 100644 index 86ca7499..00000000 --- a/test/tokenize.it-smart-contracts.code-workspace +++ /dev/null @@ -1,8 +0,0 @@ -{ - "folders": [ - { - "path": "../../../../../home/malteish/projects/tokenize.it-smart-contracts" - } - ], - "settings": {} -} From 7832e2aebf34101b9439190a8334044e26224523 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 May 2023 08:44:36 +0000 Subject: [PATCH 28/75] Bump @openzeppelin/contracts from 4.8.0 to 4.8.3 Bumps [@openzeppelin/contracts](https://github.com/OpenZeppelin/openzeppelin-contracts) from 4.8.0 to 4.8.3. - [Release notes](https://github.com/OpenZeppelin/openzeppelin-contracts/releases) - [Changelog](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/CHANGELOG.md) - [Commits](https://github.com/OpenZeppelin/openzeppelin-contracts/compare/v4.8.0...v4.8.3) --- updated-dependencies: - dependency-name: "@openzeppelin/contracts" dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 85972ad3..3ce1dd33 100644 --- a/package.json +++ b/package.json @@ -81,7 +81,7 @@ }, "dependencies": { "@opengsn/contracts": "2.2.5", - "@openzeppelin/contracts": "4.8.0" + "@openzeppelin/contracts": "4.8.3" }, "scripts": { "prepack": "yarn npmignore --auto && yarn test && yarn build ", diff --git a/yarn.lock b/yarn.lock index c0c14a7c..a05dee33 100644 --- a/yarn.lock +++ b/yarn.lock @@ -833,10 +833,10 @@ dependencies: "@openzeppelin/contracts" "^4.2.0" -"@openzeppelin/contracts@4.8.0", "@openzeppelin/contracts@^4.2.0": - version "4.8.0" - resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-4.8.0.tgz#6854c37df205dd2c056bdfa1b853f5d732109109" - integrity sha512-AGuwhRRL+NaKx73WKRNzeCxOCOCxpaqF+kp8TJ89QzAipSwZy/NoflkWaL9bywXFRhIzXt8j38sfF7KBKCPWLw== +"@openzeppelin/contracts@4.8.3", "@openzeppelin/contracts@^4.2.0": + version "4.8.3" + resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-4.8.3.tgz#cbef3146bfc570849405f59cba18235da95a252a" + integrity sha512-bQHV8R9Me8IaJoJ2vPG4rXcL7seB7YVuskr4f+f5RyOStSZetwzkWtoqDMl5erkBJy0lDRUnIR2WIkPiC0GJlg== "@resolver-engine/core@^0.3.3": version "0.3.3" From bcdc4e8089f8ee61f9d1ffd61c5518106cc33340 Mon Sep 17 00:00:00 2001 From: malteish Date: Mon, 8 May 2023 11:21:41 +0200 Subject: [PATCH 29/75] test AllowList Events --- test/AllowList.t.sol | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/test/AllowList.t.sol b/test/AllowList.t.sol index 723414be..3d530e94 100644 --- a/test/AllowList.t.sol +++ b/test/AllowList.t.sol @@ -5,6 +5,8 @@ import "../lib/forge-std/src/Test.sol"; import "../contracts/AllowList.sol"; contract AllowListTest is Test { + event Set(address indexed key, uint256 value); + AllowList list; address owner; @@ -41,6 +43,18 @@ contract AllowListTest is Test { assertTrue(list.map(address(1)) == 2); } + function testSetEvent(address x, uint256 attributes) public { + assertTrue(list.map(address(x)) == 0); + + vm.expectEmit(true, true, false, false); + emit Set(address(x), attributes); + list.set(address(x), attributes); + + vm.expectEmit(true, true, false, false); + emit Set(address(x), 0); + list.remove(address(x)); + } + function testSetFuzzingAddress(address x) public { assertTrue(list.map(address(x)) == 0); list.set(address(x), 1); From 6c0938d40e1712fa1049ecc8167adbc9dfb7192a Mon Sep 17 00:00:00 2001 From: malteish Date: Mon, 8 May 2023 12:59:08 +0200 Subject: [PATCH 30/75] test ContinuousFundraising events --- test/AllowList.t.sol | 4 +- test/ContinuousFundraising.t.sol | 100 ++++++++++++++++++------------- 2 files changed, 60 insertions(+), 44 deletions(-) diff --git a/test/AllowList.t.sol b/test/AllowList.t.sol index 3d530e94..3c548f51 100644 --- a/test/AllowList.t.sol +++ b/test/AllowList.t.sol @@ -46,11 +46,11 @@ contract AllowListTest is Test { function testSetEvent(address x, uint256 attributes) public { assertTrue(list.map(address(x)) == 0); - vm.expectEmit(true, true, false, false); + vm.expectEmit(true, true, true, true, address(list)); emit Set(address(x), attributes); list.set(address(x), attributes); - vm.expectEmit(true, true, false, false); + vm.expectEmit(true, true, true, true, address(list)); emit Set(address(x), 0); list.remove(address(x)); } diff --git a/test/ContinuousFundraising.t.sol b/test/ContinuousFundraising.t.sol index d0af4915..4f06db8e 100644 --- a/test/ContinuousFundraising.t.sol +++ b/test/ContinuousFundraising.t.sol @@ -9,6 +9,17 @@ import "./resources/FakePaymentToken.sol"; import "./resources/MaliciousPaymentToken.sol"; contract ContinuousFundraisingTest is Test { + 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 + ); + ContinuousFundraising raise; AllowList list; IFeeSettingsV1 feeSettings; @@ -243,31 +254,16 @@ contract ContinuousFundraisingTest is Test { vm.prank(buyer); vm.expectRevert("ReentrancyGuard: reentrant call"); _raise.buy(buyAmount, buyer); - - // // tests to be run when exploit is successful - // uint paymentTokensSpent = buyerPaymentBalanceBefore - _paymentToken.balanceOf(buyer); - // console.log("buyer spent: ", buyerPaymentBalanceBefore - _paymentToken.balanceOf(buyer)); - // console.log("buyer tokens:", _token.balanceOf(buyer)); - // console.log("minted tokens:", _token.totalSupply()); - // uint pricePaidForBuyerTokens = paymentTokensSpent / _token.balanceOf(buyer) * 10**_paymentTokenDecimals; - // uint pricePaidForAllTokens = _paymentToken.balanceOf(receiver) / _token.totalSupply() * 10**_paymentTokenDecimals; - // console.log("total price paid: ", pricePaidForAllTokens); - // console.log("price: ", _price); - // // minted tokens must fit payment received and price - // assertTrue(_token.totalSupply() == paymentTokensSpent / _price * 10**_paymentTokenDecimals); - // assertTrue(pricePaidForAllTokens == _price); - - // // assert internal accounting is correct - // console.log("tokens sold:", _raise.tokensSold()); - // // assertTrue(_raise.tokensSold() == _token.balanceOf(buyer)); - // // assertTrue(_raise.tokensBought(buyer) == _token.balanceOf(buyer)); } - function testBuyHappyCase() public { - uint256 tokenBuyAmount = 5 * 10 ** token.decimals(); - uint256 costInPaymentToken = (tokenBuyAmount * price) / 10 ** 18; - - assert(costInPaymentToken == 35 * 10 ** paymentTokenDecimals); // 35 payment tokens, manually calculated + function testBuyHappyCase(uint256 tokenBuyAmount) public { + vm.assume(tokenBuyAmount >= raise.minAmountPerBuyer()); + vm.assume(tokenBuyAmount <= raise.maxAmountPerBuyer()); + uint256 costInPaymentToken = Math.ceilDiv( + tokenBuyAmount * raise.tokenPrice(), + 10 ** 18 + ); + vm.assume(costInPaymentToken <= paymentToken.balanceOf(buyer)); uint256 paymentTokenBalanceBefore = paymentToken.balanceOf(buyer); @@ -276,10 +272,13 @@ contract ContinuousFundraisingTest is Test { ); vm.prank(buyer); + vm.expectEmit(true, true, true, true, address(raise)); + emit TokensBought(buyer, tokenBuyAmount, costInPaymentToken); raise.buy(tokenBuyAmount, buyer); // this test fails if 5 * 10**18 is replaced with 5 * 10**token.decimals() for this argument, even though they should be equal assertTrue( paymentToken.balanceOf(buyer) == - paymentTokenBalanceBefore - costInPaymentToken + paymentTokenBalanceBefore - costInPaymentToken, + "buyer has paid" ); assertTrue( token.balanceOf(buyer) == tokenBuyAmount, @@ -686,7 +685,9 @@ contract ContinuousFundraisingTest is Test { vm.prank(owner); raise.pause(); vm.prank(owner); - raise.setCurrencyReceiver(payable(address(buyer))); + vm.expectEmit(true, true, true, true, address(raise)); + emit CurrencyReceiverChanged(address(buyer)); + raise.setCurrencyReceiver(address(buyer)); assertTrue(raise.currencyReceiver() == address(buyer)); vm.prank(owner); @@ -705,18 +706,18 @@ contract ContinuousFundraisingTest is Test { /* try to update minAmountPerBuyer while paused */ - function testUpdateMinAmountPerBuyerPaused() public { + function testUpdateMinAmountPerBuyerPaused( + uint256 newMinAmountPerBuyer + ) public { + vm.assume(newMinAmountPerBuyer <= raise.maxAmountPerBuyer()); assertTrue(raise.minAmountPerBuyer() == minAmountPerBuyer); vm.prank(owner); raise.pause(); vm.prank(owner); - raise.setMinAmountPerBuyer(300); - assertTrue(raise.minAmountPerBuyer() == 300); - - console.log("minAmount: ", raise.minAmountPerBuyer()); - console.log("maxAmount: ", raise.maxAmountPerBuyer()); - console.log("owner: ", raise.owner()); - console.log("_owner: ", owner); + vm.expectEmit(true, true, true, true, address(raise)); + emit MinAmountPerBuyerChanged(newMinAmountPerBuyer); + raise.setMinAmountPerBuyer(newMinAmountPerBuyer); + assertTrue(raise.minAmountPerBuyer() == newMinAmountPerBuyer); uint256 _maxAmountPerBuyer = raise.maxAmountPerBuyer(); vm.expectRevert("_minAmount needs to be smaller or equal to maxAmount"); @@ -738,13 +739,18 @@ contract ContinuousFundraisingTest is Test { /* try to update maxAmountPerBuyer while paused */ - function testUpdateMaxAmountPerBuyerPaused() public { + function testUpdateMaxAmountPerBuyerPaused( + uint256 newMaxAmountPerBuyer + ) public { + vm.assume(newMaxAmountPerBuyer >= raise.minAmountPerBuyer()); assertTrue(raise.maxAmountPerBuyer() == maxAmountPerBuyer); vm.prank(owner); raise.pause(); vm.prank(owner); - raise.setMaxAmountPerBuyer(minAmountPerBuyer); - assertTrue(raise.maxAmountPerBuyer() == minAmountPerBuyer); + vm.expectEmit(true, true, true, true, address(raise)); + emit MaxAmountPerBuyerChanged(newMaxAmountPerBuyer); + raise.setMaxAmountPerBuyer(newMaxAmountPerBuyer); + assertTrue(raise.maxAmountPerBuyer() == newMaxAmountPerBuyer); uint256 _minAmountPerBuyer = raise.minAmountPerBuyer(); vm.expectRevert("_maxAmount needs to be larger or equal to minAmount"); vm.prank(owner); @@ -763,7 +769,8 @@ contract ContinuousFundraisingTest is Test { /* try to update currency and price while paused */ - function testUpdateCurrencyAndPricePaused() public { + function testUpdateCurrencyAndPricePaused(uint256 newPrice) public { + vm.assume(newPrice > 0); assertTrue(raise.tokenPrice() == price); assertTrue(raise.currency() == paymentToken); @@ -772,8 +779,10 @@ contract ContinuousFundraisingTest is Test { vm.prank(owner); raise.pause(); vm.prank(owner); - raise.setCurrencyAndTokenPrice(newPaymentToken, 700); - assertTrue(raise.tokenPrice() == 700); + vm.expectEmit(true, true, true, true, address(raise)); + emit TokenPriceAndCurrencyChanged(newPrice, newPaymentToken); + raise.setCurrencyAndTokenPrice(newPaymentToken, newPrice); + assertTrue(raise.tokenPrice() == newPrice); assertTrue(raise.currency() == newPaymentToken); vm.prank(owner); vm.expectRevert("_tokenPrice needs to be a non-zero amount"); @@ -791,15 +800,22 @@ contract ContinuousFundraisingTest is Test { /* try to update maxAmountOfTokenToBeSold while paused */ - function testUpdateMaxAmountOfTokenToBeSoldPaused() public { + function testUpdateMaxAmountOfTokenToBeSoldPaused( + uint256 newMaxAmountOfTokenToBeSold + ) public { + vm.assume(newMaxAmountOfTokenToBeSold > 0); assertTrue( raise.maxAmountOfTokenToBeSold() == maxAmountOfTokenToBeSold ); vm.prank(owner); raise.pause(); vm.prank(owner); - raise.setMaxAmountOfTokenToBeSold(minAmountPerBuyer); - assertTrue(raise.maxAmountOfTokenToBeSold() == minAmountPerBuyer); + vm.expectEmit(true, true, true, true, address(raise)); + emit MaxAmountOfTokenToBeSoldChanged(newMaxAmountOfTokenToBeSold); + raise.setMaxAmountOfTokenToBeSold(newMaxAmountOfTokenToBeSold); + assertTrue( + raise.maxAmountOfTokenToBeSold() == newMaxAmountOfTokenToBeSold + ); vm.prank(owner); vm.expectRevert( "_maxAmountOfTokenToBeSold needs to be larger than zero" From e11b29e533f0498fa5899501c98f3391a5bef133 Mon Sep 17 00:00:00 2001 From: malteish Date: Mon, 8 May 2023 13:07:38 +0200 Subject: [PATCH 31/75] test FeeSettings events --- test/FeeSettings.t.sol | 34 +++++++++++++++++++++++++--------- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/test/FeeSettings.t.sol b/test/FeeSettings.t.sol index ae707660..cedd0eeb 100644 --- a/test/FeeSettings.t.sol +++ b/test/FeeSettings.t.sol @@ -6,6 +6,14 @@ import "../contracts/Token.sol"; import "../contracts/FeeSettings.sol"; contract FeeSettingsTest is Test { + event SetFeeDenominators( + uint256 tokenFeeDenominator, + uint256 continuousFundraisingFeeDenominator, + uint256 personalInviteFeeDenominator + ); + event FeeCollectorChanged(address indexed newFeeCollector); + event ChangeProposed(Fees proposal); + FeeSettings feeSettings; Token token; Token currency; @@ -28,7 +36,7 @@ contract FeeSettingsTest is Test { uint256 public constant price = 10000000; - function testEnforceFeeDenominatorRangeinConstructor(uint8 fee) public { + function testEnforceFeeDenominatorRangeInConstructor(uint8 fee) public { vm.assume(!feeInValidRange(fee)); Fees memory _fees; @@ -54,7 +62,7 @@ contract FeeSettingsTest is Test { new FeeSettings(_fees, admin); } - function testEnforceTokenFeeDenominatorRangeinFeeChanger(uint8 fee) public { + function testEnforceTokenFeeDenominatorRangeInFeeChanger(uint8 fee) public { vm.assume(!feeInValidRange(fee)); Fees memory fees = Fees(100, 100, 100, 0); FeeSettings _feeSettings = new FeeSettings(fees, admin); @@ -71,7 +79,7 @@ contract FeeSettingsTest is Test { _feeSettings.planFeeChange(feeChange); } - function testEnforceContinuousFundraisingFeeDenominatorRangeinFeeChanger( + function testEnforceContinuousFundraisingFeeDenominatorRangeInFeeChanger( uint8 fee ) public { vm.assume(!feeInValidRange(fee)); @@ -90,7 +98,7 @@ contract FeeSettingsTest is Test { _feeSettings.planFeeChange(feeChange); } - function testEnforcePersonalInviteFeeDenominatorRangeinFeeChanger( + function testEnforcePersonalInviteFeeDenominatorRangeInFeeChanger( uint8 fee ) public { vm.assume(!feeInValidRange(fee)); @@ -188,32 +196,38 @@ contract FeeSettingsTest is Test { function testExecuteFeeChangeProperly( uint delayAnnounced, uint256 tokenFee, - uint256 investmentFee + uint256 fundraisingFee, + uint256 personalInviteFee ) public { vm.assume(delayAnnounced > 12 weeks && delayAnnounced < 100000000000); vm.assume(feeInValidRange(tokenFee)); - vm.assume(feeInValidRange(investmentFee)); + vm.assume(feeInValidRange(fundraisingFee)); + vm.assume(feeInValidRange(personalInviteFee)); Fees memory fees = Fees(50, 50, 50, 0); vm.prank(admin); FeeSettings _feeSettings = new FeeSettings(fees, admin); Fees memory feeChange = Fees({ tokenFeeDenominator: tokenFee, - continuousFundraisingFeeDenominator: investmentFee, - personalInviteFeeDenominator: 100, + continuousFundraisingFeeDenominator: fundraisingFee, + personalInviteFeeDenominator: personalInviteFee, time: block.timestamp + delayAnnounced }); vm.prank(admin); + vm.expectEmit(true, true, true, true, address(_feeSettings)); + emit ChangeProposed(feeChange); _feeSettings.planFeeChange(feeChange); vm.prank(admin); vm.warp(block.timestamp + delayAnnounced + 1); + vm.expectEmit(true, true, true, true, address(_feeSettings)); + emit SetFeeDenominators(tokenFee, fundraisingFee, personalInviteFee); _feeSettings.executeFeeChange(); assertEq(_feeSettings.tokenFeeDenominator(), tokenFee); assertEq( _feeSettings.continuousFundraisingFeeDenominator(), - investmentFee + fundraisingFee ); //assertEq(_feeSettings.change, 0); } @@ -359,6 +373,8 @@ contract FeeSettingsTest is Test { Fees memory fees = Fees(100, 100, 100, 0); vm.prank(admin); _feeSettings = new FeeSettings(fees, admin); + vm.expectEmit(true, true, true, true, address(_feeSettings)); + emit FeeCollectorChanged(newCollector); vm.prank(admin); _feeSettings.setFeeCollector(newCollector); assertEq(_feeSettings.feeCollector(), newCollector); From 7513e4a5b12d65531f18c505940ce2fcec0263f3 Mon Sep 17 00:00:00 2001 From: malteish Date: Mon, 8 May 2023 13:11:14 +0200 Subject: [PATCH 32/75] test PersonalInvite event --- test/PersonalInvite.t.sol | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/test/PersonalInvite.t.sol b/test/PersonalInvite.t.sol index c67ea403..7449f2e7 100644 --- a/test/PersonalInvite.t.sol +++ b/test/PersonalInvite.t.sol @@ -9,6 +9,15 @@ import "../contracts/FeeSettings.sol"; import "./resources/FakePaymentToken.sol"; contract PersonalInviteTest is Test { + event Deal( + address indexed currencyPayer, + address indexed tokenReceiver, + uint256 tokenAmount, + uint256 tokenPrice, + IERC20 currency, + Token indexed token + ); + PersonalInviteFactory factory; AllowList list; @@ -126,6 +135,8 @@ contract PersonalInviteTest is Test { uint256 feeCollectorCurrencyBalanceBefore = currency.balanceOf( FeeSettings(address(token.feeSettings())).feeCollector() ); + vm.expectEmit(true, true, true, true, address(expectedAddress)); + emit Deal(tokenReceiver, tokenReceiver, amount, price, currency, token); address inviteAddress = factory.deploy( salt, From 20caf4bc259131753d464d2475d66b2504b0c1e5 Mon Sep 17 00:00:00 2001 From: malteish Date: Mon, 8 May 2023 13:26:12 +0200 Subject: [PATCH 33/75] test PersonalInviteFactory events --- test/PersonalInviteFactory.t.sol | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/PersonalInviteFactory.t.sol b/test/PersonalInviteFactory.t.sol index b5e7378c..265a9dbe 100644 --- a/test/PersonalInviteFactory.t.sol +++ b/test/PersonalInviteFactory.t.sol @@ -8,6 +8,8 @@ import "../contracts/PersonalInviteFactory.sol"; import "../contracts/FeeSettings.sol"; contract PersonalInviteFactoryTest is Test { + event Deploy(address indexed addr); + PersonalInviteFactory factory; AllowList list; @@ -124,6 +126,8 @@ contract PersonalInviteFactoryTest is Test { vm.prank(buyer); currency.approve(expectedAddress, amount * price); + vm.expectEmit(true, true, true, true, address(factory)); + emit Deploy(expectedAddress); factory.deploy( salt, buyer, From 8097aab3f4f96c3eb1eb66fad252e2d001bcc1c6 Mon Sep 17 00:00:00 2001 From: malteish Date: Mon, 8 May 2023 13:27:00 +0200 Subject: [PATCH 34/75] test various salts --- test/PersonalInviteFactory.t.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/PersonalInviteFactory.t.sol b/test/PersonalInviteFactory.t.sol index 265a9dbe..c7a78f11 100644 --- a/test/PersonalInviteFactory.t.sol +++ b/test/PersonalInviteFactory.t.sol @@ -88,8 +88,8 @@ contract PersonalInviteFactoryTest is Test { ); } - function testDeployContract() public { - uint256 rawSalt = 0; + function testDeployContract(uint256 rawSalt) public { + //uint256 rawSalt = 0; bytes32 salt = bytes32(rawSalt); //bytes memory creationCode = type(PersonalInvite).creationCode; From b59b198b8e78e97348745df99820ceb9737ff73a Mon Sep 17 00:00:00 2001 From: malteish Date: Mon, 8 May 2023 13:46:13 +0200 Subject: [PATCH 35/75] test Token events --- test/Token.t.sol | 56 ++++++++++++++++++++----------------- test/TokenIntegration.t.sol | 24 ++++++++++++++++ 2 files changed, 55 insertions(+), 25 deletions(-) diff --git a/test/Token.t.sol b/test/Token.t.sol index 3b49bed7..303058d9 100644 --- a/test/Token.t.sol +++ b/test/Token.t.sol @@ -6,6 +6,9 @@ import "../contracts/Token.sol"; import "../contracts/FeeSettings.sol"; contract tokenTest is Test { + event RequirementsChanged(uint newRequirements); + event MintingAllowanceChanged(address indexed minter, uint256 newAllowance); + Token token; AllowList allowList; FeeSettings feeSettings; @@ -26,8 +29,6 @@ contract tokenTest is Test { address public constant feeSettingsOwner = 0x8109709ecfa91a80626fF3989d68f67F5B1dD128; - event RequirementsChanged(uint256 newRequirements); - function setUp() public { vm.prank(admin); allowList = new AllowList(); @@ -226,13 +227,15 @@ contract tokenTest is Test { // token.revokeRole(role, admin); // } - function testSetRequirements() public { + function testSetRequirements(uint256 newRequirements) public { bytes32 role = token.REQUIREMENT_ROLE(); vm.prank(admin); token.grantRole(role, requirer); vm.prank(requirer); - token.setRequirements(3); - assertTrue(token.requirements() == 3); + vm.expectEmit(true, true, true, true, address(token)); + emit RequirementsChanged(newRequirements); + token.setRequirements(newRequirements); + assertTrue(token.requirements() == newRequirements); } function testFailSetRequirementsWrongRole() public { @@ -241,33 +244,32 @@ contract tokenTest is Test { assertTrue(token.requirements() == 3); } - function testUpdateAllowList() public { - AllowList newAllowList = new AllowList(); // deploy new AllowList - assertTrue(token.allowList() != newAllowList); - vm.prank(admin); - token.setAllowList(newAllowList); - assertTrue(token.allowList() == newAllowList); - } - - function testUpdateAllowList0() public { - vm.expectRevert("AllowList must not be zero address"); - vm.prank(admin); - token.setAllowList(AllowList(address(0))); - } - - function testSetUpMinter() public { + function testSetUpMinter(uint256 newAllowance, uint256 mintAmount) public { + vm.assume(newAllowance < type(uint256).max / 2); // avoid overflow because of fees + vm.assume(mintAmount <= newAllowance); bytes32 roleMintAllower = token.MINTALLOWER_ROLE(); vm.prank(admin); token.grantRole(roleMintAllower, mintAllower); + vm.expectEmit(true, true, true, true, address(token)); + emit MintingAllowanceChanged(minter, newAllowance); vm.prank(mintAllower); - token.increaseMintingAllowance(minter, 2); - assertTrue(token.mintingAllowance(minter) == 2); + token.increaseMintingAllowance(minter, newAllowance); + assertTrue( + token.mintingAllowance(minter) == newAllowance, + "minting allowance should be newAllowance" + ); vm.prank(minter); - token.mint(pauser, 1); - assertTrue(token.balanceOf(pauser) == 1); - assertTrue(token.mintingAllowance(minter) == 1); + token.mint(pauser, mintAmount); + assertTrue( + token.balanceOf(pauser) == mintAmount, + "balance of pauser should be mintAmount" + ); + assertTrue( + token.mintingAllowance(minter) == newAllowance - mintAmount, + "minting allowance should be newAllowance - mintAmount" + ); // set allowance to 0 vm.prank(mintAllower); @@ -355,10 +357,14 @@ contract tokenTest is Test { vm.prank(admin); token.grantRole(roleMintAllower, mintAllower); + vm.expectEmit(true, true, true, true, address(token)); + emit MintingAllowanceChanged(minter, x); vm.startPrank(mintAllower); token.increaseMintingAllowance(minter, x); assertTrue(token.mintingAllowance(minter) == x); + vm.expectEmit(true, true, true, true, address(token)); + emit MintingAllowanceChanged(minter, x - y); token.decreaseMintingAllowance(minter, y); assertTrue(token.mintingAllowance(minter) == x - y); diff --git a/test/TokenIntegration.t.sol b/test/TokenIntegration.t.sol index f56303a0..4cef9d4e 100644 --- a/test/TokenIntegration.t.sol +++ b/test/TokenIntegration.t.sol @@ -7,6 +7,10 @@ import "../contracts/FeeSettings.sol"; import "./resources/WrongFeeSettings.sol"; contract tokenTest is Test { + event AllowListChanged(AllowList indexed newAllowList); + event NewFeeSettingsSuggested(IFeeSettingsV1 indexed _feeSettings); + event FeeSettingsChanged(IFeeSettingsV1 indexed newFeeSettings); + Token token; AllowList allowList; FeeSettings feeSettings; @@ -66,6 +70,22 @@ contract tokenTest is Test { vm.stopPrank(); } + function testUpdateAllowList() public { + AllowList newAllowList = new AllowList(); // deploy new AllowList + assertTrue(token.allowList() != newAllowList); + vm.prank(admin); + vm.expectEmit(true, true, true, true, address(token)); + emit AllowListChanged(newAllowList); + token.setAllowList(newAllowList); + assertTrue(token.allowList() == newAllowList); + } + + function testUpdateAllowList0() public { + vm.expectRevert("AllowList must not be zero address"); + vm.prank(admin); + token.setAllowList(AllowList(address(0))); + } + function testSuggestNewFeeSettingsWrongCaller(address wrongUpdater) public { vm.assume(wrongUpdater != feeSettings.owner()); Fees memory fees = Fees(UINT256_MAX, UINT256_MAX, UINT256_MAX, 0); @@ -101,6 +121,8 @@ contract tokenTest is Test { uint oldInvestmentFeeDenominator = oldFeeSettings .continuousFundraisingFeeDenominator(); uint oldTokenFeeDenominator = oldFeeSettings.tokenFeeDenominator(); + vm.expectEmit(true, true, true, true, address(token)); + emit NewFeeSettingsSuggested(newFeeSettings); vm.prank(feeSettings.owner()); token.suggestNewFeeSettings(newFeeSettings); @@ -139,6 +161,8 @@ contract tokenTest is Test { token.suggestNewFeeSettings(newFeeSettings); // accept + vm.expectEmit(true, true, true, true, address(token)); + emit FeeSettingsChanged(newFeeSettings); vm.prank(admin); token.acceptNewFeeSettings(newFeeSettings); assertTrue( From ec3d69973a4f992533458ba4ce6320b1ebabdc04 Mon Sep 17 00:00:00 2001 From: malteish Date: Mon, 8 May 2023 19:30:02 +0200 Subject: [PATCH 36/75] bump prettier solidity version --- package.json | 2 +- yarn.lock | 26 +++++++++----------------- 2 files changed, 10 insertions(+), 18 deletions(-) diff --git a/package.json b/package.json index 3ce1dd33..3e58b5df 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/yarn.lock b/yarn.lock index a05dee33..d1275d5a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -982,10 +982,10 @@ dependencies: antlr4ts "^0.5.0-alpha.4" -"@solidity-parser/parser@^0.14.5": - version "0.14.5" - resolved "https://registry.yarnpkg.com/@solidity-parser/parser/-/parser-0.14.5.tgz#87bc3cc7b068e08195c219c91cd8ddff5ef1a804" - integrity sha512-6dKnHZn7fg/iQATVEzqyUOyEidbn05q7YA2mQ9hC0MMXhhV3/JrsxmFSYZAcr7j1yUP700LLhTruvJ3MiQmjJg== +"@solidity-parser/parser@^0.16.0": + version "0.16.0" + resolved "https://registry.yarnpkg.com/@solidity-parser/parser/-/parser-0.16.0.tgz#1fb418c816ca1fc3a1e94b08bcfe623ec4e1add4" + integrity sha512-ESipEcHyRHg4Np4SqBCfcXwyxxna1DgFVz69bgpLV8vzl/NP1DtcKsJ4dJZXWQhY/Z4J2LeKBiOkOVZn9ct33Q== dependencies: antlr4ts "^0.5.0-alpha.4" @@ -3524,11 +3524,6 @@ elliptic@6.5.4, elliptic@^6.4.0, elliptic@^6.5.2, elliptic@^6.5.3, elliptic@^6.5 minimalistic-assert "^1.0.1" minimalistic-crypto-utils "^1.0.1" -emoji-regex@^10.2.1: - version "10.2.1" - resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-10.2.1.tgz#a41c330d957191efd3d9dfe6e1e8e1e9ab048b3f" - integrity sha512-97g6QgOk8zlDRdgq1WxwgTMgEWGVAQvB5Fdpgc1MkNy56la5SKP9GsMXKDOdqwn90/41a8yPwIGk1Y6WVbeMQA== - emoji-regex@^7.0.1: version "7.0.3" resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156" @@ -7703,17 +7698,14 @@ prettier-linter-helpers@^1.0.0: dependencies: fast-diff "^1.1.2" -prettier-plugin-solidity@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/prettier-plugin-solidity/-/prettier-plugin-solidity-1.0.0.tgz#5b23f48cc9c28a1246c6dd89af117234b813f48b" - integrity sha512-gRJCeZ7imbWtNYN2SudjJoPmka5r6jcd2cSTV6FC3pVCtY6LFZbeQQjpKufUEp88hXBAAnkOTOh7TA5xwj9M3A== +prettier-plugin-solidity@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/prettier-plugin-solidity/-/prettier-plugin-solidity-1.1.3.tgz#9a35124f578404caf617634a8cab80862d726cba" + integrity sha512-fQ9yucPi2sBbA2U2Xjh6m4isUTJ7S7QLc/XDDsktqqxYfTwdYKJ0EnnywXHwCGAaYbQNK+HIYPL1OemxuMsgeg== dependencies: - "@solidity-parser/parser" "^0.14.5" - emoji-regex "^10.2.1" - escape-string-regexp "^4.0.0" + "@solidity-parser/parser" "^0.16.0" semver "^7.3.8" solidity-comments-extractor "^0.0.7" - string-width "^4.2.3" prettier@^1.14.3: version "1.19.1" From 8230597252e19bdb4a7afcba266bf1d38a3291dd Mon Sep 17 00:00:00 2001 From: malteish Date: Mon, 8 May 2023 19:32:55 +0200 Subject: [PATCH 37/75] run prettier --- script/deploy.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/script/deploy.ts b/script/deploy.ts index 197643a9..f8646417 100644 --- a/script/deploy.ts +++ b/script/deploy.ts @@ -15,7 +15,7 @@ async function main() { // We get the contract to deploy const Token = await ethers.getContractFactory("Token"); - + const signers = await ethers.getSigners(); const admin = "0x6CcD9E07b035f9E6e7f086f3EaCf940187d03A29"; // testing founder @@ -26,7 +26,6 @@ async function main() { const feeSettings = "0x147addF9C8E4030F8104c713Dad2A1d76E6c85a1"; const requirements = 0x0; - const token = await Token.deploy( forwarder, feeSettings, From 0b78b95d64a316ae83b3a19ad5d9ab50fb6a2cc9 Mon Sep 17 00:00:00 2001 From: malteish Date: Mon, 8 May 2023 19:38:26 +0200 Subject: [PATCH 38/75] change prettier config, reformat all files --- contracts/ContinuousFundraising.sol | 130 ++------- contracts/FeeSettings.sol | 57 +--- contracts/PersonalInvite.sol | 41 +-- contracts/Token.sol | 98 ++----- package.json | 7 +- script/CheckToken.sol | 19 +- script/DeployCompany.sol | 8 +- script/DeployPlatform.s.sol | 9 +- script/deploy.ts | 20 +- .../ContinuousFundraising.js | 16 +- .../TokenGoerli.js | 14 +- .../TokenMainnet.js | 14 +- test/ContinuousFundraising.t.sol | 272 ++++-------------- test/ContinuousFundraisingERC2771.t.sol | 101 ++----- test/ContinuousFundraisingIntegration.t.sol | 139 ++------- test/ERC2771DomainDemonstration.t.sol | 150 ++-------- test/FeeSettings.t.sol | 251 ++++------------ test/IFeeSettings.t.sol | 8 +- test/MainnetCurrenciesInvest.t.sol | 90 ++---- test/MainnetCurrenciesPermit.t.sol | 125 ++------ test/PersonalInvite.t.sol | 245 ++++------------ test/PersonalInviteFactory.t.sol | 47 +-- test/PersonalInviteTimeLock.t.sol | 96 ++----- test/SetUpExampleCompany.t.sol | 242 ++++------------ test/Token.t.sol | 176 +++--------- test/TokenERC2612.t.sol | 68 +---- test/TokenERC2771.t.sol | 226 +++------------ test/TokenGasConsumption.t.sol | 33 +-- test/TokenIntegration.t.sol | 139 +++------ test/resources/ERC20Helper.sol | 17 +- test/resources/MaliciousPaymentToken.sol | 12 +- test/resources/WrongFeeSettings.sol | 27 +- 32 files changed, 609 insertions(+), 2288 deletions(-) diff --git a/contracts/ContinuousFundraising.sol b/contracts/ContinuousFundraising.sol index cc0d0916..4824ba6c 100644 --- a/contracts/ContinuousFundraising.sol +++ b/contracts/ContinuousFundraising.sol @@ -22,12 +22,7 @@ import "./Token.sol"; * 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; /// address that receives the currency when tokens are bought @@ -69,10 +64,7 @@ contract ContinuousFundraising is /// @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 - ); + 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); /** @@ -81,11 +73,7 @@ contract ContinuousFundraising is * @param tokenAmount Amount of tokens bought * @param currencyAmount Amount of currency paid */ - event TokensBought( - address indexed buyer, - uint256 tokenAmount, - uint256 currencyAmount - ); + 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. @@ -116,28 +104,16 @@ 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) } @@ -147,18 +123,9 @@ contract ContinuousFundraising is * @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 - ) 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" - ); + function buy(uint256 _amount, address _tokenReceiver) 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( tokensBought[_tokenReceiver] + _amount <= maxAmountPerBuyer, "Total amount of bought tokens needs to be lower than or equal to maxAmount" @@ -168,26 +135,15 @@ contract ContinuousFundraising is 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(_msgSender(), feeSettings.feeCollector(), fee); } - currency.safeTransferFrom( - _msgSender(), - currencyReceiver, - currencyAmount - fee - ); + currency.safeTransferFrom(_msgSender(), currencyReceiver, currencyAmount - fee); token.mint(_tokenReceiver, _amount); emit TokensBought(_msgSender(), _amount, currencyAmount); @@ -197,13 +153,8 @@ contract ContinuousFundraising is * @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; @@ -213,13 +164,8 @@ contract ContinuousFundraising is * @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; @@ -229,13 +175,8 @@ contract ContinuousFundraising is * @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; @@ -246,10 +187,7 @@ contract ContinuousFundraising is * @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; @@ -261,13 +199,8 @@ contract ContinuousFundraising is * @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; @@ -285,34 +218,21 @@ contract ContinuousFundraising is * @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 6af22408..a15e6818 100644 --- a/contracts/FeeSettings.sol +++ b/contracts/FeeSettings.sol @@ -54,8 +54,7 @@ contract FeeSettings is Ownable2Step, ERC165, IFeeSettingsV1 { 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; @@ -72,14 +71,10 @@ contract FeeSettings is Ownable2Step, ERC165, IFeeSettingsV1 { // 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); @@ -89,20 +84,11 @@ contract FeeSettings is Ownable2Step, ERC165, IFeeSettingsV1 { * @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; } @@ -121,18 +107,12 @@ contract FeeSettings is Ownable2Step, ERC165, IFeeSettingsV1 { * @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)"); } /** @@ -149,9 +129,7 @@ contract FeeSettings is Ownable2Step, ERC165, IFeeSettingsV1 { * @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; } @@ -161,9 +139,7 @@ contract FeeSettings is Ownable2Step, ERC165, IFeeSettingsV1 { * @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; } @@ -171,12 +147,7 @@ contract FeeSettings is Ownable2Step, ERC165, IFeeSettingsV1 { * @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(); } @@ -185,9 +156,7 @@ contract FeeSettings is Ownable2Step, ERC165, IFeeSettingsV1 { * @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/PersonalInvite.sol b/contracts/PersonalInvite.sol index 3d6a3984..8aa41d40 100644 --- a/contracts/PersonalInvite.sol +++ b/contracts/PersonalInvite.sol @@ -69,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/Token.sol b/contracts/Token.sol index 3a929cd1..4ba47c8b 100644 --- a/contracts/Token.sol +++ b/contracts/Token.sol @@ -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. @@ -110,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); @@ -131,10 +126,7 @@ 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) @@ -146,13 +138,8 @@ contract Token is ERC2771Context, ERC20Permit, Pausable, AccessControl { * @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" - ); + 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); } @@ -161,9 +148,7 @@ contract Token is ERC2771Context, ERC20Permit, Pausable, AccessControl { * @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) { + function setRequirements(uint256 _requirements) external onlyRole(REQUIREMENT_ROLE) { requirements = _requirements; emit RequirementsChanged(_requirements); } @@ -176,10 +161,7 @@ contract Token is ERC2771Context, ERC20Permit, Pausable, AccessControl { * @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); @@ -192,17 +174,12 @@ contract Token is ERC2771Context, ERC20Permit, Pausable, AccessControl { * 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); } @@ -213,10 +190,7 @@ contract Token is ERC2771Context, ERC20Permit, Pausable, AccessControl { * @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) { + function increaseMintingAllowance(address _minter, uint256 _allowance) external onlyRole(MINTALLOWER_ROLE) { mintingAllowance[_minter] += _allowance; emit MintingAllowanceChanged(_minter, mintingAllowance[_minter]); } @@ -227,10 +201,7 @@ contract Token is ERC2771Context, ERC20Permit, Pausable, AccessControl { * @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) { + function decreaseMintingAllowance(address _minter, uint256 _allowance) external onlyRole(MINTALLOWER_ROLE) { if (mintingAllowance[_minter] > _allowance) { mintingAllowance[_minter] -= _allowance; emit MintingAllowanceChanged(_minter, mintingAllowance[_minter]); @@ -246,10 +217,7 @@ contract Token is ERC2771Context, ERC20Permit, Pausable, AccessControl { * @param _amount how many tokens to mint */ function mint(address _to, uint256 _amount) external { - require( - mintingAllowance[_msgSender()] >= _amount, - "MintingAllowance too low" - ); + 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); @@ -267,10 +235,7 @@ contract Token is ERC2771Context, ERC20Permit, Pausable, AccessControl { * @param _from address that holds the tokens * @param _amount how many tokens to burn */ - function burn( - address _from, - uint256 _amount - ) external onlyRole(BURNER_ROLE) { + function burn(address _from, uint256 _amount) external onlyRole(BURNER_ROLE) { _burn(_from, _amount); } @@ -281,11 +246,7 @@ contract Token is ERC2771Context, ERC20Permit, Pausable, AccessControl { * 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)) { @@ -308,8 +269,7 @@ contract Token is ERC2771Context, ERC20Permit, Pausable, AccessControl { */ function _checkIfAllowedToTransact(address _address) internal view { require( - hasRole(TRANSFERER_ROLE, _address) || - allowList.map(_address) & requirements == requirements, + 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" ); } @@ -320,19 +280,11 @@ contract Token is ERC2771Context, ERC20Permit, Pausable, AccessControl { * @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( @@ -352,24 +304,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/package.json b/package.json index 3e58b5df..6f3a04fa 100644 --- a/package.json +++ b/package.json @@ -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 4d260138..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); diff --git a/script/DeployPlatform.s.sol b/script/DeployPlatform.s.sol index 83e1a8c5..5afbe7ee 100644 --- a/script/DeployPlatform.s.sol +++ b/script/DeployPlatform.s.sol @@ -42,15 +42,10 @@ 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)); 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 f8646417..71006784 100644 --- a/script/deploy.ts +++ b/script/deploy.ts @@ -3,7 +3,7 @@ // // When running the script with `npx hardhat run