diff --git a/contracts/fuseFaucet/SuperfluidFacuet.sol b/contracts/fuseFaucet/SuperfluidFacuet.sol new file mode 100644 index 00000000..d2d11432 --- /dev/null +++ b/contracts/fuseFaucet/SuperfluidFacuet.sol @@ -0,0 +1,127 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.0; + +import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/utils/math/SafeMathUpgradeable.sol"; + +/** + * to be deployed on base to support superfluid airdrop to G$ users + */ +contract SuperfluidFaucet is + Initializable, + UUPSUpgradeable, + AccessControlUpgradeable +{ + using SafeMathUpgradeable for uint256; + + bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE"); + + uint256 public toppingAmount; + uint256 public maxAmountPerPeriod; + uint256 public toppingPeriod; + + struct RecipientInfo { + uint256 lastWithdrawalPeriod; + uint256 totalWithdrawnThisPeriod; + } + + mapping(address => RecipientInfo) public recipientInfo; + + event WalletTopped(address recipient, uint256 amount); + event SettingsUpdated( + uint256 toppingAmount, + uint256 maxAmountPerPeriod, + uint256 toppingPeriod + ); + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + function initialize( + uint256 _toppingAmount, + uint256 _maxAmountPerPeriod, + uint256 _toppingPeriod + ) public initializer { + __AccessControl_init(); + __UUPSUpgradeable_init(); + + _setupRole(DEFAULT_ADMIN_ROLE, msg.sender); + _setupRole(ADMIN_ROLE, msg.sender); + toppingAmount = _toppingAmount; + maxAmountPerPeriod = _maxAmountPerPeriod; + toppingPeriod = _toppingPeriod; + } + + function updateSettings( + uint256 _toppingAmount, + uint256 _maxAmountPerPeriod, + uint256 _toppingPeriod + ) external onlyRole(ADMIN_ROLE) { + toppingAmount = _toppingAmount; + maxAmountPerPeriod = _maxAmountPerPeriod; + toppingPeriod = _toppingPeriod; + emit SettingsUpdated(_toppingAmount, _maxAmountPerPeriod, _toppingPeriod); + } + + function canTop(address recipient) public view returns (bool) { + if (recipient == address(0)) return false; + if (recipient.balance >= toppingAmount / 2) return false; + + uint256 amountToSend = toppingAmount.sub(recipient.balance); + if (address(this).balance < amountToSend) return false; + + uint256 currentPeriod = block.timestamp / toppingPeriod; + RecipientInfo storage info = recipientInfo[recipient]; + + if (currentPeriod > info.lastWithdrawalPeriod) { + return true; // New period, reset counters + } + + if (info.totalWithdrawnThisPeriod.add(amountToSend) > maxAmountPerPeriod) + return false; + + return true; + } + + function topWallet(address payable recipient) external onlyRole(ADMIN_ROLE) { + require(canTop(recipient), "Recipient cannot be topped up"); + + uint256 currentPeriod = block.timestamp / toppingPeriod; + RecipientInfo storage info = recipientInfo[recipient]; + + if (currentPeriod > info.lastWithdrawalPeriod) { + info.totalWithdrawnThisPeriod = 0; + info.lastWithdrawalPeriod = currentPeriod; + } + + uint256 amountToSend = toppingAmount.sub(recipient.balance); + require( + address(this).balance >= amountToSend, + "Insufficient contract balance for topping up" + ); + + info.totalWithdrawnThisPeriod = info.totalWithdrawnThisPeriod.add( + amountToSend + ); + + (bool success, ) = recipient.call{ value: amountToSend }(""); + require(success, "Failed to send Ether"); + + emit WalletTopped(recipient, amountToSend); + } + + receive() external payable {} + + function withdraw() external onlyRole(DEFAULT_ADMIN_ROLE) { + uint256 balance = address(this).balance; + payable(msg.sender).transfer(balance); + } + + function _authorizeUpgrade( + address newImplementation + ) internal override onlyRole(DEFAULT_ADMIN_ROLE) {} +} diff --git a/contracts/utils/AdminWallet.sol b/contracts/utils/AdminWallet.sol index 651de0e6..318370af 100644 --- a/contracts/utils/AdminWallet.sol +++ b/contracts/utils/AdminWallet.sol @@ -34,7 +34,8 @@ contract AdminWallet is uint64 public maxDailyNewWallets; uint64 public day; - ERC20 public gd; + ERC20 private gd_removed; + mapping(address => uint256) public lastGdBalance; // only top non whitelisted if active G$ users event AdminsAdded(address payable[] indexed admins); @@ -66,7 +67,6 @@ contract AdminWallet is addAdmins(_admins); } if (msg.sender != _owner) revokeRole(DEFAULT_ADMIN_ROLE, msg.sender); - gd = ERC20(nameService.getAddress("GOODDOLLAR")); } modifier onlyOwner() { @@ -74,10 +74,6 @@ contract AdminWallet is _; } - function upgrade() public onlyOwner { - gd = ERC20(nameService.getAddress("GOODDOLLAR")); - } - function getIdentity() public view returns (IIdentityV2) { return IIdentityV2(nameService.getAddress("IDENTITY")); } @@ -236,6 +232,8 @@ contract AdminWallet is * @param _user The address to transfer to */ function topWallet(address payable _user) public onlyAdmin reimburseGas { + ERC20 gd = ERC20(nameService.getAddress("GOODDOLLAR")); + uint256 gdBalance = gd.balanceOf(_user); require( getIdentity().isWhitelisted(_user) || gdBalance != lastGdBalance[_user], diff --git a/hardhat.config.ts b/hardhat.config.ts index 7e59f0be..3a46c5fa 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -17,26 +17,20 @@ import { airdrop } from "./scripts/governance/airdropCalculationSorted"; import { airdrop as repAirdropRecover } from "./scripts/governance/airdropCalculationRecover"; import { airdrop as goodCheckpoint } from "./scripts/governance/goodCheckpointSorted"; -import { - airdrop as gdxAirdrop, - airdropRecover as gdxAirdropRecover -} from "./scripts/gdx/gdxAirdropCalculation"; +import { airdrop as gdxAirdrop, airdropRecover as gdxAirdropRecover } from "./scripts/gdx/gdxAirdropCalculation"; import { sumStakersGdRewards } from "./scripts/staking/stakersGdRewardsCalculation"; import { verify } from "./scripts/verify"; import { ethers } from "ethers"; import { fstat, readFileSync, writeFileSync } from "fs"; config(); -const mnemonic = - process.env.MNEMONIC || - "test test test test test test test test test test test junk"; -const deployerPrivateKey = - process.env.PRIVATE_KEY || ethers.utils.hexZeroPad("0x11", 32); +const mnemonic = process.env.MNEMONIC || "test test test test test test test test test test test junk"; +const deployerPrivateKey = process.env.PRIVATE_KEY || ethers.utils.hexZeroPad("0x11", 32); const infura_api = process.env.INFURA_API; const alchemy_key = process.env.ALCHEMY_KEY; const etherscan_key = process.env.ETHERSCAN_KEY; const celoscan_key = process.env.CELOSCAN_KEY; - +const basescan_key = process.env.BASESCAN_KEY; const ethplorer_key = process.env.ETHPLORER_KEY; const MAINNET_URL = "https://mainnet.infura.io/v3/" + infura_api; @@ -70,7 +64,8 @@ const hhconfig: HardhatUserConfig = { apiKey: { mainnet: etherscan_key, celo: celoscan_key, - alfajores: celoscan_key + alfajores: celoscan_key, + base: basescan_key }, customChains: [ { @@ -99,9 +94,7 @@ const hhconfig: HardhatUserConfig = { networks: { hardhat: { - chainId: process.env.FORK_CHAIN_ID - ? Number(process.env.FORK_CHAIN_ID) - : 4447, + chainId: process.env.FORK_CHAIN_ID ? Number(process.env.FORK_CHAIN_ID) : 4447, allowUnlimitedContractSize: true, accounts: { accountsBalance: "10000000000000000000000000" @@ -254,6 +247,12 @@ const hhconfig: HardhatUserConfig = { gasPrice: 5000000000, chainId: 42220 }, + "development-base": { + accounts: { mnemonic }, + url: "https://mainnet.base.org", + initialBaseFeePerGas: 0, + gasPrice: 8e6 + }, gnosis: { accounts: [deployerPrivateKey], url: "https://rpc.gnosischain.com", @@ -279,10 +278,7 @@ task("repAirdrop", "Calculates airdrop data and merkle tree") const actions = airdrop(hre.ethers, ethplorer_key, etherscan_key); switch (taskArgs.action) { case "calculate": - return actions.collectAirdropData( - taskArgs.fusesnapshotblock, - taskArgs.ethsnapshotblock - ); + return actions.collectAirdropData(taskArgs.fusesnapshotblock, taskArgs.ethsnapshotblock); case "tree": return actions.buildMerkleTree(); case "proof": @@ -292,10 +288,7 @@ task("repAirdrop", "Calculates airdrop data and merkle tree") } }); -task( - "repAirdropRecover", - "Calculates airdrop data and merkle tree after critical bug" -) +task("repAirdropRecover", "Calculates airdrop data and merkle tree after critical bug") .addParam("action", "calculate/tree/proof") .addOptionalPositionalParam("address", "proof for address") .setAction(async (taskArgs, hre) => { @@ -344,10 +337,7 @@ task("gdxAirdropRecover", "Calculates new airdrop data for recovery") } }); -task( - "goodCheckpoint", - "Calculates good checkpoint data and merkle tree for GOOD sync" -) +task("goodCheckpoint", "Calculates good checkpoint data and merkle tree for GOOD sync") .addParam("action", "calculate/tree/proof") .addOptionalPositionalParam("address", "proof for address") .setAction(async (taskArgs, hre) => { @@ -364,17 +354,12 @@ task( } }); -task("verifyjson", "verify contracts on etherscan").setAction( - async (taskArgs, hre) => { - return verify(hre); - } -); +task("verifyjson", "verify contracts on etherscan").setAction(async (taskArgs, hre) => { + return verify(hre); +}); export default hhconfig; -task( - "sumStakersGdRewards", - "Sums the GoodDollar reward for each staker" -).setAction(async (taskArgs, hre) => { +task("sumStakersGdRewards", "Sums the GoodDollar reward for each staker").setAction(async (taskArgs, hre) => { const actions = sumStakersGdRewards(hre.ethers); return actions.getStakersGdRewards(); }); @@ -387,10 +372,7 @@ task("cleanflat", "Cleans multiple SPDX and Pragma from flattened file") }); // Remove every line started with "// SPDX-License-Identifier:" - flattened = flattened.replace( - /SPDX-License-Identifier:/gm, - "License-Identifier:" - ); + flattened = flattened.replace(/SPDX-License-Identifier:/gm, "License-Identifier:"); flattened = `// SPDX-License-Identifier: MIXED\n\n${flattened}`; diff --git a/releases/deployment.json b/releases/deployment.json index 83e45cab..9955d755 100644 --- a/releases/deployment.json +++ b/releases/deployment.json @@ -609,5 +609,10 @@ "DAI": "0xfcDB4564c18A9134002b9771816092C9693622e3", "cDAI": "0x32EEce76C2C2e8758584A83Ee2F522D4788feA0f", "COMP": "0x927b167526bAbB9be047421db732C663a0b77B11" + }, + "development-base": { + "ProxyFactory": "0x5B22F5623ECB00E288539346F389E05f65e226ae", + "SuperfluidFaucet": "0x88d18B06E55b33B94578628fcB5a6e1D198b0e73", + "AdminWallet": "0x6672C998C49635aA6825Be355fF2e731f417B674" } } diff --git a/scripts/multichain-deploy/0_proxyFactory-deploy.ts b/scripts/multichain-deploy/0_proxyFactory-deploy.ts index bd66d42c..9104a250 100644 --- a/scripts/multichain-deploy/0_proxyFactory-deploy.ts +++ b/scripts/multichain-deploy/0_proxyFactory-deploy.ts @@ -23,6 +23,9 @@ export const deployUniversalProxyFactory = async () => { s: "0x2222222222222222222222222222222222222222222222222222222222222222" }; //modify tx data a little so we get different contract address for different envs + if (name.includes("development-base")) { + deployTx.gasPrice = 7e7; + } if (name.includes("staging")) { deployTx.gasLimit = 892000; } else if (name.includes("production")) { @@ -34,12 +37,18 @@ export const deployUniversalProxyFactory = async () => { const deployer = ethers.utils.recoverAddress(txHash, signer); let [funder] = await ethers.getSigners(); - let tx = await ( - await funder.sendTransaction({ - to: deployer, - value: ethers.BigNumber.from(deployTx.gasPrice).mul(deployTx.gasLimit) - }) - ).wait(); + const curBalance = await ethers.provider.getBalance(deployer); + const deployCost = ethers.BigNumber.from(deployTx.gasPrice).mul(deployTx.gasLimit); + + let tx = {}; + if (curBalance.lt(deployCost)) { + tx = await ( + await funder.sendTransaction({ + to: deployer, + value: deployCost.sub(curBalance) + }) + ).wait(); + } if (isProduction) verifyProductionSigner(funder); @@ -47,23 +56,19 @@ export const deployUniversalProxyFactory = async () => { fundingTx: tx.transactionHash, deployer, funder: funder.address, - deployerBalance: ethers.utils.formatUnits( - await ethers.provider.getBalance(deployer) - ) + deployerBalance: ethers.utils.formatUnits(await ethers.provider.getBalance(deployer)) }); const signedTx = ethers.utils.serializeTransaction(deployTx, signer); - const result = await (await ethers.provider.sendTransaction(signedTx)).wait(); - console.log({ result }); + const proxyTx = await ethers.provider.sendTransaction(signedTx); + console.log({ proxyTx }); + const result = await proxyTx.wait(); return ethers.getContractAt("ProxyFactory1967", result.contractAddress); }; export const deployProxy = async (defaultAdmin = null) => { let release: { [key: string]: any } = dao[network.name] || {}; - if ( - network.name.match(/production|staging|fuse|development/) && - release.ProxyFactory - ) { + if (network.name.match(/production|staging|fuse|development/) && release.ProxyFactory) { throw new Error("ProxyFactory already exists for env"); } // let [root] = await ethers.getSigners(); diff --git a/scripts/multichain-deploy/7_superfluidfaucet-deploy.ts b/scripts/multichain-deploy/7_superfluidfaucet-deploy.ts new file mode 100644 index 00000000..5d29414b --- /dev/null +++ b/scripts/multichain-deploy/7_superfluidfaucet-deploy.ts @@ -0,0 +1,96 @@ +/*** + * Deploy helper contracts + * AdminWallet, Faucet, Invites + */ +import { network, ethers, upgrades, run } from "hardhat"; +import { Contract } from "ethers"; +import { defaultsDeep } from "lodash"; +import { getImplementationAddress } from "@openzeppelin/upgrades-core"; +import { deployDeterministic, verifyProductionSigner, verifyContract } from "./helpers"; +import releaser from "../releaser"; +import ProtocolSettings from "../../releases/deploy-settings.json"; +import dao from "../../releases/deployment.json"; +import { TransactionResponse } from "@ethersproject/providers"; + +const { name } = network; + +const printDeploy = async (c: Contract | TransactionResponse): Promise => { + if (c instanceof Contract) { + await c.deployed(); + console.log("deployed to: ", c.address); + } + if (c.wait) { + await c.wait(); + console.log("tx done:", c.hash); + } + return c; +}; + +export const deployHelpers = async () => { + let release: { [key: string]: any } = dao[network.name] || {}; + + let [root] = await ethers.getSigners(); + const isProduction = network.name.includes("production"); + + if (isProduction) verifyProductionSigner(root); + + //generic call permissions + let schemeMock = root; + + console.log("got signers:", { + network, + root: root.address, + schemeMock: schemeMock.address, + balance: await ethers.provider.getBalance(root.address).then(_ => _.toString()) + }); + + const walletAdmins = []; + for (let i = 0; i < 10; i++) { + const wallet = ethers.Wallet.fromMnemonic(process.env.ADMIN_WALLET_MNEMONIC, `m/44'/60'/0'/0/${i}`); + walletAdmins.push(wallet.address); + } + + const gasprice = 1e8; + console.log("deploying adminwallet", { walletAdmins }); + const AdminWallet = release.AdminWallet + ? await ethers.getContractAt("AdminWallet", release.AdminWallet) + : ((await deployDeterministic( + { + // address payable[] memory _admins, + // NameService _ns, + // address _owner, + // uint256 _gasPrice + name: "AdminWallet", + salt: "AdminWallet", + isUpgradeable: true + }, + [walletAdmins, ethers.constants.AddressZero, root.address, gasprice] + ).then(printDeploy)) as Contract); + + const Faucet = release.SuperfluidFaucet + ? await ethers.getContractAt("SuperfluidFaucet", release.SuperfluidFaucet) + : ((await deployDeterministic( + { + name: "SuperfluidFaucet", + salt: "SuperfluidFaucet", + isUpgradeable: true + }, + [ethers.utils.parseEther("0.000003"), ethers.utils.parseEther("0.000003"), 30] + ).then(printDeploy)) as Contract); + + const torelease = { + SuperfluidFaucet: Faucet.address, + AdminWallet: AdminWallet.address + }; + await releaser(torelease, network.name, "deployment", false); + + let impl = await getImplementationAddress(ethers.provider, AdminWallet.address); + await verifyContract(impl, "contracts/utils/AdminWallet.sol:AdminWallet", network.name); + impl = await getImplementationAddress(ethers.provider, Faucet.address); + await verifyContract(impl, "contracts/fuseFaucet/SuperfluidFaucet.sol:SuperfluidFaucet", network.name); +}; + +export const main = async () => { + await deployHelpers(); +}; +if (process.argv[1].includes("7_superfluidfaucet")) main(); diff --git a/scripts/multichain-deploy/helpers.ts b/scripts/multichain-deploy/helpers.ts index c07e1352..e30a3947 100644 --- a/scripts/multichain-deploy/helpers.ts +++ b/scripts/multichain-deploy/helpers.ts @@ -194,7 +194,7 @@ export const executeViaGuardian = async ( for (let i = 0; i < contracts.length; i++) { const contract = contracts[i]; if (!contract) { - console.warn("skipping executing missing contract", i, contracts[i], functionSigs[i], functionInputs[i]) + console.warn("skipping executing missing contract", i, contracts[i], functionSigs[i], functionInputs[i]); continue; } console.log("executing:", contracts[i], functionSigs[i], functionInputs[i]); @@ -399,17 +399,14 @@ export const executeViaSafe = async ( export const verifyContract = async ( address, - contractName, + contractPath, networkName = network.name, proxyName?: string, forcedConstructorArguments?: string ) => { - let networkProvider = networkName.includes("-") ? networkName.split("-")[1] : "fuse"; - networkProvider = networkProvider === "mainnet" ? "ethereum" : networkProvider; - console.log("truffle compile..."); - await exec("npx truffle compile"); - const cmd = `npx truffle run verify ${proxyName ? "--custom-proxy " + proxyName : ""} ${contractName}@${address} ${forcedConstructorArguments ? "--forceConstructorArgs string:" + forcedConstructorArguments.slice(2) : "" - } --network ${networkProvider}`; + const cmd = `yarn hardhat verify --contract ${contractPath} ${address} ${ + forcedConstructorArguments ?? "" + } --network ${networkName}`; console.log("running...:", cmd); await exec(cmd).then(({ stdout, stderr }) => { console.log("Result for:", cmd);