diff --git a/.gas-snapshot b/.gas-snapshot index d0932b4..ed3bd99 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -3,10 +3,10 @@ AddEntryTest:test_RevertWhen_EntryAlreadyExists() (gas: 42644) AddEntryTest:test_RevertWhen_InvalidAddress() (gas: 25133) AddEntryTest:test_RevertWhen_SenderIsNotTokenDeployer() (gas: 14827) CollectibleV1Test:test_Deployment() (gas: 36386) -CommunityERC20Test:test_Deployment() (gas: 27614) +CommunityERC20Test:test_Deployment() (gas: 27659) CommunityTokenDeployerTest:test_Deployment() (gas: 14805) -CreateTest:test_Create() (gas: 2251262) -CreateTest:test_Create() (gas: 2548166) +CreateTest:test_Create() (gas: 2251272) +CreateTest:test_Create() (gas: 2548179) CreateTest:test_RevertWhen_InvalidOwnerTokenAddress() (gas: 15523) CreateTest:test_RevertWhen_InvalidReceiverAddress() (gas: 15656) CreateTest:test_RevertWhen_InvalidSignerPublicKey() (gas: 17057) @@ -16,26 +16,26 @@ CreateTest:test_RevertWhen_SenderIsNotTokenDeployer() (gas: 16421) CreateTest:test_RevertWhen_SenderIsNotTokenDeployer() (gas: 16524) DeployContracts:test() (gas: 120) DeployOwnerAndMasterToken:test() (gas: 120) -DeployTest:test_Deploy() (gas: 4872082) +DeployTest:test_Deploy() (gas: 4872105) DeployTest:test_Deployment() (gas: 14947) -DeployTest:test_RevertWhen_AlreadyDeployed() (gas: 4868312) +DeployTest:test_RevertWhen_AlreadyDeployed() (gas: 4868335) DeployTest:test_RevertWhen_InvalidCommunityAddress() (gas: 51385) DeployTest:test_RevertWhen_InvalidDeployerAddress() (gas: 55272) DeployTest:test_RevertWhen_InvalidDeploymentSignature() (gas: 65617) DeployTest:test_RevertWhen_InvalidSignerPublicKey() (gas: 53433) -DeployTest:test_RevertWhen_InvalidTokenMetadata() (gas: 2671682) +DeployTest:test_RevertWhen_InvalidTokenMetadata() (gas: 2671695) DeploymentTest:test_Deployment() (gas: 14671) DeploymentTest:test_Deployment() (gas: 14671) DeploymentTest:test_Deployment() (gas: 17295) GetEntryTest:test_ReturnZeroAddressIfEntryDoesNotExist() (gas: 11906) -MintToTest:test_Deployment() (gas: 27636) +MintToTest:test_Deployment() (gas: 27681) MintToTest:test_Deployment() (gas: 36386) MintToTest:test_Deployment() (gas: 83220) MintToTest:test_MintTo() (gas: 506888) -MintToTest:test_RevertWhen_AddressesAndAmountsAreNotEqualLength() (gas: 23778) +MintToTest:test_RevertWhen_AddressesAndAmountsAreNotEqualLength() (gas: 29695) MintToTest:test_RevertWhen_MaxSupplyIsReached() (gas: 20653) MintToTest:test_RevertWhen_MaxSupplyIsReached() (gas: 502655) -MintToTest:test_RevertWhen_MaxSupplyReached() (gas: 122988) +MintToTest:test_RevertWhen_MaxSupplyReached() (gas: 128905) MintToTest:test_RevertWhen_SenderIsNotOwner() (gas: 31544) OwnerTokenTest:test_Deployment() (gas: 83220) RemoteBurnTest:test_Deployment() (gas: 36386) @@ -54,13 +54,13 @@ SetMasterTokenFactoryAddressTest:test_Deployment() (gas: 14805) SetMasterTokenFactoryAddressTest:test_RevertWhen_InvalidTokenFactoryAddress() (gas: 12992) SetMasterTokenFactoryAddressTest:test_RevertWhen_SenderIsNotOwner() (gas: 12465) SetMasterTokenFactoryAddressTest:test_SetOwnerTokenFactoryAddress() (gas: 22861) -SetMaxSupplyTest:test_Deployment() (gas: 27614) +SetMaxSupplyTest:test_Deployment() (gas: 27659) SetMaxSupplyTest:test_Deployment() (gas: 83242) SetMaxSupplyTest:test_RevertWhen_CalledBecauseMaxSupplyIsLocked() (gas: 14327) -SetMaxSupplyTest:test_RevertWhen_MaxSupplyLowerThanTotalSupply() (gas: 148572) +SetMaxSupplyTest:test_RevertWhen_MaxSupplyLowerThanTotalSupply() (gas: 155732) SetMaxSupplyTest:test_RevertWhen_SenderIsNotOwner() (gas: 12527) -SetMaxSupplyTest:test_RevertWhen_SenderIsNotOwner() (gas: 12817) -SetMaxSupplyTest:test_SetMaxSupply() (gas: 15597) +SetMaxSupplyTest:test_RevertWhen_SenderIsNotOwner() (gas: 21402) +SetMaxSupplyTest:test_SetMaxSupply() (gas: 23955) SetOwnerTokenFactoryAddressTest:test_Deployment() (gas: 14805) SetOwnerTokenFactoryAddressTest:test_RevertWhen_InvalidTokenFactoryAddress() (gas: 12970) SetOwnerTokenFactoryAddressTest:test_RevertWhen_SenderIsNotOwner() (gas: 12443) diff --git a/contracts/CommunityOwnable.sol b/contracts/CommunityOwnable.sol new file mode 100644 index 0000000..74e5804 --- /dev/null +++ b/contracts/CommunityOwnable.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: Mozilla Public License 2.0 + +pragma solidity ^0.8.17; + +import { IERC721 } from "@openzeppelin/contracts/token/ERC721/IERC721.sol"; + +contract CommunityOwnable { + error CommunityOwnable_InvalidTokenAddress(); + error CommunityOwnable_NotAuthorized(); + + address public immutable ownerToken; + address public immutable masterToken; + + constructor(address _ownerToken, address _masterToken) { + ownerToken = _ownerToken; + masterToken = _masterToken; + + if (ownerToken == address(0) && masterToken == address(0)) { + revert CommunityOwnable_InvalidTokenAddress(); + } + } + + /// @dev Reverts if the msg.sender does not possess either an OwnerToken or a MasterToken. + modifier onlyCommunityOwnerOrTokenMaster() { + if ( + (ownerToken != address(0) && IERC721(ownerToken).balanceOf(msg.sender) == 0) + && (masterToken != address(0) && IERC721(masterToken).balanceOf(msg.sender) == 0) + ) { + revert CommunityOwnable_NotAuthorized(); + } + _; + } +} diff --git a/contracts/tokens/BaseToken.sol b/contracts/tokens/BaseToken.sol index f657f43..f9019e0 100644 --- a/contracts/tokens/BaseToken.sol +++ b/contracts/tokens/BaseToken.sol @@ -6,12 +6,11 @@ import { IERC721 } from "@openzeppelin/contracts/token/ERC721/IERC721.sol"; import { ERC721Enumerable } from "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol"; import { Context } from "@openzeppelin/contracts/utils/Context.sol"; import { Counters } from "@openzeppelin/contracts/utils/Counters.sol"; +import { CommunityOwnable } from "../CommunityOwnable.sol"; -abstract contract BaseToken is Context, ERC721Enumerable { +abstract contract BaseToken is Context, ERC721Enumerable, CommunityOwnable { using Counters for Counters.Counter; - error BaseToken_InvalidTokenAddress(); - error BaseToken_NotAuthorized(); error BaseToken_MaxSupplyLowerThanTotalSupply(); error BaseToken_MaxSupplyReached(); error BaseToken_NotRemoteBurnable(); @@ -25,10 +24,6 @@ abstract contract BaseToken is Context, ERC721Enumerable { * If we want unlimited total supply we should set maxSupply to 2^256-1. */ uint256 public maxSupply; - - address public immutable ownerToken; - address public immutable masterToken; - /** * If set to true, the contract owner can burn any token. */ @@ -52,34 +47,19 @@ abstract contract BaseToken is Context, ERC721Enumerable { address _masterToken ) ERC721(_name, _symbol) + CommunityOwnable(_ownerToken, _masterToken) { maxSupply = _maxSupply; remoteBurnable = _remoteBurnable; transferable = _transferable; baseTokenURI = _baseTokenURI; - ownerToken = _ownerToken; - masterToken = _masterToken; - - if (ownerToken == address(0) && masterToken == address(0)) { - revert BaseToken_InvalidTokenAddress(); - } - } - - modifier onlyOwner() { - if ( - (ownerToken != address(0) && IERC721(ownerToken).balanceOf(msg.sender) == 0) - && (masterToken != address(0) && IERC721(masterToken).balanceOf(msg.sender) == 0) - ) { - revert BaseToken_NotAuthorized(); - } - _; } // Events // External functions - function setMaxSupply(uint256 newMaxSupply) external virtual onlyOwner { + function setMaxSupply(uint256 newMaxSupply) external virtual onlyCommunityOwnerOrTokenMaster { if (newMaxSupply < totalSupply()) { revert BaseToken_MaxSupplyLowerThanTotalSupply(); } @@ -92,7 +72,7 @@ abstract contract BaseToken is Context, ERC721Enumerable { * URI autogenerated based on the base URI passed at construction. * */ - function mintTo(address[] memory addresses) public onlyOwner { + function mintTo(address[] memory addresses) public onlyCommunityOwnerOrTokenMaster { if (_tokenIdTracker.current() + addresses.length > maxSupply) { revert BaseToken_MaxSupplyReached(); } @@ -109,7 +89,7 @@ abstract contract BaseToken is Context, ERC721Enumerable { * @notice remoteBurn allows the owner to burn a token * @param tokenIds The list of token IDs to be burned */ - function remoteBurn(uint256[] memory tokenIds) public onlyOwner { + function remoteBurn(uint256[] memory tokenIds) public onlyCommunityOwnerOrTokenMaster { if (!remoteBurnable) revert BaseToken_NotRemoteBurnable(); for (uint256 i = 0; i < tokenIds.length; i++) { diff --git a/contracts/tokens/CommunityERC20.sol b/contracts/tokens/CommunityERC20.sol index 26ea3ad..20416bd 100644 --- a/contracts/tokens/CommunityERC20.sol +++ b/contracts/tokens/CommunityERC20.sol @@ -4,8 +4,9 @@ pragma solidity ^0.8.17; import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import { Context } from "@openzeppelin/contracts/utils/Context.sol"; +import { CommunityOwnable } from "../CommunityOwnable.sol"; -contract CommunityERC20 is Context, Ownable, ERC20 { +contract CommunityERC20 is Context, Ownable, ERC20, CommunityOwnable { error CommunityERC20_MaxSupplyLowerThanTotalSupply(); error CommunityERC20_MaxSupplyReached(); error CommunityERC20_MismatchingAddressesAndAmountsLengths(); @@ -21,9 +22,12 @@ contract CommunityERC20 is Context, Ownable, ERC20 { string memory _name, string memory _symbol, uint8 _decimals, - uint256 _maxSupply + uint256 _maxSupply, + address _ownerToken, + address _masterToken ) ERC20(_name, _symbol) + CommunityOwnable(_ownerToken, _masterToken) { maxSupply = _maxSupply; customDecimals = _decimals; @@ -33,7 +37,7 @@ contract CommunityERC20 is Context, Ownable, ERC20 { // External functions - function setMaxSupply(uint256 newMaxSupply) external onlyOwner { + function setMaxSupply(uint256 newMaxSupply) external onlyCommunityOwnerOrTokenMaster { if (newMaxSupply < totalSupply()) { revert CommunityERC20_MaxSupplyLowerThanTotalSupply(); } @@ -45,7 +49,7 @@ contract CommunityERC20 is Context, Ownable, ERC20 { * an amount specified in `amounts`. * */ - function mintTo(address[] memory addresses, uint256[] memory amounts) external onlyOwner { + function mintTo(address[] memory addresses, uint256[] memory amounts) external onlyCommunityOwnerOrTokenMaster { if (addresses.length != amounts.length) { revert CommunityERC20_MismatchingAddressesAndAmountsLengths(); } diff --git a/contracts/tokens/OwnerToken.sol b/contracts/tokens/OwnerToken.sol index 4cbc75e..d9cf1e4 100644 --- a/contracts/tokens/OwnerToken.sol +++ b/contracts/tokens/OwnerToken.sol @@ -2,6 +2,7 @@ pragma solidity ^0.8.17; import { BaseToken } from "./BaseToken.sol"; +import { CommunityOwnable } from "../CommunityOwnable.sol"; contract OwnerToken is BaseToken { bytes public signerPublicKey; @@ -21,11 +22,11 @@ contract OwnerToken is BaseToken { _mintTo(addresses); } - function setMaxSupply(uint256 _newMaxSupply) external override onlyOwner { + function setMaxSupply(uint256 _newMaxSupply) external override onlyCommunityOwnerOrTokenMaster { revert("max supply locked"); } - function setSignerPublicKey(bytes memory _newSignerPublicKey) external onlyOwner { + function setSignerPublicKey(bytes memory _newSignerPublicKey) external onlyCommunityOwnerOrTokenMaster { signerPublicKey = _newSignerPublicKey; } } diff --git a/script/DeployOwnerAndMasterToken.s.sol b/script/DeployOwnerAndMasterToken.s.sol index de6fa7f..6450250 100644 --- a/script/DeployOwnerAndMasterToken.s.sol +++ b/script/DeployOwnerAndMasterToken.s.sol @@ -15,18 +15,18 @@ contract DeployOwnerAndMasterToken is BaseScript { vm.startBroadcast(broadcaster); OwnerToken ownerToken = new OwnerToken( - ownerTokenConfig.name, - ownerTokenConfig.symbol, - ownerTokenConfig.baseURI, - broadcaster, - ownerTokenConfig.signerPublicKey + ownerTokenConfig.name, + ownerTokenConfig.symbol, + ownerTokenConfig.baseURI, + broadcaster, + ownerTokenConfig.signerPublicKey ); MasterToken masterToken = new MasterToken( - masterTokenConfig.name, - masterTokenConfig.symbol, - masterTokenConfig.baseURI, - address(ownerToken) + masterTokenConfig.name, + masterTokenConfig.symbol, + masterTokenConfig.baseURI, + address(ownerToken) ); vm.stopBroadcast(); diff --git a/script/DeploymentConfig.s.sol b/script/DeploymentConfig.s.sol index fc7f85d..7517949 100644 --- a/script/DeploymentConfig.s.sol +++ b/script/DeploymentConfig.s.sol @@ -20,7 +20,9 @@ contract DeploymentConfig is Script { address public immutable deployer; constructor(address _broadcaster) { - if (_broadcaster == address(0)) revert DeploymentConfig_InvalidDeployerAddress(); + if (_broadcaster == address(0)) { + revert DeploymentConfig_InvalidDeployerAddress(); + } deployer = _broadcaster; if (block.chainid == 31_337) { (ownerTokenConfig, masterTokenConfig) = getOrCreateAnvilEthConfig(); diff --git a/test/CollectibleV1.t.sol b/test/CollectibleV1.t.sol index cf87e96..fba2a6e 100644 --- a/test/CollectibleV1.t.sol +++ b/test/CollectibleV1.t.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.17; import { Test } from "forge-std/Test.sol"; import { DeployOwnerAndMasterToken } from "../script/DeployOwnerAndMasterToken.s.sol"; import { DeploymentConfig } from "../script/DeploymentConfig.s.sol"; +import { CommunityOwnable } from "../contracts/CommunityOwnable.sol"; import { BaseToken } from "../contracts/tokens/BaseToken.sol"; import { OwnerToken } from "../contracts/tokens/OwnerToken.sol"; import { MasterToken } from "../contracts/tokens/MasterToken.sol"; @@ -31,7 +32,7 @@ contract CollectibleV1Test is Test { name, symbol, maxSupply, - remoteBurnable, + remoteBurnable, transferable, baseURI, address(ownerToken), @@ -60,7 +61,7 @@ contract MintToTest is CollectibleV1Test { } function test_RevertWhen_SenderIsNotOwner() public { - vm.expectRevert(BaseToken.BaseToken_NotAuthorized.selector); + vm.expectRevert(CommunityOwnable.CommunityOwnable_NotAuthorized.selector); collectibleV1.mintTo(accounts); } @@ -98,7 +99,7 @@ contract RemoteBurnTest is CollectibleV1Test { function test_RevertWhen_SenderIsNotOwner() public { uint256[] memory ids = new uint256[](1); ids[0] = 0; - vm.expectRevert(BaseToken.BaseToken_NotAuthorized.selector); + vm.expectRevert(CommunityOwnable.CommunityOwnable_NotAuthorized.selector); collectibleV1.remoteBurn(ids); } diff --git a/test/CommunityERC20.t.sol b/test/CommunityERC20.t.sol index 4e48144..de20eb4 100644 --- a/test/CommunityERC20.t.sol +++ b/test/CommunityERC20.t.sol @@ -3,11 +3,17 @@ pragma solidity ^0.8.17; import { Test } from "forge-std/Test.sol"; import { CommunityERC20 } from "../contracts/tokens/CommunityERC20.sol"; +import { DeploymentConfig } from "../script/DeploymentConfig.s.sol"; +import { DeployOwnerAndMasterToken } from "../script/DeployOwnerAndMasterToken.s.sol"; +import { OwnerToken } from "../contracts/tokens/OwnerToken.sol"; +import { MasterToken } from "../contracts/tokens/MasterToken.sol"; +import { CommunityOwnable } from "../contracts/CommunityOwnable.sol"; contract CommunityERC20Test is Test { CommunityERC20 internal communityToken; address[] internal accounts = new address[](4); + address internal deployer; string internal name = "Test"; string internal symbol = "TEST"; @@ -15,7 +21,19 @@ contract CommunityERC20Test is Test { uint8 internal decimals = 18; function setUp() public virtual { - communityToken = new CommunityERC20(name, symbol, decimals, maxSupply); + DeployOwnerAndMasterToken deployment = new DeployOwnerAndMasterToken(); + (OwnerToken ownerToken, MasterToken masterToken, DeploymentConfig deploymentConfig) = deployment.run(); + + deployer = deploymentConfig.deployer(); + + communityToken = new CommunityERC20( + name, + symbol, + decimals, + maxSupply, + address(ownerToken), + address(masterToken) + ); accounts[0] = makeAddr("one"); accounts[1] = makeAddr("two"); @@ -38,7 +56,7 @@ contract SetMaxSupplyTest is CommunityERC20Test { function test_RevertWhen_SenderIsNotOwner() public { vm.prank(makeAddr("notOwner")); - vm.expectRevert(bytes("Ownable: caller is not the owner")); + vm.expectRevert(CommunityOwnable.CommunityOwnable_NotAuthorized.selector); communityToken.setMaxSupply(1000); } @@ -48,12 +66,18 @@ contract SetMaxSupplyTest is CommunityERC20Test { amounts[1] = 15; amounts[2] = 5; amounts[3] = 20; + + vm.startPrank(deployer); + communityToken.mintTo(accounts, amounts); // totalSupply is now 50 vm.expectRevert(CommunityERC20.CommunityERC20_MaxSupplyLowerThanTotalSupply.selector); communityToken.setMaxSupply(40); + + vm.stopPrank(); } function test_SetMaxSupply() public { + vm.prank(deployer); communityToken.setMaxSupply(1000); assertEq(communityToken.maxSupply(), 1000); } @@ -71,6 +95,7 @@ contract MintToTest is CommunityERC20Test { amounts[2] = 5; vm.expectRevert(CommunityERC20.CommunityERC20_MismatchingAddressesAndAmountsLengths.selector); + vm.prank(deployer); communityToken.mintTo(accounts, amounts); } @@ -82,6 +107,7 @@ contract MintToTest is CommunityERC20Test { amounts[3] = 1; // this should exceed max supply vm.expectRevert(CommunityERC20.CommunityERC20_MaxSupplyReached.selector); + vm.prank(deployer); communityToken.mintTo(accounts, amounts); } } diff --git a/test/OwnerToken.t.sol b/test/OwnerToken.t.sol index 9467d71..2b047da 100644 --- a/test/OwnerToken.t.sol +++ b/test/OwnerToken.t.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.17; import { Test } from "forge-std/Test.sol"; import { DeployOwnerAndMasterToken } from "../script/DeployOwnerAndMasterToken.s.sol"; import { DeploymentConfig } from "../script/DeploymentConfig.s.sol"; +import { CommunityOwnable } from "../contracts/CommunityOwnable.sol"; import { BaseToken } from "../contracts/tokens/BaseToken.sol"; import { OwnerToken } from "../contracts/tokens/OwnerToken.sol"; import { MasterToken } from "../contracts/tokens/MasterToken.sol"; @@ -43,7 +44,7 @@ contract SetMaxSupplyTest is OwnerTokenTest { } function test_RevertWhen_SenderIsNotOwner() public { - vm.expectRevert(BaseToken.BaseToken_NotAuthorized.selector); + vm.expectRevert(CommunityOwnable.CommunityOwnable_NotAuthorized.selector); ownerToken.setMaxSupply(1000); } @@ -60,7 +61,7 @@ contract SetSignerPublicKeyTest is OwnerTokenTest { } function test_RevertWhen_SenderIsNotOwner() public { - vm.expectRevert(BaseToken.BaseToken_NotAuthorized.selector); + vm.expectRevert(CommunityOwnable.CommunityOwnable_NotAuthorized.selector); ownerToken.setSignerPublicKey(bytes("some key")); }