feat: apps contract and updated scripts (#268)

* feat: FleekApps contract

* feat: update scripts

* wip: more helper scripts

* chore: remove unused import

* test: fix deploy test

* chore: fix testing networks

* fix: contract helper scripts
This commit is contained in:
Felipe Mendes 2023-06-02 17:12:09 -03:00 committed by GitHub
parent 74d4a4eb9c
commit 869b9f6e78
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 376 additions and 135 deletions

View File

@ -2,6 +2,7 @@
cache
artifacts
deployments/hardhat
deployments/local
gas-report
# Foundry

View File

@ -0,0 +1,58 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;
import "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol";
import "@openzeppelin/contracts/utils/Base64.sol";
import "./util/FleekSVG.sol";
import "./FleekERC721.sol";
contract FleekApps is Initializable, ERC721Upgradeable {
using Strings for address;
using Base64 for bytes;
uint256 public bindCount;
mapping(uint256 => uint256) public bindings;
FleekERC721 private main;
/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
_disableInitializers();
}
function initialize(string memory _name, string memory _symbol, address _mainAddress) public initializer {
__ERC721_init(_name, _symbol);
main = FleekERC721(_mainAddress);
}
modifier _requireMainMinted(uint256 _tokenId) {
require(main.ownerOf(_tokenId) != address(0), "Main token does not exist");
_;
}
function mint(address _to, uint256 _tokenId) public _requireMainMinted(_tokenId) {
_mint(_to, bindCount);
bindings[bindCount] = _tokenId;
bindCount++;
}
function tokenURI(uint256 _bindId) public view virtual override(ERC721Upgradeable) returns (string memory) {
(string memory name, string memory ens, string memory logo, string memory color, string memory ipfsHash) = main
.getAppData(bindings[_bindId]);
// prettier-ignore
return string(abi.encodePacked(_baseURI(),
abi.encodePacked('{',
'"owner":"', ownerOf(_bindId).toHexString(), '",',
'"name":"', name, '",',
'"image":"', FleekSVG.generateBase64(name, ens, logo, color), '",',
'"external_url":"ipfs://', ipfsHash, '"',
'}').encode()
));
}
function _baseURI() internal view virtual override returns (string memory) {
return "data:application/json;base64,";
}
}

View File

@ -202,6 +202,15 @@ contract FleekERC721 is
return (app.name, app.description, app.externalURL, app.ENS, app.currentBuild, app.logo, app.color);
}
function getAppData(
uint256 tokenId
) public view returns (string memory, string memory, string memory, string memory, string memory) {
_requireMinted(tokenId);
Token storage app = _apps[tokenId];
return (app.name, app.ENS, app.logo, app.color.toColorString(), app.builds[app.currentBuild].ipfsHash);
}
/**
* @dev Returns the last minted tokenId.
*/

View File

@ -9,7 +9,8 @@ import '@openzeppelin/hardhat-upgrades';
import * as dotenv from 'dotenv';
import { HardhatUserConfig } from 'hardhat/types';
import { task, types } from 'hardhat/config';
import deploy from './scripts/deploy';
import deployFleekERC721 from './scripts/deploy/deploy-fleek-erc721';
import deployFleekApps from './scripts/deploy/deploy-fleek-apps';
dotenv.config();
@ -26,7 +27,7 @@ const {
} = process.env;
const config: HardhatUserConfig = {
defaultNetwork: 'hardhat',
defaultNetwork: 'local',
networks: {
hardhat: {
chainId: 31337,
@ -57,6 +58,10 @@ const config: HardhatUserConfig = {
accounts: PRIVATE_KEY ? [PRIVATE_KEY] : [],
chainId: 1,
},
local: {
url: 'http://localhost:8545',
accounts: PRIVATE_KEY ? [PRIVATE_KEY] : [],
},
},
gasReporter: {
enabled: REPORT_GAS === 'true' || false,
@ -97,9 +102,9 @@ export default config;
// Use the following command to deploy where the network flag can be replaced with the network you choose:
// npx hardhat deploy --network goerli --new-proxy-instance --name "FleekNFAs" --symbol "FLKNFA" --billing "[10000, 20000]"
task('deploy', 'Deploy the contracts')
task('deploy:FleekERC721', 'Deploy the FleekERC721 contract')
.addFlag('newProxyInstance', 'Force to deploy a new proxy instance')
.addOptionalParam('name', 'The collection name', 'FleekNFAs', types.string)
.addOptionalParam('name', 'The collection name', 'Fleek NFAs', types.string)
.addOptionalParam('symbol', 'The collection symbol', 'FLKNFA', types.string)
.addOptionalParam(
'billing',
@ -107,4 +112,10 @@ task('deploy', 'Deploy the contracts')
[],
types.json
)
.setAction(deploy);
.setAction(deployFleekERC721);
task('deploy:FleekApps', 'Deploy the FleekApps contract')
.addFlag('newProxyInstance', 'Force to deploy a new proxy instance')
.addOptionalParam('name', 'The collection name', 'NFA - Apps', types.string)
.addOptionalParam('symbol', 'The collection symbol', 'NFAA', types.string)
.setAction(deployFleekApps);

View File

@ -6,13 +6,11 @@
"scripts": {
"test": "yarn test:hardhat && yarn test:foundry",
"test:foundry": "forge test -vvv --fork-url mainnet --fork-block-number 16876149",
"test:hardhat": "hardhat test",
"test:hardhat": "hardhat test --network hardhat",
"format": "prettier --write \"./**/*.{js,json,sol,ts}\"",
"node:hardhat": "hardhat node",
"deploy:hardhat": "hardhat deploy --network hardhat",
"deploy:mumbai": "hardhat deploy --network mumbai",
"deploy:sepolia": "hardhat deploy --network sepolia",
"deploy:goerli": "hardhat deploy --network goerli",
"deploy:FleekERC721": "hardhat deploy:FleekERC721",
"deploy:FleekApps": "hardhat deploy:FleekApps",
"compile": "hardhat compile",
"verify:mumbai": "npx hardhat run ./scripts/verify.js --network mumbai",
"verify:goerli": "npx hardhat run ./scripts/verify.js --network goerli",

View File

@ -1,105 +0,0 @@
const {
deployStore,
getCurrentAddressIfSameBytecode,
} = require('./utils/deploy-store');
const { getProxyAddress, proxyStore } = require('./utils/proxy-store');
// --- Script Settings ---
const CONTRACT_NAME = 'FleekERC721';
const DEFAULT_PROXY_SETTINGS = {
unsafeAllow: ['external-library-linking'],
};
const LIBRARIES_TO_DEPLOY = ['FleekSVG'];
const libraryDeployment = async (hre) => {
console.log('Deploying Libraries...');
const libraries = {};
for (const lib of LIBRARIES_TO_DEPLOY) {
const libAddress = await getCurrentAddressIfSameBytecode(lib);
if (libAddress) {
console.log(`Library "${lib}" already deployed at ${libAddress}`);
libraries[lib] = libAddress;
continue;
}
const libContract = await hre.ethers.getContractFactory(lib);
const libInstance = await libContract.deploy();
await libInstance.deployed();
await deployStore(hre.network.name, lib, libInstance, false);
console.log(`Library "${lib}" deployed at ${libInstance.address}`);
libraries[lib] = libInstance.address;
}
return libraries;
};
module.exports = async (taskArgs, hre) => {
const { newProxyInstance, name, symbol, billing } = taskArgs;
const network = hre.network.name;
console.log(':: Starting Deployment ::');
console.log('Network:', network);
console.log('Contract:', CONTRACT_NAME);
console.log(':: Arguments ::');
console.log(taskArgs);
console.log();
const deployArguments = [name, symbol, billing];
const libraries = await libraryDeployment(hre);
const Contract = await ethers.getContractFactory(CONTRACT_NAME, {
libraries,
});
const proxyAddress = await getProxyAddress(CONTRACT_NAME, network);
let deployResult;
try {
if (!proxyAddress || newProxyInstance)
throw new Error('new-proxy-instance');
console.log(`Trying to upgrade proxy contract at: "${proxyAddress}"`);
deployResult = await upgrades.upgradeProxy(
proxyAddress,
Contract,
DEFAULT_PROXY_SETTINGS
);
console.log('\x1b[32m');
console.log(
`Contract ${CONTRACT_NAME} upgraded at "${deployResult.address}" by account "${deployResult.signer.address}"`
);
console.log('\x1b[0m');
} catch (e) {
if (
e.message === 'new-proxy-instance' ||
e.message.includes("doesn't look like an ERC 1967 proxy")
) {
console.log(`Failed to upgrade proxy contract: "${e.message?.trim()}"`);
console.log('Creating new proxy contract...');
deployResult = await upgrades.deployProxy(
Contract,
deployArguments,
DEFAULT_PROXY_SETTINGS
);
await deployResult.deployed();
await proxyStore(CONTRACT_NAME, deployResult.address, network);
console.log('\x1b[32m');
console.log(
`Contract ${CONTRACT_NAME} deployed at "${deployResult.address}" by account "${deployResult.signer.address}"`
);
console.log('\x1b[0m');
} else {
throw e;
}
try {
await deployStore(network, CONTRACT_NAME, deployResult);
} catch (e) {
console.error('Could not write deploy files', e);
}
}
return deployResult;
};

View File

@ -0,0 +1,31 @@
import { HardhatRuntimeEnvironment } from 'hardhat/types';
import { deployLibraries } from './deploy-libraries';
import { deployContractWithProxy } from './deploy-proxy-contract';
import { getContract } from '../util';
import { Contract } from 'ethers';
type TaskArgs = {
newProxyInstance: boolean;
name: string;
symbol: string;
};
export default async (
{ newProxyInstance, name, symbol }: TaskArgs,
hre: HardhatRuntimeEnvironment
): Promise<Contract> => {
console.log('Deploying FleekApps...');
const libraries = await deployLibraries(['FleekSVG'], hre);
const mainContract = await getContract('FleekERC721');
return deployContractWithProxy(
{
name: 'FleekApps',
newProxyInstance,
args: [name, symbol, mainContract.address],
libraries,
},
hre
);
};

View File

@ -0,0 +1,29 @@
import { HardhatRuntimeEnvironment } from 'hardhat/types';
import { deployLibraries } from './deploy-libraries';
import { deployContractWithProxy } from './deploy-proxy-contract';
import { Contract } from 'ethers';
type TaskArgs = {
newProxyInstance: boolean;
name: string;
symbol: string;
billing: number[];
};
export default async (
{ newProxyInstance, name, symbol, billing }: TaskArgs,
hre: HardhatRuntimeEnvironment
): Promise<Contract> => {
console.log('Deploying FleekERC721...');
const libraries = await deployLibraries(['FleekSVG'], hre);
return deployContractWithProxy(
{
name: 'FleekERC721',
newProxyInstance,
args: [name, symbol, billing],
libraries,
},
hre
);
};

View File

@ -0,0 +1,30 @@
import {
deployStore,
getCurrentAddressIfSameBytecode,
} from '../utils/deploy-store';
import { HardhatRuntimeEnvironment } from 'hardhat/types';
export const deployLibraries = async (
librariesToDeploy: string[],
hre: HardhatRuntimeEnvironment
) => {
console.log('Deploying Libraries...');
const libraries: Record<string, string> = {};
for (const lib of librariesToDeploy) {
const libAddress: string = await getCurrentAddressIfSameBytecode(lib);
if (libAddress) {
console.log(`Library "${lib}" already deployed at ${libAddress}`);
libraries[lib] = libAddress;
continue;
}
const libContract = await hre.ethers.getContractFactory(lib);
const libInstance = await libContract.deploy();
await libInstance.deployed();
await deployStore(hre.network.name, lib, libInstance, false);
console.log(`Library "${lib}" deployed at ${libInstance.address}`);
libraries[lib] = libInstance.address;
}
return libraries;
};

View File

@ -0,0 +1,88 @@
import { getProxyAddress, proxyStore } from '../utils/proxy-store';
import { deployStore } from '../utils/deploy-store';
import { UpgradeProxyOptions } from '@openzeppelin/hardhat-upgrades/dist/utils';
import { Contract } from 'ethers';
import { HardhatRuntimeEnvironment } from 'hardhat/types';
const DEFAULT_PROXY_SETTINGS: UpgradeProxyOptions = {
unsafeAllow: ['external-library-linking'],
};
type DeployContractArgs = {
name: string;
newProxyInstance: boolean;
args: unknown[];
libraries?: Record<string, string>;
};
export const deployContractWithProxy = async (
{ name, newProxyInstance, args, libraries }: DeployContractArgs,
hre: HardhatRuntimeEnvironment
): Promise<Contract> => {
// const { newProxyInstance, name, symbol, billing } = taskArgs;
const network = hre.network.name;
console.log(`Deploying: ${name}`);
console.log('Arguments:', args);
console.log();
const Contract = await hre.ethers.getContractFactory(name, {
libraries,
});
const proxyAddress = await getProxyAddress(name, network);
let deployResult;
try {
if (!proxyAddress || newProxyInstance)
throw new Error('new-proxy-instance');
console.log(`Trying to upgrade proxy contract at: "${proxyAddress}"`);
deployResult = await hre.upgrades.upgradeProxy(
proxyAddress,
Contract,
DEFAULT_PROXY_SETTINGS
);
console.log('\x1b[32m');
console.log(
`Contract ${name} upgraded at "${
deployResult.address
}" by account "${await deployResult.signer.getAddress()}"`
);
console.log('\x1b[0m');
} catch (e) {
if (
e instanceof Error &&
(e.message === 'new-proxy-instance' ||
e.message.includes("doesn't look like an ERC 1967 proxy"))
) {
console.log(`Failed to upgrade proxy contract: "${e.message?.trim()}"`);
console.log('Creating new proxy contract...');
deployResult = await hre.upgrades.deployProxy(
Contract,
args,
DEFAULT_PROXY_SETTINGS
);
await deployResult.deployed();
await proxyStore(name, deployResult.address, network);
console.log('\x1b[32m');
console.log(
`Contract ${name} deployed at "${
deployResult.address
}" by account "${await deployResult.signer.getAddress()}"`
);
console.log('\x1b[0m');
} else {
throw e;
}
try {
await deployStore(network, name, deployResult);
} catch (e) {
console.error('Could not write deploy files', e);
}
}
return deployResult;
};

View File

@ -0,0 +1,18 @@
// npx hardhat run scripts/generate-image.ts --network local
import { getContract } from './util';
export const generateImage = async (
name: string,
ens: string,
logo: string,
color: string
) => {
const contract = await getContract('FleekSVG');
const svg = await contract.generateBase64(name, ens, logo, color);
console.log('SVG:', svg);
};
generateImage('Fleek', '', '', '#123456');

View File

@ -0,0 +1,15 @@
// npx hardhat run scripts/get-app.ts --network local
import { getContract, parseDataURI } from './util';
const getApp = async (tokenId: number) => {
const contract = await getContract('FleekApps');
const transaction = await contract.tokenURI(tokenId);
const parsed = parseDataURI(transaction);
console.log('App:', parsed);
};
getApp(0);

View File

@ -0,0 +1,16 @@
// npx hardhat run scripts/mint-app.ts --network local
import { getContract } from './util';
const mintApp = async (nfaId: number) => {
const contract = await getContract('FleekApps');
const transaction = await contract.mint(
'0x7ed735b7095c05d78df169f991f2b7f1a1f1a049',
nfaId
);
console.log('Minted app', transaction.hash);
};
mintApp(0);

View File

@ -44,8 +44,9 @@ const DEFAULT_MINTS = {
'aave', // name
'Earn interest, borrow assets, and build applications', // description
'https://aave.com/', // external url
'aave.eth', // ens
'6ea6ad16c46ae85faced7e50555ff7368422f57', // commit hash
'', // ens
'6ea6ad16c46ae85faced7e50555ff7368422f57', // commit hash,
'bafybeifc5pgon43a2xoeevwq45ftwghzbgtjxc7k4dqlzhqh432wpahigm', // ipfs hash
'https://github.com/org/repo', // repo
path.resolve(__dirname, '../assets/aave.svg'), // svg
],
@ -53,9 +54,10 @@ const DEFAULT_MINTS = {
'Uniswap', // name
'Swap, earn, and build on the leading decentralized crypto trading protocol', // description
'https://uniswap.org/', // external url
'uniswap.eth', // ens
'', // ens
'6ea6ad16c46ae85faced7e50555ff7368422f57', // commit hash
'https://github.com/org/repo', // repo
'bafybeidwf6m2lhkdifuxqucgaq547bwyxk2mljwmazvhmyryjr6yjoe3nu', // ipfs hash
path.resolve(__dirname, '../assets/uniswap.svg'), // svg
],
yearn: [
@ -78,7 +80,7 @@ const DEFAULT_MINTS = {
],
};
const params = DEFAULT_MINTS.fleek;
const params = DEFAULT_MINTS.uniswap;
const mintTo = '0x7ED735b7095C05d78dF169F991f2b7f1A1F1A049';
const verifier = '0x7ED735b7095C05d78dF169F991f2b7f1A1F1A049';
@ -90,11 +92,15 @@ const verifier = '0x7ED735b7095C05d78dF169F991f2b7f1A1F1A049';
console.log('SVG Path: ', svgPath);
params.push(await getSVGBase64(svgPath));
console.log('SVG length: ', params[params.length - 1].length);
params.push(await getSVGColor(svgPath));
params.push(
(await getSVGColor(svgPath))
.reduce((a, b, i) => a | (b << ((2 - i) * 8)), 0)
.toString()
);
params.push(false);
params.push(verifier);
const transaction = await contract.mint(...params);
console.log('Response: ', transaction);
console.log('Response: ', transaction.hash);
})();

View File

@ -0,0 +1,13 @@
// npx hardhat run scripts/owner-of.ts --network local
import { getContract } from './util';
const ownerOf = async (tokenId: number) => {
const contract = await getContract('FleekERC721');
const owner = await contract.ownerOf(tokenId);
console.log('Owner:', owner);
};
ownerOf(0);

View File

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

View File

@ -1,14 +1,26 @@
module.exports.getContract = async function (contractName) {
const proxyDeployments =
require(`../deployments/${hre.network.name}/proxy.json`)[contractName];
const deployment = require(`../deployments/${hre.network.name}/${contractName}.json`);
if (!proxyDeployments || !proxyDeployments.length) {
if (!deployment) {
throw new Error(
`No proxy deployments found for "${contractName}" under "${hre.network.name}"`
`No deployment found for "${contractName}" under "${hre.network.name}"`
);
}
const latestDeployment = proxyDeployments[0];
console.log(`Using latest deployment for "${deployment.address}":`);
return hre.ethers.getContractAt(contractName, latestDeployment.address);
return hre.ethers.getContractAt(contractName, deployment.address);
};
module.exports.parseDataURI = function (dataURI) {
if (!dataURI.startsWith('data:')) throw new Error('Invalid data URI');
const content = dataURI.replace('data:', '');
const [type, data] = content.split(';base64,');
switch (type) {
case 'application/json':
return JSON.parse(Buffer.from(data, 'base64').toString('utf-8'));
default:
throw new Error(`Unsupported data URI type: ${type}`);
}
};

View File

@ -81,7 +81,20 @@ const getCurrentAddressIfSameBytecode = async (contractName) => {
hre.network.name,
contractName
));
return deployData.bytecode === bytecode ? deployData.address : null;
if (deployData.bytecode === bytecode) {
try {
const contract = await hre.ethers.getContractAt(
contractName,
deployData.address
);
return contract.address;
} catch {
console.log(
`Contract ${contractName} at ${deployData.address} is not deployed`
);
}
}
}
return null;

View File

@ -2,11 +2,11 @@ import { expect } from 'chai';
import * as hre from 'hardhat';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
import deploy from '../../../scripts/deploy';
import deploy from '../../../scripts/deploy/deploy-fleek-erc721';
import { getImplementationAddress } from '@openzeppelin/upgrades-core';
import { Contract } from 'ethers';
import { loadFixture } from '@nomicfoundation/hardhat-network-helpers';
import { Errors, TestConstants } from '../contracts/FleekERC721/helpers';
import { TestConstants } from '../contracts/FleekERC721/helpers';
const taskArgs = {
newProxyInstance: false,
@ -28,7 +28,7 @@ const getImplementationContract = async (
const deployFixture = async () => {
const [owner] = await hre.ethers.getSigners();
const proxy = (await deploy(taskArgs, hre)) as Contract;
const proxy = await deploy(taskArgs, hre);
const implementation = await getImplementationContract(proxy.address);