diff --git a/contracts/meme/MemeActivityChecker.sol b/contracts/meme/MemeActivityChecker.sol new file mode 100644 index 0000000..d631568 --- /dev/null +++ b/contracts/meme/MemeActivityChecker.sol @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +// Meme Factory interface +interface IMemeFactory { + function mapAccountActivities(address multisig) external view returns (uint256); +} + +/// @dev Zero address. +error ZeroAddress(); + +/// @dev Zero value. +error ZeroValue(); + +/// @title MemeActivityChecker - Smart contract for meme contract interaction service staking activity check +/// @author Aleksandr Kuperman - +/// @author Andrey Lebedev - +/// @author David Vilela - +contract MemeActivityChecker { + // Liveness ratio in the format of 1e18 + uint256 public immutable livenessRatio; + // Meme Factory contract address + address public immutable memeFactory; + + /// @dev StakingNativeToken initialization. + /// @param _memeFactory Meme Factory contract address. + /// @param _livenessRatio Liveness ratio in the format of 1e18. + constructor(address _memeFactory, uint256 _livenessRatio) { + // Check the zero address + if (_memeFactory == address(0)) { + revert ZeroAddress(); + } + + // Check for zero value + if (_livenessRatio == 0) { + revert ZeroValue(); + } + + memeFactory = _memeFactory; + livenessRatio = _livenessRatio; + } + + /// @dev Gets service multisig nonces. + /// @param multisig Service multisig address. + /// @return nonces Set of a single service multisig nonce. + function getMultisigNonces(address multisig) external view virtual returns (uint256[] memory nonces) { + nonces = new uint256[](1); + // The nonce is equal to the meme factory contract interaction corresponding to a multisig activity + nonces[0] = IMemeFactory(memeFactory).mapAccountActivities(multisig); + } + + /// @dev Checks if the service multisig liveness ratio passes the defined liveness threshold. + /// @notice The formula for calculating the ratio is the following: + /// currentNonce - service multisig nonce at time now (block.timestamp); + /// lastNonce - service multisig nonce at the previous checkpoint or staking time (tsStart); + /// ratio = (currentNonce - lastNonce) / (block.timestamp - tsStart). + /// @param curNonces Current service multisig set of a single nonce. + /// @param lastNonces Last service multisig set of a single nonce. + /// @param ts Time difference between current and last timestamps. + /// @return ratioPass True, if the liveness ratio passes the check. + function isRatioPass( + uint256[] memory curNonces, + uint256[] memory lastNonces, + uint256 ts + ) external view virtual returns (bool ratioPass) { + // If the checkpoint was called in the exact same block, the ratio is zero + // If the current nonce is not greater than the last nonce, the ratio is zero + if (ts > 0 && curNonces[0] > lastNonces[0]) { + uint256 ratio = ((curNonces[0] - lastNonces[0]) * 1e18) / ts; + ratioPass = (ratio >= livenessRatio); + } + } +} \ No newline at end of file diff --git a/scripts/deployment/deploy_14_meme_activity_checker.js b/scripts/deployment/deploy_14_meme_activity_checker.js new file mode 100644 index 0000000..3290907 --- /dev/null +++ b/scripts/deployment/deploy_14_meme_activity_checker.js @@ -0,0 +1,77 @@ +/*global process*/ + +const { ethers } = require("hardhat"); +const { LedgerSigner } = require("@anders-t/ethers-ledger"); + +async function main() { + const fs = require("fs"); + const globalsFile = "globals.json"; + const dataFromJSON = fs.readFileSync(globalsFile, "utf8"); + let parsedData = JSON.parse(dataFromJSON); + const useLedger = parsedData.useLedger; + const derivationPath = parsedData.derivationPath; + const providerName = parsedData.providerName; + const gasPriceInGwei = parsedData.gasPriceInGwei; + const memeFactoryAddress = parsedData.memeFactoryAddress; + const livenessRatio = parsedData.livenessRatio; + + let networkURL = parsedData.networkURL; + if (providerName === "polygon") { + if (!process.env.ALCHEMY_API_KEY_MATIC) { + console.log("set ALCHEMY_API_KEY_MATIC env variable"); + } + networkURL += process.env.ALCHEMY_API_KEY_MATIC; + } else if (providerName === "polygonAmoy") { + if (!process.env.ALCHEMY_API_KEY_AMOY) { + console.log("set ALCHEMY_API_KEY_AMOY env variable"); + return; + } + networkURL += process.env.ALCHEMY_API_KEY_AMOY; + } + + const provider = new ethers.providers.JsonRpcProvider(networkURL); + const signers = await ethers.getSigners(); + + let EOA; + if (useLedger) { + EOA = new LedgerSigner(provider, derivationPath); + } else { + EOA = signers[0]; + } + // EOA address + const deployer = await EOA.getAddress(); + console.log("EOA is:", deployer); + + // Transaction signing and execution + console.log("14. EOA to deploy MemeActivityChecker"); + const gasPrice = ethers.utils.parseUnits(gasPriceInGwei, "gwei"); + const MemeActivityChecker = await ethers.getContractFactory("MemeActivityChecker"); + console.log("You are signing the following transaction: MemeActivityChecker.connect(EOA).deploy()"); + const memeActivityChecker = await MemeActivityChecker.connect(EOA).deploy(memeFactoryAddress, + livenessRatio, { gasPrice }); + const result = await memeActivityChecker.deployed(); + + // Transaction details + console.log("Contract deployment: MemeActivityChecker"); + console.log("Contract address:", memeActivityChecker.address); + console.log("Transaction:", result.deployTransaction.hash); + // Wait half a minute for the transaction completion + await new Promise(r => setTimeout(r, 30000)); + + // Writing updated parameters back to the JSON file + parsedData.memeActivityCheckerAddress = memeActivityChecker.address; + fs.writeFileSync(globalsFile, JSON.stringify(parsedData)); + + // Contract verification + if (parsedData.contractVerification) { + const execSync = require("child_process").execSync; + execSync("npx hardhat verify --constructor-args scripts/deployment/verify_14_meme_activity_checker.js --network " + providerName + " " + memeActivityChecker.address, { encoding: "utf-8" }); + } +} + +main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error); + process.exit(1); + }); diff --git a/scripts/deployment/globals_base_mainnet_contribute_alpha.json b/scripts/deployment/globals_base_mainnet_contribute_alpha.json index 005aa11..e5405b1 100644 --- a/scripts/deployment/globals_base_mainnet_contribute_alpha.json +++ b/scripts/deployment/globals_base_mainnet_contribute_alpha.json @@ -31,7 +31,7 @@ "livenessPeriod":"86400", "timeForEmissions":"2592000", "numAgentInstances":"1", - "agentIds":["14"], + "agentIds":[], "threshold":"0", "configHash":"0x0000000000000000000000000000000000000000000000000000000000000000", "proxyHash":"0xb89c1b3bdf2cf8827818646bce9a8f6e372885f8c55e5c07acbd307cb133b000", diff --git a/scripts/deployment/globals_base_mainnet_meme_alpha.json b/scripts/deployment/globals_base_mainnet_meme_alpha.json new file mode 100644 index 0000000..0660223 --- /dev/null +++ b/scripts/deployment/globals_base_mainnet_meme_alpha.json @@ -0,0 +1,42 @@ +{ + "contractVerification":true, + "useLedger":true, + "derivationPath":"m/44'/60'/2'/0/0", + "providerName":"base", + "networkURL":"https://mainnet.base.org", + "gasPriceInGwei":"2", + "bridgeMediatorAddress":"0xE49CB081e8d96920C38aA7AB90cb0294ab4Bc8EA", + "gnosisSafeAddress":"0x69f4D1788e39c87893C980c06EdF4b7f686e2938", + "gnosisSafeProxyFactoryAddress":"0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC", + "fallbackHandlerAddress":"0x017062a1dE2FE6b99BE3d9d37841FeD19F573804", + "olasAddress":"0x54330d28ca3357F294334BDC454a032e7f353416", + "multisigProxyHash130":"0xb89c1b3bdf2cf8827818646bce9a8f6e372885f8c55e5c07acbd307cb133b000", + "serviceRegistryAddress":"0x3C1fF68f5aa342D296d4DEe4Bb1cACCA912D95fE", + "serviceRegistryTokenUtilityAddress":"0x34C895f302D0b5cf52ec0Edd3945321EB0f83dd5", + "serviceManagerTokenAddress":"0x63e66d7ad413C01A7b49C7FF4e3Bb765C4E4bd1b", + "gnosisSafeMultisigImplementationAddress":"0xBb7e1D6Cb6F243D6bdE81CE92a9f2aFF7Fbe7eac", + "stakingTokenAddress":"0xEB5638eefE289691EcE01943f768EDBF96258a80", + "stakingFactoryAddress":"0x1cEe30D08943EB58EFF84DD1AB44a6ee6FEff63a", + "memeFactoryAddress":"", + "livenessRatio":"34722222222222", + "memeActivityCheckerAddress":"", + "stakingParams": + { + "metadataHash":"", + "maxNumServices":"100", + "rewardsPerSecond":"4398148148152", + "minStakingDeposit":"50000000000000000000", + "minNumStakingPeriods":"3", + "maxNumInactivityPeriods":"3", + "livenessPeriod":"86400", + "timeForEmissions":"2592000", + "numAgentInstances":"1", + "agentIds":[], + "threshold":"0", + "configHash":"0x0000000000000000000000000000000000000000000000000000000000000000", + "proxyHash":"0xb89c1b3bdf2cf8827818646bce9a8f6e372885f8c55e5c07acbd307cb133b000", + "serviceRegistry":"0x3C1fF68f5aa342D296d4DEe4Bb1cACCA912D95fE", + "activityChecker":"" + }, + "stakingTokenInstanceAddress":"" +} \ No newline at end of file diff --git a/scripts/deployment/globals_celo_mainnet_meme_alpha.json b/scripts/deployment/globals_celo_mainnet_meme_alpha.json new file mode 100644 index 0000000..6b9a9c9 --- /dev/null +++ b/scripts/deployment/globals_celo_mainnet_meme_alpha.json @@ -0,0 +1,40 @@ +{ + "contractVerification":true, + "useLedger":true, + "derivationPath":"m/44'/60'/2'/0/0", + "providerName":"celo", + "networkURL":"https://forno.celo.org", + "gasPriceInGwei":"2", + "bridgeMediatorAddress":"0x397125902ED2cA2d42104F621f448A2cE1bC8Fb7", + "gnosisSafeAddress":"0x69f4D1788e39c87893C980c06EdF4b7f686e2938", + "gnosisSafeProxyFactoryAddress":"0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC", + "olasAddress":"0xaCFfAe8e57Ec6E394Eb1b41939A8CF7892DbDc51", + "multisigProxyHash130":"0xb89c1b3bdf2cf8827818646bce9a8f6e372885f8c55e5c07acbd307cb133b000", + "serviceRegistryAddress":"0xE3607b00E75f6405248323A9417ff6b39B244b50", + "serviceRegistryTokenUtilityAddress":"0x3d77596beb0f130a4415df3D2D8232B3d3D31e44", + "serviceManagerTokenAddress":"0x34C895f302D0b5cf52ec0Edd3945321EB0f83dd5", + "stakingTokenAddress":"0xe1E1B286EbE95b39F785d8069f2248ae9C41b7a9", + "stakingFactoryAddress":"0x1c2cD884127b080F940b7546c1e9aaf525b1FA55", + "memeFactoryAddress":"", + "livenessRatio":"34722222222222", + "memeActivityCheckerAddress":"", + "stakingParams": + { + "metadataHash":"", + "maxNumServices":"100", + "rewardsPerSecond":"4398148148152", + "minStakingDeposit":"50000000000000000000", + "minNumStakingPeriods":"3", + "maxNumInactivityPeriods":"3", + "livenessPeriod":"86400", + "timeForEmissions":"2592000", + "numAgentInstances":"1", + "agentIds":[], + "threshold":"0", + "configHash":"0x0000000000000000000000000000000000000000000000000000000000000000", + "proxyHash":"0xb89c1b3bdf2cf8827818646bce9a8f6e372885f8c55e5c07acbd307cb133b000", + "serviceRegistry":"0xE3607b00E75f6405248323A9417ff6b39B244b50", + "activityChecker":"" + }, + "stakingTokenInstanceAddress":"" +} \ No newline at end of file diff --git a/scripts/deployment/globals_gnosis_mainnet_pearl_beta3.json b/scripts/deployment/globals_gnosis_mainnet_pearl_beta3.json new file mode 100644 index 0000000..5c69804 --- /dev/null +++ b/scripts/deployment/globals_gnosis_mainnet_pearl_beta3.json @@ -0,0 +1,39 @@ +{ + "contractVerification":true, + "useLedger":true, + "derivationPath":"m/44'/60'/2'/0/0", + "providerName":"gnosis", + "networkURL":"https://rpc.gnosischain.com", + "gasPriceInGwei":"4", + "gnosisSafeAddress":"0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552", + "gnosisSafeProxyFactoryAddress":"0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2", + "serviceRegistryAddress":"0x9338b5153AE39BB89f50468E608eD9d764B755fD", + "serviceRegistryTokenUtilityAddress":"0xa45E64d13A30a51b91ae0eb182e88a40e9b18eD8", + "olasAddress":"0xcE11e14225575945b8E6Dc0D4F2dD4C570f79d9f", + "multisigProxyHash130":"0xb89c1b3bdf2cf8827818646bce9a8f6e372885f8c55e5c07acbd307cb133b000", + "stakingNativeTokenAddress":"", + "stakingTokenAddress":"0xEa00be6690a871827fAfD705440D20dd75e67AB1", + "agentMechAddress":"0x77af31De935740567Cf4fF1986D04B2c964A786a", + "livenessRatio":"46296296296296", + "stakingActivityCheckerAddress":"0x155547857680A6D51bebC5603397488988DEb1c8", + "stakingFactoryAddress":"0xb0228CA253A88Bc8eb4ca70BCAC8f87b381f4700", + "stakingParams": + { + "metadataHash":"", + "maxNumServices":"100", + "rewardsPerSecond":"4398148148152", + "minStakingDeposit":"50000000000000000000", + "minNumStakingPeriods":"3", + "maxNumInactivityPeriods":"2", + "livenessPeriod":"86400", + "timeForEmissions":"2592000", + "numAgentInstances":"1", + "agentIds":["25"], + "threshold":"0", + "configHash":"0x0000000000000000000000000000000000000000000000000000000000000000", + "proxyHash":"0xb89c1b3bdf2cf8827818646bce9a8f6e372885f8c55e5c07acbd307cb133b000", + "serviceRegistry":"0x9338b5153AE39BB89f50468E608eD9d764B755fD", + "activityChecker":"0x155547857680A6D51bebC5603397488988DEb1c8" + }, + "stakingTokenInstanceAddress":"" +} \ No newline at end of file diff --git a/scripts/deployment/globals_gnosis_mainnet_pearl_beta4.json b/scripts/deployment/globals_gnosis_mainnet_pearl_beta4.json new file mode 100644 index 0000000..6b69dbf --- /dev/null +++ b/scripts/deployment/globals_gnosis_mainnet_pearl_beta4.json @@ -0,0 +1,39 @@ +{ + "contractVerification":true, + "useLedger":true, + "derivationPath":"m/44'/60'/2'/0/0", + "providerName":"gnosis", + "networkURL":"https://rpc.gnosischain.com", + "gasPriceInGwei":"4", + "gnosisSafeAddress":"0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552", + "gnosisSafeProxyFactoryAddress":"0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2", + "serviceRegistryAddress":"0x9338b5153AE39BB89f50468E608eD9d764B755fD", + "serviceRegistryTokenUtilityAddress":"0xa45E64d13A30a51b91ae0eb182e88a40e9b18eD8", + "olasAddress":"0xcE11e14225575945b8E6Dc0D4F2dD4C570f79d9f", + "multisigProxyHash130":"0xb89c1b3bdf2cf8827818646bce9a8f6e372885f8c55e5c07acbd307cb133b000", + "stakingNativeTokenAddress":"", + "stakingTokenAddress":"0xEa00be6690a871827fAfD705440D20dd75e67AB1", + "agentMechAddress":"0x77af31De935740567Cf4fF1986D04B2c964A786a", + "livenessRatio":"46296296296296", + "stakingActivityCheckerAddress":"0x155547857680A6D51bebC5603397488988DEb1c8", + "stakingFactoryAddress":"0xb0228CA253A88Bc8eb4ca70BCAC8f87b381f4700", + "stakingParams": + { + "metadataHash":"", + "maxNumServices":"100", + "rewardsPerSecond":"439814814815", + "minStakingDeposit":"50000000000000000000", + "minNumStakingPeriods":"3", + "maxNumInactivityPeriods":"2", + "livenessPeriod":"86400", + "timeForEmissions":"2592000", + "numAgentInstances":"1", + "agentIds":["25"], + "threshold":"0", + "configHash":"0x0000000000000000000000000000000000000000000000000000000000000000", + "proxyHash":"0xb89c1b3bdf2cf8827818646bce9a8f6e372885f8c55e5c07acbd307cb133b000", + "serviceRegistry":"0x9338b5153AE39BB89f50468E608eD9d764B755fD", + "activityChecker":"0x155547857680A6D51bebC5603397488988DEb1c8" + }, + "stakingTokenInstanceAddress":"" +} \ No newline at end of file diff --git a/scripts/deployment/verify_14_meme_activity_checker.js b/scripts/deployment/verify_14_meme_activity_checker.js new file mode 100644 index 0000000..1e6ef38 --- /dev/null +++ b/scripts/deployment/verify_14_meme_activity_checker.js @@ -0,0 +1,11 @@ +const fs = require("fs"); +const globalsFile = "globals.json"; +const dataFromJSON = fs.readFileSync(globalsFile, "utf8"); +const parsedData = JSON.parse(dataFromJSON); +const memeFactoryAddress = parsedData.memeFactoryAddress; +const livenessRatio = parsedData.livenessRatio; + +module.exports = [ + memeFactoryAddress, + livenessRatio +]; \ No newline at end of file