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 cache
artifacts artifacts
deployments/hardhat deployments/hardhat
deployments/local
gas-report gas-report
# Foundry # 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); 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. * @dev Returns the last minted tokenId.
*/ */

View File

@ -9,7 +9,8 @@ import '@openzeppelin/hardhat-upgrades';
import * as dotenv from 'dotenv'; import * as dotenv from 'dotenv';
import { HardhatUserConfig } from 'hardhat/types'; import { HardhatUserConfig } from 'hardhat/types';
import { task, types } from 'hardhat/config'; 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(); dotenv.config();
@ -26,7 +27,7 @@ const {
} = process.env; } = process.env;
const config: HardhatUserConfig = { const config: HardhatUserConfig = {
defaultNetwork: 'hardhat', defaultNetwork: 'local',
networks: { networks: {
hardhat: { hardhat: {
chainId: 31337, chainId: 31337,
@ -57,6 +58,10 @@ const config: HardhatUserConfig = {
accounts: PRIVATE_KEY ? [PRIVATE_KEY] : [], accounts: PRIVATE_KEY ? [PRIVATE_KEY] : [],
chainId: 1, chainId: 1,
}, },
local: {
url: 'http://localhost:8545',
accounts: PRIVATE_KEY ? [PRIVATE_KEY] : [],
},
}, },
gasReporter: { gasReporter: {
enabled: REPORT_GAS === 'true' || false, 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: // 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]" // 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') .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('symbol', 'The collection symbol', 'FLKNFA', types.string)
.addOptionalParam( .addOptionalParam(
'billing', 'billing',
@ -107,4 +112,10 @@ task('deploy', 'Deploy the contracts')
[], [],
types.json 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": { "scripts": {
"test": "yarn test:hardhat && yarn test:foundry", "test": "yarn test:hardhat && yarn test:foundry",
"test:foundry": "forge test -vvv --fork-url mainnet --fork-block-number 16876149", "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}\"", "format": "prettier --write \"./**/*.{js,json,sol,ts}\"",
"node:hardhat": "hardhat node", "node:hardhat": "hardhat node",
"deploy:hardhat": "hardhat deploy --network hardhat", "deploy:FleekERC721": "hardhat deploy:FleekERC721",
"deploy:mumbai": "hardhat deploy --network mumbai", "deploy:FleekApps": "hardhat deploy:FleekApps",
"deploy:sepolia": "hardhat deploy --network sepolia",
"deploy:goerli": "hardhat deploy --network goerli",
"compile": "hardhat compile", "compile": "hardhat compile",
"verify:mumbai": "npx hardhat run ./scripts/verify.js --network mumbai", "verify:mumbai": "npx hardhat run ./scripts/verify.js --network mumbai",
"verify:goerli": "npx hardhat run ./scripts/verify.js --network goerli", "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 'aave', // name
'Earn interest, borrow assets, and build applications', // description 'Earn interest, borrow assets, and build applications', // description
'https://aave.com/', // external url 'https://aave.com/', // external url
'aave.eth', // ens '', // ens
'6ea6ad16c46ae85faced7e50555ff7368422f57', // commit hash '6ea6ad16c46ae85faced7e50555ff7368422f57', // commit hash,
'bafybeifc5pgon43a2xoeevwq45ftwghzbgtjxc7k4dqlzhqh432wpahigm', // ipfs hash
'https://github.com/org/repo', // repo 'https://github.com/org/repo', // repo
path.resolve(__dirname, '../assets/aave.svg'), // svg path.resolve(__dirname, '../assets/aave.svg'), // svg
], ],
@ -53,9 +54,10 @@ const DEFAULT_MINTS = {
'Uniswap', // name 'Uniswap', // name
'Swap, earn, and build on the leading decentralized crypto trading protocol', // description 'Swap, earn, and build on the leading decentralized crypto trading protocol', // description
'https://uniswap.org/', // external url 'https://uniswap.org/', // external url
'uniswap.eth', // ens '', // ens
'6ea6ad16c46ae85faced7e50555ff7368422f57', // commit hash '6ea6ad16c46ae85faced7e50555ff7368422f57', // commit hash
'https://github.com/org/repo', // repo 'https://github.com/org/repo', // repo
'bafybeidwf6m2lhkdifuxqucgaq547bwyxk2mljwmazvhmyryjr6yjoe3nu', // ipfs hash
path.resolve(__dirname, '../assets/uniswap.svg'), // svg path.resolve(__dirname, '../assets/uniswap.svg'), // svg
], ],
yearn: [ yearn: [
@ -78,7 +80,7 @@ const DEFAULT_MINTS = {
], ],
}; };
const params = DEFAULT_MINTS.fleek; const params = DEFAULT_MINTS.uniswap;
const mintTo = '0x7ED735b7095C05d78dF169F991f2b7f1A1F1A049'; const mintTo = '0x7ED735b7095C05d78dF169F991f2b7f1A1F1A049';
const verifier = '0x7ED735b7095C05d78dF169F991f2b7f1A1F1A049'; const verifier = '0x7ED735b7095C05d78dF169F991f2b7f1A1F1A049';
@ -90,11 +92,15 @@ const verifier = '0x7ED735b7095C05d78dF169F991f2b7f1A1F1A049';
console.log('SVG Path: ', svgPath); console.log('SVG Path: ', svgPath);
params.push(await getSVGBase64(svgPath)); params.push(await getSVGBase64(svgPath));
console.log('SVG length: ', params[params.length - 1].length); 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(false);
params.push(verifier); params.push(verifier);
const transaction = await contract.mint(...params); 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 // npx hardhat run scripts/tokenURI.js --network mumbai/sepolia/goerli
const { getContract } = require('./util'); const { getContract, parseDataURI } = require('./util');
// TODO: make this arguments // TODO: make this arguments
const tokenId = 0; const tokenId = 0;
@ -9,9 +9,7 @@ const tokenId = 0;
const transaction = await contract.tokenURI(tokenId); const transaction = await contract.tokenURI(tokenId);
const parsed = JSON.parse( const parsed = parseDataURI(transaction);
Buffer.from(transaction.slice(29), 'base64').toString('utf-8')
);
console.log('Response: ', parsed); console.log('Response: ', parsed);
})(); })();

View File

@ -1,14 +1,26 @@
module.exports.getContract = async function (contractName) { module.exports.getContract = async function (contractName) {
const proxyDeployments = const deployment = require(`../deployments/${hre.network.name}/${contractName}.json`);
require(`../deployments/${hre.network.name}/proxy.json`)[contractName];
if (!proxyDeployments || !proxyDeployments.length) { if (!deployment) {
throw new Error( 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, hre.network.name,
contractName 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; return null;

View File

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