From 758f6a29d3e67be6f636aaa3a482f65ea03c3a44 Mon Sep 17 00:00:00 2001 From: Aleksey Bykhun Date: Thu, 13 Apr 2023 17:17:32 -0400 Subject: [PATCH] create ArtgenePlatform global config (#100) * create ArtgenePlatform global config * move proxy implementation address to constants * check contract not empty before setting * update vanity deployer * update salt * move free functions to artgene utils * cleanup code * platform: add vanity constant, rename methods * remove utils * platform: transfer ownership after deploy * add task hh accounts * platform: check that it's deployed to correct address * platform: deploy to mainnet * deploy to mainnet --- contracts/Artgene721.sol | 6 +- contracts/Artgene721Base.sol | 20 +- contracts/Artgene721Implementation.sol | 34 +-- contracts/ArtgenePlatform.sol | 59 +++++ contracts/interfaces/IArtgene721.sol | 2 + contracts/interfaces/IArtgenePlatform.sol | 14 ++ hardhat.config.ts | 12 +- scripts/deploy-platform.ts | 254 ++++++++++++++++++++++ scripts/deploy-proxy.ts | 4 +- test/artgene-implementation.js | 3 + 10 files changed, 377 insertions(+), 31 deletions(-) create mode 100644 contracts/ArtgenePlatform.sol create mode 100644 contracts/interfaces/IArtgenePlatform.sol create mode 100644 scripts/deploy-platform.ts diff --git a/contracts/Artgene721.sol b/contracts/Artgene721.sol index c4ae3189..7ecab488 100644 --- a/contracts/Artgene721.sol +++ b/contracts/Artgene721.sol @@ -67,8 +67,6 @@ import "./interfaces/IArtgene721.sol"; type StartFromTokenIdOne is bool; contract Artgene721 is Proxy { - address internal constant proxyImplementation = - 0x00000721187b81D0aDac9d1E4D7Fd623ac788559; StartFromTokenIdOne internal constant START_FROM_ONE = StartFromTokenIdOne.wrap(true); @@ -85,7 +83,7 @@ contract Artgene721 is Proxy { MintConfig memory configValues ) { Address.functionDelegateCall( - proxyImplementation, + ARTGENE_PROXY_IMPLEMENTATION, abi.encodeWithSelector( IArtgene721Implementation.initialize.selector, name, @@ -104,6 +102,6 @@ contract Artgene721 is Proxy { } function _implementation() internal pure override returns (address) { - return address(proxyImplementation); + return address(ARTGENE_PROXY_IMPLEMENTATION); } } diff --git a/contracts/Artgene721Base.sol b/contracts/Artgene721Base.sol index cb99e5e5..b740bcda 100644 --- a/contracts/Artgene721Base.sol +++ b/contracts/Artgene721Base.sol @@ -21,8 +21,10 @@ import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "./interfaces/INFTExtension.sol"; import "./interfaces/IRenderer.sol"; import "./interfaces/IArtgene721.sol"; +import "./interfaces/IArtgenePlatform.sol"; import "./utils/OpenseaProxy.sol"; + /** * @title contract by artgene.xyz */ @@ -90,11 +92,13 @@ contract Artgene721Base is using SafeERC20 for IERC20; uint256 internal constant SALE_STARTS_AT_INFINITY = 2**256 - 1; - uint256 internal constant PLATFORM_FEE = 500; // of 10,000 = 5% uint256 internal constant MAX_PER_MINT_LIMIT = 50; // based on ERC721A limitations address internal constant OPENSEA_CONDUIT = 0x1E0049783F008A0085193E00003D00cd54003c71; + uint256 public PLATFORM_FEE; // of 10,000 + address payable PLATFORM_ADDRESS; + uint256 public startTimestamp = SALE_STARTS_AT_INFINITY; uint256 public reserved; @@ -163,6 +167,8 @@ contract Artgene721Base is maxPerMint = MAX_PER_MINT_LIMIT; isOpenSeaProxyActive = true; + (PLATFORM_FEE, PLATFORM_ADDRESS) = IArtgenePlatform(ARTGENE_PLATFORM_ADDRESS).getPlatformInfo(); + _configure( _config.publicPrice, _config.maxTokensPerMint, @@ -569,7 +575,7 @@ contract Artgene721Base is modifier onlyDeveloper() { require( - payable(msg.sender) == PLATFORM_ADDRESS(), + payable(msg.sender) == PLATFORM_ADDRESS, "Caller is not developer" ); _; @@ -580,7 +586,7 @@ contract Artgene721Base is uint256 amount = (balance * (10000 - PLATFORM_FEE)) / 10000; address payable receiver = getPayoutReceiver(); - address payable dev = PLATFORM_ADDRESS(); + address payable dev = PLATFORM_ADDRESS; Address.sendValue(receiver, amount); Address.sendValue(dev, balance - amount); @@ -600,20 +606,16 @@ contract Artgene721Base is uint256 amount = (balance * (10000 - PLATFORM_FEE)) / 10000; address payable receiver = getPayoutReceiver(); - address payable dev = PLATFORM_ADDRESS(); + address payable dev = PLATFORM_ADDRESS; token.safeTransfer(receiver, amount); token.safeTransfer(dev, balance - amount); } - function DEVELOPER() public pure returns (string memory _url) { + function PLATFORM() public pure returns (string memory _url) { _url = "https://artgene.xyz"; } - function PLATFORM_ADDRESS() internal pure returns (address payable _dev) { - _dev = payable(0x704C043CeB93bD6cBE570C6A2708c3E1C0310587); - } - // -------- ERC721 overrides -------- function supportsInterface(bytes4 interfaceId) diff --git a/contracts/Artgene721Implementation.sol b/contracts/Artgene721Implementation.sol index faf46582..acabd9bd 100644 --- a/contracts/Artgene721Implementation.sol +++ b/contracts/Artgene721Implementation.sol @@ -21,6 +21,7 @@ import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "./interfaces/INFTExtension.sol"; import "./interfaces/IRenderer.sol"; import "./interfaces/IArtgene721.sol"; +import "./interfaces/IArtgenePlatform.sol"; import "./utils/OpenseaProxy.sol"; import "./utils/operator-filterer/upgradable/DefaultOperatorFiltererUpgradeable.sol"; @@ -92,15 +93,17 @@ contract Artgene721Implementation is using Address for address; using SafeERC20 for IERC20; - uint256 internal constant SALE_STARTS_AT_INFINITY = 2**256 - 1; - uint256 internal constant PLATFORM_FEE = 500; // of 10,000 = 5% + uint256 internal constant SALE_STARTS_AT_INFINITY = 2 ** 256 - 1; uint256 internal constant MAX_PER_MINT_LIMIT = 50; // based on ERC721A limitations address internal constant OPENSEA_CONDUIT = 0x1E0049783F008A0085193E00003D00cd54003c71; uint256 public constant VERSION = 3; - uint256 public startTimestamp = SALE_STARTS_AT_INFINITY; + uint256 public startTimestamp; + + uint256 public PLATFORM_FEE; // of 10,000 + address payable PLATFORM_ADDRESS; uint256 public reserved; uint256 public maxSupply; @@ -165,6 +168,8 @@ contract Artgene721Implementation is isOpenSeaProxyActive = true; isOpenSeaTransferFilterEnabled = true; + (PLATFORM_FEE, PLATFORM_ADDRESS) = IArtgenePlatform(ARTGENE_PLATFORM_ADDRESS).getPlatformInfo(); + __ERC721A_init(_name, _symbol); __ReentrancyGuard_init(); __Ownable_init(); @@ -231,7 +236,12 @@ contract Artgene721Implementation is // on the other hand, it's impossible to call this function in proxy, // so the real initializer is the only initializer /// @custom:oz-upgrades-unsafe-allow constructor - constructor() initializer {} + constructor() initializer { + // NB: this is run only once per implementation, when it's deployed + // NB: this is NOT run when deploying Proxy + require(address(this) == ARTGENE_PROXY_IMPLEMENTATION, "Only deployable to vanity address"); + + } function _baseURI() internal view override returns (string memory) { return BASE_URI; @@ -586,7 +596,7 @@ contract Artgene721Implementation is modifier onlyDeveloper() { require( - payable(msg.sender) == PLATFORM_ADDRESS(), + payable(msg.sender) == PLATFORM_ADDRESS, "Caller is not developer" ); _; @@ -597,10 +607,10 @@ contract Artgene721Implementation is uint256 amount = (balance * (10000 - PLATFORM_FEE)) / 10000; address payable receiver = getPayoutReceiver(); - address payable dev = PLATFORM_ADDRESS(); + address payable platform = PLATFORM_ADDRESS; Address.sendValue(receiver, amount); - Address.sendValue(dev, balance - amount); + Address.sendValue(platform, balance - amount); } function withdraw() public virtual onlyOwner { @@ -617,20 +627,16 @@ contract Artgene721Implementation is uint256 amount = (balance * (10000 - PLATFORM_FEE)) / 10000; address payable receiver = getPayoutReceiver(); - address payable dev = PLATFORM_ADDRESS(); + address payable platform = PLATFORM_ADDRESS; token.safeTransfer(receiver, amount); - token.safeTransfer(dev, balance - amount); + token.safeTransfer(platform, balance - amount); } - function DEVELOPER() public pure returns (string memory _url) { + function PLATFORM() public pure returns (string memory _url) { _url = "https://artgene.xyz"; } - function PLATFORM_ADDRESS() internal pure returns (address payable _dev) { - _dev = payable(0x704C043CeB93bD6cBE570C6A2708c3E1C0310587); - } - // -------- ERC721 overrides -------- function _beforeTokenTransfers( diff --git a/contracts/ArtgenePlatform.sol b/contracts/ArtgenePlatform.sol new file mode 100644 index 00000000..3c83ff95 --- /dev/null +++ b/contracts/ArtgenePlatform.sol @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts/access/Ownable.sol"; +import "@openzeppelin/contracts/utils/StorageSlot.sol"; + +import "./interfaces/IArtgenePlatform.sol"; + +contract ArtgenePlatform is Ownable, IArtgenePlatform { + + uint256 platformFee; // in bps + address payable platformAddress; + + constructor() { + require( + ARTGENE_PLATFORM_ADDRESS == address(this), + "ArtgenePlatformConfig: platform address mismatch" + ); + + platformFee = 500; + platformAddress = payable(0x704C043CeB93bD6cBE570C6A2708c3E1C0310587); + } + + function setPlatformFee(uint256 _platformFee) public onlyOwner { + require( + _platformFee <= 1000, + "ArtgenePlatformConfig: platform fee cannot be more than 10%" + ); + + platformFee = _platformFee; + } + + function setPlatformAddress( + address payable _platformAddress + ) public onlyOwner { + platformAddress = _platformAddress; + } + + function getPlatformFee() external view override returns (uint256) { + return platformFee; + } + + function getPlatformAddress() + external + view + override + returns (address payable) + { + return platformAddress; + } + + function getPlatformInfo() + external + view + returns (uint256, address payable) + { + return (platformFee, platformAddress); + } +} diff --git a/contracts/interfaces/IArtgene721.sol b/contracts/interfaces/IArtgene721.sol index b17cdfaf..2fe2801a 100644 --- a/contracts/interfaces/IArtgene721.sol +++ b/contracts/interfaces/IArtgene721.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.9; +address constant ARTGENE_PROXY_IMPLEMENTATION = 0x000007214f56DaF21c803252cc610360C70C01D5; + struct MintConfig { uint256 publicPrice; uint256 maxTokensPerMint; diff --git a/contracts/interfaces/IArtgenePlatform.sol b/contracts/interfaces/IArtgenePlatform.sol new file mode 100644 index 00000000..ab105f11 --- /dev/null +++ b/contracts/interfaces/IArtgenePlatform.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +address constant ARTGENE_PLATFORM_ADDRESS = 0xAaaeEee77ED0D0ffCc2813333b796E367f1E12d9; + +interface IArtgenePlatform { + function getPlatformFee() external view returns (uint256); + function getPlatformAddress() external view returns (address payable); + + function getPlatformInfo() external view returns (uint256, address payable); + + function setPlatformFee(uint256 _platformFee) external; + function setPlatformAddress(address payable _platformAddress) external; +} diff --git a/hardhat.config.ts b/hardhat.config.ts index f72ba6c8..c9ca57e5 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -1,7 +1,7 @@ import fs from "fs"; -import { stdout } from "process"; import "dotenv/config"; -import { HardhatUserConfig } from "hardhat/config"; +import { stdout } from "process"; +import { HardhatUserConfig, task } from "hardhat/config"; import { generateMnemonic } from "bip39"; @@ -72,6 +72,14 @@ const getRemappings = () => { .map((line) => line.trim().split("=")); }; +task("accounts", "Prints the list of accounts", async (taskArgs, hre) => { + const accounts = await hre.ethers.getSigners(); + + for (const account of accounts) { + console.log(account.address); + } +}); + const config: HardhatUserConfig = { networks: { hardhat: { diff --git a/scripts/deploy-platform.ts b/scripts/deploy-platform.ts new file mode 100644 index 00000000..06ef3e40 --- /dev/null +++ b/scripts/deploy-platform.ts @@ -0,0 +1,254 @@ +import hre, { ethers } from "hardhat"; +import fs from "fs"; +import { getContractAddress, parseEther } from "ethers/lib/utils"; +import { Address } from "hardhat-deploy/dist/types"; +import { Signer } from "ethers"; + +const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + +const GAS_PRICE_GWEI = "35"; + +export const PLATFORM_ADDRESS = "0xAaaeEee77ED0D0ffCc2813333b796E367f1E12d9"; +export const PLATFORM_DEPLOYER_ADDRESS = + "0xE4A02093339a9a908cF9d897481813Ddb5494d44"; + +export const sendAllFunds = async (account: Signer, to: Address) => { + const balance = await account.getBalance(); + + const gasPrice = hre.ethers.utils.parseUnits(GAS_PRICE_GWEI, "gwei"); + const gasCost = gasPrice.mul(21000); + + console.log(" ==> balance ==>", balance.sub(gasCost).toString()); + + return await account.sendTransaction({ + to: to, + value: balance.sub(gasCost), + gasLimit: 21_000, + gasPrice: gasPrice, + }); +}; + +export const getVanityDeployer = async () => { + // if (hre.network.name === "hardhat") { + // // impersonate the vanity deployer + // await hre.network.provider.request({ + // method: "hardhat_impersonateAccount", + // params: [PLATFORM_DEPLOYER_ADDRESS], + // }); + + // return await hre.ethers.getSigner(PLATFORM_DEPLOYER_ADDRESS); + // } + + // load account from process.env.PLATFORM_PRIVATE_DEPLOYER + const vanityKey = process.env.PLATFORM_PRIVATE_DEPLOYER; + + if (!vanityKey) { + throw new Error("PLATFORM_PRIVATE_DEPLOYER is not set"); + } + + return new hre.ethers.Wallet(vanityKey, hre.ethers.provider); +}; + +export const computeVanityAddress = async () => { + const vanityDeployer = await getVanityDeployer(); + + console.log("vanity deployer address is", vanityDeployer.address); + + const transactionCount = await vanityDeployer.getTransactionCount(); + + const vanityAddress = getContractAddress({ + from: vanityDeployer.address, + nonce: transactionCount, + }); + + return vanityAddress; +}; + +export async function main() { + const [admin] = await hre.ethers.getSigners(); + + // // skip waiting if running on hardhat network + // if (hre.network.name == "hardhat") { + + // await hre.network.provider.request({ + // method: "hardhat_reset" + // }); + + // } + + const Artgene721Implementation = await hre.ethers.getContractFactory( + "Artgene721Implementation" + ); + + if ( + Artgene721Implementation.bytecode.includes( + PLATFORM_ADDRESS.toLowerCase().slice(7) + ) + ) { + console.log( + "\nBytecode includes platform vanity address", + PLATFORM_ADDRESS + ); + } else { + console.log( + "Bytecode does not include platform vanity address", + Artgene721Implementation.bytecode, + PLATFORM_ADDRESS + ); + + // IGNORE THIS ERROR BECAUSE NOT USING VANITY ANYMORE + // throw new Error("Artgene721 bytecode does not include vanity address"); + } + + const futureAddress = await computeVanityAddress(); + console.log("Future address:", futureAddress); + console.log("Vanity address:", PLATFORM_ADDRESS); + + if (futureAddress === PLATFORM_ADDRESS) { + console.log( + "Address matches vanity address", + futureAddress, + PLATFORM_ADDRESS + ); + } else { + console.log( + "Address does not match vanity address", + futureAddress, + PLATFORM_ADDRESS + ); + + throw new Error( + `Address does not match vanity address: ${futureAddress} != ${PLATFORM_ADDRESS}` + ); + } + + console.log("\n\nDeploying ArtgenePlatform implementation..."); + + const vanity = await getVanityDeployer(); + + // check nonce of vanity account and if > 0, exit + const vanityNonce = await vanity.getTransactionCount(); + if (vanityNonce > 0) { + console.log("Vanity account has nonce > 0, exiting"); + return; + } + + // if balance of vanity account is 0, top it up + const vanityBalance = await vanity.getBalance(); + + console.log("vanity deployer address is", vanity.address); + console.log("vanity deployer balance is", vanityBalance); + + if (vanityBalance.eq(0)) { + console.log("Vanity account has balance 0"); + if (false && hre.network.name == "hardhat") { + // use hardhat_setBalance + await hre.network.provider.send("hardhat_setBalance", [ + vanity.address, + parseEther("0.1").toHexString(), + ]); + } else { + // use sendTransaction + + // // top up with Ethereum + await admin.sendTransaction({ + to: vanity.address, + value: hre.ethers.utils.parseEther("0.05"), + }); + } + } + + const ArtgenePlatform = await hre.ethers.getContractFactory( + "ArtgenePlatform" + ); + + // const platform = await ArtgenePlatform.attach(PLATFORM_ADDRESS); + + const platform = await ArtgenePlatform.connect(vanity).deploy({ + maxFeePerGas: ethers.utils.parseUnits(GAS_PRICE_GWEI, "gwei"), + maxPriorityFeePerGas: ethers.utils.parseUnits("2", "gwei"), + nonce: vanityNonce, + ...((hre.network.name == "mainnet" || hre.network.name == "goerli") && { + gasLimit: 1_000_000, + }), + }); + + await platform.deployed(); + + console.log("ArtgenePlatform implementation deployed to:", platform.address); + + // output gas spent, gas price and gas limit + const receipt = await platform.deployTransaction.wait(); + console.log("Gas used:", receipt.gasUsed.toString()); + console.log("Gas price:", GAS_PRICE_GWEI); + console.log( + "Gas cost (gwei)", + receipt.gasUsed.mul(ethers.BigNumber.from(GAS_PRICE_GWEI)).toString() + ); + + console.log( + "Transferring ownership to", + process.env.TRANSFER_TO ?? admin.address + ); + + await platform.connect(vanity).transferOwnership( + // admin.address, + process.env.TRANSFER_TO ?? admin.address, + { + maxFeePerGas: ethers.utils.parseUnits(GAS_PRICE_GWEI, "gwei"), + maxPriorityFeePerGas: ethers.utils.parseUnits("2", "gwei"), + gasLimit: 1_000_000, + } + ); + + console.log("Transferred successfully"); + + console.log("Sending all funds to", admin.address); + + // send all funds to admin + await sendAllFunds(vanity, admin.address); + + console.log("Sent successfully"); + + // write file to scripts/params.js + + const args: string[] = []; + + fs.writeFileSync( + "./scripts/params.js", + `module.exports = ${JSON.stringify(args)}`, + "utf8" + ); + + // skip waiting if running on hardhat network + if (hre.network.name == "hardhat") { + return; + } + + // print that we are waiting + console.log("Waiting 3 seconds..."); + + await delay(3000); + + // send verification request + console.log("Verifying...", JSON.stringify(args)); + + // verify contract + await hre.run("verify", { + contract: "contracts/ArtgenePlatform.sol:ArtgenePlatform", + address: platform.address, + constructorArgs: "./scripts/params.js", + network: "mainnet", + }); + +} + +// call main only if executed directly +if (process.argv[1] === __filename) { + // 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; + }); +} diff --git a/scripts/deploy-proxy.ts b/scripts/deploy-proxy.ts index ecf2b2f9..0d704692 100644 --- a/scripts/deploy-proxy.ts +++ b/scripts/deploy-proxy.ts @@ -8,9 +8,9 @@ const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); const GAS_PRICE_GWEI = "50"; -export const IMPLEMENTATION_ADDRESS = "0x00000721187b81D0aDac9d1E4D7Fd623ac788559"; +export const IMPLEMENTATION_ADDRESS = "0x000007214f56DaF21c803252cc610360C70C01D5"; export const IMPLEMENTATION_DEPLOYER_ADDRESS = - "0x768FcE871872DA304762f0A8274E2e8c2CA9458E"; + "0x1a597827e5d8818689200d521A28E477514db8B2"; export const sendAllFunds = async (account: Signer, to: Address) => { const balance = await account.getBalance(); diff --git a/test/artgene-implementation.js b/test/artgene-implementation.js index c9598b3f..36ecdcc2 100644 --- a/test/artgene-implementation.js +++ b/test/artgene-implementation.js @@ -16,6 +16,7 @@ const LimitAmountSaleExtension = artifacts.require("LimitAmountSaleExtension"); const Artgene721 = artifacts.require("Artgene721"); const { IMPLEMENTATION_ADDRESS, main: getImplementation } = require("../scripts/deploy-proxy.ts"); +const { main: getPlatform } = require("../scripts/deploy-platform.ts"); const ether = new BigNumber(1e18); @@ -29,6 +30,8 @@ contract("Artgene721Implementation – Implementation", accounts => { const code = await web3.eth.getCode(IMPLEMENTATION_ADDRESS); if (code === "0x") { + await getPlatform(); + await getImplementation(); }