refactor: move project to foundry template and introduce tests

This commit does a couple of things:

- moves the project to our foundry template structure and workflows
- removes hardhat usage and dependencies
- removes unused contracts
- ports existing JS tests to foundry tests
- adds additional tests for `CommunityERC20` contract
- Introduces deploy scripts written in solidity which are also covered
  by tests

The projects can now be build and tests with:

```
$ forge build
```

```
$ forge test
```

Test deployments can be done via

```
$ forge script script/DeployOwnerToken.sol
```
This commit is contained in:
r4bbit 2023-08-28 13:27:47 +02:00
parent b112414fae
commit c2f500c2e5
No known key found for this signature in database
GPG Key ID: E95F1E9447DC91A9
55 changed files with 1390 additions and 8918 deletions

20
.editorconfig Normal file
View File

@ -0,0 +1,20 @@
# EditorConfig http://EditorConfig.org
# top-most EditorConfig file
root = true
# All files
[*]
charset = utf-8
end_of_line = lf
indent_size = 2
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
[*.sol]
indent_size = 4
[*.tree]
indent_size = 1

12
.env.example Normal file
View File

@ -0,0 +1,12 @@
export API_KEY_ALCHEMY="YOUR_API_KEY_ALCHEMY"
export API_KEY_ARBISCAN="YOUR_API_KEY_ARBISCAN"
export API_KEY_BSCSCAN="YOUR_API_KEY_BSCSCAN"
export API_KEY_ETHERSCAN="YOUR_API_KEY_ETHERSCAN"
export API_KEY_GNOSISSCAN="YOUR_API_KEY_GNOSISSCAN"
export API_KEY_INFURA="YOUR_API_KEY_INFURA"
export API_KEY_OPTIMISTIC_ETHERSCAN="YOUR_API_KEY_OPTIMISTIC_ETHERSCAN"
export API_KEY_POLYGONSCAN="YOUR_API_KEY_POLYGONSCAN"
export API_KEY_SNOWTRACE="YOUR_API_KEY_SNOWTRACE"
export MNEMONIC="YOUR_MNEMONIC"
export FOUNDRY_PROFILE="default"

27
.gas-snapshot Normal file
View File

@ -0,0 +1,27 @@
CollectibleV1Test:test_Deployment() (gas: 38626)
CommunityERC20Test:test_Deployment() (gas: 29720)
MintToTest:test_Deployment() (gas: 29742)
MintToTest:test_Deployment() (gas: 38626)
MintToTest:test_Deployment() (gas: 85415)
MintToTest:test_MintTo() (gas: 509977)
MintToTest:test_RevertWhen_AddressesAndAmountsAreNotEqualLength() (gas: 24193)
MintToTest:test_RevertWhen_MaxSupplyIsReached() (gas: 23267)
MintToTest:test_RevertWhen_MaxSupplyIsReached() (gas: 505463)
MintToTest:test_RevertWhen_MaxSupplyReached() (gas: 123426)
MintToTest:test_RevertWhen_SenderIsNotOwner() (gas: 36358)
OwnerTokenTest:test_Deployment() (gas: 85415)
RemoteBurnTest:test_Deployment() (gas: 38626)
RemoteBurnTest:test_Deployment() (gas: 85437)
RemoteBurnTest:test_RemoteBurn() (gas: 455285)
RemoteBurnTest:test_RevertWhen_RemoteBurn() (gas: 19499)
RemoteBurnTest:test_RevertWhen_SenderIsNotOwner() (gas: 25211)
SetMaxSupplyTest:test_Deployment() (gas: 29720)
SetMaxSupplyTest:test_Deployment() (gas: 85437)
SetMaxSupplyTest:test_RevertWhen_CalledBecauseMaxSupplyIsLocked() (gas: 16521)
SetMaxSupplyTest:test_RevertWhen_MaxSupplyLowerThanTotalSupply() (gas: 149095)
SetMaxSupplyTest:test_RevertWhen_SenderIsNotOwner() (gas: 12852)
SetMaxSupplyTest:test_RevertWhen_SenderIsNotOwner() (gas: 17335)
SetMaxSupplyTest:test_SetMaxSupply() (gas: 15597)
SetSignerPublicKeyTest:test_Deployment() (gas: 85415)
SetSignerPublicKeyTest:test_RevertWhen_SenderIsNotOwner() (gas: 18036)
SetSignerPublicKeyTest:test_SetSignerPublicKey() (gas: 26357)

2
.gitattributes vendored Normal file
View File

@ -0,0 +1,2 @@
lib/** linguist-vendored

12
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View File

@ -0,0 +1,12 @@
## Description
Describe the changes made in your pull request here.
## Checklist
Ensure you completed **all of the steps** below before submitting your pull request:
- [ ] Added natspec comments?
- [ ] Ran `forge snapshot`?
- [ ] Ran `pnpm lint`?
- [ ] Ran `forge test`?

View File

@ -1,36 +1,119 @@
name: CI
on:
pull_request:
branches:
- main
name: "CI"
env:
FOUNDRY_PROFILE: ci
API_KEY_ALCHEMY: ${{ secrets.API_KEY_ALCHEMY }}
FOUNDRY_PROFILE: "ci"
on:
workflow_dispatch:
pull_request:
push:
branches:
- "main"
jobs:
check:
strategy:
fail-fast: true
name: Foundry project
runs-on: ubuntu-latest
lint:
runs-on: "ubuntu-latest"
steps:
- uses: actions/checkout@v3
- name: "Check out the repo"
uses: "actions/checkout@v3"
with:
submodules: recursive
submodules: "recursive"
- name: Install Foundry
uses: foundry-rs/foundry-toolchain@v1
- name: "Install Foundry"
uses: "foundry-rs/foundry-toolchain@v1"
- name: "Install Pnpm"
uses: "pnpm/action-setup@v2"
with:
version: nightly
version: "8"
- name: Run Forge build
run: |
forge --version
forge build --sizes
id: build
- name: "Install Node.js"
uses: "actions/setup-node@v3"
with:
cache: "pnpm"
node-version: "lts/*"
- name: Run Forge tests
- name: "Install the Node.js dependencies"
run: "pnpm install"
- name: "Lint the contracts"
run: "pnpm lint"
- name: "Add lint summary"
run: |
forge test -vvv
id: test
echo "## Lint result" >> $GITHUB_STEP_SUMMARY
echo "✅ Passed" >> $GITHUB_STEP_SUMMARY
build:
runs-on: "ubuntu-latest"
steps:
- name: "Check out the repo"
uses: "actions/checkout@v3"
with:
submodules: "recursive"
- name: "Install Foundry"
uses: "foundry-rs/foundry-toolchain@v1"
- name: "Build the contracts and print their size"
run: "forge build --sizes"
- name: "Add build summary"
run: |
echo "## Build result" >> $GITHUB_STEP_SUMMARY
echo "✅ Passed" >> $GITHUB_STEP_SUMMARY
test:
needs: ["lint", "build"]
runs-on: "ubuntu-latest"
steps:
- name: "Check out the repo"
uses: "actions/checkout@v3"
with:
submodules: "recursive"
- name: "Install Foundry"
uses: "foundry-rs/foundry-toolchain@v1"
- name: "Show the Foundry config"
run: "forge config"
- name: "Generate a fuzz seed that changes weekly to avoid burning through RPC allowance"
run: >
echo "FOUNDRY_FUZZ_SEED=$(
echo $(($EPOCHSECONDS - $EPOCHSECONDS % 604800))
)" >> $GITHUB_ENV
- name: "Run the tests"
run: "forge test"
- name: "Add test summary"
run: |
echo "## Tests result" >> $GITHUB_STEP_SUMMARY
echo "✅ Passed" >> $GITHUB_STEP_SUMMARY
coverage:
needs: ["lint", "build"]
runs-on: "ubuntu-latest"
steps:
- name: "Check out the repo"
uses: "actions/checkout@v3"
with:
submodules: "recursive"
- name: "Install Foundry"
uses: "foundry-rs/foundry-toolchain@v1"
- name: "Generate the coverage report using the unit and the integration tests"
run: 'forge coverage --match-path "test/**/*.sol" --report lcov'
- name: "Upload coverage report to Codecov"
uses: "codecov/codecov-action@v3"
with:
files: "./lcov.info"
- name: "Add coverage summary"
run: |
echo "## Coverage result" >> $GITHUB_STEP_SUMMARY
echo "✅ Uploaded to Codecov" >> $GITHUB_STEP_SUMMARY

22
.gitignore vendored
View File

@ -1,11 +1,23 @@
# directories
cache
node_modules
.env
out
coverage
coverage.json
typechain
typechain-types
#Hardhat files
cache
artifacts
.certora_internal
# files
*.env
*.log
.DS_Store
.pnp.*
lcov.info
yarn.lock
# broadcasts
!broadcast
broadcast/*
broadcast/*/31337/

6
.gitmodules vendored Normal file
View File

@ -0,0 +1,6 @@
[submodule "lib/forge-std"]
path = lib/forge-std
url = https://github.com/foundry-rs/forge-std
[submodule "lib/openzeppelin-contracts"]
path = lib/openzeppelin-contracts
url = https://github.com/OpenZeppelin/openzeppelin-contracts

20
.prettierignore Normal file
View File

@ -0,0 +1,20 @@
# directories
broadcast
cache
lib
node_modules
out
# files
*.env
*.log
.DS_Store
.pnp.*
lcov.info
package-lock.json
pnpm-lock.yaml
yarn.lock
package.json
.solhint.json
slither.config.json

14
.solhint.json Normal file
View File

@ -0,0 +1,14 @@
{
"extends": "solhint:recommended",
"rules": {
"code-complexity": ["error", 8],
"compiler-version": ["error", ">=0.8.17"],
"func-name-mixedcase": "off",
"func-visibility": ["error", { "ignoreConstructors": true }],
"max-line-length": ["error", 120],
"named-parameters-mapping": "warn",
"no-console": "off",
"not-rely-on-time": "off"
}
}

15
PROPERTIES.md Normal file
View File

@ -0,0 +1,15 @@
## Protocol properties and invariants
Below is a list of all documented properties and invariants of this project that must hold true.
- **Property** - Describes the property of the project / protocol that should ultimately be tested and formaly verified.
- **Type** - Properties are split into 5 main types: **Valid State**, **State Transition**, **Variable Transition**,
**High-Level Property**, **Unit Test**
- **Risk** - One of **High**, **Medium** and **Low**, depending on the property's risk factor
- **Tested** - Whether this property has been (fuzz) tested
| **Property** | **Type** | **Risk** | **Tested** |
| ------------ | -------- | -------- | ---------- |
| | | | |
| | | | |
| | | | |

View File

@ -1,13 +1,88 @@
# Sample Hardhat Project
# Communities Contracts [![Github Actions][gha-badge]][gha] [![Foundry][foundry-badge]][foundry]
This project demonstrates a basic Hardhat use case. It comes with a sample contract, a test for that contract, and a script that deploys that contract.
[gha]: https://github.com/status-im/communities-contracts/actions
[gha-badge]: https://github.com/status-im/communities-contracts/actions/workflows/ci.yml/badge.svg
[foundry]: https://getfoundry.sh/
[foundry-badge]: https://img.shields.io/badge/Built%20with-Foundry-FFDB1C.svg
Try running some of the following tasks:
## Usage
```shell
npx hardhat help
npx hardhat test
REPORT_GAS=true npx hardhat test
npx hardhat node
npx hardhat run scripts/deploy.ts
This is a list of the most frequently needed commands.
### Build
Build the contracts:
```sh
$ forge build
```
### Clean
Delete the build artifacts and cache directories:
```sh
$ forge clean
```
### Compile
Compile the contracts:
```sh
$ forge build
```
### Coverage
Get a test coverage report:
```sh
$ forge coverage
```
### Deploy
Deploy to Anvil:
```sh
$ forge script script/DeployOwnerToken.s.sol --broadcast --fork-url http://localhost:8545
```
For this script to work, you need to have a `MNEMONIC` environment variable set to a valid
[BIP39 mnemonic](https://iancoleman.io/bip39/).
For instructions on how to deploy to a testnet or mainnet, check out the
[Solidity Scripting](https://book.getfoundry.sh/tutorials/solidity-scripting.html) tutorial.
### Format
Format the contracts:
```sh
$ forge fmt
```
### Gas Usage
Get a gas report:
```sh
$ forge test --gas-report
```
### Lint
Lint the contracts:
```sh
$ pnpm lint
```
### Test
Run the tests:
```sh
$ forge test
```

28
codecov.yml Normal file
View File

@ -0,0 +1,28 @@
codecov:
require_ci_to_pass: false
comment: false
ignore:
- "script"
- "test"
coverage:
status:
project:
default:
# advanced settings
# Prevents PR from being blocked with a reduction in coverage.
# Note, if we want to re-enable this, a `threshold` value can be used
# allow coverage to drop by x% while still posting a success status.
# `informational`: https://docs.codecov.com/docs/commit-status#informational
# `threshold`: https://docs.codecov.com/docs/commit-status#threshold
informational: true
patch:
default:
# advanced settings
# Prevents PR from being blocked with a reduction in coverage.
# Note, if we want to re-enable this, a `threshold` value can be used
# allow coverage to drop by x% while still posting a success status.
# `informational`: https://docs.codecov.com/docs/commit-status#informational
# `threshold`: https://docs.codecov.com/docs/commit-status#threshold
informational: true

View File

@ -8,10 +8,7 @@ import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
import "@openzeppelin/contracts/utils/Context.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
abstract contract BaseToken is
Context,
ERC721Enumerable
{
abstract contract BaseToken is Context, ERC721Enumerable {
using Counters for Counters.Counter;
// State variables
@ -47,7 +44,9 @@ abstract contract BaseToken is
string memory _baseTokenURI,
address _ownerToken,
address _masterToken
) ERC721(_name, _symbol) {
)
ERC721(_name, _symbol)
{
maxSupply = _maxSupply;
remoteBurnable = _remoteBurnable;
transferable = _transferable;
@ -60,8 +59,8 @@ abstract contract BaseToken is
modifier onlyOwner() {
require(
(ownerToken == address(0) || IERC721(ownerToken).balanceOf(msg.sender) > 0) ||
(masterToken == address(0) || IERC721(masterToken).balanceOf(msg.sender) > 0),
(ownerToken == address(0) || IERC721(ownerToken).balanceOf(msg.sender) > 0)
|| (masterToken == address(0) || IERC721(masterToken).balanceOf(msg.sender) > 0),
"Not authorized"
);
@ -72,7 +71,7 @@ abstract contract BaseToken is
// External functions
function setMaxSupply(uint256 newMaxSupply) virtual external onlyOwner {
function setMaxSupply(uint256 newMaxSupply) external virtual onlyOwner {
require(newMaxSupply >= totalSupply(), "MAX_SUPPLY_LOWER_THAN_TOTAL_SUPPLY");
maxSupply = newMaxSupply;
}
@ -109,13 +108,7 @@ abstract contract BaseToken is
/**
* @dev See {IERC165-supportsInterface}.
*/
function supportsInterface(bytes4 interfaceId)
public
view
virtual
override(ERC721Enumerable)
returns (bool)
{
function supportsInterface(bytes4 interfaceId) public view virtual override(ERC721Enumerable) returns (bool) {
return super.supportsInterface(interfaceId);
}
@ -147,7 +140,11 @@ abstract contract BaseToken is
address to,
uint256 firstTokenId,
uint256 batchSize
) internal virtual override(ERC721Enumerable) {
)
internal
virtual
override(ERC721Enumerable)
{
if (from != address(0) && to != address(0) && !transferable) {
revert("not transferable");
}

View File

@ -1,49 +0,0 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.17;
import "./core/ModularERC721.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
contract BuyableERC721 is ModularERC721 {
using SafeERC20 for IERC20;
address public beneficiary;
address public paymentToken;
uint256 public tokenPrice;
constructor(
string memory name,
string memory symbol,
string memory baseTokenURI,
address _beneficiary,
address _paymentToken,
uint256 _tokenPrice
) ModularERC721(name, symbol, baseTokenURI) {
beneficiary = _beneficiary;
paymentToken = _paymentToken;
tokenPrice = _tokenPrice;
}
function setBeneficiary(address _account) public {
require(hasRole(ADMIN_ROLE, _msgSender()), "ModularERC721: must have admin role");
require(_account != address(0x0), "BuyableERC721: beneficiary cannot be 0x00");
beneficiary = _account;
}
function setPaymentToken(address _token) public {
require(hasRole(ADMIN_ROLE, _msgSender()), "ModularERC721: must have admin role");
require(_token != address(0x0), "BuyableERC721: token cannot be 0x00");
paymentToken = _token;
}
function setTokenPrice(uint256 _price) public {
require(hasRole(ADMIN_ROLE, _msgSender()), "ModularERC721: must have admin role");
tokenPrice = _price;
}
function mint() public {
IERC20(paymentToken).safeTransferFrom(msg.sender, beneficiary, tokenPrice);
_mintTo(msg.sender);
}
}

View File

@ -1,49 +0,0 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.17;
import "./core/ModularERC721.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
contract BuyableERC721 is ModularERC721 {
using SafeERC20 for IERC20;
address public beneficiary;
address public paymentToken;
uint256 public tokenPrice;
constructor(
string memory name,
string memory symbol,
string memory baseTokenURI,
address _beneficiary,
address _paymentToken,
uint256 _tokenPrice
) ModularERC721(name, symbol, baseTokenURI) {
beneficiary = _beneficiary;
paymentToken = _paymentToken;
tokenPrice = _tokenPrice;
}
function setBeneficiary(address _account) public {
require(hasRole(ADMIN_ROLE, _msgSender()), "ModularERC721: must have admin role");
require(_account != address(0x0), "BuyableERC721: beneficiary cannot be 0x00");
beneficiary = _account;
}
function setPaymentToken(address _token) public {
require(hasRole(ADMIN_ROLE, _msgSender()), "ModularERC721: must have admin role");
require(_token != address(0x0), "BuyableERC721: token cannot be 0x00");
paymentToken = _token;
}
function setTokenPrice(uint256 _price) public {
require(hasRole(ADMIN_ROLE, _msgSender()), "ModularERC721: must have admin role");
tokenPrice = _price;
}
function mint() public {
IERC20(paymentToken).safeTransferFrom(msg.sender, beneficiary, tokenPrice);
_mintTo(msg.sender);
}
}

View File

@ -1,26 +0,0 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.17;
import "./BuyableERC721.sol";
contract BuyableSoulbound is BuyableERC721 {
constructor(
string memory name,
string memory symbol,
string memory baseTokenURI,
address _beneficiary,
address _paymentToken,
uint256 _tokenPrice
) BuyableERC721 (name, symbol, baseTokenURI, _beneficiary, _paymentToken, _tokenPrice) {
}
function _beforeTokenTransfer(
address from,
address to,
uint256 firstTokenId,
uint256 batchSize
) internal override {
require(from == address(0) || to == address(0), "BuyableERC721: cannot be transferred");
super._beforeTokenTransfer(from, to, firstTokenId, batchSize);
}
}

View File

@ -13,14 +13,7 @@ contract CollectibleV1 is BaseToken {
string memory _baseTokenURI,
address _ownerToken,
address _masterToken
) BaseToken(
_name,
_symbol,
_maxSupply,
_remoteBurnable,
_transferable,
_baseTokenURI,
_ownerToken,
_masterToken) {
}
)
BaseToken(_name, _symbol, _maxSupply, _remoteBurnable, _transferable, _baseTokenURI, _ownerToken, _masterToken)
{ }
}

View File

@ -5,11 +5,7 @@ import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/utils/Context.sol";
contract CommunityERC20 is
Context,
Ownable,
ERC20
{
contract CommunityERC20 is Context, Ownable, ERC20 {
/**
* If we want unlimited total supply we should set maxSupply to 2^256-1.
*/
@ -22,7 +18,9 @@ contract CommunityERC20 is
string memory _symbol,
uint8 _decimals,
uint256 _maxSupply
) ERC20(_name, _symbol) {
)
ERC20(_name, _symbol)
{
maxSupply = _maxSupply;
customDecimals = _decimals;
}

View File

@ -9,14 +9,7 @@ contract MasterToken is BaseToken {
string memory _symbol,
string memory _baseTokenURI,
address _ownerToken
) BaseToken(
_name,
_symbol,
type(uint256).max,
true,
false,
_baseTokenURI,
_ownerToken,
address(0x0)) {
}
)
BaseToken(_name, _symbol, type(uint256).max, true, false, _baseTokenURI, _ownerToken, address(0x0))
{ }
}

View File

@ -17,15 +17,8 @@ contract OwnerToken is BaseToken {
string memory _masterSymbol,
string memory _masterBaseTokenURI,
bytes memory _signerPublicKey
) BaseToken(
_name,
_symbol,
1,
false,
true,
_baseTokenURI,
address(this),
address(this))
)
BaseToken(_name, _symbol, 1, false, true, _baseTokenURI, address(this), address(this))
{
signerPublicKey = _signerPublicKey;
MasterToken masterToken = new MasterToken(_masterName, _masterSymbol, _masterBaseTokenURI, address(this));
@ -35,7 +28,7 @@ contract OwnerToken is BaseToken {
_mintTo(addresses);
}
function setMaxSupply(uint256 _newMaxSupply) override external onlyOwner {
function setMaxSupply(uint256 _newMaxSupply) external override onlyOwner {
revert("max supply locked");
}

View File

@ -1,75 +0,0 @@
// SPDX-License-Identifier: Mozilla Public License 2.0
pragma solidity ^0.8.17;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Burnable.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Pausable.sol";
import "@openzeppelin/contracts/utils/Context.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
contract BaseERC721 is
Context,
ERC721Enumerable,
ERC721Burnable,
ERC721Pausable
{
using Counters for Counters.Counter;
Counters.Counter private _tokenIdTracker;
string private _baseTokenURI;
constructor(
string memory name,
string memory symbol,
string memory baseTokenURI
) ERC721(name, symbol) {
_baseTokenURI = baseTokenURI;
}
function _baseURI() internal view virtual override returns (string memory) {
return _baseTokenURI;
}
/**
* @dev Creates a new token for `to`. Its token ID will be automatically
* assigned (and available on the emitted {IERC721-Transfer} event), and the token
* URI autogenerated based on the base URI passed at construction.
*
* See {ERC721-_mint}.
*
* Requirements:
*
* - the caller must have the `MINTER_ROLE`.
*/
function _mintTo(address to) internal {
// We cannot just use balanceOf to create the new tokenId because tokens
// can be burned (destroyed), so we need a separate counter.
_mint(to, _tokenIdTracker.current());
_tokenIdTracker.increment();
}
function _beforeTokenTransfer(
address from,
address to,
uint256 firstTokenId,
uint256 batchSize
) internal virtual override(ERC721, ERC721Enumerable, ERC721Pausable) {
super._beforeTokenTransfer(from, to, firstTokenId, batchSize);
}
/**
* @dev See {IERC165-supportsInterface}.
*/
function supportsInterface(bytes4 interfaceId)
public
view
virtual
override(ERC721, ERC721Enumerable)
returns (bool)
{
return super.supportsInterface(interfaceId);
}
}

View File

@ -1,23 +0,0 @@
// SPDX-License-Identifier: Mozilla Public License 2.0
pragma solidity ^0.8.17;
import "@openzeppelin/contracts/access/AccessControlEnumerable.sol";
import "@openzeppelin/contracts/utils/Address.sol";
contract BaseModular is AccessControlEnumerable {
bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE");
bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE");
bytes32 public constant BURNER_ROLE = keccak256("BURNER_ROLE");
function addModule(bytes32 role, address account) public {
require(hasRole(ADMIN_ROLE, _msgSender()), "ModularERC721: must have admin role");
require(Address.isContract(account), "ModularERC721: module must be a contract");
_grantRole(role, account);
}
function removeModule(bytes32 role, address account) public onlyRole(ADMIN_ROLE) {
require(hasRole(ADMIN_ROLE, _msgSender()), "ModularERC721: must have admin role");
_grantRole(role, account);
}
}

View File

@ -1,89 +0,0 @@
// SPDX-License-Identifier: Mozilla Public License 2.0
pragma solidity ^0.8.17;
import "./BaseModular.sol";
import "./BaseERC721.sol";
contract ModularERC721 is
BaseModular,
BaseERC721
{
constructor(
string memory name,
string memory symbol,
string memory baseTokenURI
) BaseERC721(name, symbol, baseTokenURI) {
_setupRole(ADMIN_ROLE, _msgSender());
_setupRole(MINTER_ROLE, _msgSender());
_setupRole(PAUSER_ROLE, _msgSender());
}
/**
* @dev Creates a new token for `to`. Its token ID will be automatically
* assigned (and available on the emitted {IERC721-Transfer} event), and the token
* URI autogenerated based on the base URI passed at construction.
*
* See {ERC721-_mint}.
*
* Requirements:
*
* - the caller must have the `MINTER_ROLE`.
*/
function mintTo(address to) public virtual {
require(hasRole(MINTER_ROLE, _msgSender()), "ModularERC721: must have minter role");
_mintTo(to);
}
/**
* @dev Pauses all token transfers.
*
* See {ERC721Pausable} and {Pausable-_pause}.
*
* Requirements:
*
* - the caller must have the `PAUSER_ROLE`.
*/
function pause() public virtual {
require(hasRole(PAUSER_ROLE, _msgSender()), "ModularERC721: must have pauser role");
_pause();
}
/**
* @dev Unpauses all token transfers.
*
* See {ERC721Pausable} and {Pausable-_unpause}.
*
* Requirements:
*
* - the caller must have the `PAUSER_ROLE`.
*/
function unpause() public virtual {
require(hasRole(PAUSER_ROLE, _msgSender()), "ModularERC721: must have pauser role");
_unpause();
}
/**
* @dev See {IERC165-supportsInterface}.
*/
function supportsInterface(bytes4 interfaceId)
public
view
virtual
override(AccessControlEnumerable, BaseERC721)
returns (bool)
{
return super.supportsInterface(interfaceId);
}
/**
* @dev Burns `tokenId`.
*
* Requirements:
*
* - The caller must have the BURNER_ROLE role.
*/
function burnToken(uint256 tokenId) public {
require(hasRole(MINTER_ROLE, _msgSender()), "ModularERC721: must have burner role");
_burn(tokenId);
}
}

View File

@ -1,379 +0,0 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
/**
* @title DataTypes
* @author Lens Protocol
*
* @notice A standard library of data types used throughout the Lens Protocol.
*/
library DataTypes {
/**
* @notice An enum containing the different states the protocol can be in, limiting certain actions.
*
* @param Unpaused The fully unpaused state.
* @param PublishingPaused The state where only publication creation functions are paused.
* @param Paused The fully paused state.
*/
enum ProtocolState {
Unpaused,
PublishingPaused,
Paused
}
/**
* @notice An enum specifically used in a helper function to easily retrieve the publication type for integrations.
*
* @param Post A standard post, having a URI, a collect module but no pointer to another publication.
* @param Comment A comment, having a URI, a collect module and a pointer to another publication.
* @param Mirror A mirror, having a pointer to another publication, but no URI or collect module.
* @param Nonexistent An indicator showing the queried publication does not exist.
*/
enum PubType {
Post,
Comment,
Mirror,
Nonexistent
}
/**
* @notice A struct containing the necessary information to reconstruct an EIP-712 typed data signature.
*
* @param v The signature's recovery parameter.
* @param r The signature's r parameter.
* @param s The signature's s parameter
* @param deadline The signature's deadline
*/
struct EIP712Signature {
uint8 v;
bytes32 r;
bytes32 s;
uint256 deadline;
}
/**
* @notice A struct containing profile data.
*
* @param pubCount The number of publications made to this profile.
* @param followModule The address of the current follow module in use by this profile, can be empty.
* @param followNFT The address of the followNFT associated with this profile, can be empty..
* @param handle The profile's associated handle.
* @param imageURI The URI to be used for the profile's image.
* @param followNFTURI The URI to be used for the follow NFT.
*/
struct ProfileStruct {
uint256 pubCount;
address followModule;
address followNFT;
string handle;
string imageURI;
string followNFTURI;
}
/**
* @notice A struct containing data associated with each new publication.
*
* @param profileIdPointed The profile token ID this publication points to, for mirrors and comments.
* @param pubIdPointed The publication ID this publication points to, for mirrors and comments.
* @param contentURI The URI associated with this publication.
* @param referenceModule The address of the current reference module in use by this publication, can be empty.
* @param collectModule The address of the collect module associated with this publication, this exists for all publication.
* @param collectNFT The address of the collectNFT associated with this publication, if any.
*/
struct PublicationStruct {
uint256 profileIdPointed;
uint256 pubIdPointed;
string contentURI;
address referenceModule;
address collectModule;
address collectNFT;
}
/**
* @notice A struct containing the parameters required for the `createProfile()` function.
*
* @param to The address receiving the profile.
* @param handle The handle to set for the profile, must be unique and non-empty.
* @param imageURI The URI to set for the profile image.
* @param followModule The follow module to use, can be the zero address.
* @param followModuleInitData The follow module initialization data, if any.
* @param followNFTURI The URI to use for the follow NFT.
*/
struct CreateProfileData {
address to;
string handle;
string imageURI;
address followModule;
bytes followModuleInitData;
string followNFTURI;
}
/**
* @notice A struct containing the parameters required for the `setDefaultProfileWithSig()` function. Parameters are
* the same as the regular `setDefaultProfile()` function, with an added EIP712Signature.
*
* @param wallet The address of the wallet setting the default profile.
* @param profileId The token ID of the profile which will be set as default, or zero.
* @param sig The EIP712Signature struct containing the profile owner's signature.
*/
struct SetDefaultProfileWithSigData {
address wallet;
uint256 profileId;
EIP712Signature sig;
}
/**
* @notice A struct containing the parameters required for the `setFollowModuleWithSig()` function. Parameters are
* the same as the regular `setFollowModule()` function, with an added EIP712Signature.
*
* @param profileId The token ID of the profile to change the followModule for.
* @param followModule The followModule to set for the given profile, must be whitelisted.
* @param followModuleInitData The data to be passed to the followModule for initialization.
* @param sig The EIP712Signature struct containing the profile owner's signature.
*/
struct SetFollowModuleWithSigData {
uint256 profileId;
address followModule;
bytes followModuleInitData;
EIP712Signature sig;
}
/**
* @notice A struct containing the parameters required for the `setDispatcherWithSig()` function. Parameters are the same
* as the regular `setDispatcher()` function, with an added EIP712Signature.
*
* @param profileId The token ID of the profile to set the dispatcher for.
* @param dispatcher The dispatcher address to set for the profile.
* @param sig The EIP712Signature struct containing the profile owner's signature.
*/
struct SetDispatcherWithSigData {
uint256 profileId;
address dispatcher;
EIP712Signature sig;
}
/**
* @notice A struct containing the parameters required for the `setProfileImageURIWithSig()` function. Parameters are the same
* as the regular `setProfileImageURI()` function, with an added EIP712Signature.
*
* @param profileId The token ID of the profile to set the URI for.
* @param imageURI The URI to set for the given profile image.
* @param sig The EIP712Signature struct containing the profile owner's signature.
*/
struct SetProfileImageURIWithSigData {
uint256 profileId;
string imageURI;
EIP712Signature sig;
}
/**
* @notice A struct containing the parameters required for the `setFollowNFTURIWithSig()` function. Parameters are the same
* as the regular `setFollowNFTURI()` function, with an added EIP712Signature.
*
* @param profileId The token ID of the profile for which to set the followNFT URI.
* @param followNFTURI The follow NFT URI to set.
* @param sig The EIP712Signature struct containing the followNFT's associated profile owner's signature.
*/
struct SetFollowNFTURIWithSigData {
uint256 profileId;
string followNFTURI;
EIP712Signature sig;
}
/**
* @notice A struct containing the parameters required for the `post()` function.
*
* @param profileId The token ID of the profile to publish to.
* @param contentURI The URI to set for this new publication.
* @param collectModule The collect module to set for this new publication.
* @param collectModuleInitData The data to pass to the collect module's initialization.
* @param referenceModule The reference module to set for the given publication, must be whitelisted.
* @param referenceModuleInitData The data to be passed to the reference module for initialization.
*/
struct PostData {
uint256 profileId;
string contentURI;
address collectModule;
bytes collectModuleInitData;
address referenceModule;
bytes referenceModuleInitData;
}
/**
* @notice A struct containing the parameters required for the `postWithSig()` function. Parameters are the same as
* the regular `post()` function, with an added EIP712Signature.
*
* @param profileId The token ID of the profile to publish to.
* @param contentURI The URI to set for this new publication.
* @param collectModule The collectModule to set for this new publication.
* @param collectModuleInitData The data to pass to the collectModule's initialization.
* @param referenceModule The reference module to set for the given publication, must be whitelisted.
* @param referenceModuleInitData The data to be passed to the reference module for initialization.
* @param sig The EIP712Signature struct containing the profile owner's signature.
*/
struct PostWithSigData {
uint256 profileId;
string contentURI;
address collectModule;
bytes collectModuleInitData;
address referenceModule;
bytes referenceModuleInitData;
EIP712Signature sig;
}
/**
* @notice A struct containing the parameters required for the `comment()` function.
*
* @param profileId The token ID of the profile to publish to.
* @param contentURI The URI to set for this new publication.
* @param profileIdPointed The profile token ID to point the comment to.
* @param pubIdPointed The publication ID to point the comment to.
* @param referenceModuleData The data passed to the reference module.
* @param collectModule The collect module to set for this new publication.
* @param collectModuleInitData The data to pass to the collect module's initialization.
* @param referenceModule The reference module to set for the given publication, must be whitelisted.
* @param referenceModuleInitData The data to be passed to the reference module for initialization.
*/
struct CommentData {
uint256 profileId;
string contentURI;
uint256 profileIdPointed;
uint256 pubIdPointed;
bytes referenceModuleData;
address collectModule;
bytes collectModuleInitData;
address referenceModule;
bytes referenceModuleInitData;
}
/**
* @notice A struct containing the parameters required for the `commentWithSig()` function. Parameters are the same as
* the regular `comment()` function, with an added EIP712Signature.
*
* @param profileId The token ID of the profile to publish to.
* @param contentURI The URI to set for this new publication.
* @param profileIdPointed The profile token ID to point the comment to.
* @param pubIdPointed The publication ID to point the comment to.
* @param referenceModuleData The data passed to the reference module.
* @param collectModule The collectModule to set for this new publication.
* @param collectModuleInitData The data to pass to the collectModule's initialization.
* @param referenceModule The reference module to set for the given publication, must be whitelisted.
* @param referenceModuleInitData The data to be passed to the reference module for initialization.
* @param sig The EIP712Signature struct containing the profile owner's signature.
*/
struct CommentWithSigData {
uint256 profileId;
string contentURI;
uint256 profileIdPointed;
uint256 pubIdPointed;
bytes referenceModuleData;
address collectModule;
bytes collectModuleInitData;
address referenceModule;
bytes referenceModuleInitData;
EIP712Signature sig;
}
/**
* @notice A struct containing the parameters required for the `mirror()` function.
*
* @param profileId The token ID of the profile to publish to.
* @param profileIdPointed The profile token ID to point the mirror to.
* @param pubIdPointed The publication ID to point the mirror to.
* @param referenceModuleData The data passed to the reference module.
* @param referenceModule The reference module to set for the given publication, must be whitelisted.
* @param referenceModuleInitData The data to be passed to the reference module for initialization.
*/
struct MirrorData {
uint256 profileId;
uint256 profileIdPointed;
uint256 pubIdPointed;
bytes referenceModuleData;
address referenceModule;
bytes referenceModuleInitData;
}
/**
* @notice A struct containing the parameters required for the `mirrorWithSig()` function. Parameters are the same as
* the regular `mirror()` function, with an added EIP712Signature.
*
* @param profileId The token ID of the profile to publish to.
* @param profileIdPointed The profile token ID to point the mirror to.
* @param pubIdPointed The publication ID to point the mirror to.
* @param referenceModuleData The data passed to the reference module.
* @param referenceModule The reference module to set for the given publication, must be whitelisted.
* @param referenceModuleInitData The data to be passed to the reference module for initialization.
* @param sig The EIP712Signature struct containing the profile owner's signature.
*/
struct MirrorWithSigData {
uint256 profileId;
uint256 profileIdPointed;
uint256 pubIdPointed;
bytes referenceModuleData;
address referenceModule;
bytes referenceModuleInitData;
EIP712Signature sig;
}
/**
* @notice A struct containing the parameters required for the `followWithSig()` function. Parameters are the same
* as the regular `follow()` function, with the follower's (signer) address and an EIP712Signature added.
*
* @param follower The follower which is the message signer.
* @param profileIds The array of token IDs of the profiles to follow.
* @param datas The array of arbitrary data to pass to the followModules if needed.
* @param sig The EIP712Signature struct containing the follower's signature.
*/
struct FollowWithSigData {
address follower;
uint256[] profileIds;
bytes[] datas;
EIP712Signature sig;
}
/**
* @notice A struct containing the parameters required for the `collectWithSig()` function. Parameters are the same as
* the regular `collect()` function, with the collector's (signer) address and an EIP712Signature added.
*
* @param collector The collector which is the message signer.
* @param profileId The token ID of the profile that published the publication to collect.
* @param pubId The publication to collect's publication ID.
* @param data The arbitrary data to pass to the collectModule if needed.
* @param sig The EIP712Signature struct containing the collector's signature.
*/
struct CollectWithSigData {
address collector;
uint256 profileId;
uint256 pubId;
bytes data;
EIP712Signature sig;
}
/**
* @notice A struct containing the parameters required for the `setProfileMetadataWithSig()` function.
*
* @param profileId The profile ID for which to set the metadata.
* @param metadata The metadata string to set for the profile and user.
* @param sig The EIP712Signature struct containing the user's signature.
*/
struct SetProfileMetadataWithSigData {
uint256 profileId;
string metadata;
EIP712Signature sig;
}
/**
* @notice A struct containing the parameters required for the `toggleFollowWithSig()` function.
*
* @param follower The follower which is the message signer.
* @param profileIds The token ID array of the profiles.
* @param enables The array of booleans to enable/disable follows.
* @param sig The EIP712Signature struct containing the follower's signature.
*/
struct ToggleFollowWithSigData {
address follower;
uint256[] profileIds;
bool[] enables;
EIP712Signature sig;
}
}

View File

@ -1,6 +0,0 @@
// SPDX-License-Identifier: Mozilla Public License 2.0
pragma solidity ^0.8.17;
library Helpers {
}

View File

@ -1,10 +0,0 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.17;
import {ERC20} from '@openzeppelin/contracts/token/ERC20/ERC20.sol';
contract Currency is ERC20('Currency Test', 'TEST') {
function mint(address to, uint256 amount) external {
_mint(to, amount);
}
}

55
foundry.toml Normal file
View File

@ -0,0 +1,55 @@
# Full reference https://github.com/foundry-rs/foundry/tree/master/config
[profile.default]
auto_detect_solc = false
block_timestamp = 1_680_220_800 # March 31, 2023 at 00:00 GMT
bytecode_hash = "none"
cbor_metadata = false
evm_version = "paris"
fuzz = { runs = 1_000 }
gas_reports = ["*"]
libs = ["lib"]
optimizer = true
optimizer_runs = 10_000
out = "out"
script = "script"
solc = "0.8.17"
src = "contracts"
test = "test"
[profile.ci]
fuzz = { runs = 10_000 }
verbosity = 4
[etherscan]
# arbitrum_one = { key = "${API_KEY_ARBISCAN}" }
# avalanche = { key = "${API_KEY_SNOWTRACE}" }
# bnb_smart_chain = { key = "${API_KEY_BSCSCAN}" }
# gnosis_chain = { key = "${API_KEY_GNOSISSCAN}" }
# goerli = { key = "${API_KEY_ETHERSCAN}" }
# mainnet = { key = "${API_KEY_ETHERSCAN}" }
# optimism = { key = "${API_KEY_OPTIMISTIC_ETHERSCAN}" }
# polygon = { key = "${API_KEY_POLYGONSCAN}" }
# sepolia = { key = "${API_KEY_ETHERSCAN}" }
[fmt]
bracket_spacing = true
int_types = "long"
line_length = 120
multiline_func_header = "all"
number_underscore = "thousands"
quote_style = "double"
tab_width = 4
wrap_comments = true
[rpc_endpoints]
# arbitrum_one = "https://arbitrum-mainnet.infura.io/v3/${API_KEY_INFURA}"
# avalanche = "https://avalanche-mainnet.infura.io/v3/${API_KEY_INFURA}"
# bnb_smart_chain = "https://bsc-dataseed.binance.org"
# gnosis_chain = "https://rpc.gnosischain.com"
# goerli = "https://goerli.infura.io/v3/${API_KEY_INFURA}"
localhost = "http://localhost:8545"
# mainnet = "https://eth-mainnet.g.alchemy.com/v2/${API_KEY_ALCHEMY}"
# optimism = "https://optimism-mainnet.infura.io/v3/${API_KEY_INFURA}"
# polygon = "https://polygon-mainnet.infura.io/v3/${API_KEY_INFURA}"
# sepolia = "https://sepolia.infura.io/v3/${API_KEY_INFURA}"

View File

@ -1,17 +0,0 @@
import { HardhatUserConfig } from "hardhat/config";
import "@nomicfoundation/hardhat-toolbox";
import "@openzeppelin/test-helpers";
const config: HardhatUserConfig = {
solidity: {
version: "0.8.17",
settings: {
optimizer: {
enabled: true,
runs: 200,
},
},
},
};
export default config;

1
lib/forge-std vendored Submodule

@ -0,0 +1 @@
Subproject commit da26c0455d3ddf147564f7367b5bf8c8823355b9

1
lib/openzeppelin-contracts vendored Submodule

@ -0,0 +1 @@
Subproject commit fd81a96f01cc42ef1c9a5399364968d0e07e9e90

View File

@ -1,28 +1,31 @@
{
"devDependencies": {
"@ethersproject/abi": "^5.4.7",
"@ethersproject/providers": "^5.4.7",
"@nomicfoundation/hardhat-chai-matchers": "^1.0.0",
"@nomicfoundation/hardhat-network-helpers": "^1.0.0",
"@nomicfoundation/hardhat-toolbox": "^2.0.0",
"@nomiclabs/hardhat-ethers": "^2.0.0",
"@nomiclabs/hardhat-etherscan": "^3.0.0",
"@typechain/ethers-v5": "^10.1.0",
"@typechain/hardhat": "^6.1.2",
"@types/chai": "^4.2.0",
"@types/mocha": "^9.1.0",
"@types/node": ">=12.0.0",
"chai": "^4.2.0",
"ethers": "^5.4.7",
"hardhat": "^2.12.2",
"hardhat-gas-reporter": "^1.0.8",
"solidity-coverage": "^0.8.0",
"ts-node": ">=8.0.0",
"typechain": "^8.1.0",
"typescript": ">=4.5.0"
"name": "status-im/communities-contracts",
"description": "Smart contract related to Status communities.",
"version": "1.0.0",
"author": {
"name": "0x-r4bbit",
"url": "https://github.com/vacp2p"
},
"dependencies": {
"@openzeppelin/contracts": "^4.8.0",
"@openzeppelin/test-helpers": "^0.5.16"
"devDependencies": {
"prettier": "^3.0.0",
"solhint-community": "^3.6.0"
},
"keywords": [
"blockchain",
"ethereum",
"forge",
"foundry",
"smart-contracts",
"solidity",
"template"
],
"private": true,
"scripts": {
"clean": "rm -rf cache out",
"lint": "pnpm lint:sol && pnpm prettier:check",
"lint:sol": "forge fmt --check && pnpm solhint {script,src,test}/**/*.sol",
"prettier:check": "prettier --check **/*.{json,md,yml} --ignore-path=.prettierignore",
"prettier:write": "prettier --write **/*.{json,md,yml} --ignore-path=.prettierignore"
}
}

453
pnpm-lock.yaml Normal file
View File

@ -0,0 +1,453 @@
lockfileVersion: '6.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
devDependencies:
prettier:
specifier: ^3.0.0
version: 3.0.0
solhint-community:
specifier: ^3.6.0
version: 3.6.0
packages:
/@babel/code-frame@7.22.5:
resolution: {integrity: sha512-Xmwn266vad+6DAqEB2A6V/CcZVp62BbwVmcOJc2RPuwih1kw02TjQvWVWlcKGbBPd+8/0V5DEkOcizRGYsspYQ==}
engines: {node: '>=6.9.0'}
dependencies:
'@babel/highlight': 7.22.5
dev: true
/@babel/helper-validator-identifier@7.22.5:
resolution: {integrity: sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==}
engines: {node: '>=6.9.0'}
dev: true
/@babel/highlight@7.22.5:
resolution: {integrity: sha512-BSKlD1hgnedS5XRnGOljZawtag7H1yPfQp0tdNJCHoH6AZ+Pcm9VvkrK59/Yy593Ypg0zMxH2BxD1VPYUQ7UIw==}
engines: {node: '>=6.9.0'}
dependencies:
'@babel/helper-validator-identifier': 7.22.5
chalk: 2.4.2
js-tokens: 4.0.0
dev: true
/@solidity-parser/parser@0.16.1:
resolution: {integrity: sha512-PdhRFNhbTtu3x8Axm0uYpqOy/lODYQK+MlYSgqIsq2L8SFYEHJPHNUiOTAJbDGzNjjr1/n9AcIayxafR/fWmYw==}
dependencies:
antlr4ts: 0.5.0-alpha.4
dev: true
/ajv@6.12.6:
resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==}
dependencies:
fast-deep-equal: 3.1.3
fast-json-stable-stringify: 2.1.0
json-schema-traverse: 0.4.1
uri-js: 4.4.1
dev: true
/ajv@8.12.0:
resolution: {integrity: sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==}
dependencies:
fast-deep-equal: 3.1.3
json-schema-traverse: 1.0.0
require-from-string: 2.0.2
uri-js: 4.4.1
dev: true
/ansi-regex@5.0.1:
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
engines: {node: '>=8'}
dev: true
/ansi-styles@3.2.1:
resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==}
engines: {node: '>=4'}
dependencies:
color-convert: 1.9.3
dev: true
/ansi-styles@4.3.0:
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
engines: {node: '>=8'}
dependencies:
color-convert: 2.0.1
dev: true
/antlr4@4.13.0:
resolution: {integrity: sha512-zooUbt+UscjnWyOrsuY/tVFL4rwrAGwOivpQmvmUDE22hy/lUA467Rc1rcixyRwcRUIXFYBwv7+dClDSHdmmew==}
engines: {node: '>=16'}
dev: true
/antlr4ts@0.5.0-alpha.4:
resolution: {integrity: sha512-WPQDt1B74OfPv/IMS2ekXAKkTZIHl88uMetg6q3OTqgFxZ/dxDXI0EWLyZid/1Pe6hTftyg5N7gel5wNAGxXyQ==}
dev: true
/argparse@2.0.1:
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
dev: true
/ast-parents@0.0.1:
resolution: {integrity: sha512-XHusKxKz3zoYk1ic8Un640joHbFMhbqneyoZfoKnEGtf2ey9Uh/IdpcQplODdO/kENaMIWsD0nJm4+wX3UNLHA==}
dev: true
/astral-regex@2.0.0:
resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==}
engines: {node: '>=8'}
dev: true
/balanced-match@1.0.2:
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
dev: true
/brace-expansion@2.0.1:
resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==}
dependencies:
balanced-match: 1.0.2
dev: true
/callsites@3.1.0:
resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
engines: {node: '>=6'}
dev: true
/chalk@2.4.2:
resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==}
engines: {node: '>=4'}
dependencies:
ansi-styles: 3.2.1
escape-string-regexp: 1.0.5
supports-color: 5.5.0
dev: true
/chalk@4.1.2:
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
engines: {node: '>=10'}
dependencies:
ansi-styles: 4.3.0
supports-color: 7.2.0
dev: true
/color-convert@1.9.3:
resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==}
dependencies:
color-name: 1.1.3
dev: true
/color-convert@2.0.1:
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
engines: {node: '>=7.0.0'}
dependencies:
color-name: 1.1.4
dev: true
/color-name@1.1.3:
resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==}
dev: true
/color-name@1.1.4:
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
dev: true
/commander@10.0.1:
resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==}
engines: {node: '>=14'}
dev: true
/cosmiconfig@8.2.0:
resolution: {integrity: sha512-3rTMnFJA1tCOPwRxtgF4wd7Ab2qvDbL8jX+3smjIbS4HlZBagTlpERbdN7iAbWlrfxE3M8c27kTwTawQ7st+OQ==}
engines: {node: '>=14'}
dependencies:
import-fresh: 3.3.0
js-yaml: 4.1.0
parse-json: 5.2.0
path-type: 4.0.0
dev: true
/emoji-regex@8.0.0:
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
dev: true
/error-ex@1.3.2:
resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==}
dependencies:
is-arrayish: 0.2.1
dev: true
/escape-string-regexp@1.0.5:
resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==}
engines: {node: '>=0.8.0'}
dev: true
/fast-deep-equal@3.1.3:
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
dev: true
/fast-diff@1.3.0:
resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==}
dev: true
/fast-json-stable-stringify@2.1.0:
resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==}
dev: true
/fs.realpath@1.0.0:
resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
dev: true
/glob@8.1.0:
resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==}
engines: {node: '>=12'}
dependencies:
fs.realpath: 1.0.0
inflight: 1.0.6
inherits: 2.0.4
minimatch: 5.1.6
once: 1.4.0
dev: true
/has-flag@3.0.0:
resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==}
engines: {node: '>=4'}
dev: true
/has-flag@4.0.0:
resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
engines: {node: '>=8'}
dev: true
/ignore@5.2.4:
resolution: {integrity: sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==}
engines: {node: '>= 4'}
dev: true
/import-fresh@3.3.0:
resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==}
engines: {node: '>=6'}
dependencies:
parent-module: 1.0.1
resolve-from: 4.0.0
dev: true
/inflight@1.0.6:
resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==}
dependencies:
once: 1.4.0
wrappy: 1.0.2
dev: true
/inherits@2.0.4:
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
dev: true
/is-arrayish@0.2.1:
resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==}
dev: true
/is-fullwidth-code-point@3.0.0:
resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
engines: {node: '>=8'}
dev: true
/js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
dev: true
/js-yaml@4.1.0:
resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==}
hasBin: true
dependencies:
argparse: 2.0.1
dev: true
/json-parse-even-better-errors@2.3.1:
resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==}
dev: true
/json-schema-traverse@0.4.1:
resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
dev: true
/json-schema-traverse@1.0.0:
resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==}
dev: true
/lines-and-columns@1.2.4:
resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
dev: true
/lodash.truncate@4.4.2:
resolution: {integrity: sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==}
dev: true
/lodash@4.17.21:
resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
dev: true
/minimatch@5.1.6:
resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==}
engines: {node: '>=10'}
dependencies:
brace-expansion: 2.0.1
dev: true
/once@1.4.0:
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
dependencies:
wrappy: 1.0.2
dev: true
/parent-module@1.0.1:
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
engines: {node: '>=6'}
dependencies:
callsites: 3.1.0
dev: true
/parse-json@5.2.0:
resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==}
engines: {node: '>=8'}
dependencies:
'@babel/code-frame': 7.22.5
error-ex: 1.3.2
json-parse-even-better-errors: 2.3.1
lines-and-columns: 1.2.4
dev: true
/path-type@4.0.0:
resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==}
engines: {node: '>=8'}
dev: true
/pluralize@8.0.0:
resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==}
engines: {node: '>=4'}
dev: true
/prettier@2.8.8:
resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==}
engines: {node: '>=10.13.0'}
hasBin: true
requiresBuild: true
dev: true
optional: true
/prettier@3.0.0:
resolution: {integrity: sha512-zBf5eHpwHOGPC47h0zrPyNn+eAEIdEzfywMoYn2XPi0P44Zp0tSq64rq0xAREh4auw2cJZHo9QUob+NqCQky4g==}
engines: {node: '>=14'}
hasBin: true
dev: true
/punycode@2.3.0:
resolution: {integrity: sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==}
engines: {node: '>=6'}
dev: true
/require-from-string@2.0.2:
resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
engines: {node: '>=0.10.0'}
dev: true
/resolve-from@4.0.0:
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
engines: {node: '>=4'}
dev: true
/semver@6.3.1:
resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
hasBin: true
dev: true
/slice-ansi@4.0.0:
resolution: {integrity: sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==}
engines: {node: '>=10'}
dependencies:
ansi-styles: 4.3.0
astral-regex: 2.0.0
is-fullwidth-code-point: 3.0.0
dev: true
/solhint-community@3.6.0:
resolution: {integrity: sha512-3WGi8nB9VSdC7B3xawktFoQkJEgX6rsUe7jWZJteDBdix+tAOGN+2ZhRr7Cyv1s+h5BRLSsNOXoh7Vg9KEeQbg==}
hasBin: true
dependencies:
'@solidity-parser/parser': 0.16.1
ajv: 6.12.6
antlr4: 4.13.0
ast-parents: 0.0.1
chalk: 4.1.2
commander: 10.0.1
cosmiconfig: 8.2.0
fast-diff: 1.3.0
glob: 8.1.0
ignore: 5.2.4
js-yaml: 4.1.0
lodash: 4.17.21
pluralize: 8.0.0
semver: 6.3.1
strip-ansi: 6.0.1
table: 6.8.1
text-table: 0.2.0
optionalDependencies:
prettier: 2.8.8
dev: true
/string-width@4.2.3:
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
engines: {node: '>=8'}
dependencies:
emoji-regex: 8.0.0
is-fullwidth-code-point: 3.0.0
strip-ansi: 6.0.1
dev: true
/strip-ansi@6.0.1:
resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
engines: {node: '>=8'}
dependencies:
ansi-regex: 5.0.1
dev: true
/supports-color@5.5.0:
resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==}
engines: {node: '>=4'}
dependencies:
has-flag: 3.0.0
dev: true
/supports-color@7.2.0:
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
engines: {node: '>=8'}
dependencies:
has-flag: 4.0.0
dev: true
/table@6.8.1:
resolution: {integrity: sha512-Y4X9zqrCftUhMeH2EptSSERdVKt/nEdijTOacGD/97EKjhQ/Qs8RTlEGABSJNNN8lac9kheH+af7yAkEWlgneA==}
engines: {node: '>=10.0.0'}
dependencies:
ajv: 8.12.0
lodash.truncate: 4.4.2
slice-ansi: 4.0.0
string-width: 4.2.3
strip-ansi: 6.0.1
dev: true
/text-table@0.2.0:
resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==}
dev: true
/uri-js@4.4.1:
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
dependencies:
punycode: 2.3.0
dev: true
/wrappy@1.0.2:
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
dev: true

7
prettierrc.yml Normal file
View File

@ -0,0 +1,7 @@
bracketSpacing: true
printWidth: 120
proseWrap: "always"
singleQuote: false
tabWidth: 2
trailingComma: "all"
useTabs: false

3
remappings.txt Normal file
View File

@ -0,0 +1,3 @@
forge-std/=lib/forge-std/src/
@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts

38
script/Base.s.sol Normal file
View File

@ -0,0 +1,38 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity >=0.8.17 <=0.9.0;
import { Script } from "forge-std/Script.sol";
abstract contract BaseScript is Script {
/// @dev Included to enable compilation of the script without a $MNEMONIC environment variable.
string internal constant TEST_MNEMONIC = "test test test test test test test test test test test junk";
/// @dev The address of the transaction broadcaster.
address internal broadcaster;
/// @dev Used to derive the broadcaster's address if $ETH_FROM is not defined.
string internal mnemonic;
/// @dev Initializes the transaction broadcaster like this:
///
/// - If $ETH_FROM is defined, use it.
/// - Otherwise, derive the broadcaster address from $MNEMONIC.
/// - If $MNEMONIC is not defined, default to a test mnemonic.
///
/// The use case for $ETH_FROM is to specify the broadcaster key and its address via the command line.
constructor() {
address from = vm.envOr({ name: "ETH_FROM", defaultValue: address(0) });
if (from != address(0)) {
broadcaster = from;
} else {
mnemonic = vm.envOr({ name: "MNEMONIC", defaultValue: TEST_MNEMONIC });
(broadcaster,) = deriveRememberKey({ mnemonic: mnemonic, index: 0 });
}
}
modifier broadcast() {
vm.startBroadcast(broadcaster);
_;
vm.stopBroadcast();
}
}

View File

@ -0,0 +1,39 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.17;
import { Vm } from "forge-std/Vm.sol";
import { BaseScript } from "./Base.s.sol";
import { DeploymentConfig } from "./DeploymentConfig.s.sol";
import { OwnerToken } from "../contracts/OwnerToken.sol";
import { MasterToken } from "../contracts/MasterToken.sol";
contract DeployOwnerToken is BaseScript {
function run() external returns (OwnerToken, MasterToken, DeploymentConfig) {
DeploymentConfig deploymentConfig = new DeploymentConfig(broadcaster);
DeploymentConfig.TokenConfig memory ownerTokenConfig = deploymentConfig.getOwnerTokenConfig();
DeploymentConfig.TokenConfig memory masterTokenConfig = deploymentConfig.getMasterTokenConfig();
vm.recordLogs();
vm.startBroadcast(broadcaster);
OwnerToken ownerToken = new OwnerToken(
ownerTokenConfig.name,
ownerTokenConfig.symbol,
ownerTokenConfig.baseURI,
masterTokenConfig.name,
masterTokenConfig.symbol,
masterTokenConfig.baseURI,
ownerTokenConfig.signerPublicKey
);
// Need to retrieve master token address from logs as
// we can't access it otherwise
Vm.Log[] memory entries = vm.getRecordedLogs();
address masterTokenAddress = abi.decode(entries[0].data, (address));
MasterToken masterToken = MasterToken(masterTokenAddress);
vm.stopBroadcast();
return (ownerToken, masterToken, deploymentConfig);
}
}

View File

@ -0,0 +1,51 @@
//// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.17;
import { Script } from "forge-std/Script.sol";
contract DeploymentConfig is Script {
error DeploymentConfig_InvalidDeployerAddress();
struct TokenConfig {
string name;
string symbol;
string baseURI;
bytes signerPublicKey;
}
TokenConfig public ownerTokenConfig;
TokenConfig public masterTokenConfig;
address public immutable deployer;
constructor(address _broadcaster) {
if (block.chainid == 31_337) {
(ownerTokenConfig, masterTokenConfig) = getOrCreateAnvilEthConfig();
} else {
revert("no network config for this chain");
}
if (_broadcaster == address(0)) revert DeploymentConfig_InvalidDeployerAddress();
deployer = _broadcaster;
}
function getOrCreateAnvilEthConfig() public pure returns (TokenConfig memory, TokenConfig memory) {
TokenConfig memory _ownerTokenConfig = TokenConfig({
name: "Owner",
symbol: "OWNR",
baseURI: "http://local.owner",
signerPublicKey: bytes("some public key")
});
TokenConfig memory _masterTokenConfig =
TokenConfig({ name: "Master", symbol: "MSTR", baseURI: "http://local.master", signerPublicKey: "" });
return (_ownerTokenConfig, _masterTokenConfig);
}
function getOwnerTokenConfig() public view returns (TokenConfig memory) {
return ownerTokenConfig;
}
function getMasterTokenConfig() public view returns (TokenConfig memory) {
return masterTokenConfig;
}
}

View File

@ -1,18 +0,0 @@
import { ethers } from "hardhat";
import { pn } from "./utils";
async function main() {
const CommunityERC20 = await ethers.getContractFactory("CommunityERC20");
const contract = await CommunityERC20.deploy("Test", "TEST", 100);
const instance = await contract.deployed();
const tx = instance.deployTransaction;
const rec = await tx.wait();
console.log("CommunityERC20 deployed. Gas used:", pn(rec.gasUsed));
}
// We recommend this pattern to be able to use async/await everywhere
// and properly handle errors.
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});

View File

@ -1,36 +0,0 @@
import { ethers } from "hardhat";
import { pn } from "./utils";
async function main() {
const TestToken = await ethers.getContractFactory("Currency");
const testToken = await TestToken.deploy();
const ownerTokenAddress = testToken.address;
const masterTokenAddress = testToken.address;
const CollectibleV1 = await ethers.getContractFactory("CollectibleV1");
const contract = await CollectibleV1.deploy(
"Test",
"TEST",
100,
true,
true,
"http://local.dev",
ownerTokenAddress,
masterTokenAddress
);
const instance = await contract.deployed();
const tx = instance.deployTransaction;
const rec = await tx.wait();
console.log(
`CollectibleV1 deployed at ${instance.address}. Gas used: ${pn(
rec.gasUsed
)}`
);
}
// We recommend this pattern to be able to use async/await everywhere
// and properly handle errors.
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});

View File

@ -1,41 +0,0 @@
import { ethers } from "hardhat";
import { pn } from "./utils";
async function main() {
const [deployer] = await ethers.getSigners();
const CollectibleV1 = await ethers.getContractFactory("OwnerToken");
const contract = await CollectibleV1.deploy(
"Test",
"TEST",
"http://local.dev",
"Test 2",
"TEST 2",
"http://local2.dev",
"0x12345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678"
);
const instance = await contract.deployed();
const tx = instance.deployTransaction;
const rec = await tx.wait();
console.log(`OwnerToken deployed at ${instance.address}`);
console.log(`Gas used: ${pn(rec.gasUsed)}`);
console.log("Master token deployed at", rec.events[0].args.masterToken);
const mt = await ethers.getContractAt(
"MasterToken",
rec.events[0].args.masterToken
);
console.log("Name:", await instance.name());
console.log("Max Supply:", (await instance.maxSupply()).toString());
console.log("Total Supply:", (await instance.totalSupply()).toString());
console.log(
"Deployer balance:",
(await instance.balanceOf(deployer.address)).toString()
);
}
// We recommend this pattern to be able to use async/await everywhere
// and properly handle errors.
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});

9
slither.config.json Normal file
View File

@ -0,0 +1,9 @@
{
"detectors_to_exclude": "naming-convention,reentrancy-events,solc-version,timestamp",
"filter_paths": "(lib|test)",
"solc_remaps": [
"@openzeppelin/contracts=lib/openzeppelin-contracts/contracts/",
"forge-std/=lib/forge-std/src/"
]
}

View File

@ -9,9 +9,9 @@ then
fi
certoraRun \
./contracts/mvp/CollectibleV1.sol \
--verify CollectibleV1:./specs/mvp/CollectibleV1.spec \
--packages @openzeppelin=node_modules/@openzeppelin \
./contracts/CollectibleV1.sol \
--verify CollectibleV1:./specs/CollectibleV1.spec \
--packages @openzeppelin=lib/openzeppelin-contracts \
--optimistic_loop \
--loop_iter 3 \
--rule_sanity \

View File

@ -1,85 +0,0 @@
import { time, loadFixture } from "@nomicfoundation/hardhat-network-helpers";
import { anyValue } from "@nomicfoundation/hardhat-chai-matchers/withArgs";
import { expect } from "chai";
import { ethers } from "hardhat";
import { Currency } from "../typechain-types/contracts/mocks/Currency";
import { BuyableSoulbound } from "../typechain-types/contracts/BuyableSoulbound";
import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers";
import {
BN,
constants,
expectEvent,
expectRevert,
} from "@openzeppelin/test-helpers";
const tokenUtils = {
fromUnit: (amount: number) => new BN(amount).mul(new BN(10).pow(new BN(18)))
};
describe("BuyableSoulbound", function () {
describe("deployment", function () {
let owner: SignerWithAddress;
let accounts: SignerWithAddress[];
let beneficiary: SignerWithAddress;
let currency: Currency;
let token: BuyableSoulbound;
beforeEach(async () => {
[owner, beneficiary, ...accounts] = await ethers.getSigners();
const Currency = await ethers.getContractFactory("Currency");
currency = await Currency.deploy();
const BuyableSoulbound = await ethers.getContractFactory("BuyableSoulbound");
token = await BuyableSoulbound.deploy(
"Test Soulbound",
"SOUL",
"http://test.local",
beneficiary.address,
currency.address,
tokenUtils.fromUnit(10).toString()
);
});
it("deploys with right attributes", async () => {
expect(await token.name()).to.equal("Test Soulbound");
expect(await token.symbol()).to.equal("SOUL");
expect(await token.beneficiary()).to.equal(beneficiary.address);
expect(await token.paymentToken()).to.equal(currency.address);
expect(await token.tokenPrice()).to.equal("10000000000000000000");
});
const transferCurrency = async (account: SignerWithAddress, amount: any) => {
await currency.mint(account.address, amount.toString());
};
const approveCurrency = async (owner: SignerWithAddress, spender: string, amount: any) => {
await currency.connect(owner).approve(spender, amount.toString());
};
it("allows users to mint paying with Currency", async () => {
const a = accounts[0];
await transferCurrency(a, tokenUtils.fromUnit(10));
await approveCurrency(a, token.address, tokenUtils.fromUnit(10));
await token.connect(a).mint();
});
it("fails with not enough allowance", async () => {
const a = accounts[0];
await transferCurrency(a, tokenUtils.fromUnit(10));
await approveCurrency(a, token.address, tokenUtils.fromUnit(5));
await expect(
token.connect(a).mint()
).to.be.revertedWith("ERC20: insufficient allowance");
});
it("fails with not enough balance", async () => {
const a = accounts[0];
await transferCurrency(a, tokenUtils.fromUnit(5));
await approveCurrency(a, token.address, tokenUtils.fromUnit(10));
await expect(
token.connect(a).mint()
).to.be.revertedWith("ERC20: transfer amount exceeds balance");
});
});
});

View File

@ -1,85 +0,0 @@
import { time, loadFixture } from "@nomicfoundation/hardhat-network-helpers";
import { anyValue } from "@nomicfoundation/hardhat-chai-matchers/withArgs";
import { expect } from "chai";
import { ethers } from "hardhat";
import { Currency } from "../typechain-types/contracts/mocks/Currency";
import { BuyableSoulbound } from "../typechain-types/contracts/BuyableSoulbound";
import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers";
import {
BN,
constants,
expectEvent,
expectRevert,
} from "@openzeppelin/test-helpers";
const tokenUtils = {
fromUnit: (amount: number) => new BN(amount).mul(new BN(10).pow(new BN(18)))
};
describe("BuyableSoulbound", function () {
describe("deployment", function () {
let owner: SignerWithAddress;
let accounts: SignerWithAddress[];
let beneficiary: SignerWithAddress;
let currency: Currency;
let token: BuyableSoulbound;
beforeEach(async () => {
[owner, beneficiary, ...accounts] = await ethers.getSigners();
const Currency = await ethers.getContractFactory("Currency");
currency = await Currency.deploy();
const BuyableSoulbound = await ethers.getContractFactory("BuyableSoulbound");
token = await BuyableSoulbound.deploy(
"Test Soulbound",
"SOUL",
"http://test.local",
beneficiary.address,
currency.address,
tokenUtils.fromUnit(10).toString()
);
});
it("deploys with right attributes", async () => {
expect(await token.name()).to.equal("Test Soulbound");
expect(await token.symbol()).to.equal("SOUL");
expect(await token.beneficiary()).to.equal(beneficiary.address);
expect(await token.paymentToken()).to.equal(currency.address);
expect(await token.tokenPrice()).to.equal("10000000000000000000");
});
const transferCurrency = async (account: SignerWithAddress, amount: any) => {
await currency.mint(account.address, amount.toString());
};
const approveCurrency = async (owner: SignerWithAddress, spender: string, amount: any) => {
await currency.connect(owner).approve(spender, amount.toString());
};
it("allows users to mint paying with Currency", async () => {
const a = accounts[0];
await transferCurrency(a, tokenUtils.fromUnit(10));
await approveCurrency(a, token.address, tokenUtils.fromUnit(10));
await token.connect(a).mint();
});
it("fails with not enough allowance", async () => {
const a = accounts[0];
await transferCurrency(a, tokenUtils.fromUnit(10));
await approveCurrency(a, token.address, tokenUtils.fromUnit(5));
await expect(
token.connect(a).mint()
).to.be.revertedWith("ERC20: insufficient allowance");
});
it("fails with not enough balance", async () => {
const a = accounts[0];
await transferCurrency(a, tokenUtils.fromUnit(5));
await approveCurrency(a, token.address, tokenUtils.fromUnit(10));
await expect(
token.connect(a).mint()
).to.be.revertedWith("ERC20: transfer amount exceeds balance");
});
});
});

View File

@ -1,85 +0,0 @@
import { time, loadFixture } from "@nomicfoundation/hardhat-network-helpers";
import { anyValue } from "@nomicfoundation/hardhat-chai-matchers/withArgs";
import { expect } from "chai";
import { ethers } from "hardhat";
import { Currency } from "../typechain-types/contracts/mocks/Currency";
import { BuyableSoulbound } from "../typechain-types/contracts/BuyableSoulbound";
import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers";
import {
BN,
constants,
expectEvent,
expectRevert,
} from "@openzeppelin/test-helpers";
const tokenUtils = {
fromUnit: (amount: number) => new BN(amount).mul(new BN(10).pow(new BN(18)))
};
describe("BuyableSoulbound", function () {
describe("deployment", function () {
let owner: SignerWithAddress;
let accounts: SignerWithAddress[];
let beneficiary: SignerWithAddress;
let currency: Currency;
let token: BuyableSoulbound;
beforeEach(async () => {
[owner, beneficiary, ...accounts] = await ethers.getSigners();
const Currency = await ethers.getContractFactory("Currency");
currency = await Currency.deploy();
const BuyableSoulbound = await ethers.getContractFactory("BuyableSoulbound");
token = await BuyableSoulbound.deploy(
"Test Soulbound",
"SOUL",
"http://test.local",
beneficiary.address,
currency.address,
tokenUtils.fromUnit(10).toString()
);
});
it("deploys with right attributes", async () => {
expect(await token.name()).to.equal("Test Soulbound");
expect(await token.symbol()).to.equal("SOUL");
expect(await token.beneficiary()).to.equal(beneficiary.address);
expect(await token.paymentToken()).to.equal(currency.address);
expect(await token.tokenPrice()).to.equal("10000000000000000000");
});
const transferCurrency = async (account: SignerWithAddress, amount: any) => {
await currency.mint(account.address, amount.toString());
};
const approveCurrency = async (owner: SignerWithAddress, spender: string, amount: any) => {
await currency.connect(owner).approve(spender, amount.toString());
};
it("allows users to mint paying with Currency", async () => {
const a = accounts[0];
await transferCurrency(a, tokenUtils.fromUnit(10));
await approveCurrency(a, token.address, tokenUtils.fromUnit(10));
await token.connect(a).mint();
});
it("fails with not enough allowance", async () => {
const a = accounts[0];
await transferCurrency(a, tokenUtils.fromUnit(10));
await approveCurrency(a, token.address, tokenUtils.fromUnit(5));
await expect(
token.connect(a).mint()
).to.be.revertedWith("ERC20: insufficient allowance");
});
it("fails with not enough balance", async () => {
const a = accounts[0];
await transferCurrency(a, tokenUtils.fromUnit(5));
await approveCurrency(a, token.address, tokenUtils.fromUnit(10));
await expect(
token.connect(a).mint()
).to.be.revertedWith("ERC20: transfer amount exceeds balance");
});
});
});

117
test/CollectibleV1.t.sol Normal file
View File

@ -0,0 +1,117 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.17;
import { Test } from "forge-std/Test.sol";
import { DeployOwnerToken } from "../script/DeployOwnerToken.s.sol";
import { DeploymentConfig } from "../script/DeploymentConfig.s.sol";
import { OwnerToken } from "../contracts/OwnerToken.sol";
import { MasterToken } from "../contracts/MasterToken.sol";
import { CollectibleV1 } from "../contracts/CollectibleV1.sol";
contract CollectibleV1Test is Test {
CollectibleV1 internal collectibleV1;
address internal deployer;
address[] internal accounts = new address[](4);
string internal name = "Test";
string internal symbol = "TEST";
string internal baseURI = "http://local.dev";
uint256 internal maxSupply = 4;
bool internal remoteBurnable = true;
bool internal transferable = true;
function setUp() public virtual {
DeployOwnerToken deployment = new DeployOwnerToken();
(OwnerToken ownerToken, MasterToken masterToken, DeploymentConfig deploymentConfig) = deployment.run();
deployer = deploymentConfig.deployer();
collectibleV1 = new CollectibleV1(
name,
symbol,
maxSupply,
remoteBurnable,
transferable,
baseURI,
address(ownerToken),
address(masterToken)
);
accounts[0] = makeAddr("one");
accounts[1] = makeAddr("two");
accounts[2] = makeAddr("three");
accounts[3] = makeAddr("four");
}
function test_Deployment() public {
assertEq(collectibleV1.name(), name);
assertEq(collectibleV1.symbol(), symbol);
assertEq(collectibleV1.maxSupply(), maxSupply);
assertEq(collectibleV1.remoteBurnable(), remoteBurnable);
assertEq(collectibleV1.transferable(), transferable);
assertEq(collectibleV1.baseTokenURI(), baseURI);
}
}
contract MintToTest is CollectibleV1Test {
function setUp() public virtual override {
CollectibleV1Test.setUp();
}
function test_RevertWhen_SenderIsNotOwner() public {
vm.expectRevert(bytes("Not authorized"));
collectibleV1.mintTo(accounts);
}
function test_RevertWhen_MaxSupplyIsReached() public {
vm.startPrank(deployer);
collectibleV1.mintTo(accounts);
address[] memory otherAddresses = new address[](1);
otherAddresses[0] = makeAddr("anotherAccount");
vm.expectRevert(bytes("MAX_SUPPLY_REACHED"));
collectibleV1.mintTo(otherAddresses);
assertEq(collectibleV1.maxSupply(), maxSupply);
assertEq(collectibleV1.totalSupply(), maxSupply);
}
function test_MintTo() public {
uint256 length = accounts.length;
for (uint8 i = 0; i < length; i++) {
assertEq(collectibleV1.balanceOf(accounts[i]), 0);
}
vm.prank(deployer);
collectibleV1.mintTo(accounts);
for (uint8 i = 0; i < length; i++) {
assertEq(collectibleV1.balanceOf(accounts[i]), 1);
}
}
}
contract RemoteBurnTest is CollectibleV1Test {
function setUp() public virtual override {
CollectibleV1Test.setUp();
}
function test_RevertWhen_SenderIsNotOwner() public {
uint256[] memory ids = new uint256[](1);
ids[0] = 0;
vm.expectRevert(bytes("Not authorized"));
collectibleV1.remoteBurn(ids);
}
function test_RemoteBurn() public {
vm.startPrank(deployer);
collectibleV1.mintTo(accounts);
assertEq(collectibleV1.totalSupply(), maxSupply);
uint256[] memory ids = new uint256[](1);
ids[0] = 0;
collectibleV1.remoteBurn(ids);
assertEq(collectibleV1.balanceOf(accounts[0]), 0);
assertEq(collectibleV1.totalSupply(), maxSupply - 1);
}
}

94
test/CommunityERC20.t.sol Normal file
View File

@ -0,0 +1,94 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.17;
import { Test } from "forge-std/Test.sol";
import { DeployOwnerToken } from "../script/DeployOwnerToken.s.sol";
import { DeploymentConfig } from "../script/DeploymentConfig.s.sol";
import { CommunityERC20 } from "../contracts/CommunityERC20.sol";
contract CommunityERC20Test is Test {
CommunityERC20 internal communityToken;
address internal deployer;
address[] internal accounts = new address[](4);
string internal name = "Test";
string internal symbol = "TEST";
uint256 internal maxSupply = 100;
uint8 internal decimals = 18;
function setUp() public virtual {
DeployOwnerToken deployment = new DeployOwnerToken();
(,, DeploymentConfig deploymentConfig) = deployment.run();
deployer = deploymentConfig.deployer();
communityToken = new CommunityERC20(name, symbol, decimals, maxSupply);
accounts[0] = makeAddr("one");
accounts[1] = makeAddr("two");
accounts[2] = makeAddr("three");
accounts[3] = makeAddr("four");
}
function test_Deployment() public {
assertEq(communityToken.name(), name);
assertEq(communityToken.symbol(), symbol);
assertEq(communityToken.maxSupply(), maxSupply);
assertEq(communityToken.decimals(), decimals);
}
}
contract SetMaxSupplyTest is CommunityERC20Test {
function setUp() public virtual override {
CommunityERC20Test.setUp();
}
function test_RevertWhen_SenderIsNotOwner() public {
vm.prank(makeAddr("notOwner"));
vm.expectRevert(bytes("Ownable: caller is not the owner"));
communityToken.setMaxSupply(1000);
}
function test_RevertWhen_MaxSupplyLowerThanTotalSupply() public {
uint256[] memory amounts = new uint256[](4);
amounts[0] = 10;
amounts[1] = 15;
amounts[2] = 5;
amounts[3] = 20;
communityToken.mintTo(accounts, amounts); // totalSupply is now 50
vm.expectRevert(bytes("MAX_SUPPLY_LOWER_THAN_TOTAL_SUPPLY"));
communityToken.setMaxSupply(40);
}
function test_SetMaxSupply() public {
communityToken.setMaxSupply(1000);
assertEq(communityToken.maxSupply(), 1000);
}
}
contract MintToTest is CommunityERC20Test {
function setUp() public virtual override {
CommunityERC20Test.setUp();
}
function test_RevertWhen_AddressesAndAmountsAreNotEqualLength() public {
uint256[] memory amounts = new uint256[](3);
amounts[0] = 10;
amounts[1] = 15;
amounts[2] = 5;
vm.expectRevert(bytes("WRONG_LENGTHS"));
communityToken.mintTo(accounts, amounts);
}
function test_RevertWhen_MaxSupplyReached() public {
uint256[] memory amounts = new uint256[](4);
amounts[0] = 50;
amounts[1] = 25;
amounts[2] = 25;
amounts[3] = 1; // this should exceed max supply
vm.expectRevert(bytes("MAX_SUPPLY_REACHED"));
communityToken.mintTo(accounts, amounts);
}
}

101
test/OwnerToken.t.sol Normal file
View File

@ -0,0 +1,101 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.17;
import { Test } from "forge-std/Test.sol";
import { DeployOwnerToken } from "../script/DeployOwnerToken.s.sol";
import { DeploymentConfig } from "../script/DeploymentConfig.s.sol";
import { OwnerToken } from "../contracts/OwnerToken.sol";
import { MasterToken } from "../contracts/MasterToken.sol";
contract OwnerTokenTest is Test {
OwnerToken internal ownerToken;
MasterToken internal masterToken;
DeploymentConfig internal deploymentConfig;
address internal deployer;
function setUp() public virtual {
DeployOwnerToken deployment = new DeployOwnerToken();
(ownerToken, masterToken, deploymentConfig) = deployment.run();
deployer = deploymentConfig.deployer();
}
function test_Deployment() public {
DeploymentConfig.TokenConfig memory ownerTokenConfig = deploymentConfig.getOwnerTokenConfig();
DeploymentConfig.TokenConfig memory masterTokenConfig = deploymentConfig.getMasterTokenConfig();
assertEq(ownerToken.name(), ownerTokenConfig.name);
assertEq(ownerToken.symbol(), ownerTokenConfig.symbol);
assertEq(ownerToken.baseTokenURI(), ownerTokenConfig.baseURI);
assertEq(ownerToken.signerPublicKey(), ownerTokenConfig.signerPublicKey);
assertEq(ownerToken.remoteBurnable(), false);
assertEq(ownerToken.transferable(), true);
assertEq(masterToken.name(), masterTokenConfig.name);
assertEq(masterToken.symbol(), masterTokenConfig.symbol);
assertEq(masterToken.baseTokenURI(), masterTokenConfig.baseURI);
}
}
contract SetMaxSupplyTest is OwnerTokenTest {
function setUp() public virtual override {
OwnerTokenTest.setUp();
}
function test_RevertWhen_SenderIsNotOwner() public {
vm.expectRevert(bytes("Not authorized"));
ownerToken.setMaxSupply(1000);
}
function test_RevertWhen_CalledBecauseMaxSupplyIsLocked() public {
vm.startPrank(deployer);
vm.expectRevert(bytes("max supply locked"));
ownerToken.setMaxSupply(1000);
}
}
contract SetSignerPublicKeyTest is OwnerTokenTest {
function setUp() public virtual override {
OwnerTokenTest.setUp();
}
function test_RevertWhen_SenderIsNotOwner() public {
vm.expectRevert(bytes("Not authorized"));
ownerToken.setSignerPublicKey(bytes("some key"));
}
function test_SetSignerPublicKey() public {
vm.startPrank(deployer);
ownerToken.setSignerPublicKey(bytes("some key"));
assertEq(ownerToken.signerPublicKey(), bytes("some key"));
}
}
contract MintToTest is OwnerTokenTest {
function setUp() public virtual override {
OwnerTokenTest.setUp();
}
function test_RevertWhen_MaxSupplyIsReached() public {
address[] memory accounts = new address[](1);
accounts[0] = makeAddr("anotherAccount");
vm.startPrank(deployer);
vm.expectRevert(bytes("MAX_SUPPLY_REACHED"));
ownerToken.mintTo(accounts);
}
}
contract RemoteBurnTest is OwnerTokenTest {
function setUp() public virtual override {
OwnerTokenTest.setUp();
}
function test_RevertWhen_RemoteBurn() public {
vm.startPrank(deployer);
vm.expectRevert(bytes("NOT_REMOTE_BURNABLE"));
uint256[] memory ids = new uint256[](1);
ids[0] = 0;
ownerToken.remoteBurn(ids);
}
}

View File

@ -1,121 +0,0 @@
import { time, loadFixture } from "@nomicfoundation/hardhat-network-helpers";
import { anyValue } from "@nomicfoundation/hardhat-chai-matchers/withArgs";
import { expect } from "chai";
import { ethers } from "hardhat";
import { CollectibleV1 } from "../../typechain-types/contracts/mvp/CollectibleV1";
import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers";
import {
BN,
constants,
expectEvent,
expectRevert,
} from "@openzeppelin/test-helpers";
const tokenUtils = {
fromUnit: (amount: number) => new BN(amount).mul(new BN(10).pow(new BN(18)))
};
describe("CollectibleV1", function () {
describe("deployment", function () {
let owner: SignerWithAddress;
let accounts: SignerWithAddress[];
let token: CollectibleV1;
let maxSupply: typeof BN;
before(async () => {
[owner, ...accounts] = await ethers.getSigners();
const CollectibleV1 = await ethers.getContractFactory("CollectibleV1");
maxSupply = new BN(2).pow(new BN(256)).sub(new BN(1));
token = await CollectibleV1.deploy(
"Test Token",
"TEST",
maxSupply.toString(),
false,
true,
"http://test.local",
);
});
it("deploys with right attributes", async () => {
expect(await token.name()).to.equal("Test Token");
expect(await token.symbol()).to.equal("TEST");
expect(await token.maxSupply()).to.equal(maxSupply);
expect(await token.remoteBurnable()).to.equal(false);
expect(await token.transferable()).to.equal(true);
expect(await token.baseTokenURI()).to.equal("http://test.local");
});
it("normal user cannot mint", async () => {
const a = accounts[0];
await expect(token.connect(a).mintTo([a.address])).to.be.revertedWith(
"Ownable: caller is not the owner"
);
});
it("owner can mint", async () => {
const addresses = accounts.map((a) => a.address);
for (let i = 0; i < addresses.length; i++) {
expect(await token.balanceOf(addresses[i])).to.equal("0");
}
await token.connect(owner).mintTo(addresses);
for (let i = 0; i < addresses.length; i++) {
expect(await token.balanceOf(addresses[i])).to.equal("1");
}
});
it("owner can mint until max supply is reached", async () => {
const CollectibleV1 = await ethers.getContractFactory("CollectibleV1");
const maxSupply = BN(4);
const token = await CollectibleV1.deploy(
"Test Token",
"TEST",
maxSupply.toString(),
true,
true,
"http://test.local"
);
await token.connect(owner).mintTo([accounts[0].address]);
await token.connect(owner).mintTo([accounts[1].address]);
await token
.connect(owner)
.mintTo([accounts[2].address, accounts[3].address]);
await expect(
token.connect(owner).mintTo([accounts[4].address])
).to.be.revertedWith("MAX_SUPPLY_REACHED");
expect(await token.maxSupply()).to.equal(4);
expect(await token.totalSupply()).to.equal(4);
});
it("normal user cannot burn", async () => {
const a = accounts[0];
await expect(token.connect(a).remoteBurn([0])).to.be.revertedWith(
"Ownable: caller is not the owner"
);
});
// it("fails with not enough allowance", async () => {
// const a = accounts[0];
// await transferCurrency(a, tokenUtils.fromUnit(10));
// await approveCurrency(a, token.address, tokenUtils.fromUnit(5));
// await expect(
// token.connect(a).mint()
// ).to.be.revertedWith("ERC20: insufficient allowance");
// });
// it("fails with not enough balance", async () => {
// const a = accounts[0];
// await transferCurrency(a, tokenUtils.fromUnit(5));
// await approveCurrency(a, token.address, tokenUtils.fromUnit(10));
// await expect(
// token.connect(a).mint()
// ).to.be.revertedWith("ERC20: transfer amount exceeds balance");
// });
});
});

View File

@ -1,106 +0,0 @@
import { time, loadFixture } from "@nomicfoundation/hardhat-network-helpers";
import { anyValue } from "@nomicfoundation/hardhat-chai-matchers/withArgs";
import { expect } from "chai";
import { ethers } from "hardhat";
import { CollectibleV1 } from "../../typechain-types/contracts/mvp/CollectibleV1";
import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers";
// import BigNumber from "bignumber.js";
import {
constants,
expectEvent,
expectRevert,
} from "@openzeppelin/test-helpers";
const BN = (n: string | number) => ethers.BigNumber.from(n.toString());
const tokenUtils = {
fromUnit: (amount: number) => BN(amount).mul(BN(10).pow(BN(18))),
};
describe("CollectibleV1", function () {
describe("deployment", function () {
let owner: SignerWithAddress;
let accounts: SignerWithAddress[];
let token: CollectibleV1;
let maxSupply: ReturnType<typeof BN>;
before(async () => {
[owner, ...accounts] = await ethers.getSigners();
const CollectibleV1 = await ethers.getContractFactory("CollectibleV1");
maxSupply = BN(2).pow(BN(256)).sub(BN(1));
token = await CollectibleV1.deploy(
"Test Token",
"TEST",
maxSupply.toString(),
true,
true,
"http://test.local"
);
});
it("deploys with right attributes", async () => {
expect(await token.name()).to.equal("Test Token");
expect(await token.symbol()).to.equal("TEST");
expect(await token.maxSupply()).to.equal(maxSupply);
expect(await token.remoteBurnable()).to.equal(true);
expect(await token.transferable()).to.equal(true);
expect(await token.baseTokenURI()).to.equal("http://test.local");
});
it("normal user cannot mint", async () => {
const a = accounts[0];
await expect(token.connect(a).mintTo([a.address])).to.be.revertedWith(
"Ownable: caller is not the owner"
);
});
it("owner can mint", async () => {
const addresses = accounts.map((a) => a.address);
for (let i = 0; i < addresses.length; i++) {
expect(await token.balanceOf(addresses[i])).to.equal("0");
}
await token.connect(owner).mintTo(addresses);
for (let i = 0; i < addresses.length; i++) {
expect(await token.balanceOf(addresses[i])).to.equal("1");
}
});
it("normal user cannot burn", async () => {
const a = accounts[0];
await expect(token.connect(a).remoteBurn([0])).to.be.revertedWith(
"Ownable: caller is not the owner"
);
});
it("owner can remote burn", async () => {
const totalSupplyBefore = await token.totalSupply();
await token.connect(owner).remoteBurn([0]);
const totalSupplyAfter = await token.totalSupply();
expect(totalSupplyAfter.toString()).to.equal(
totalSupplyBefore.sub(BN(1))
);
});
// it("fails with not enough allowance", async () => {
// const a = accounts[0];
// await transferCurrency(a, tokenUtils.fromUnit(10));
// await approveCurrency(a, token.address, tokenUtils.fromUnit(5));
// await expect(
// token.connect(a).mint()
// ).to.be.revertedWith("ERC20: insufficient allowance");
// });
// it("fails with not enough balance", async () => {
// const a = accounts[0];
// await transferCurrency(a, tokenUtils.fromUnit(5));
// await approveCurrency(a, token.address, tokenUtils.fromUnit(10));
// await expect(
// token.connect(a).mint()
// ).to.be.revertedWith("ERC20: transfer amount exceeds balance");
// });
});
});

View File

@ -1 +0,0 @@
declare module '@openzeppelin/test-helpers';

View File

@ -1,10 +0,0 @@
{
"compilerOptions": {
"target": "es2020",
"module": "commonjs",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true
}
}

7489
yarn.lock

File diff suppressed because it is too large Load Diff