feat: fleek erc721 (#5)

* wip: refactor on current nft contract

* wip: FleekERC721 contract

* refactor: FleekERC721

* feat: add token controller control functions

* fix: sintax wise issues for compilation

* Fix errors and make the contract environment ready for compiling.

* Remove fleekbuilds.sol & update erc721 with a fix

* Update config files.

* fix: working deploy

* Make set methods public, add comments and notes to clarify issues.

* Update package.json to add the deploy script & remove package-lock.json from the repository

* Add two deploy scripts for local and mumbai deployments, update hardhat config to match two types and package.json

* Update TokenURI and the metadata struct

* Update deploy script and package.json to match the mumbai deploy script

* Add setTokenName, setTokenDescription, setTokenImage

* Add events to all set functions

* Foundry init configs

* Add foundry tests init (name, symbol, placeholder functions)

* test: hardhat (#21)

* chore: update hardhat config

* test: add FleekERC721 tests and remove not used SitesNFTs suit

* test: verify ERC721 compatibility

* Content type on second abi.encodePacked call in tokenURI

* Fix abi encoding on tokenURI

* chore: update hardhat config

* test: add FleekERC721 tests and remove not used SitesNFTs suit

* test: verify ERC721 compatibility

* Content type on second abi.encodePacked call in tokenURI

* test: improve assertion using deep equality

* chore: remove 0.4.24 version from hardhat compilers

* refactor: clear empty bytes from bytes32

* refactor: change properties from bytes32 to string

Co-authored-by: janison <jsonsivar@gmail.com>

* feat: add interaction scripts

* feat: add function signature to remove token controllers on transfer functions

* Update test commands & add forge-cache and out to .gitignore

* refactor: change token controller role validation to _beforeTokenTransfer function

* refactor: remove upgradeTokenBuild and fix burn requirement

* refactor: add isTokenController and move _clearTokenControllers to FleekAccessControl contract

* refactor: remove localhost and wrong mumbai deployments

* refactor: rename polygonMumbai to mumbai

* refactor: remove twiced name on gitignore

* chore: mumbai deployments

* refactor: util script to get contract using hardhat defined network

* chore: move forge-std as a submodule

* chore: move forge-std as a submodule

Co-authored-by: EmperorOrokuSaki <artie.eth@gmail.com>
Co-authored-by: daltoncoder <71679972+daltoncoder@users.noreply.github.com>
Co-authored-by: janison <jsonsivar@gmail.com>
This commit is contained in:
Felipe Mendes 2022-12-12 16:56:17 -03:00 committed by GitHub
parent 315672243b
commit 94c364836e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
34 changed files with 3482 additions and 5139 deletions

3
.env.example Normal file
View File

@ -0,0 +1,3 @@
API_URL=https://matic-mumbai.chainstacklabs.com/
PRIVATE_KEY =0x
POLYSCAN_API=

10
.gitignore vendored
View File

@ -3,4 +3,12 @@ node_modules
# hardhat
cache
artifacts
artifacts
deployments/localhost
# NPM
package-lock.json
# Foundry
out
forge-cache

3
.gitmodules vendored Normal file
View File

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

View File

@ -1,31 +0,0 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;
import "../interfaces/IFleek.sol";
import "./FleekBuilds.sol";
import "./FleekAccessControl.sol";
abstract contract Fleek is IFleek, FleekBuilds {
string name;
string description;
constructor(string memory _name, string memory _description) {
name = _name;
description = _description;
}
function setName(
string calldata _name
) external override requireController {
name = _name;
emit MetadataUpdated(name, description);
}
function setDescription(
string calldata _description
) external override requireController {
description = _description;
emit MetadataUpdated(name, description);
}
}

View File

@ -2,33 +2,59 @@
pragma solidity ^0.8.7;
import "../interfaces/IFleekSite.sol";
import "@openzeppelin/contracts/access/AccessControl.sol";
abstract contract FleekAccessControl is AccessControl {
bytes32 public constant OWNER_ROLE = keccak256("OWNER_ROLE");
bytes32 public constant CONTROLLER_ROLE = keccak256("CONTROLLER_ROLE");
bytes32 public constant COLLECTION_OWNER_ROLE =
keccak256("COLLECTION_OWNER_ROLE");
bytes32 public constant COLLECTION_CONTROLLER_ROLE =
keccak256("COLLECTION_CONTROLLER_ROLE");
constructor() {
_setRoleAdmin(OWNER_ROLE, DEFAULT_ADMIN_ROLE);
_grantRole(OWNER_ROLE, msg.sender);
_setRoleAdmin(COLLECTION_OWNER_ROLE, DEFAULT_ADMIN_ROLE);
_grantRole(COLLECTION_OWNER_ROLE, msg.sender);
}
modifier requireOwner() {
modifier requireCollectionOwner() {
require(
hasRole(OWNER_ROLE, msg.sender),
"FleekAccessControl: must have owner role"
hasRole(COLLECTION_OWNER_ROLE, msg.sender),
"FleekAccessControl: must have collection owner role"
);
_;
}
modifier requireController() {
bool hasPermission = hasRole(CONTROLLER_ROLE, msg.sender) ||
hasRole(DEFAULT_ADMIN_ROLE, msg.sender);
modifier requireCollectionController() {
require(
hasPermission,
"FleekAccessControl: caller is not a controller"
hasRole(COLLECTION_OWNER_ROLE, msg.sender) ||
hasRole(COLLECTION_CONTROLLER_ROLE, msg.sender),
"FleekAccessControl: must have collection controller role"
);
_;
}
modifier requireTokenController(uint256 tokenId) {
require(
hasRole(_tokenRole(tokenId, "CONTROLLER"), msg.sender),
"FleekAccessControl: must have token role"
);
_;
}
function isTokenController(
uint256 tokenId,
address account
) public view returns (bool) {
return hasRole(_tokenRole(tokenId, "CONTROLLER"), account);
}
function _tokenRole(
uint256 tokenId,
string memory role
) internal pure returns (bytes32) {
return keccak256(abi.encodePacked("TOKEN_", role, tokenId));
}
function _clearTokenControllers(uint256 tokenId) internal {
// TODO: Remove token controllers from AccessControl
}
}

View File

@ -1,31 +0,0 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;
import "../interfaces/IFleekBuilds.sol";
import "./FleekAccessControl.sol";
abstract contract FleekBuilds is IFleekBuilds, FleekAccessControl {
build[] public builds;
function update(
build calldata _newBuild
) external override requireController {
builds.push(_newBuild);
emit Upgraded(_newBuild);
}
function getCurrentBuild() external view override returns (build memory) {
return builds[builds.length - 1];
}
function getBuildById(
uint256 _buildId
) external view override returns (build memory) {
return builds[_buildId];
}
function getBuilds() external view override returns (build[] memory) {
return builds;
}
}

234
contracts/FleekERC721.sol Normal file
View File

@ -0,0 +1,234 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
import "@openzeppelin/contracts/utils/Base64.sol";
import "./FleekAccessControl.sol";
contract FleekERC721 is ERC721, FleekAccessControl {
using Strings for uint256;
using Counters for Counters.Counter;
event NewBuild(uint256 indexed token, string indexed commit_hash);
event NewTokenName(uint256 indexed token, string indexed name);
event NewTokenDescription(uint256 indexed token, string indexed description);
event NewTokenImage(uint256 indexed token, string indexed image);
event NewTokenExternalURL(uint256 indexed token, string indexed external_url);
event NewTokenENS(uint256 indexed token, string indexed ENS);
struct Build {
string commit_hash;
string git_repository;
string author;
}
/**
* The properties are stored as string to keep consistency with
* other token contracts, we might consider changing for bytes32
* in the future due to gas optimization
*/
struct App {
string name; // Name of the site
string description; // Description about the site
string image; // Preview Image IPFS Link
string external_url; // Site URL
string ENS; // ENS ID
uint256 current_build; // The current build number (Increments by one with each change, starts at zero)
mapping(uint256 => Build) builds; // Mapping to build details for each build number
}
Counters.Counter private _tokenIds;
mapping(uint256 => App) private _apps;
constructor(
string memory _name,
string memory _symbol
) ERC721(_name, _symbol) {}
modifier requireTokenOwner(uint256 tokenId) {
require(
msg.sender == ownerOf(tokenId),
"FleekERC721: must be token owner"
);
_;
}
function mint(
address to,
string memory name,
string memory description,
string memory image,
string memory external_url,
string memory ENS,
string memory commit_hash,
string memory git_repository,
string memory author
) public payable requireCollectionOwner returns (uint256) {
uint256 tokenId = _tokenIds.current();
_mint(to, tokenId);
_tokenIds.increment();
App storage app = _apps[tokenId];
app.name = name;
app.description = description;
app.image = image;
app.external_url = external_url;
app.ENS = ENS;
// The mint interaction is considered to be the first build of the site. Updates from now on all increment the current_build by one and update the mapping.
app.current_build = 0;
app.builds[0] = Build(commit_hash, git_repository, author);
return tokenId;
}
function tokenURI(
uint256 tokenId
) public view virtual override returns (string memory) {
_requireMinted(tokenId);
address owner = ownerOf(tokenId);
App storage app = _apps[tokenId];
bytes memory dataURI = abi.encodePacked(
'{',
'"name":"', app.name, '",',
'"description":"', app.description, '",',
'"owner":"', Strings.toHexString(uint160(owner), 20), '",',
'"external_url":"', app.external_url, '",',
'"image":"', app.image, '",',
'"attributes": [',
'{"trait_type": "ENS", "value":"', app.ENS,'"},',
'{"trait_type": "Commit Hash", "value":"', app.builds[app.current_build].commit_hash,'"},',
'{"trait_type": "Repository", "value":"', app.builds[app.current_build].git_repository,'"},',
'{"trait_type": "Author", "value":"', app.builds[app.current_build].author,'"},',
'{"trait_type": "Version", "value":"', Strings.toString(app.current_build),'"}',
']',
'}'
);
return string(abi.encodePacked(_baseURI(), Base64.encode((dataURI))));
}
function addTokenController(
uint256 tokenId,
address controller
) public requireTokenOwner(tokenId) {
_requireMinted(tokenId);
_grantRole(_tokenRole(tokenId, "CONTROLLER"), controller);
}
function removeTokenController(
uint256 tokenId,
address controller
) public requireTokenOwner(tokenId) {
_requireMinted(tokenId);
_revokeRole(_tokenRole(tokenId, "CONTROLLER"), controller);
}
function supportsInterface(
bytes4 interfaceId
) public view virtual override(ERC721, AccessControl) returns (bool) {
return super.supportsInterface(interfaceId);
}
/**
* @dev Override of _beforeTokenTransfer of ERC721.
* Here it needs to update the token controller roles for mint, burn and transfer.
* IMPORTANT: The function for clearing token controllers is not implemented yet.
*/
function _beforeTokenTransfer(
address from,
address to,
uint256 tokenId,
uint256 batchSize
) internal virtual override {
if (from != address(0) && to != address(0)) {
// Transfer
_clearTokenControllers(tokenId);
_grantRole(_tokenRole(tokenId, "CONTROLLER"), to);
} else if (from == address(0)) {
// Mint
_grantRole(_tokenRole(tokenId, "CONTROLLER"), to);
} else if (to == address(0)) {
// Burn
_clearTokenControllers(tokenId);
}
super._beforeTokenTransfer(from, to, tokenId, batchSize);
}
function _baseURI() internal view virtual override returns (string memory) {
return "data:application/json;base64,";
}
function setTokenExternalURL(
uint256 tokenId,
string memory _tokenExternalURL
) public virtual requireTokenController(tokenId) {
_requireMinted(tokenId);
_apps[tokenId].external_url = _tokenExternalURL;
emit NewTokenExternalURL(tokenId, _tokenExternalURL);
}
function setTokenENS(
uint256 tokenId,
string memory _tokenENS
) public virtual requireTokenController(tokenId) {
_requireMinted(tokenId);
_apps[tokenId].ENS = _tokenENS;
emit NewTokenENS(tokenId, _tokenENS);
}
function setTokenName(
uint256 tokenId,
string memory _tokenName
) public virtual requireTokenController(tokenId) {
_requireMinted(tokenId);
_apps[tokenId].name = _tokenName;
emit NewTokenName(tokenId, _tokenName);
}
function setTokenDescription(
uint256 tokenId,
string memory _tokenDescription
) public virtual requireTokenController(tokenId) {
_requireMinted(tokenId);
_apps[tokenId].description = _tokenDescription;
emit NewTokenDescription(tokenId, _tokenDescription);
}
function setTokenImage(
uint256 tokenId,
string memory _tokenImage
) public virtual requireTokenController(tokenId) {
_requireMinted(tokenId);
_apps[tokenId].image = _tokenImage;
emit NewTokenImage(tokenId, _tokenImage);
}
function setTokenBuild(
uint256 tokenId,
string memory _commit_hash,
string memory _git_repository,
string memory _author
) public virtual requireTokenController(tokenId) {
_requireMinted(tokenId);
_apps[tokenId].builds[++_apps[tokenId].current_build] = Build(
_commit_hash,
_git_repository,
_author
);
emit NewBuild(tokenId, _commit_hash);
}
function burn(
uint256 tokenId
) public virtual requireTokenOwner(tokenId) {
super._burn(tokenId);
if (bytes(_apps[tokenId].external_url).length != 0) {
delete _apps[tokenId];
}
}
}

View File

@ -1,35 +0,0 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;
import "./Fleek.sol";
import "../interfaces/IFleekSite.sol";
contract FleekSite is IFleekSite, Fleek {
string thumbnail;
string external_url;
constructor(
string memory _name,
string memory _description,
string memory _thumbnail,
string memory _external_url
) Fleek(_name, _description) {
thumbnail = _thumbnail;
external_url = _external_url;
}
function setThumbnail(
string calldata _thumbnail
) external override requireController {
thumbnail = _thumbnail;
emit SiteMetadataUpdated(name, description, thumbnail, external_url);
}
function setExternalUrl(
string calldata _external_url
) external override requireController {
external_url = _external_url;
emit SiteMetadataUpdated(name, description, thumbnail, external_url);
}
}

View File

@ -1,86 +0,0 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
import "@openzeppelin/contracts/access/AccessControl.sol";
import "../interfaces/ISitesNFTs.sol";
contract SitesNFTs is ISitesNFTs, ERC721URIStorage, AccessControl {
using Counters for Counters.Counter;
Counters.Counter private _tokenIds;
string private baseURI;
bytes32 public constant MINTER_ROLE =
0x4d494e5445525f524f4c45000000000000000000000000000000000000000000; // "MINTER_ROLE"
modifier canMint() {
bool isMinterOrAdmin = hasRole(MINTER_ROLE, msg.sender) ||
hasRole(DEFAULT_ADMIN_ROLE, msg.sender);
require(isMinterOrAdmin, "Caller has no permission to mint.");
_;
}
modifier canChangeBaseURI() {
require(hasRole(DEFAULT_ADMIN_ROLE, msg.sender));
_;
}
constructor(string memory name, string memory symbol) ERC721(name, symbol) {
baseURI = "data:application/json;base64,";
_setupRole(DEFAULT_ADMIN_ROLE, msg.sender);
}
function mint(
string memory base64EncodedMetadata,
address account
) public override canMint returns (uint256) {
uint256 newItemId = _tokenIds.current();
_safeMint(account, newItemId);
_setTokenURI(newItemId, base64EncodedMetadata);
_tokenIds.increment();
return newItemId;
}
function updateTokenURI(
address tokenHolderAddress,
uint256 tokenId,
string memory base64EncodedMetadata
) public override canMint {
address tokenOwner = ownerOf(tokenId);
require(
tokenOwner == tokenHolderAddress,
"Address does not own provided tokenId"
);
_setTokenURI(tokenId, base64EncodedMetadata);
}
function supportsInterface(
bytes4 interfaceId
)
public
view
virtual
override(IERC165, ERC721, AccessControl)
returns (bool)
{
return super.supportsInterface(interfaceId);
}
function setBaseURI(
string memory _newBbaseURI
) public override canChangeBaseURI returns (string memory) {
baseURI = _newBbaseURI;
return baseURI;
}
function getCurrentTokenId() public view override returns (uint256) {
return _tokenIds.current();
}
function _baseURI() internal view override returns (string memory) {
return baseURI;
}
}

20
deploy/local_deploy.js Normal file
View File

@ -0,0 +1,20 @@
module.exports = async ({ getNamedAccounts, deployments }) => {
const { deploy, log } = deployments;
const namedAccounts = await getNamedAccounts();
const { deployer } = namedAccounts;
const deployResult = await deploy('FleekERC721', {
from: deployer,
args: ['FleekSites', 'FLKSITE'],
});
if (deployResult.newlyDeployed) {
log(
`contract FleekSites deployed at ${deployResult.address} using ${deployResult.receipt.gasUsed} gas`
);
} else {
log(`using pre-existing contract FleekSites at ${deployResult.address}`);
}
};
//You can put an array of tags below. Tags can be anything and say when a this script should be run. So you can write different scripts for local, prod or other deploys
//For example when you run 'npx hardhat --network hardhat deploy --tags local' hardhat will run all deploy scripts that have the tag local, could be multiple dif scripts
module.exports.tags = ['local'];

20
deploy/mumbai_deploy.js Normal file
View File

@ -0,0 +1,20 @@
module.exports = async ({ getNamedAccounts, deployments }) => {
const { deploy, log } = deployments;
const namedAccounts = await getNamedAccounts();
const { privateKey } = namedAccounts;
const deployResult = await deploy('FleekERC721', {
from: privateKey,
args: ['FleekSites', 'FLKSITE'],
});
if (deployResult.newlyDeployed) {
log(
`contract FleekSites deployed at ${deployResult.address} using ${deployResult.receipt.gasUsed} gas`
);
} else {
log(`using pre-existing contract FleekSites at ${deployResult.address}`);
}
};
//You can put an array of tags below. Tags can be anything and say when a this script should be run. So you can write different scripts for local, prod or other deploys
//For example when you run 'npx hardhat --network hardhat deploy --tags local' hardhat will run all deploy scripts that have the tag local, could be multiple dif scripts
module.exports.tags = ['mumbai'];

View File

@ -0,0 +1 @@
80001

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

6
foundry.toml Normal file
View File

@ -0,0 +1,6 @@
[profile.default]
src = 'contracts'
out = 'out'
libs = ['node_modules', 'lib']
test = 'test/foundry'
cache_path = 'forge-cache'

View File

@ -1,122 +0,0 @@
require('@nomiclabs/hardhat-waffle');
require('@nomiclabs/hardhat-etherscan');
require('hardhat-deploy');
require('solidity-coverage');
require('hardhat-gas-reporter');
require('hardhat-contract-sizer');
require('dotenv').config();
/**
* @type import('hardhat/config').HardhatUserConfig
*/
const MAINNET_RPC_URL =
process.env.MAINNET_RPC_URL ||
process.env.ALCHEMY_MAINNET_RPC_URL ||
'https://eth-mainnet.alchemyapi.io/v2/your-api-key';
const GOERLI_RPC_URL =
process.env.GOERLI_RPC_URL ||
'https://eth-goerli.alchemyapi.io/v2/your-api-key';
const POLYGON_MAINNET_RPC_URL =
process.env.POLYGON_MAINNET_RPC_URL ||
'https://polygon-mainnet.alchemyapi.io/v2/your-api-key';
const POLYGON_MUMBAI_RPC_URL =
process.env.POLYGON_MUMBAI_RPC_URL ||
'https://polygon-mumbai.g.alchemy.com/v2/aIjNlC4r4aLYOHrdCTFT_JUX6OJsOsu0';
const PRIVATE_KEY = process.env.PRIVATE_KEY || '0x';
// optional
const MNEMONIC = process.env.MNEMONIC || 'your mnemonic';
// Your API key for Etherscan, obtain one at https://etherscan.io/
const ETHERSCAN_API_KEY =
process.env.ETHERSCAN_API_KEY || 'Your etherscan API key';
const POLYGONSCAN_API_KEY =
process.env.POLYGONSCAN_API_KEY || 'Your polygonscan API key';
const REPORT_GAS = process.env.REPORT_GAS || false;
module.exports = {
defaultNetwork: 'hardhat',
networks: {
hardhat: {
// // If you want to do some forking, uncomment this
// forking: {
// url: MAINNET_RPC_URL
// }
chainId: 31337,
},
localhost: {
chainId: 31337,
},
// goerli: {
// url: GOERLI_RPC_URL,
// accounts: PRIVATE_KEY !== undefined ? [PRIVATE_KEY] : [],
// // accounts: {
// // mnemonic: MNEMONIC,
// // },
// saveDeployments: true,
// chainId: 5,
// },
// mainnet: {
// url: MAINNET_RPC_URL,
// accounts: PRIVATE_KEY !== undefined ? [PRIVATE_KEY] : [],
// // accounts: {
// // mnemonic: MNEMONIC,
// // },
// saveDeployments: true,
// chainId: 1,
// },
// polygon: {
// url: POLYGON_MAINNET_RPC_URL,
// accounts: PRIVATE_KEY !== undefined ? [PRIVATE_KEY] : [],
// saveDeployments: true,
// chainId: 137,
// },
// poligonMumbai: {
// url: POLYGON_MUMBAI_RPC_URL,
// accounts: PRIVATE_KEY !== undefined ? [PRIVATE_KEY] : [],
// saveDeployments: true,
// chainId: 80001,
// }
},
etherscan: {
// npx hardhat verify --network <NETWORK> <CONTRACT_ADDRESS> <CONSTRUCTOR_PARAMETERS>
apiKey: {
goerli: ETHERSCAN_API_KEY,
polygon: POLYGONSCAN_API_KEY,
},
},
gasReporter: {
enabled: REPORT_GAS,
currency: 'USD',
outputFile: 'gas-report.txt',
noColors: true,
// coinmarketcap: process.env.COINMARKETCAP_API_KEY,
},
contractSizer: {
runOnCompile: false,
only: ['NftMarketplace'],
},
namedAccounts: {
deployer: {
default: 0, // here this will by default take the first account as deployer
1: 0, // similarly on mainnet it will take the first account as deployer. Note though that depending on how hardhat network are configured, the account 0 on one network can be different than on another
},
player: {
default: 1,
},
},
solidity: {
compilers: [
{
version: '0.8.7',
},
{
version: '0.4.24',
},
],
},
mocha: {
timeout: 200000, // 200 seconds max for running tests
},
};

67
hardhat.config.ts Normal file
View File

@ -0,0 +1,67 @@
import '@nomiclabs/hardhat-ethers';
import '@nomiclabs/hardhat-web3';
import '@nomicfoundation/hardhat-chai-matchers';
import 'hardhat-deploy';
import 'solidity-coverage';
import 'hardhat-gas-reporter';
import 'hardhat-contract-sizer';
import * as dotenv from 'dotenv';
import { HardhatUserConfig } from 'hardhat/types';
dotenv.config();
const {
API_URL = 'https://polygon-mainnet.alchemyapi.io/v2/your-api-key',
PRIVATE_KEY,
REPORT_GAS,
} = process.env;
const config: HardhatUserConfig = {
defaultNetwork: 'hardhat',
networks: {
hardhat: {
chainId: 31337,
},
localhost: {
chainId: 31337,
},
mumbai: {
url: API_URL,
accounts: PRIVATE_KEY ? [PRIVATE_KEY] : [],
saveDeployments: true,
chainId: 80001,
},
},
gasReporter: {
enabled: REPORT_GAS === 'true' || false,
currency: 'USD',
outputFile: 'gas-report.txt',
noColors: true,
// coinmarketcap: process.env.COINMARKETCAP_API_KEY,
},
contractSizer: {
runOnCompile: false,
only: ['NftMarketplace'],
},
namedAccounts: {
deployer: {
default: 1, // here this will by default take the first account as deployer
1: 0,
},
privateKey: {
default: `privatekey://${PRIVATE_KEY}`,
},
},
solidity: {
compilers: [
{
version: '0.8.7',
},
],
},
mocha: {
timeout: 200000, // 200 seconds max for running tests
},
};
export default config;

View File

@ -1,14 +0,0 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;
import "./IFleekBuilds.sol";
import "@openzeppelin/contracts/access/IAccessControl.sol";
interface IFleek is IFleekBuilds, IAccessControl {
event MetadataUpdated(string name, string description);
function setName(string calldata _name) external;
function setDescription(string calldata _description) external;
}

View File

@ -1,26 +0,0 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;
interface IFleekBuilds {
struct build {
string _uri;
string _hash;
string _repo;
string _repository;
}
event InitialVersionDeploy();
event Upgraded(build _build);
function update(build calldata _newBuild) external;
function getCurrentBuild() external view returns (build memory);
function getBuildById(
uint256 _buildId
) external view returns (build memory);
function getBuilds() external view returns (build[] memory);
}

View File

@ -1,18 +0,0 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;
import "./IFleek.sol";
interface IFleekSite is IFleek {
event SiteMetadataUpdated(
string name,
string description,
string thumbnail,
string external_url
);
function setThumbnail(string calldata _thumbnail) external;
function setExternalUrl(string calldata _external_url) external;
}

View File

@ -1,27 +0,0 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;
import "@openzeppelin/contracts/interfaces/IERC721.sol";
import "@openzeppelin/contracts/access/IAccessControl.sol";
/// @title SitesNFTs - A contract for managing sites NFTs
/// @dev See
interface ISitesNFTs is IERC721 {
function mint(
string memory base64EncodedMetadata,
address account
) external returns (uint256);
function updateTokenURI(
address tokenHolderAddress,
uint256 tokenId,
string memory base64EncodedMetadata
) external;
function setBaseURI(
string memory _newBbaseURI
) external returns (string memory);
function getCurrentTokenId() external view returns (uint256);
}

1
lib/forge-std Submodule

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

View File

@ -1,37 +1,50 @@
{
"name": "sites_nfts",
"version": "1.0.0",
"name": "fleek_contracts",
"version": "0.0.1",
"description": "",
"main": "index.js",
"scripts": {
"test": "hardhat test",
"format": "prettier --write \"./**/*.{js,ts,sol}\""
"test": "hardhat test && forge test --via-ir",
"test:foundry": "forge test --via-ir",
"test:hardhat": "hardhat test",
"format": "prettier --write \"./**/*.{js,ts,sol}\"",
"node:hardhat": "hardhat node --tags local",
"deploy:local": "hardhat deploy --tags local",
"deploy:mumbai": "hardhat deploy --tags mumbai --network mumbai",
"compile": "hardhat compile"
},
"repository": {
"type": "git",
"url": "git+https://github.com/FleekHQ/sites_nfts.git"
"url": "git+https://github.com/FleekHQ/contracts.git"
},
"author": "",
"license": "ISC",
"bugs": {
"url": "https://github.com/FleekHQ/sites_nfts/issues"
"url": "https://github.com/FleekHQ/contracts/issues"
},
"homepage": "https://github.com/FleekHQ/sites_nfts#readme",
"homepage": "https://github.com/FleekHQ/contracts#readme",
"devDependencies": {
"@nomicfoundation/hardhat-chai-matchers": "^1.0.5",
"@nomicfoundation/hardhat-network-helpers": "^1.0.7",
"@nomicfoundation/hardhat-toolbox": "^2.0.0",
"@nomiclabs/hardhat-ethers": "^2.1.1",
"@nomiclabs/hardhat-ethers": "^2.2.1",
"@nomiclabs/hardhat-etherscan": "^3.1.0",
"@nomiclabs/hardhat-waffle": "^2.0.3",
"@nomiclabs/hardhat-web3": "^2.0.0",
"@openzeppelin/contracts": "^4.7.3",
"@types/mocha": "^10.0.1",
"chai": "^4.3.6",
"dotenv": "^16.0.2",
"ethereum-waffle": "^3.4.4",
"ethers": "^5.7.2",
"hardhat": "^2.11.2",
"hardhat-contract-sizer": "^2.6.1",
"hardhat-deploy": "^0.11.15",
"hardhat-gas-reporter": "^1.0.9",
"minimist": "^1.2.7",
"prettier": "^2.7.1",
"prettier-plugin-solidity": "^1.0.0",
"solidity-coverage": "^0.8.2"
"solidity-coverage": "^0.8.2",
"ts-node": "^10.9.1",
"typescript": "^4.9.3",
"web3": "^1.8.1"
}
}

2
remappings.txt Normal file
View File

@ -0,0 +1,2 @@
ds-test/=lib/forge-std/lib/ds-test/src/
forge-std/=lib/forge-std/src/

View File

@ -1,19 +0,0 @@
async function main() {
const [deployer] = await ethers.getSigners();
console.log('Deploying contracts with the account:', deployer.address);
console.log('Account balance:', (await deployer.getBalance()).toString());
const SitesNFTs = await ethers.getContractFactory('SitesNFTs');
const sitesNFTs = await SitesNFTs.deploy('Sites NFTs', 'SNFT');
console.log('SitesNFTs address:', sitesNFTs.address);
}
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});

23
scripts/mint.js Normal file
View File

@ -0,0 +1,23 @@
// npx hardhat run scripts/mint.js --network mumbai
const { getContract } = require('./util');
// TODO: make this arguments
const params = [
'0x7ED735b7095C05d78dF169F991f2b7f1A1F1A049', // to
'Fleek App', // name
'Description', // description
'https://fleek.network/fleek-network-logo-minimal.png', // image
'https://fleek.co/', // external url
'fleek.eth', // ens
'6ea6ad16c46ae85faced7e50555ff7368422f57', // commit hash
'https://github.com/org/repo', // repo
'fleek', // author
];
(async () => {
const contract = getContract('FleekERC721');
const transaction = await contract.mint(...params);
console.log('Response: ', transaction);
})();

17
scripts/tokenURI.js Normal file
View File

@ -0,0 +1,17 @@
// npx hardhat run scripts/tokenURI.js --network mumbai
const { getContract } = require('./util');
// TODO: make this arguments
const tokenId = 1;
(async () => {
const contract = await getContract('FleekERC721');
const transaction = await contract.tokenURI(tokenId);
const parsed = JSON.parse(
Buffer.from(transaction.slice(29), 'base64').toString('utf-8')
);
console.log('Response: ', parsed);
})();

18
scripts/upgrade.js Normal file
View File

@ -0,0 +1,18 @@
// npx hardhat run scripts/upgrade.js --network mumbai
const { getContract } = require('./util');
// TODO: make this arguments
const params = [
1, // tokenId
'97e7908f70f0862d753c66689ff09e70caa43df2', // commit hash
'https://github.com/org/new-repo', // repo
'new-author', // author
];
(async () => {
const contract = await getContract('FleekERC721');
const transaction = await contract.setTokenBuild(...params);
console.log('Response: ', transaction);
})();

7
scripts/util.js Normal file
View File

@ -0,0 +1,7 @@
module.exports.getContract = async function (contractName) {
const {
address,
} = require(`../deployments/${hre.network.name}/${contractName}.json`);
return hre.ethers.getContractAt(contractName, address);
};

175
test/FleekERC721.ts Normal file
View File

@ -0,0 +1,175 @@
import { loadFixture } from '@nomicfoundation/hardhat-network-helpers';
import { expect } from 'chai';
import { ethers } from 'hardhat';
import web3 from 'web3';
describe('FleekERC721', () => {
const COLLECTION_OWNER_ROLE = web3.utils.keccak256('COLLECTION_OWNER_ROLE');
const MINT_PARAMS = Object.freeze({
name: 'Fleek Test App',
description: 'Fleek Test App Description',
image: 'https://fleek.co/image.png',
ens: 'fleek.eth',
externalUrl: 'https://fleek.co',
commitHash: 'b72e47171746b6a9e29b801af9cb655ecf4d665c',
gitRepository: 'https://github.com/fleekxyz/contracts',
author: 'author',
});
const COLLECTION_PARAMS = Object.freeze({
name: 'FleekERC721',
symbol: 'FLEEK',
});
const defaultFixture = async () => {
// Contracts are deployed using the first signer/account by default
const [owner, otherAccount] = await ethers.getSigners();
const Contract = await ethers.getContractFactory('FleekERC721');
const contract = await Contract.deploy(
COLLECTION_PARAMS.name,
COLLECTION_PARAMS.symbol
);
return { owner, otherAccount, contract };
};
describe('Deployment', () => {
it('should assign the name and the symbol of the ERC721 contract', async () => {
const { contract } = await loadFixture(defaultFixture);
expect(await contract.name()).to.equal(COLLECTION_PARAMS.name);
expect(await contract.symbol()).to.equal(COLLECTION_PARAMS.symbol);
});
it('should assign the owner of the contract', async () => {
const { owner, contract } = await loadFixture(defaultFixture);
expect(
await contract.hasRole(COLLECTION_OWNER_ROLE, owner.address)
).to.equal(true);
});
it('should support ERC721 interface', async () => {
const { contract } = await loadFixture(defaultFixture);
expect(await contract.supportsInterface('0x80ac58cd')).to.equal(true);
});
});
describe('Minting', () => {
it('should be able to mint a new token', async () => {
const { owner, contract } = await loadFixture(defaultFixture);
const response = await contract.mint(
owner.address,
MINT_PARAMS.name,
MINT_PARAMS.description,
MINT_PARAMS.image,
MINT_PARAMS.externalUrl,
MINT_PARAMS.ens,
MINT_PARAMS.commitHash,
MINT_PARAMS.gitRepository,
MINT_PARAMS.author
);
expect(response.value).to.be.instanceOf(ethers.BigNumber);
expect(response.value.toNumber()).to.equal(0);
});
it('should not be able to mint a new token if not the owner', async () => {
const { otherAccount, contract } = await loadFixture(defaultFixture);
await expect(
contract
.connect(otherAccount)
.mint(
otherAccount.address,
MINT_PARAMS.name,
MINT_PARAMS.description,
MINT_PARAMS.image,
MINT_PARAMS.externalUrl,
MINT_PARAMS.ens,
MINT_PARAMS.commitHash,
MINT_PARAMS.gitRepository,
MINT_PARAMS.author
)
).to.be.revertedWith(
'FleekAccessControl: must have collection owner role'
);
});
});
describe('Token', () => {
let tokenId: number;
let fixture: Awaited<ReturnType<typeof defaultFixture>>;
before(async () => {
fixture = await loadFixture(defaultFixture);
const { contract } = fixture;
const response = await contract.mint(
fixture.owner.address,
MINT_PARAMS.name,
MINT_PARAMS.description,
MINT_PARAMS.image,
MINT_PARAMS.externalUrl,
MINT_PARAMS.ens,
MINT_PARAMS.commitHash,
MINT_PARAMS.gitRepository,
MINT_PARAMS.author
);
tokenId = response.value.toNumber();
});
it('should return the token URI', async () => {
const { contract } = fixture;
const tokenURI = await contract.tokenURI(tokenId);
const tokenURIDecoded = Buffer.from(
tokenURI.replace('data:application/json;base64,', ''),
'base64'
).toString('ascii');
const parsedURI = JSON.parse(tokenURIDecoded);
expect(parsedURI).to.eql({
owner: fixture.owner.address.toLowerCase(),
name: MINT_PARAMS.name,
description: MINT_PARAMS.description,
image: MINT_PARAMS.image,
external_url: MINT_PARAMS.externalUrl,
attributes: [
{
trait_type: 'ENS',
value: MINT_PARAMS.ens,
},
{
trait_type: 'Commit Hash',
value: MINT_PARAMS.commitHash,
},
{
trait_type: 'Repository',
value: MINT_PARAMS.gitRepository,
},
{
trait_type: 'Author',
value: MINT_PARAMS.author,
},
{
trait_type: 'Version',
value: '0',
},
],
});
});
it('should match the token owner', async () => {
const { contract, owner } = fixture;
const tokenOwner = await contract.ownerOf(tokenId);
expect(tokenOwner).to.equal(owner.address);
});
});
});

View File

@ -1,50 +0,0 @@
const { expect } = require('chai');
const { loadFixture } = require('ethereum-waffle');
const { ethers } = require('hardhat');
const hre = require('hardhat');
describe('FleekSite contract', function () {
//TODO check values are setted right on the contract
const _name = 'Fleek Site';
const _description = 'Fleek Site Description';
const _thumbnail = 'https://fleek.co';
const _externalUrl = 'https://fleek.co';
async function deploy() {
const [owner] = await hre.ethers.getSigners();
const FleekSite = await hre.ethers.getContractFactory('FleekSite');
const hardhatFleekSite = await FleekSite.deploy(
'Fleek Site',
'Fleek Site Description',
'https://fleek.co',
'https://fleek.co'
);
return { owner, hardhatFleekSite };
}
describe('Deployment', () => {
it('Deploy FleekSit contract with name Fleek Site and builds[] should be 0', async () => {
const { hardhatFleekSite } = await loadFixture(deploy);
const currentBuilds = await hardhatFleekSite.getBuilds();
expect(currentBuilds.length).to.equal(0);
});
it('Deployment should assign to OWNER_ROLE the DEFAULT_ADMIN_ROLE', async () => {
const { hardhatFleekSite } = await loadFixture(deploy);
const OWNER_ROLE = 'OWNER_ROLE';
const DEFAULT_ADMIN_ROLE_STRING = '';
const role = await hardhatFleekSite.getRoleAdmin(
ethers.utils.formatBytes32String(OWNER_ROLE)
);
expect(role).to.equal(
ethers.utils.formatBytes32String(DEFAULT_ADMIN_ROLE_STRING)
);
});
});
});

View File

@ -1,262 +0,0 @@
const { expect } = require('chai');
const { loadFixture } = require('ethereum-waffle');
describe('SitesNFTs contract', function () {
const name = 'Sites NFTs';
const symbol = 'SNFT';
async function deploy() {
const [owner, address1, address2] = await hre.ethers.getSigners();
const SitesNFTs = await hre.ethers.getContractFactory('SitesNFTs');
const hardhatSitesNFTs = await SitesNFTs.deploy(name, symbol);
return { owner, address1, address2, hardhatSitesNFTs };
}
describe('Deployment', () => {
it('Deployment should assign the name and the symbol of the ERC721 contract', async () => {
const { hardhatSitesNFTs } = await loadFixture(deploy);
const contractName = await hardhatSitesNFTs.name();
const contractSymbol = await hardhatSitesNFTs.symbol();
expect(contractName).to.equal(name);
expect(contractSymbol).to.equal(symbol);
});
it('Deployment should assign the deployer DEFAULT_ADMIN_ROLE', async () => {
const { hardhatSitesNFTs, owner } = await loadFixture(deploy);
const DEFAULT_ADMIN_ROLE_STRING = '';
const hasAdminRole = await hardhatSitesNFTs.hasRole(
ethers.utils.formatBytes32String(DEFAULT_ADMIN_ROLE_STRING),
await owner.getAddress()
);
expect(hasAdminRole).to.equal(true);
});
it('Deployment should assign initial tokenId to 0', async () => {
const { hardhatSitesNFTs } = await loadFixture(deploy);
const currentTokenId = await hardhatSitesNFTs.getCurrentTokenId();
expect(currentTokenId).to.equal(0);
});
});
describe('Access control', () => {
it('User with DEFAULT_ADMIN_ROLE should be able to assign MINTER_ROLE to another user', async () => {
const { hardhatSitesNFTs, address1 } = await loadFixture(deploy);
const MINTER_ROLE = 'MINTER_ROLE';
await hardhatSitesNFTs.grantRole(
ethers.utils.formatBytes32String(MINTER_ROLE),
await address1.getAddress()
);
const hasMinterRole = await hardhatSitesNFTs.hasRole(
ethers.utils.formatBytes32String(MINTER_ROLE),
await address1.getAddress()
);
expect(hasMinterRole).to.equal(true);
});
it('User with DEFAULT_ADMIN_ROLE should be able to assign MINTER_ROLE to himself and still have DEFAULT_ADMIN_ROLE', async () => {
const { hardhatSitesNFTs, owner } = await loadFixture(deploy);
const MINTER_ROLE = 'MINTER_ROLE';
const DEFAULT_ADMIN_ROLE = '';
await hardhatSitesNFTs.grantRole(
ethers.utils.formatBytes32String(MINTER_ROLE),
await owner.getAddress()
);
const hasMinterRole = await hardhatSitesNFTs.hasRole(
ethers.utils.formatBytes32String(MINTER_ROLE),
await owner.getAddress()
);
const hasAdminRole = await hardhatSitesNFTs.hasRole(
ethers.utils.formatBytes32String(DEFAULT_ADMIN_ROLE),
await owner.getAddress()
);
expect(hasMinterRole).to.equal(true);
expect(hasAdminRole).to.equal(true);
});
it('User with DEFAULT_ADMIN_ROLE should be able to assign DEFAULT_ADMIN_ROLE to another user', async () => {
const { hardhatSitesNFTs, address1 } = await loadFixture(deploy);
const DEFAULT_ADMIN_ROLE = '';
await hardhatSitesNFTs.grantRole(
ethers.utils.formatBytes32String(DEFAULT_ADMIN_ROLE),
await address1.getAddress()
);
const hasAdminRole = await hardhatSitesNFTs.hasRole(
ethers.utils.formatBytes32String(DEFAULT_ADMIN_ROLE),
await address1.getAddress()
);
expect(hasAdminRole).to.equal(true);
});
it('User with DEFAULT_ADMIN_ROLE should be able to assign DEFAULT_ADMIN_ROLE to another user and still have DEFAULT_ADMIN_ROLE', async () => {
const { hardhatSitesNFTs, owner, address1 } = await loadFixture(deploy);
const DEFAULT_ADMIN_ROLE = '';
await hardhatSitesNFTs.grantRole(
ethers.utils.formatBytes32String(DEFAULT_ADMIN_ROLE),
await address1.getAddress()
);
let hasAdminRole = await hardhatSitesNFTs.hasRole(
ethers.utils.formatBytes32String(DEFAULT_ADMIN_ROLE),
await address1.getAddress()
);
expect(hasAdminRole).to.equal(true);
hasAdminRole = await hardhatSitesNFTs.hasRole(
ethers.utils.formatBytes32String(DEFAULT_ADMIN_ROLE),
await owner.getAddress()
);
expect(hasAdminRole).to.equal(true);
});
it('User without DEFAULT_ADMIN_ROLE shouldnt be able to assign DEFAULT_ADMIN_ROLE to another user', async () => {
const [owner, address1, address2] = await ethers.getSigners();
const SitesNFTs = await ethers.getContractFactory('SitesNFTs');
const hardhatSitesNFTs = await SitesNFTs.deploy('Sites NFTs', 'SNFT');
const DEFAULT_ADMIN_ROLE = '';
try {
await hardhatSitesNFTs
.connect(address1)
.grantRole(
ethers.utils.formatBytes32String(DEFAULT_ADMIN_ROLE),
await address2.getAddress()
);
} catch (e) {}
const hasAdminRole = await hardhatSitesNFTs.hasRole(
ethers.utils.formatBytes32String(DEFAULT_ADMIN_ROLE),
await address2.getAddress()
);
expect(hasAdminRole).to.equal(false);
});
it('User without DEFAULT_ADMIN_ROLE shouldnt be able to assign MINTER_ROLE to another user', async () => {
const [owner, address1, address2] = await ethers.getSigners();
const SitesNFTs = await ethers.getContractFactory('SitesNFTs');
const hardhatSitesNFTs = await SitesNFTs.deploy('Sites NFTs', 'SNFT');
const MINTER_ROLE = 'MINTER_ROLE';
try {
await hardhatSitesNFTs
.connect(address1)
.grantRole(
ethers.utils.formatBytes32String(MINTER_ROLE),
await address2.getAddress()
);
} catch (e) {}
const hasMinterRole = await hardhatSitesNFTs.hasRole(
ethers.utils.formatBytes32String(MINTER_ROLE),
await address2.getAddress()
);
expect(hasMinterRole).to.equal(false);
});
});
describe('Minting', () => {
it('User with DEFAULT_ADMIN_ROLE should be able to mint', async () => {
const { hardhatSitesNFTs, address1 } = await loadFixture(deploy);
const tokenURI = 'tokenURI';
await hardhatSitesNFTs.mint(tokenURI, await address1.getAddress());
const balance = await hardhatSitesNFTs.balanceOf(
await address1.getAddress()
);
expect(balance).to.equal(1);
});
it('User with MINTER_ROLE should be able to mint', async () => {
const { hardhatSitesNFTs, address1, address2 } = await loadFixture(
deploy
);
const MINTER_ROLE = 'MINTER_ROLE';
const tokenURI = 'tokenURI';
await hardhatSitesNFTs.grantRole(
ethers.utils.formatBytes32String(MINTER_ROLE),
await address1.getAddress()
);
await hardhatSitesNFTs.hasRole(
ethers.utils.formatBytes32String(MINTER_ROLE),
await address1.getAddress()
);
await hardhatSitesNFTs
.connect(address1)
.mint(tokenURI, await address2.getAddress());
const balance = await hardhatSitesNFTs.balanceOf(
await address2.getAddress()
);
expect(balance).to.equal(1);
});
it('User without MINTER_ROLE or DEFAULT_ADMIN_ROLE shouldnt be able to mint', async () => {
const [owner, address1, address2] = await ethers.getSigners();
const SitesNFTs = await ethers.getContractFactory('SitesNFTs');
const hardhatSitesNFTs = await SitesNFTs.deploy('Sites NFTs', 'SNFT');
const tokenURI = 'tokenURI';
try {
await hardhatSitesNFTs
.connect(address1)
.mint(tokenURI, await address2.getAddress());
} catch (e) {}
const balance = await hardhatSitesNFTs.balanceOf(
await address2.getAddress()
);
expect(balance).to.equal(0);
});
it('Minted NFT should have data:application/json;base64, baseURI', async () => {
const { hardhatSitesNFTs, address1 } = await loadFixture(deploy);
const tokenURI = 'tokenURI';
await hardhatSitesNFTs.mint(tokenURI, await address1.getAddress());
const mintedNFT = await hardhatSitesNFTs.tokenURI(0);
expect(mintedNFT.includes('data:application/json;base64,')).to.equal(
true
);
});
});
});

58
test/foundry/apps.t.sol Normal file
View File

@ -0,0 +1,58 @@
pragma solidity ^0.8.7;
import "forge-std/Test.sol";
import "../../contracts/FleekERC721.sol";
contract ContractBTest is Test {
FleekERC721 fleekContract;
uint256 testNumber;
function setUp() public {
fleekContract = new FleekERC721('Test Contract', 'FLKAPS');
}
function testName() public {
assertEq(fleekContract.name(), 'Test Contract'));
}
function testSymbol() public {
assertEq(fleekContract.symbol(), 'FLKAPS'));
}
function testMint() public {
}
function testTokenURI() public {
}
function testBurn() public {
}
function testSetTokenName() public {
}
function testSetTokenDescription() public {
}
function testSetTokenImage() public {
}
function testSetTokenExternalURL() public {
}
function testSetTokenBuild() public {
}
function testUpgradeTokenBuild() public {
}
function testSetTokenENS() public {
}
function testAddTokenController() public {
}
function testRemoveTokenController() public {
}
}

5725
yarn.lock

File diff suppressed because it is too large Load Diff