diff --git a/README.md b/README.md index d7fd036..6cf896b 100644 --- a/README.md +++ b/README.md @@ -14,9 +14,9 @@ Administrators can distribute rewards and update parameters related to reward di Main contract code function introduction: -1. LockingManager: This contract is a main function contract, allowing users to lock tokens to apply to become a sequencer, receive rewards, unlock tokens to exit the sequencer, reward distribution, and other management functions can refer to the contract source code. -2. LockingEscrow: This contract keeps locked tokens by LockingManager -3. MetisSequencerSet: This contract controls blocks production +1. LockingPool: This contract is a main function contract, allowing users to lock tokens to apply to become a sequencer, receive rewards, unlock tokens to exit the sequencer, reward distribution, and other management functions can refer to the contract source code. +2. LockingInfo: This contract keeps locked tokens information for LockingPool +3. MetisSequencerSet: This contract controls blocks production on metis layer2 # Example diff --git a/contracts/LockingEscrow.sol b/contracts/LockingInfo.sol similarity index 97% rename from contracts/LockingEscrow.sol rename to contracts/LockingInfo.sol index 3fa5550..d38c51e 100644 --- a/contracts/LockingEscrow.sol +++ b/contracts/LockingInfo.sol @@ -7,10 +7,10 @@ import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; import {IL1ERC20Bridge} from "./interfaces/IL1ERC20Bridge.sol"; -import {ILockingEscrow} from "./interfaces/ILockingEscrow.sol"; +import {ILockingInfo} from "./interfaces/ILockingInfo.sol"; import {ISeqeuncerInfo} from "./interfaces/ISeqeuncerInfo.sol"; -contract LockingEscrow is ILockingEscrow, OwnableUpgradeable { +contract LockingInfo is ILockingInfo, OwnableUpgradeable { error NotManager(); using SafeERC20 for IERC20; @@ -178,11 +178,11 @@ contract LockingEscrow is ILockingEscrow, OwnableUpgradeable { emit UnlockInit( _seq.signer, _seqId, + reward, _seq.nonce, _seq.deactivationBatch, _seq.deactivationTime, - _seq.unlockClaimTime, - reward + _seq.unlockClaimTime ); } diff --git a/contracts/LockingManager.sol b/contracts/LockingPool.sol similarity index 87% rename from contracts/LockingManager.sol rename to contracts/LockingPool.sol index 71cbc36..c18b847 100644 --- a/contracts/LockingManager.sol +++ b/contracts/LockingPool.sol @@ -3,12 +3,12 @@ pragma solidity 0.8.20; import {PausableUpgradeable} from "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol"; -import {ILockingEscrow} from "./interfaces/ILockingEscrow.sol"; -import {ILockingManager} from "./interfaces/ILockingManager.sol"; +import {ILockingInfo} from "./interfaces/ILockingInfo.sol"; +import {ILockingPool} from "./interfaces/ILockingPool.sol"; import {SeqeuncerInfo} from "./SeqeuncerInfo.sol"; -contract LockingManager is ILockingManager, PausableUpgradeable, SeqeuncerInfo { +contract LockingPool is ILockingPool, PausableUpgradeable, SeqeuncerInfo { error NotMpc(); error BFTFail(); @@ -19,7 +19,7 @@ contract LockingManager is ILockingManager, PausableUpgradeable, SeqeuncerInfo { uint256 endEpoch; // end epoch number for current batch } - ILockingEscrow public escorow; + ILockingInfo public escorow; // delay time for unlock uint256 public WITHDRAWAL_DELAY; @@ -45,7 +45,7 @@ contract LockingManager is ILockingManager, PausableUpgradeable, SeqeuncerInfo { endEpoch: 0 }); - escorow = ILockingEscrow(_escorow); + escorow = ILockingInfo(_escorow); __Pausable_init(); __LockingBadge_init(); @@ -96,49 +96,20 @@ contract LockingManager is ILockingManager, PausableUpgradeable, SeqeuncerInfo { /** * @dev updateSigner Allow sqeuencer to update new signers to replace old signer addresses,and NFT holder will be transfer driectly - * @param _seqId unique integer to identify a sequencer. + * @param _seqId the sequencer id * @param _signerPubkey the new signer pubkey address */ function updateSigner( uint256 _seqId, bytes calldata _signerPubkey ) external whitelistRequired { - Sequencer storage seq = sequencers[_seqId]; - if (seq.status != Status.Active) { - revert SeqNotActive(); - } - - // only update by the signer - address signer = seq.signer; - if (signer != msg.sender) { - revert NotSeqSigner(); - } - - require(_signerPubkey.length == 64, "invalid pubkey"); - address newSigner = address(uint160(uint256(keccak256(_signerPubkey)))); - require(newSigner != address(0), "empty address"); - - // the new signer should not be a signer before - if (seqSigners[newSigner] != 0) { - revert OwnedSigner(); - } - seq.signer = newSigner; - seqSigners[newSigner] = _seqId; - - // invalid it - seqSigners[signer] = type(uint256).max; - - // set signer updating batch id - seq.updatingBatch = currentBatch.id; - - uint256 nonce = seq.nonce + 1; - seq.nonce = nonce; - - emit SignerChange(_seqId, signer, newSigner, nonce, _signerPubkey); + _updateSigner(_seqId, currentBatch.id, _signerPubkey); } /** * @dev lockFor lock Metis and participate in the sequencer node + * the msg.sender will be owner of the seqeuncer + * the owner has abilities to leverage lock/relock/unlock/cliam * @param _signer Sequencer signer address * @param _amount Amount of L1 metis token to lock for. * @param _signerPubkey Sequencer signer pubkey, it should be uncompressed @@ -163,7 +134,7 @@ contract LockingManager is ILockingManager, PausableUpgradeable, SeqeuncerInfo { owner: msg.sender, signer: _signer, pubkey: _signerPubkey, - rewardRecipient: address(0), // the recepient should be update afterward + rewardRecipient: address(0), // update it using `setSequencerRewardRecipient` after then status: Status.Active }); @@ -278,7 +249,7 @@ contract LockingManager is ILockingManager, PausableUpgradeable, SeqeuncerInfo { delete seqOwners[seq.owner]; // invalid it - seqSigners[seq.signer] = type(uint256).max; + _invalidSignerAddress(seq.signer); escorow.finalizeUnlock{value: msg.value}( msg.sender, @@ -404,6 +375,7 @@ contract LockingManager is ILockingManager, PausableUpgradeable, SeqeuncerInfo { seq.nonce = nonce; escorow.initializeUnlock{value: msg.value}(_seqId, _l2Gas, seq); + // clear reward at last seq.reward = 0; } } diff --git a/contracts/SeqeuncerInfo.sol b/contracts/SeqeuncerInfo.sol index d961b84..01c6863 100644 --- a/contracts/SeqeuncerInfo.sol +++ b/contracts/SeqeuncerInfo.sol @@ -15,9 +15,13 @@ contract SeqeuncerInfo is OwnableUpgradeable, ISeqeuncerInfo { mapping(uint256 seqId => Sequencer _seq) public sequencers; // sequencer owner address => sequencerId + // Note: sequencerId starts from 1 + // seqeuncer does not exist if the seqId is 0 mapping(address owner => uint256 seqId) public seqOwners; // sequencer signer address => sequencerId + // the signer can't be reused afterward if the sequencer exits or updates its pubkey + // It means that the signer is invalid if the seqId is type(uint256).max mapping(address signer => uint256 seqId) public seqSigners; // sequencer status => count @@ -127,6 +131,61 @@ contract SeqeuncerInfo is OwnableUpgradeable, ISeqeuncerInfo { return _seqId; } + /** + * @dev _updateSigner Allow sqeuencer to update new signers to replace old signer addresses,and NFT holder will be transfer driectly + * @param _seqId unique integer to identify a sequencer + * @param _batchId current batch id + * @param _signerPubkey the new signer pubkey address + */ + function _updateSigner( + uint256 _seqId, + uint256 _batchId, + bytes memory _signerPubkey + ) internal { + Sequencer storage seq = sequencers[_seqId]; + if (seq.status != Status.Active) { + revert SeqNotActive(); + } + + // only update by the signer + address signer = seq.signer; + if (signer != msg.sender) { + revert NotSeqSigner(); + } + + address newSigner = _getAddrByPubkey(_signerPubkey); + // the new signer should not be a signer before + if (seqSigners[newSigner] != 0) { + revert OwnedSigner(); + } + seq.signer = newSigner; + seqSigners[newSigner] = _seqId; + + // invalid it + _invalidSignerAddress(signer); + + // set signer updating batch id + seq.updatingBatch = _batchId; + + uint256 nonce = seq.nonce + 1; + seq.nonce = nonce; + + emit SignerChange(_seqId, signer, newSigner, nonce, _signerPubkey); + } + + function _getAddrByPubkey( + bytes memory _signerPubkey + ) internal pure returns (address) { + require(_signerPubkey.length == 64, "invalid pubkey"); + address newSigner = address(uint160(uint256(keccak256(_signerPubkey)))); + require(newSigner != address(0), "empty address"); + return newSigner; + } + + function _invalidSignerAddress(address _signer) internal { + seqSigners[_signer] = type(uint256).max; + } + /** * @dev This empty reserved space is put in place to allow future versions to add new * variables without shifting down storage in the inheritance chain. diff --git a/contracts/interfaces/ILockingEscrow.sol b/contracts/interfaces/ILockingInfo.sol similarity index 98% rename from contracts/interfaces/ILockingEscrow.sol rename to contracts/interfaces/ILockingInfo.sol index 931fb4d..0219e45 100644 --- a/contracts/interfaces/ILockingEscrow.sol +++ b/contracts/interfaces/ILockingInfo.sol @@ -3,7 +3,7 @@ pragma solidity 0.8.20; import {ISeqeuncerInfo} from "./ISeqeuncerInfo.sol"; -interface ILockingEscrow { +interface ILockingInfo { /** * @dev Emitted when min lock amount update in 'UpdateMinAmounts' * @param _newMinLock new min lock. @@ -89,11 +89,11 @@ interface ILockingEscrow { event UnlockInit( address indexed user, uint256 indexed sequencerId, + uint256 amount, uint256 nonce, uint256 deactivationBatch, uint256 deactivationTime, - uint256 unlockClaimTime, - uint256 indexed amount + uint256 unlockClaimTime ); /** diff --git a/contracts/interfaces/ILockingManager.sol b/contracts/interfaces/ILockingPool.sol similarity index 96% rename from contracts/interfaces/ILockingManager.sol rename to contracts/interfaces/ILockingPool.sol index fc0a579..bc052b2 100644 --- a/contracts/interfaces/ILockingManager.sol +++ b/contracts/interfaces/ILockingPool.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-2.0-or-later pragma solidity 0.8.20; -interface ILockingManager { +interface ILockingPool { /** * @dev Emitted when WITHDRAWAL_DELAY is updated. * @param _cur current withdraw delay time diff --git a/ts-src/deploy/01_LockingEscrow.ts b/ts-src/deploy/01_LockingInfo.ts similarity index 88% rename from ts-src/deploy/01_LockingEscrow.ts rename to ts-src/deploy/01_LockingInfo.ts index 338d3a9..56eed81 100644 --- a/ts-src/deploy/01_LockingEscrow.ts +++ b/ts-src/deploy/01_LockingInfo.ts @@ -1,5 +1,5 @@ import { DeployFunction } from "hardhat-deploy/types"; -import { LockingEscrowContractName } from "../utils/constant"; +import { LockingInfoContractName } from "../utils/constant"; const func: DeployFunction = async function (hre) { if (!hre.network.tags["l1"]) { @@ -35,7 +35,7 @@ const func: DeployFunction = async function (hre) { l2Chainid, ); - await hre.deployments.deploy(LockingEscrowContractName, { + await hre.deployments.deploy(LockingInfoContractName, { from: deployer, proxy: { proxyContract: "OpenZeppelinTransparentProxy", @@ -51,6 +51,6 @@ const func: DeployFunction = async function (hre) { }); }; -func.tags = [LockingEscrowContractName, "l1"]; +func.tags = [LockingInfoContractName, "l1"]; export default func; diff --git a/ts-src/deploy/02_LockingManager.ts b/ts-src/deploy/02_LockingPool.ts similarity index 64% rename from ts-src/deploy/02_LockingManager.ts rename to ts-src/deploy/02_LockingPool.ts index 97042d7..dc8e272 100644 --- a/ts-src/deploy/02_LockingManager.ts +++ b/ts-src/deploy/02_LockingPool.ts @@ -1,7 +1,7 @@ import { DeployFunction } from "hardhat-deploy/types"; import { - LockingEscrowContractName, - LockingManagerContractName, + LockingInfoContractName, + LockingPoolContractName, } from "../utils/constant"; const func: DeployFunction = async function (hre) { @@ -11,18 +11,18 @@ const func: DeployFunction = async function (hre) { const { deployer } = await hre.getNamedAccounts(); - const { address: LockingEscrowAddress } = await hre.deployments.get( - LockingEscrowContractName, + const { address: LockingInfoAddress } = await hre.deployments.get( + LockingInfoContractName, ); - await hre.deployments.deploy(LockingManagerContractName, { + await hre.deployments.deploy(LockingPoolContractName, { from: deployer, proxy: { proxyContract: "OpenZeppelinTransparentProxy", execute: { init: { methodName: "initialize", - args: [LockingEscrowAddress], + args: [LockingInfoAddress], }, }, }, @@ -31,6 +31,6 @@ const func: DeployFunction = async function (hre) { }); }; -func.tags = [LockingManagerContractName, "l1"]; +func.tags = [LockingPoolContractName, "l1"]; export default func; diff --git a/ts-src/deploy/04_L1Config.ts b/ts-src/deploy/04_L1Config.ts index ef9d0d1..8cbfc6e 100644 --- a/ts-src/deploy/04_L1Config.ts +++ b/ts-src/deploy/04_L1Config.ts @@ -1,7 +1,7 @@ import { DeployFunction } from "hardhat-deploy/types"; import { - LockingEscrowContractName, - LockingManagerContractName, + LockingInfoContractName, + LockingPoolContractName, } from "../utils/constant"; const func: DeployFunction = async function (hre) { @@ -9,22 +9,22 @@ const func: DeployFunction = async function (hre) { throw new Error(`current network ${hre.network.name} is not an L1`); } - const { address: LockingEscrowAddress } = await hre.deployments.get( - LockingEscrowContractName, + const { address: LockingInfoAddress } = await hre.deployments.get( + LockingInfoContractName, ); - const { address: LockingManagerAddress } = await hre.deployments.get( - LockingManagerContractName, + const { address: LockingPoolAddress } = await hre.deployments.get( + LockingPoolContractName, ); - const lockingEscrow = await hre.ethers.getContractAt( - "LockingEscrow", - LockingEscrowAddress, + const lockingInfo = await hre.ethers.getContractAt( + LockingInfoContractName, + LockingInfoAddress, ); - if ((await lockingEscrow.manager()) == hre.ethers.ZeroAddress) { + if ((await lockingInfo.manager()) == hre.ethers.ZeroAddress) { console.log("updating manager address for LockingEscrow"); - const tx = await lockingEscrow.initManager(LockingManagerAddress); + const tx = await lockingInfo.initManager(LockingPoolAddress); console.log(`done block=${tx.blockNumber} tx=${tx.hash}`); } }; diff --git a/ts-src/tasks/dev.ts b/ts-src/tasks/dev.ts new file mode 100644 index 0000000..e69de29 diff --git a/ts-src/tasks/l1.ts b/ts-src/tasks/l1.ts index 8aec56f..07a20be 100644 --- a/ts-src/tasks/l1.ts +++ b/ts-src/tasks/l1.ts @@ -3,8 +3,8 @@ import fs from "fs"; import { parseDuration, trimPubKeyPrefix } from "../utils/params"; import { - LockingEscrowContractName, - LockingManagerContractName, + LockingInfoContractName, + LockingPoolContractName, } from "../utils/constant"; task("l1:whitelist", "Whitelist an sequencer address") @@ -20,13 +20,13 @@ task("l1:whitelist", "Whitelist an sequencer address") throw new Error(`${hre.network.name} is not an l1`); } - const { address: LockingManagerAddress } = await hre.deployments.get( - LockingManagerContractName, + const { address: LockingPoolAddress } = await hre.deployments.get( + LockingPoolContractName, ); const lockingManager = await hre.ethers.getContractAt( - LockingManagerContractName, - LockingManagerAddress, + LockingPoolContractName, + LockingPoolAddress, ); const addr = args["addr"]; @@ -60,13 +60,13 @@ task("l1:lock", "Lock Metis to LockingPool contract") const amountInWei = hre.ethers.parseEther(args["amount"]); - const { address: LockingEscrowAddress } = await hre.deployments.get( - LockingEscrowContractName, + const { address: LockingInfoAddress } = await hre.deployments.get( + LockingInfoContractName, ); const lockingEscrow = await hre.ethers.getContractAt( - LockingEscrowContractName, - LockingEscrowAddress, + LockingInfoContractName, + LockingInfoAddress, ); // min/max lock check @@ -82,8 +82,8 @@ task("l1:lock", "Lock Metis to LockingPool contract") throw new Error(`maxLock is ${hre.ethers.formatEther(maxLock)}`); } - const { address: LockingManagerAddress } = await hre.deployments.get( - LockingManagerContractName, + const { address: LockingPoolAddress } = await hre.deployments.get( + LockingPoolContractName, ); const [signer] = await hre.ethers.getSigners(); @@ -97,8 +97,8 @@ task("l1:lock", "Lock Metis to LockingPool contract") console.log("Locking Metis for", seqWallet.address); const lockingManager = await hre.ethers.getContractAt( - LockingManagerContractName, - LockingManagerAddress, + LockingPoolContractName, + LockingPoolAddress, ); console.log("checking whitelist status"); @@ -119,13 +119,13 @@ task("l1:lock", "Lock Metis to LockingPool contract") console.log("checking the allowance"); const allowance = await metis.allowance( seqWallet.address, - LockingEscrowAddress, + LockingInfoAddress, ); if (allowance < amountInWei) { console.log("approving Metis to LockingEscrow"); const tx = await metis .connect(seqWallet) - .approve(LockingEscrowAddress, amountInWei); + .approve(LockingInfoAddress, amountInWei); await tx.wait(2); } @@ -148,13 +148,13 @@ task("l1:update-lock-amount", "Update locking amount condition") throw new Error(`${hre.network.name} is not an l1`); } - const { address: LockingEscrowAddress } = await hre.deployments.get( - LockingEscrowContractName, + const { address: LockingInfoAddress } = await hre.deployments.get( + LockingInfoContractName, ); const lockingEscrow = await hre.ethers.getContractAt( - LockingEscrowContractName, - LockingEscrowAddress, + LockingInfoContractName, + LockingInfoAddress, ); let actions = 0; @@ -200,13 +200,13 @@ task("l1:update-mpc-address", "Update MPC address for LockingPool contract") const { address: lockingPoolAddress } = await hre.deployments.get("LockingPool"); - const { address: LockingManagerAddress } = await hre.deployments.get( - LockingManagerContractName, + const { address: LockingPoolAddress } = await hre.deployments.get( + LockingPoolContractName, ); const lockingManager = await hre.ethers.getContractAt( - LockingManagerContractName, - LockingManagerAddress, + LockingPoolContractName, + LockingPoolAddress, ); const newAddr = args["addr"]; @@ -246,13 +246,13 @@ task("l1:update-exit-delay", "update exit delay time duration") throw new Error(`${hre.network.name} is not an l1`); } - const { address: LockingManagerAddress } = await hre.deployments.get( - LockingManagerContractName, + const { address: LockingPoolAddress } = await hre.deployments.get( + LockingPoolContractName, ); const lockingManager = await hre.ethers.getContractAt( - LockingManagerContractName, - LockingManagerAddress, + LockingPoolContractName, + LockingPoolAddress, ); const duration = parseDuration(args["duration"]); @@ -269,13 +269,13 @@ task("l1:update-reward-per-block", "update reward per block") throw new Error(`${hre.network.name} is not an l1`); } - const { address: LockingManagerAddress } = await hre.deployments.get( - LockingManagerContractName, + const { address: LockingPoolAddress } = await hre.deployments.get( + LockingPoolContractName, ); const lockingManager = await hre.ethers.getContractAt( - LockingManagerContractName, - LockingManagerAddress, + LockingPoolContractName, + LockingPoolAddress, ); console.log( diff --git a/ts-src/test/LockingManager.ts b/ts-src/test/LockingManager.ts index dd9f690..5ea68cf 100644 --- a/ts-src/test/LockingManager.ts +++ b/ts-src/test/LockingManager.ts @@ -2,13 +2,12 @@ import { ethers, deployments } from "hardhat"; import { expect } from "chai"; import { loadFixture } from "@nomicfoundation/hardhat-toolbox/network-helpers"; import { - LockingEscrowContractName, - LockingManagerContractName, + LockingInfoContractName, + LockingPoolContractName, + l2MetisAddr, } from "../utils/constant"; import { trimPubKeyPrefix } from "../utils/params"; -const l2MetisAddr = "0xDeadDeAddeAddEAddeadDEaDDEAdDeaDDeAD0000"; - describe("lockingManager", async () => { async function fixture() { const wallets = new Array(5) @@ -35,7 +34,7 @@ describe("lockingManager", async () => { } const lockingEscrowProxy = await deployments.deploy( - LockingEscrowContractName, + LockingInfoContractName, { from: admin.address, proxy: { @@ -56,12 +55,12 @@ describe("lockingManager", async () => { ); const lockingEscrow = await ethers.getContractAt( - LockingEscrowContractName, + LockingInfoContractName, lockingEscrowProxy.address, ); const lockingManagerProxy = await deployments.deploy( - LockingManagerContractName, + LockingPoolContractName, { from: admin.address, proxy: { @@ -77,7 +76,7 @@ describe("lockingManager", async () => { ); const lockingManager = await ethers.getContractAt( - LockingManagerContractName, + LockingPoolContractName, lockingManagerProxy.address, ); @@ -161,13 +160,12 @@ describe("lockingManager", async () => { ).to.be.revertedWith("invalid pubkey"); await expect( - lockingManager - .connect(wallet0) - .lockFor( - wallet0, - minLock, - trimPubKeyPrefix(wallet1.signingKey.publicKey), - ), + lockingManager.connect(wallet0).lockFor( + wallet0, + + minLock, + trimPubKeyPrefix(wallet1.signingKey.publicKey), + ), ).to.be.revertedWith("pubkey and address mismatch"); }); diff --git a/ts-src/utils/constant.ts b/ts-src/utils/constant.ts index fb712cd..b732345 100644 --- a/ts-src/utils/constant.ts +++ b/ts-src/utils/constant.ts @@ -1,5 +1,7 @@ -export const LockingEscrowContractName = "LockingEscrow"; +export const LockingInfoContractName = "LockingInfo"; -export const LockingManagerContractName = "LockingManager"; +export const LockingPoolContractName = "LockingPool"; export const SequencerSetContractName = "MetisSequencerSet"; + +export const l2MetisAddr = "0xDeadDeAddeAddEAddeadDEaDDEAdDeaDDeAD0000";