diff --git a/contracts/LockingBadge.sol b/contracts/LockingBadge.sol new file mode 100644 index 0000000..b668005 --- /dev/null +++ b/contracts/LockingBadge.sol @@ -0,0 +1,173 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.20; + +import {ERC721EnumerableUpgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC721/extensions/ERC721EnumerableUpgradeable.sol"; +import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import {ILockingBadge} from "./interfaces/ILockingBadge.sol"; + +contract LockingBadge is + ERC721EnumerableUpgradeable, + OwnableUpgradeable, + ILockingBadge +{ + // sequencers count threshold + uint256 public threshold; + + // whitelist + mapping(address => bool) public whitelist; + + // sequencerId => sequencer + mapping(uint256 seqId => Sequencer _seq) public sequencers; + + // sequencer owner => sequencerId + mapping(address owner => uint256 seqId) public seqOwners; + + /** + * @dev Modifier to make a function callable only the msg.sender is in the whitelist. + */ + modifier whitelistRequired() { + if (!whitelist[msg.sender]) { + revert NotWhitelisted(); + } + _; + } + + function __LockingBadge_init() internal { + threshold = 10; + + __ERC721_init("Metis Sequencer", "MS"); + __Ownable_init(); + } + + /** + * @dev setWhitelist Allow owner to update white address list + * @param _addr the address who can lock token + * @param _yes white address state + */ + function setWhitelist(address _addr, bool _yes) external onlyOwner { + whitelist[_addr] = _yes; + emit SetWhitelist(_addr, _yes); + } + + /** + * @dev setThreshold allow owner to update the threshold + * @param _threshold restrict the sequencer count + */ + function setThreshold(uint256 _threshold) external onlyOwner { + threshold = _threshold; + emit SetThreshold(_threshold); + } + + /** + * @dev setSequencerRewardRecipient Allow sequencer owner to set a reward recipient + * @param _seqId The sequencerId + * @param _recipient Who will receive the reward token + */ + function setSequencerRewardRecipient( + uint256 _seqId, + address _recipient + ) external whitelistRequired { + Sequencer storage seq = sequencers[_seqId]; + + if (seq.owner != msg.sender) { + revert NotSeqOwner(); + } + + if (seq.status != Status.Active) { + revert SeqNotActive(); + } + + if (_recipient == address(0)) { + revert NullAddress(); + } + + seq.rewardRecipient = _recipient; + emit SequencerRewardRecipientChanged(_seqId, _recipient); + } + + /** + * @dev setSequencerOwner update sequencer owner + * @param _seqId The sequencerId + * @param _owner the new owner + */ + function setSequencerOwner( + uint256 _seqId, + address _owner + ) external whitelistRequired { + if (_owner == address(0)) { + revert NullAddress(); + } + + Sequencer storage seq = sequencers[_seqId]; + if (seq.status != Status.Active) { + revert SeqNotActive(); + } + + if (seq.owner != msg.sender) { + revert NotSeqOwner(); + } + + seq.owner = _owner; + emit SequencerOwnerChanged(_seqId, _owner); + } + + /** + * @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 _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(); + } + + if (seq.signer != msg.sender) { + revert NotSeq(); + } + + require(_signerPubkey.length == 64, "invalid pubkey"); + address newSigner = address(uint160(uint256(keccak256(_signerPubkey)))); + seq.signer = newSigner; + _transfer2(msg.sender, newSigner, _seqId); + } + + function _mintFor( + address _caller, + address _to + ) internal returns (uint256 _seqId) { + if (seqOwners[_caller] != 0) { + revert OwnedSequencer(); + } + + if (balanceOf(_to) != 0) { + revert OwnedBadge(); + } + + // tokenId starts from 1 + _seqId = totalSupply() + 1; + if (_seqId > threshold) { + revert ThresholdExceed(); + } + + // it will check the _to must not be empty address + _mint(_to, _seqId); + return _seqId; + } + + // revert if user do a transfer external + function _transfer(address, address, uint256) internal pure override { + revert("transfer is not available"); + } + + // it's for updating signer key, used by the updateSigner func + function _transfer2(address _from, address _to, uint256 _tokenId) internal { + if (balanceOf(_to) == 1) { + revert OwnedBadge(); + } + super._transfer(_from, _to, _tokenId); + } +} diff --git a/contracts/LockingEscrow.sol b/contracts/LockingEscrow.sol new file mode 100644 index 0000000..44b407e --- /dev/null +++ b/contracts/LockingEscrow.sol @@ -0,0 +1,274 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.20; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +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 {ILockingBadge} from "./interfaces/ILockingBadge.sol"; + +contract LockingEscrow is ILockingEscrow, OwnableUpgradeable { + error NotManager(); + + using SafeERC20 for IERC20; + + address public bridge; // L1 metis bridge address + address public l1Token; // L1 metis token address + address public l2Token; // L2 metis token address + uint256 public l2ChainId; // L2 metis chainId + + uint256 public minLock; // min lock amount + uint256 public maxLock; // max lock amount + + uint256 public totalLocked; // the total locked amount + uint256 public totalRewardsLiquidated; + + // Locking manager address + address public manager; + + // the reward payer + address public rewardPayer; + + modifier OnlyManager() { + if (msg.sender != manager) { + revert NotManager(); + } + _; + } + + function initialize( + address _bridge, + address _l1Token, + address _l2Token, + uint256 _l2ChainId + ) external initializer { + bridge = _bridge; + l1Token = _l1Token; + l2Token = _l2Token; + l2ChainId = _l2ChainId; + + minLock = 20_000 ether; + maxLock = 100_000 ether; + + __Ownable_init(); + } + + function initManager(address _manager) external onlyOwner { + require(manager == address(0), "manager has been initialized"); + manager = _manager; + } + + /** + * @dev updateMinAmounts Allow owner to update min lock amount + * @param _minLock new min lock amount + */ + function setMinLock(uint256 _minLock) external onlyOwner { + require(_minLock > 0, "_minLock=0"); + minLock = _minLock; + emit SetMinLock(_minLock); + } + + /** + * @dev setMaxLock Allow owner to update max lock amount + * @param _maxLock new max lock amount + */ + function setMaxLock(uint256 _maxLock) external onlyOwner { + require(_maxLock >= minLock, "maxLock= minLock && _amount <= maxLock, "invalid amount"); + + // check if the pubkey matches the address + require(_signerPubkey.length == 64, "invalid pubkey"); + address gotSigner = address(uint160(uint256(keccak256(_signerPubkey)))); + // Note: the Manager contract ensures that the _signer can't be empty address + require(gotSigner == _signer, "pubkey and address mismatch"); + + // use local variable to save gas + uint256 _tatalLocked = totalLocked + _amount; + totalLocked = _tatalLocked; + + IERC20(l1Token).safeTransferFrom(_signer, address(this), _amount); + emit Locked( + _signer, + _id, + _batchId, + 1, // nocne starts from 1 for a new sequencer + _amount, + _tatalLocked, + _signerPubkey + ); + } + + /** + * @dev increaseLocked lock tokens to the sequencer, it can only be called from manager contract + * @param _seqId the sequencer id + * @param _nonce the sequencer nonce + * @param _owner the sequencer owner address + * @param _locked the locked amount of the sequencer at last + * @param _incoming amount from current transaction + * @param _fromReward use reward to lock + */ + function increaseLocked( + uint256 _seqId, + uint256 _nonce, + address _owner, + uint256 _locked, + uint256 _incoming, + uint256 _fromReward + ) external override OnlyManager { + require(_locked <= maxLock, "locked>maxLock"); + + // get increased number and transfer it into escrow + uint256 increased = _incoming + _fromReward; + require(increased > 0, "No new locked added"); + IERC20(l1Token).safeTransferFrom(_owner, address(this), _incoming); + + // get current total locked and emit event + uint256 _totalLocked = totalLocked + increased; + totalLocked = _totalLocked; + + emit Relocked(_seqId, increased, totalLocked); + emit LockUpdate(_seqId, _nonce, _locked); + } + + /** + * @dev initializeUnlock the first step to unlock + * current reward will be distributed + * @param _seqId the sequencer id + * @param _seq the current sequencer state + * @param _l2gas the l2gas for L1bridge + */ + function initializeUnlock( + uint256 _seqId, + uint32 _l2gas, + ILockingBadge.Sequencer calldata _seq + ) external payable override OnlyManager { + _liquidateReward(_seqId, _seq.reward, _seq.rewardRecipient, _l2gas); + emit UnlockInit( + _seq.signer, + _seqId, + _seq.nonce, + _seq.deactivationBatch, + _seq.deactivationTime, + _seq.unlockClaimTime, + _seq.reward + ); + } + + /** + * @dev finalizeUnlock the last step to unlock + * @param _operator the sequencer id + * @param _seqId the sequencer id + * @param _amount locked amount + * @param _reward reward amount + * @param _recipient recipient + * @param _l2gas the l2gas for L1bridge + */ + function finalizeUnlock( + address _operator, + uint256 _seqId, + uint256 _amount, + uint256 _reward, + address _recipient, + uint32 _l2gas + ) external payable OnlyManager { + // update totalLocked value + uint256 _tatalLocked = totalLocked - _amount; + totalLocked = _tatalLocked; + + IERC20(l1Token).safeTransfer(_operator, _amount); + if (_reward > 0) { + _liquidateReward(_seqId, _reward, _recipient, _l2gas); + } + emit Unlocked(_operator, _seqId, _amount, _tatalLocked); + } + + function liquidateReward( + uint256 _seqId, + uint256 _amount, + address _recipient, + uint32 _l2gas + ) external payable override OnlyManager { + _liquidateReward(_seqId, _amount, _recipient, _l2gas); + } + + /** + * @dev distributeReward reward distribution + * @param _batchId The batchId that submitted the reward is that + */ + function distributeReward( + uint256 _batchId, + uint256 _totalReward + ) external OnlyManager { + // reward income + IERC20(l1Token).safeTransferFrom( + rewardPayer, + address(this), + _totalReward + ); + emit BatchSubmitReward(_batchId); + } + + function _liquidateReward( + uint256 _seqId, + uint256 _amount, + address _recipient, + uint32 _l2gas + ) internal { + _bridgeTo(_recipient, _amount, _l2gas); + uint256 total = totalRewardsLiquidated + _amount; + totalRewardsLiquidated = total; + emit ClaimRewards(_seqId, _recipient, _amount, total); + } + + function _bridgeTo( + address _recipient, + uint256 _amount, + uint32 _l2gas + ) internal { + if (_amount == 0) { + return; + } + + IERC20(l1Token).safeIncreaseAllowance(bridge, _amount); + IL1ERC20Bridge(bridge).depositERC20ToByChainId{value: msg.value}( + l2ChainId, + l1Token, + l2Token, + _recipient, + _amount, + _l2gas, + "" + ); + } +} diff --git a/contracts/LockingInfo.sol b/contracts/LockingInfo.sol deleted file mode 100644 index 476d5bf..0000000 --- a/contracts/LockingInfo.sol +++ /dev/null @@ -1,339 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.20; - -import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; - -contract LockingInfo is Ownable { - mapping(uint256 => uint256) public sequencerNonce; - address public immutable lockingPool; - - /** - * @dev Emitted when sequencer locks in '_lockFor()' in LockingPool. - * @param signer sequencer address. - * @param sequencerId unique integer to identify a sequencer. - * @param nonce to synchronize the events in themis. - * @param activationBatch sequencer's first epoch as proposer. - * @param amount locking amount. - * @param total total locking amount. - * @param signerPubkey public key of the sequencer - */ - event Locked( - address indexed signer, - uint256 indexed sequencerId, - uint256 nonce, - uint256 indexed activationBatch, - uint256 amount, - uint256 total, - bytes signerPubkey - ); - - /** - * @dev Emitted when sequencer unlocks in 'unlockClaim()' - * @param user address of the sequencer. - * @param sequencerId unique integer to identify a sequencer. - * @param amount locking amount. - * @param total total locking amount. - */ - event Unlocked( - address indexed user, - uint256 indexed sequencerId, - uint256 amount, - uint256 total - ); - - /** - * @dev Emitted when sequencer unlocks in '_unlock()'. - * @param user address of the sequencer. - * @param sequencerId unique integer to identify a sequencer. - * @param nonce to synchronize the events in themis. - * @param deactivationBatch last batch for sequencer. - * @param deactivationTime unlock block timestamp. - * @param unlockClaimTime when user can claim locked token. - * @param amount locking amount - */ - event UnlockInit( - address indexed user, - uint256 indexed sequencerId, - uint256 nonce, - uint256 deactivationBatch, - uint256 deactivationTime, - uint256 unlockClaimTime, - uint256 indexed amount - ); - - /** - * @dev Emitted when the sequencer public key is updated in 'updateSigner()'. - * @param sequencerId unique integer to identify a sequencer. - * @param nonce to synchronize the events in themis. - * @param oldSigner oldSigner old address of the sequencer. - * @param newSigner newSigner new address of the sequencer. - * @param signerPubkey signerPubkey public key of the sequencer. - */ - event SignerChange( - uint256 indexed sequencerId, - uint256 nonce, - address indexed oldSigner, - address indexed newSigner, - bytes signerPubkey - ); - - /** - * @dev Emitted when the sequencer increase lock amoun in 'relock()'. - * @param sequencerId unique integer to identify a sequencer. - * @param amount locking new amount - * @param total the total locking amount - */ - event Relocked(uint256 indexed sequencerId, uint256 amount, uint256 total); - - /** - * @dev Emitted when the proxy update threshold in 'updateSequencerThreshold()'. - * @param newThreshold new threshold - * @param oldThreshold old threshold - */ - event ThresholdChange(uint256 newThreshold, uint256 oldThreshold); - - /** - * @dev Emitted when the proxy update threshold in 'updateWithdrawDelayTimeValue()'. - * @param newWithrawDelayTime new withdraw delay time - * @param oldWithrawDelayTime old withdraw delay time - */ - event WithrawDelayTimeChange( - uint256 newWithrawDelayTime, - uint256 oldWithrawDelayTime - ); - - /** - * @dev Emitted when the proxy update threshold in 'updateBlockReward()'. - * @param newReward new block reward - * @param oldReward old block reward - */ - event RewardUpdate(uint256 newReward, uint256 oldReward); - - /** - * @dev Emitted when sequencer relocking in 'relock()'. - * @param sequencerId unique integer to identify a sequencer. - * @param nonce to synchronize the events in themis. - * @param newAmount the updated lock amount. - */ - event LockUpdate( - uint256 indexed sequencerId, - uint256 indexed nonce, - uint256 indexed newAmount - ); - - /** - * @dev Emitted when sequencer withdraw rewards in 'withdrawRewards' or 'unlockClaim' - * @param sequencerId unique integer to identify a sequencer. - * @param recipient the address receive reward tokens - * @param amount the reward amount. - * @param totalAmount total rewards liquidated - */ - event ClaimRewards( - uint256 indexed sequencerId, - address recipient, - uint256 indexed amount, - uint256 indexed totalAmount - ); - - /** - * @dev Emitted when batch update in 'batchSubmitRewards' - * @param _newBatchId new batchId. - */ - event BatchSubmitReward(uint256 _newBatchId); - - /** - * @dev Emitted when batch update in 'updateEpochLength' - * @param _oldEpochLength old epoch length. - * @param _newEpochLength new epoch length. - * @param _effectiveBatch effective batch id. - */ - event UpdateEpochLength( - uint256 _oldEpochLength, - uint256 _newEpochLength, - uint256 _effectiveBatch - ); - - modifier onlyLockingPool() { - require(lockingPool == msg.sender, "Invalid sender, not locking pool"); - _; - } - - constructor(address _lockingPool) { - lockingPool = _lockingPool; - } - - /** - * @dev updateNonce can update nonce for sequencrs by owner - * @param sequencerIds the sequencer ids. - * @param nonces the sequencer nonces - */ - function updateNonce( - uint256[] calldata sequencerIds, - uint256[] calldata nonces - ) external onlyOwner { - require(sequencerIds.length == nonces.length, "args length mismatch"); - - for (uint256 i = 0; i < sequencerIds.length; ++i) { - sequencerNonce[sequencerIds[i]] = nonces[i]; - } - } - - /** - * @dev logLocked log event Locked - */ - function logLocked( - address signer, - bytes memory signerPubkey, - uint256 sequencerId, - uint256 activationBatch, - uint256 amount, - uint256 total - ) external onlyLockingPool { - sequencerNonce[sequencerId] = sequencerNonce[sequencerId] + 1; - emit Locked( - signer, - sequencerId, - sequencerNonce[sequencerId], - activationBatch, - amount, - total, - signerPubkey - ); - } - - /** - * @dev logUnlocked log event logUnlocked - */ - function logUnlocked( - address user, - uint256 sequencerId, - uint256 amount, - uint256 total - ) external onlyLockingPool { - emit Unlocked(user, sequencerId, amount, total); - } - - /** - * @dev logUnlockInit log event logUnlockInit - */ - function logUnlockInit( - address user, - uint256 sequencerId, - uint256 deactivationBatch, - uint256 deactivationTime, - uint256 unlockClaimTime, - uint256 amount - ) external onlyLockingPool { - sequencerNonce[sequencerId] = sequencerNonce[sequencerId] + 1; - emit UnlockInit( - user, - sequencerId, - sequencerNonce[sequencerId], - deactivationBatch, - deactivationTime, - unlockClaimTime, - amount - ); - } - - /** - * @dev logSignerChange log event SignerChange - */ - function logSignerChange( - uint256 sequencerId, - address oldSigner, - address newSigner, - bytes memory signerPubkey - ) external onlyLockingPool { - sequencerNonce[sequencerId] = sequencerNonce[sequencerId] + 1; - emit SignerChange( - sequencerId, - sequencerNonce[sequencerId], - oldSigner, - newSigner, - signerPubkey - ); - } - - /** - * @dev logRelockd log event Relocked - */ - function logRelockd( - uint256 sequencerId, - uint256 amount, - uint256 total - ) external onlyLockingPool { - emit Relocked(sequencerId, amount, total); - } - - /** - * @dev logThresholdChange log event ThresholdChange - */ - function logThresholdChange( - uint256 newThreshold, - uint256 oldThreshold - ) external onlyLockingPool { - emit ThresholdChange(newThreshold, oldThreshold); - } - - /** - * @dev logWithrawDelayTimeChange log event WithrawDelayTimeChange - */ - function logWithrawDelayTimeChange( - uint256 newWithrawDelayTime, - uint256 oldWithrawDelayTime - ) external onlyLockingPool { - emit WithrawDelayTimeChange(newWithrawDelayTime, oldWithrawDelayTime); - } - - /** - * @dev logRewardUpdate log event RewardUpdate - */ - function logRewardUpdate( - uint256 newReward, - uint256 oldReward - ) external onlyLockingPool { - emit RewardUpdate(newReward, oldReward); - } - - /** - * @dev logLockUpdate log event LockUpdate - */ - function logLockUpdate( - uint256 sequencerId, - uint256 totalLock - ) external onlyLockingPool { - sequencerNonce[sequencerId] = sequencerNonce[sequencerId] + 1; - emit LockUpdate(sequencerId, sequencerNonce[sequencerId], totalLock); - } - - /** - * @dev logClaimRewards log event ClaimRewards - */ - function logClaimRewards( - uint256 sequencerId, - address recipient, - uint256 amount, - uint256 totalAmount - ) external onlyLockingPool { - emit ClaimRewards(sequencerId, recipient, amount, totalAmount); - } - - /** - * @dev logBatchSubmitReward log event BatchSubmitReward - */ - function logBatchSubmitReward(uint256 newBatchId) external onlyLockingPool { - emit BatchSubmitReward(newBatchId); - } - - /** - * @dev logUpdateEpochLength log event UpdateEpochLength - */ - function logUpdateEpochLength( - uint256 oldEpochLength, - uint256 newEpochLength, - uint256 effectiveBatch - ) external onlyLockingPool { - emit UpdateEpochLength(oldEpochLength, newEpochLength, effectiveBatch); - } -} diff --git a/contracts/LockingManager.sol b/contracts/LockingManager.sol new file mode 100644 index 0000000..e06a5f7 --- /dev/null +++ b/contracts/LockingManager.sol @@ -0,0 +1,339 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.20; + +import {PausableUpgradeable} from "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol"; + +import {LockingBadge} from "./LockingBadge.sol"; + +import {ILockingEscrow} from "./interfaces/ILockingEscrow.sol"; +import {ILockingManager} from "./interfaces/ILockingManager.sol"; + +contract LockingManager is PausableUpgradeable, LockingBadge, ILockingManager { + error NotMpc(); + + struct BatchState { + uint256 id; // current batch id + uint256 number; // current batch block number + uint256 startEpoch; // start epoch number for current batch + uint256 endEpoch; // end epoch number for current batch + } + + ILockingEscrow public escorow; + + // delay time for unlock + uint256 public WITHDRAWAL_DELAY; + + // reward per L2 block + uint256 public BLOCK_REWARD; + + // the mpc address + address public mpcAddress; + + // current batch state + BatchState public batchState; + + function initialize(address _escorow) external initializer { + WITHDRAWAL_DELAY = 21 days; + BLOCK_REWARD = 761000 gwei; + + // init batch state + batchState = BatchState({ + id: 1, + number: block.number, + startEpoch: 0, + endEpoch: 0 + }); + + escorow = ILockingEscrow(_escorow); + + __Pausable_init(); + __LockingBadge_init(); + } + + /** + * @dev updateMpc update the mpc address + * @param _newMpc new mpc address + */ + function updateMpc(address _newMpc) external onlyOwner { + mpcAddress = _newMpc; + emit UpdateMpc(_newMpc); + } + + /** + * @dev setPause + */ + function setPause() external onlyOwner { + _pause(); + } + + /** + * @dev setUnpause + */ + function setUnpause() external onlyOwner { + _unpause(); + } + + /** + * @dev updateWithdrawDelayTimeValue Allow owner to set withdraw delay time. + * @param _time new withdraw delay time + */ + function updateWithdrawDelayTimeValue(uint256 _time) external onlyOwner { + require(_time > 0, "dalayTime==0"); + uint256 pre = WITHDRAWAL_DELAY; + WITHDRAWAL_DELAY = _time; + emit WithrawDelayTimeChange(_time, pre); + } + + /** + * @dev updateBlockReward Allow owner to set per block reward + * @param newReward the block reward + */ + function updateBlockReward(uint256 newReward) external onlyOwner { + require(newReward != 0, "invalid newReward"); + uint256 pre = BLOCK_REWARD; + BLOCK_REWARD = newReward; + emit RewardUpdate(newReward, pre); + } + + /** + * @dev lockFor lock Metis and participate in the sequencer node + * @param _signer Sequencer signer address + * @param _amount Amount of L1 metis token to lock for. + * @param _signerPubkey Sequencer signer pubkey, it should be uncompressed + */ + function lockFor( + address _signer, + uint256 _amount, + bytes calldata _signerPubkey + ) external whenNotPaused whitelistRequired { + uint256 seqId = _mintFor(msg.sender, _signer); + uint256 batchId = batchState.id; + + sequencers[seqId] = Sequencer({ + amount: _amount, + reward: 0, + activationBatch: 0, + deactivationBatch: 0, + updatingBatch: batchId, + deactivationTime: 0, + unlockClaimTime: 0, + nonce: 1, + owner: msg.sender, + signer: _signer, + pubkey: _signerPubkey, + rewardRecipient: address(0), // the recepient should be update afterward + status: Status.Active + }); + + escorow.newSequencer(seqId, _signer, batchId, _amount, _signerPubkey); + } + + /** + * @dev relock allow sequencer operator to increase the amount of locked positions + * @param _seqId the id of your sequencer + * @param _amount amount of token to relock, it can be 0 if you want to relock your rewrad + * @param _lockReward use true if lock the current rewards + */ + function relock( + uint256 _seqId, + uint256 _amount, + bool _lockReward + ) external whenNotPaused whitelistRequired { + Sequencer storage seq = sequencers[_seqId]; + if (seq.status != Status.Active) { + revert SeqNotActive(); + } + + if (seq.owner != msg.sender) { + revert NotSeqOwner(); + } + + uint256 _fromReward = 0; + if (_lockReward) { + _fromReward = seq.reward; + } + + uint256 locked = seq.amount + _amount + _fromReward; + uint256 nonce = seq.nonce + 1; + + seq.nonce = nonce; + seq.amount = locked; + + escorow.increaseLocked( + _seqId, + nonce, + msg.sender, + locked, + _amount, + _fromReward + ); + } + + /** + * @dev unlock your Metis and exit the sequencer node + * the reward will be arrived by L1Bridge first + * and you need to wait the exit period and call + * + * + * @param _seqId sequencer id + * @param _l2Gas the L2 gas limit for L1Bridge. + * the reward is distributed by bridge + * so you need to pay the ETH as the bridge fee + */ + function unlock( + uint256 _seqId, + uint32 _l2Gas + ) external payable whenNotPaused whitelistRequired { + _unlock(_seqId, false, _l2Gas); + } + + /** + * @dev unlockClaim claim your locked tokens after the waiting period is passed + * + * @param _seqId sequencer id + * @param _l2Gas bridge reward to L2 gasLimit + */ + function unlockClaim( + uint256 _seqId, + uint32 _l2Gas + ) external payable whenNotPaused whitelistRequired { + Sequencer storage seq = sequencers[_seqId]; + if (seq.owner != msg.sender) { + revert NotSeqOwner(); + } + + address recipient = seq.rewardRecipient; + if (recipient == address(0)) { + revert NoRewardRecipient(); + } + + // operator can only claim after WITHDRAWAL_DELAY + require( + seq.status == Status.Inactive && + seq.unlockClaimTime <= block.timestamp, + "Not allowed to cliam" + ); + + uint256 amount = seq.amount; + uint256 reward = seq.reward; + // uint256 nonce = seq.nonce + 1; + + seq.amount = 0; + seq.reward = 0; + // seq.nonce = nonce; + seq.status = Status.Unlocked; + + _burn(_seqId); + escorow.finalizeUnlock{value: msg.value}( + msg.sender, + _seqId, + amount, + reward, + seq.rewardRecipient, + _l2Gas + ); + } + + /** + * @dev withdrawRewards withdraw current rewards + * + * @param _seqId unique integer to identify a sequencer. + * @param _l2Gas bridge reward to L2 gasLimit + */ + function withdrawRewards( + uint256 _seqId, + uint32 _l2Gas + ) external payable whenNotPaused whitelistRequired { + Sequencer storage seq = sequencers[_seqId]; + if (seq.owner != msg.sender) { + revert NotSeqOwner(); + } + address recipient = seq.rewardRecipient; + if (recipient == address(0)) { + revert NoRewardRecipient(); + } + uint256 reward = seq.reward; + if (reward > 0) { + seq.reward = 0; + escorow.liquidateReward{value: msg.value}( + _seqId, + reward, + recipient, + _l2Gas + ); + } + } + + /** + * @dev batchSubmitRewards Allow to submit L2 sequencer block information, and attach Metis reward tokens for reward distribution + * @param _batchId The batchId that submitted the reward is that + * @param _startEpoch The startEpoch that submitted the reward is that + * @param _endEpoch The endEpoch that submitted the reward is that + * @param _seqs Those sequencers can receive rewards + * @param _blocks How many blocks each sequencer finished. + */ + function batchSubmitRewards( + uint256 _batchId, + uint256 _startEpoch, + uint256 _endEpoch, + address[] calldata _seqs, + uint256[] calldata _blocks + ) external payable returns (uint256 totalReward) { + if (msg.sender != mpcAddress) { + revert NotMpc(); + } + require( + _seqs.length == _blocks.length && _seqs.length > 0, + "mismatch length" + ); + + BatchState storage bs = batchState; + uint256 nextBatch = bs.id + 1; + require(nextBatch == _batchId, "invalid batch id"); + bs.id = nextBatch; + + require(bs.endEpoch + 1 == _startEpoch, "invalid startEpoch"); + require(_startEpoch < _endEpoch, "invalid endEpoch"); + + for (uint256 i = 0; i < _seqs.length; i++) { + uint256 reward = _blocks[i] * BLOCK_REWARD; + uint256 seqId = tokenOfOwnerByIndex(_seqs[i], 0); + Sequencer storage seq = sequencers[seqId]; + seq.reward += reward; + totalReward += reward; + } + + escorow.distributeReward(_batchId, totalReward); + } + + function _unlock(uint256 _seqId, bool _force, uint32 _l2Gas) internal { + Sequencer storage seq = sequencers[_seqId]; + + if (!_force) { + if (seq.owner != msg.sender) { + revert NotSeqOwner(); + } + // todo: 1/3 check + } + + address recipient = seq.rewardRecipient; + if (recipient == address(0)) { + revert NoRewardRecipient(); + } + + if (seq.status != Status.Active) { + revert SeqNotActive(); + } + + seq.status = Status.Inactive; + seq.deactivationBatch = batchState.id; + seq.deactivationTime = block.timestamp; + seq.unlockClaimTime = block.timestamp + WITHDRAWAL_DELAY; + seq.reward = 0; + + uint256 nonce = seq.nonce + 1; + seq.nonce = nonce; + + escorow.initializeUnlock{value: msg.value}(_seqId, _l2Gas, seq); + } +} diff --git a/contracts/LockingNFT.sol b/contracts/LockingNFT.sol deleted file mode 100644 index 8c64518..0000000 --- a/contracts/LockingNFT.sol +++ /dev/null @@ -1,45 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.20; - -import {ERC721Enumerable, ERC721} from "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol"; -import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; - -contract LockingNFT is ERC721Enumerable, Ownable { - constructor(string memory name, string memory symbol) ERC721(name, symbol) { - require(bytes(name).length > 0, "invalid name"); - require(bytes(symbol).length > 0, "invalid symbol"); - } - - /** - * @dev mint a NFT, on behalf of the user successfully applied to become a sequencer - * @param to the signer address of sequencer - * @param tokenId mint token id - */ - function mint(address to, uint256 tokenId) external onlyOwner { - require( - balanceOf(to) == 0, - "Sequencers MUST NOT own multiple lock position" - ); - _safeMint(to, tokenId); - } - - /** - * @dev burn a NFT, give up the sequencer role on behalf of the user - * @param tokenId the NFT token id - */ - function burn(uint256 tokenId) external onlyOwner { - _burn(tokenId); - } - - function _transfer( - address from, - address to, - uint256 tokenId - ) internal override onlyOwner { - require( - balanceOf(to) == 0, - "Sequencers MUST NOT own multiple lock position" - ); - super._transfer(from, to, tokenId); - } -} diff --git a/contracts/LockingPool.sol b/contracts/LockingPool.sol deleted file mode 100644 index f5df10f..0000000 --- a/contracts/LockingPool.sol +++ /dev/null @@ -1,1059 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.20; - -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; -import {PausableUpgradeable} from "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol"; -import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; -import {ILockingPool} from "./interfaces/ILockingPool.sol"; -import {LockingInfo} from "./LockingInfo.sol"; -import {LockingNFT} from "./LockingNFT.sol"; -import {IL1ERC20Bridge} from "./interfaces/IL1ERC20Bridge.sol"; - -contract LockingPool is ILockingPool, OwnableUpgradeable, PausableUpgradeable { - using SafeERC20 for IERC20; - - enum Status { - Inactive, - Active, - Unlocked - } // Unlocked means sequencer exist - - struct MpcHistoryItem { - uint256 startBlock; - address newMpcAddress; - } - - struct State { - uint256 amount; - uint256 lockerCount; - } - - struct StateChange { - int256 amount; - int256 lockerCount; - } - - struct Sequencer { - uint256 amount; // sequencer current lock amount - uint256 reward; // sequencer current reward - uint256 activationBatch; // sequencer activation batch id - uint256 deactivationBatch; // sequencer deactivation batch id - uint256 deactivationTime; // sequencer deactivation timestamp - uint256 unlockClaimTime; // sequencer unlock lock amount timestamp, has a withdraw delay time - address signer; // sequencer signer address - address rewardRecipient; // seqeuncer rewarder recipient address - Status status; // sequencer status - } - - uint256 internal constant INCORRECT_SEQUENCER_ID = 2 ** 256 - 1; - - address public bridge; // L1 metis bridge address - address public l1Token; // L1 metis token address - address public l2Token; // L2 metis token address - LockingInfo public logger; // logger lockingPool event - LockingNFT public NFTContract; // NFT for locker - uint256 public WITHDRAWAL_DELAY; // delay time for unlock - uint256 public currentBatch; // current batch id - uint256 public totalLocked; // total locked amount of all sequencers - uint256 public NFTCounter; // current nft holder count - uint256 public totalRewardsLiquidated; // total rewards had been liquidated - address[] public signers; // all signers - uint256 public currentUnlockedInit; // sequencer unlock queue count, need have a limit - uint256 public lastRewardEpochId; // the last epochId for update reward - uint256 public lastRewardTime; // the last reward time for update reward - - // genesis variables - uint256 public perSecondReward; // reward per second - uint256 public minLock; // min lock Metis token - uint256 public maxLock; // max lock Metis token - uint256 public signerUpdateLimit; // sequencer signer need have a update limit,how many batches are not allowed to update the signer - address public mpcAddress; // current mpc address for batch submit reward - uint256 public sequencerThreshold; // maximum sequencer limit - - mapping(uint256 => Sequencer) public sequencers; - mapping(address => uint256) public signerToSequencer; - mapping(uint256 => bool) public batchSubmitHistory; // batch submit - - // current Batch lock power and lockers count - State public sequencerState; - mapping(uint256 => StateChange) public sequencerStateChanges; - - // sequencerId to last signer update Batch - mapping(uint256 => uint256) public latestSignerUpdateBatch; - - // white address list who can lock token - mapping(address => bool) public whiteListAddresses; - // A whitelist address can only be bound to one sequencer - mapping(address => address) public whiteListBoundSequencer; - - // mpc history - MpcHistoryItem[] public mpcHistory; // recent mpc - - uint256 public l2ChainId; // the l2 chainId - - /** - * @dev Emitted when nft contract update in 'UpdateLockingInfo' - * @param _newLockingInfo new contract address. - */ - event UpdateLockingInfo(address _newLockingInfo); - /** - * @dev Emitted when nft contract update in 'UpdateNFTContract' - * @param _newNftContract new contract address. - */ - event UpdateNFTContract(address _newNftContract); - - /** - * @dev Emitted when current batch update in 'SetCurrentBatch' - * @param _newCurrentBatch new batch id. - */ - event SetCurrentBatch(uint256 _newCurrentBatch); - - /** - * @dev Emitted when signer update limit update in 'UpdateSignerUpdateLimit' - * @param _newLimit new limit. - */ - event UpdateSignerUpdateLimit(uint256 _newLimit); - - /** - * @dev Emitted when min lock amount update in 'UpdateMinAmounts' - * @param _newMinLock new min lock. - */ - event UpdateMinAmounts(uint256 _newMinLock); - - /** - * @dev Emitted when min lock amount update in 'UpdateMaxAmounts' - * @param _newMaxLock new max lock. - */ - event UpdateMaxAmounts(uint256 _newMaxLock); - - /** - * @dev Emitted when mpc address update in 'UpdateMpc' - * @param _newMpc new min lock. - */ - event UpdateMpc(address _newMpc); - - /** - * @dev Emitted when white address update in 'setWhiteListAddress' - * @param user the address who can lock token - * @param verified white address state - */ - event WhiteListAdded(address user, bool verified); - - /** - * @dev Emitted when reward recipient address update in 'setSequencerRewardRecipient' - * @param sequencerId the sequencerId - * @param recipient the address receive reward token - */ - event SequencerRewardRecipientChanged( - uint256 sequencerId, - address recipient - ); - - function initialize( - address _bridge, - address _l1Token, - address _l2Token, - address _NFTContract, - address _mpc, - uint256 _l2ChainId - ) external initializer { - require(_bridge != address(0), "invalid _bridge"); - require(_l1Token != address(0), "invalid _l1Token"); - require(_l2Token != address(0), "invalid _l2Token"); - require(_NFTContract != address(0), "invalid _NFTContract"); - require(_mpc != address(0), "_mpc is zero address"); - - bridge = _bridge; - l1Token = _l1Token; - l2Token = _l2Token; - NFTContract = LockingNFT(_NFTContract); - - require(!isContract(_mpc), "_mpc is a contract"); - mpcAddress = _mpc; - - mpcHistory.push( - MpcHistoryItem({startBlock: block.number, newMpcAddress: _mpc}) - ); - - WITHDRAWAL_DELAY = 21 days; // sequencer exit withdraw delay time - currentBatch = 1; // default start from batch 1 - perSecondReward = 1 * (10 ** 8); // per second reward - minLock = 20000 * (10 ** 18); // min lock amount - maxLock = 100000 * (10 ** 18); // max lock amount - signerUpdateLimit = 10; // how many batches are not allowed to update the signer - sequencerThreshold = 10; // allow max sequencers - NFTCounter = 1; // sequencer id - - l2ChainId = _l2ChainId; // the l2 chainId - - __Ownable_init(); - __Pausable_init(); - } - - /** - Admin Methods - */ - - /** - * @dev forceUnlock Allow owner to force a sequencer node to exit - * @param sequencerId unique integer to identify a sequencer. - * @param l2Gas bridge reward to L2 gasLimit - */ - function forceUnlock(uint256 sequencerId, uint32 l2Gas) external onlyOwner { - Status status = sequencers[sequencerId].status; - require( - sequencers[sequencerId].activationBatch > 0 && - sequencers[sequencerId].deactivationBatch == 0 && - status == Status.Active, - "invalid sequencer status" - ); - _unlock(sequencerId, currentBatch, true, l2Gas); - } - - /** - * @dev updateNFTContract Allow owner update the NFT contract address - * @param _nftContract new NFT contract address - */ - function updateNFTContract(address _nftContract) external onlyOwner { - require(_nftContract != address(0), "invalid _nftContract"); - NFTContract = LockingNFT(_nftContract); - emit UpdateNFTContract(_nftContract); - } - - /** - * @dev updateLockingInfo Allow owner update the locking info contract address - * @param _lockingInfo new locking info contract address - */ - function updateLockingInfo(address _lockingInfo) external onlyOwner { - require(_lockingInfo != address(0), "invalid _lockingInfo"); - logger = LockingInfo(_lockingInfo); - emit UpdateLockingInfo(_lockingInfo); - } - - /** - * @dev updateSequencerThreshold Allow owner to set max sequencer threshold - * @param newThreshold the new threshold - */ - function updateSequencerThreshold(uint256 newThreshold) external onlyOwner { - require(newThreshold != 0, "invalid newThreshold"); - sequencerThreshold = newThreshold; - logger.logThresholdChange(newThreshold, sequencerThreshold); - } - - /** - * @dev updatePerSecondReward Allow owner to set per block reward - * @param newReward the new reward - */ - function updatePerSecondReward(uint256 newReward) external onlyOwner { - require(newReward != 0, "invalid newReward"); - perSecondReward = newReward; - logger.logRewardUpdate(newReward, perSecondReward); - } - - /** - * @dev updateWithdrawDelayTimeValue Allow owner to set withdraw delay time. - * @param newWithdrawDelayTime new withdraw delay time - */ - function updateWithdrawDelayTimeValue( - uint256 newWithdrawDelayTime - ) external onlyOwner { - require(newWithdrawDelayTime > 0, "invalid newWithdrawDelayTime"); - WITHDRAWAL_DELAY = newWithdrawDelayTime; - logger.logWithrawDelayTimeChange( - newWithdrawDelayTime, - WITHDRAWAL_DELAY - ); - } - - /** - * @dev updateSignerUpdateLimit Allow owner to set signer update max limit - * @param _limit new limit - */ - function updateSignerUpdateLimit(uint256 _limit) external onlyOwner { - require(_limit > 0, "invalid _limit"); - signerUpdateLimit = _limit; - emit UpdateSignerUpdateLimit(_limit); - } - - /** - * @dev updateMinAmounts Allow owner to update min lock amount - * @param _minLock new min lock amount - */ - function updateMinAmounts(uint256 _minLock) external onlyOwner { - require(_minLock > 0, "invalid _minLock"); - minLock = _minLock; - emit UpdateMinAmounts(_minLock); - } - - /** - * @dev updateMaxAmounts Allow owner to update max lock amount - * @param _maxLock new max lock amount - */ - function updateMaxAmounts(uint256 _maxLock) external onlyOwner { - require(_maxLock > 0, "invalid _maxLock"); - maxLock = _maxLock; - emit UpdateMaxAmounts(_maxLock); - } - - /** - * @dev updateMpc Allow owner to update new mpc address - * @param _newMpc new mpc - */ - function updateMpc(address _newMpc) external onlyOwner { - require(!isContract(_newMpc), "_newMpc is a contract"); - require(_newMpc != address(0), "_newMpc is zero address"); - mpcAddress = _newMpc; - mpcHistory.push( - MpcHistoryItem({startBlock: block.number, newMpcAddress: _newMpc}) - ); - - emit UpdateMpc(_newMpc); - } - - /** - * @dev setWhiteListAddress Allow owner to update white address list - * @param user the address who can lock token - * @param verified white address state - */ - function setWhiteListAddress( - address user, - bool verified - ) external onlyOwner { - require(whiteListAddresses[user] != verified, "state not change"); - whiteListAddresses[user] = verified; - - emit WhiteListAdded(user, verified); - } - - /** - * @dev setPause can set the contract not suspended status - */ - function setPause() external onlyOwner { - _pause(); - } - - /** - * @dev setUnpause can cancel the suspended state - */ - function setUnpause() external onlyOwner { - _unpause(); - } - - /** - * @dev lockFor is used to lock Metis and participate in the sequencer block node application - * @param user sequencer signer address - * @param amount Amount of L1 metis token to lock for. - * @param signerPubkey sequencer signer pubkey - */ - function lockFor( - address user, - uint256 amount, - bytes memory signerPubkey - ) external override whenNotPaused { - require( - whiteListAddresses[msg.sender], - "msg sender should be in the white list" - ); - require( - currentSequencerSetSize() < sequencerThreshold, - "no more slots" - ); - require(amount >= minLock, "amount less than minLock"); - require(amount <= maxLock, "amount large than maxLock"); - require( - whiteListBoundSequencer[msg.sender] == address(0), - "had bound sequencer" - ); - - _lockFor(user, amount, signerPubkey); - whiteListBoundSequencer[msg.sender] = user; - _transferTokenFrom(msg.sender, address(this), amount); - } - - /** - * @dev unlock is used to unlock Metis and exit the sequencer node - * - * @param sequencerId sequencer id - * @param l2Gas bridge reward to L2 gasLimit - */ - function unlock( - uint256 sequencerId, - uint32 l2Gas - ) external payable override { - require( - whiteListAddresses[msg.sender], - "msg sender should be in the white list" - ); - require( - whiteListBoundSequencer[msg.sender] == - sequencers[sequencerId].signer, - "whiteAddress and boundSequencer mismatch" - ); - require( - sequencers[sequencerId].rewardRecipient != address(0), - "rewardRecipient not set" - ); - - Status status = sequencers[sequencerId].status; - require( - sequencers[sequencerId].activationBatch > 0 && - sequencers[sequencerId].deactivationBatch == 0 && - status == Status.Active, - "invalid sequencer status" - ); - - uint256 exitBatch = currentBatch + 1; // notice period - _unlock(sequencerId, exitBatch, false, l2Gas); - } - - /** - * @dev unlockClaim Because unlock has a waiting period, after the waiting period is over, you can claim locked tokens - * - * @param sequencerId sequencer id - * @param l2Gas bridge reward to L2 gasLimit - */ - function unlockClaim( - uint256 sequencerId, - uint32 l2Gas - ) external payable override { - require( - whiteListAddresses[msg.sender], - "msg sender should be in the white list" - ); - require( - whiteListBoundSequencer[msg.sender] == - sequencers[sequencerId].signer, - "whiteAddress and boundSequencer mismatch" - ); - require( - sequencers[sequencerId].rewardRecipient != address(0), - "rewardRecipient not set" - ); - - uint256 deactivationBatch = sequencers[sequencerId].deactivationBatch; - uint256 unlockClaimTime = sequencers[sequencerId].unlockClaimTime; - - // can only claim after WITHDRAWAL_DELAY - require( - deactivationBatch > 0 && - unlockClaimTime <= block.timestamp && - sequencers[sequencerId].status != Status.Unlocked, - "claim not allowed" - ); - - uint256 amount = sequencers[sequencerId].amount; - uint256 newTotalLocked = totalLocked - amount; - totalLocked = newTotalLocked; - - // Check for unclaimed rewards - _liquidateRewards( - sequencerId, - sequencers[sequencerId].rewardRecipient, - l2Gas - ); - - sequencers[sequencerId].amount = 0; - sequencers[sequencerId].signer = address(0); - - signerToSequencer[ - sequencers[sequencerId].signer - ] = INCORRECT_SEQUENCER_ID; - sequencers[sequencerId].status = Status.Unlocked; - - // Reduce the number of unlockInit queue - currentUnlockedInit--; - - // withdraw locked token - _transferToken(msg.sender, amount); - - logger.logUnlocked(msg.sender, sequencerId, amount, newTotalLocked); - NFTContract.burn(sequencerId); - } - - /** - * @dev relock Allow sequencer to increase the amount of locked positions - * @param sequencerId unique integer to identify a sequencer. - * @param amount Amount of L1 metis token to relock for. - * @param lockRewards Whether to lock the current rewards - */ - function relock( - uint256 sequencerId, - uint256 amount, - bool lockRewards - ) external override whenNotPaused { - require( - sequencers[sequencerId].amount > 0, - "invalid sequencer locked amount" - ); - require(sequencers[sequencerId].deactivationBatch == 0, "no relocking"); - require( - whiteListAddresses[msg.sender], - "msg sender should be in the white list" - ); - require( - whiteListBoundSequencer[msg.sender] == - sequencers[sequencerId].signer, - "whiteAddress and boundSequencer mismatch" - ); - - uint256 relockAmount = amount; - - if (lockRewards) { - amount = amount + sequencers[sequencerId].reward; - sequencers[sequencerId].reward = 0; - } - require(amount > 0, "invalid relock amount"); - - uint256 newTotalLocked = totalLocked + amount; - totalLocked = newTotalLocked; - sequencers[sequencerId].amount = - sequencers[sequencerId].amount + - amount; - require( - sequencers[sequencerId].amount <= maxLock, - "amount large than maxLock" - ); - - updateTimeline(int256(amount), 0, 0); - _transferTokenFrom(msg.sender, address(this), relockAmount); - - logger.logLockUpdate(sequencerId, sequencers[sequencerId].amount); - logger.logRelockd( - sequencerId, - sequencers[sequencerId].amount, - newTotalLocked - ); - } - - /** - * @dev withdrawRewards withdraw current rewards - * - * @param sequencerId unique integer to identify a sequencer. - * @param l2Gas bridge reward to L2 gasLimit - */ - function withdrawRewards( - uint256 sequencerId, - uint32 l2Gas - ) external payable override { - require( - whiteListAddresses[msg.sender], - "msg sender should be in the white list" - ); - require( - whiteListBoundSequencer[msg.sender] == - sequencers[sequencerId].signer, - "whiteAddress and boundSequencer mismatch" - ); - - Sequencer storage sequencerInfo = sequencers[sequencerId]; - _liquidateRewards(sequencerId, sequencerInfo.rewardRecipient, l2Gas); - } - - /** - * @dev updateSigner Allow sqeuencer to update new signers to replace old signer addresses,and NFT holder will be transfer driectly - * @param sequencerId unique integer to identify a sequencer. - * @param signerPubkey the new signer pubkey address - */ - function updateSigner( - uint256 sequencerId, - bytes memory signerPubkey - ) external { - require( - whiteListAddresses[msg.sender], - "msg sender should be in the white list" - ); - require( - whiteListBoundSequencer[msg.sender] == - sequencers[sequencerId].signer, - "whiteAddress and boundSequencer mismatch" - ); - require( - sequencers[sequencerId].deactivationBatch == 0, - "exited sequencer" - ); - - address signer = _getAndAssertSigner(signerPubkey); - uint256 _currentBatch = currentBatch; - require( - _currentBatch >= - latestSignerUpdateBatch[sequencerId] + signerUpdateLimit, - "not allowed" - ); - - address currentSigner = sequencers[sequencerId].signer; - // update signer event - logger.logSignerChange( - sequencerId, - currentSigner, - signer, - signerPubkey - ); - - // swap signer in the list - _removeSigner(currentSigner); - _insertSigner(signer); - - signerToSequencer[currentSigner] = INCORRECT_SEQUENCER_ID; - signerToSequencer[signer] = sequencerId; - sequencers[sequencerId].signer = signer; - whiteListBoundSequencer[msg.sender] = signer; - - // reset update time to current time - latestSignerUpdateBatch[sequencerId] = _currentBatch; - - // transfer NFT driectly - NFTContract.transferFrom(msg.sender, signer, sequencerId); - } - - /** - * @dev batchSubmitRewards Allow to submit L2 sequencer block information, and attach Metis reward tokens for reward distribution - * @param batchId The batchId that submitted the reward is that - * @param payeer Who Pays the Reward Tokens - * @param startEpoch The startEpoch that submitted the reward is that - * @param endEpoch The endEpoch that submitted the reward is that - * @param _sequencers Those sequencers can receive rewards - * @param finishedBlocks How many blocks each sequencer finished. - * @param signature Confirmed by mpc and signed for reward distribution - */ - function batchSubmitRewards( - uint256 batchId, - address payeer, - uint256 startEpoch, - uint256 endEpoch, - address[] memory _sequencers, - uint256[] memory finishedBlocks, - bytes memory signature - ) external payable returns (uint256) { - uint256 nextBatch = currentBatch + 1; - require(nextBatch == batchId, "invalid batch id"); - require(_sequencers.length == finishedBlocks.length, "mismatch length"); - require(lastRewardEpochId < startEpoch, "invalid startEpoch"); - require(startEpoch < endEpoch, "invalid endEpoch"); - - lastRewardEpochId = endEpoch; - // check mpc signature - bytes32 operationHash = keccak256( - abi.encodePacked( - block.chainid, - batchId, - startEpoch, - endEpoch, - _sequencers, - finishedBlocks, - address(this) - ) - ); - operationHash = ECDSA.toEthSignedMessageHash(operationHash); - address signer = ECDSA.recover(operationHash, signature); - require(signer == mpcAddress, "invalid mpc signature"); - - // calc total reward - uint256 totalReward = perSecondReward * - (block.timestamp - lastRewardTime); - lastRewardTime = block.timestamp; - - // calc total finished blocks - uint256 totalFinishedBlocks; - for (uint256 i = 0; i < finishedBlocks.length; ) { - unchecked { - totalFinishedBlocks += finishedBlocks[i]; - ++i; - } - } - - // distribute reward - for (uint256 i = 0; i < _sequencers.length; ) { - require( - signerToSequencer[_sequencers[i]] > 0, - "sequencer not exist" - ); - require( - isSequencer(signerToSequencer[_sequencers[i]]), - "invalid sequencer" - ); - - uint256 reward = _calculateReward( - totalReward, - totalFinishedBlocks, - finishedBlocks[i] - ); - _increaseReward(_sequencers[i], reward); - - unchecked { - ++i; - } - } - - _finalizeCommit(); - logger.logBatchSubmitReward(batchId); - - // reward income - IERC20(l1Token).safeTransferFrom(payeer, address(this), totalReward); - return totalReward; - } - - /** - * @dev setSequencerRewardRecipient Allow sequencer owner to set a reward recipient - * @param sequencerId The sequencerId - * @param recipient Who will receive the reward token - */ - function setSequencerRewardRecipient( - uint256 sequencerId, - address recipient - ) external { - require( - whiteListAddresses[msg.sender], - "msg sender should be in the white list" - ); - require( - whiteListBoundSequencer[msg.sender] == - sequencers[sequencerId].signer, - "whiteAddress and boundSequencer mismatch" - ); - require(recipient != address(0), "invalid recipient"); - - Sequencer storage sequencerInfo = sequencers[sequencerId]; - sequencerInfo.rewardRecipient = recipient; - - emit SequencerRewardRecipientChanged(sequencerId, recipient); - } - - // query owenr by NFT token id - function ownerOf(uint256 tokenId) external view override returns (address) { - return NFTContract.ownerOf(tokenId); - } - - // query current lock amount by sequencer id - function sequencerLock( - uint256 sequencerId - ) external view override returns (uint256) { - return sequencers[sequencerId].amount; - } - - // get sequencer id by address - function getSequencerId( - address user - ) external view override returns (uint256) { - return NFTContract.tokenOfOwnerByIndex(user, 0); - } - - // get sequencer reward by sequencer id - function sequencerReward( - uint256 sequencerId - ) external view override returns (uint256) { - return sequencers[sequencerId].reward; - } - - // get total lock amount for all sequencers - function currentSequencerSetTotalLock() - external - view - override - returns (uint256) - { - return sequencerState.amount; - } - - /** - * @dev fetchMpcAddress query mpc address by L1 block height, used by batch-submitter - * @param blockHeight the L1 block height - */ - function fetchMpcAddress( - uint256 blockHeight - ) external view override returns (address) { - address result; - for (uint256 i = mpcHistory.length - 1; i >= 0; i--) { - if (blockHeight >= mpcHistory[i].startBlock) { - result = mpcHistory[i].newMpcAddress; - break; - } - } - - return result; - } - - /* - public functions - */ - - // query whether an id is a sequencer - function isSequencer(uint256 sequencerId) public view returns (bool) { - return - _isSequencer( - sequencers[sequencerId].status, - sequencers[sequencerId].amount, - sequencers[sequencerId].deactivationBatch, - currentBatch - ); - } - - // get all sequencer count - function currentSequencerSetSize() public view override returns (uint256) { - return sequencerState.lockerCount; - } - - /* - internal functions - */ - - /** - * @dev updateTimeline Used to update sequencerState information - * @param amount The number of locked positions changed - * @param lockerCount The number of lock sequencer changed - * @param targetBatch When does the change take effect - */ - function updateTimeline( - int256 amount, - int256 lockerCount, - uint256 targetBatch - ) internal { - if (targetBatch == 0) { - // update total lock and sequencer count - if (amount > 0) { - sequencerState.amount = sequencerState.amount + uint256(amount); - } else if (amount < 0) { - sequencerState.amount = - sequencerState.amount - - uint256(amount * -1); - } - - if (lockerCount > 0) { - sequencerState.lockerCount = - sequencerState.lockerCount + - uint256(lockerCount); - } else if (lockerCount < 0) { - sequencerState.lockerCount = - sequencerState.lockerCount - - uint256(lockerCount * -1); - } - } else { - sequencerStateChanges[targetBatch].amount += amount; - sequencerStateChanges[targetBatch].lockerCount += lockerCount; - } - } - - function _lockFor( - address user, - uint256 amount, - bytes memory signerPubkey - ) internal returns (uint256) { - address signer = _getAndAssertSigner(signerPubkey); - require(user == signer, "user and signerPubkey mismatch"); - - uint256 _currentBatch = currentBatch; - uint256 sequencerId = NFTCounter; - - uint256 newTotalLocked = totalLocked + amount; - totalLocked = newTotalLocked; - - sequencers[sequencerId] = Sequencer({ - reward: 0, - amount: amount, - activationBatch: _currentBatch, - deactivationBatch: 0, - deactivationTime: 0, - unlockClaimTime: 0, - signer: signer, - rewardRecipient: address(0), - status: Status.Active - }); - - latestSignerUpdateBatch[sequencerId] = _currentBatch; - - signerToSequencer[signer] = sequencerId; - updateTimeline(int256(amount), 1, 0); - NFTCounter = sequencerId + 1; - _insertSigner(signer); - - logger.logLocked( - signer, - signerPubkey, - sequencerId, - _currentBatch, - amount, - newTotalLocked - ); - NFTContract.mint(user, sequencerId); - return sequencerId; - } - - // The function restricts the sequencer's exit if the number of total locked sequencers divided by 3 is less than the number of - // sequencers that have already exited. This would effectively freeze the sequencer's unlock function until a sufficient number of - // new sequencers join the system. - function _unlock( - uint256 sequencerId, - uint256 exitBatch, - bool force, - uint32 l2Gas - ) internal { - if (!force) { - // Ensure that the number of exit sequencer is less than 1/3 of the total - require( - currentUnlockedInit + 1 <= sequencerState.lockerCount / 3, - "unlock not allowed" - ); - } - - uint256 amount = sequencers[sequencerId].amount; - address sequencer = NFTContract.ownerOf(sequencerId); - - sequencers[sequencerId].status = Status.Inactive; - sequencers[sequencerId].deactivationBatch = exitBatch; - sequencers[sequencerId].deactivationTime = block.timestamp; - sequencers[sequencerId].unlockClaimTime = - block.timestamp + - WITHDRAWAL_DELAY; - - uint256 targetBatch = exitBatch <= currentBatch ? 0 : exitBatch; - updateTimeline(-(int256(amount)), -1, targetBatch); - - currentUnlockedInit++; - - _removeSigner(sequencers[sequencerId].signer); - _liquidateRewards( - sequencerId, - sequencers[sequencerId].rewardRecipient, - l2Gas - ); - - logger.logUnlockInit( - sequencer, - sequencerId, - exitBatch, - sequencers[sequencerId].deactivationTime, - sequencers[sequencerId].unlockClaimTime, - amount - ); - } - - function _finalizeCommit() internal { - uint256 nextBatch = currentBatch + 1; - batchSubmitHistory[nextBatch] = true; - - StateChange memory changes = sequencerStateChanges[nextBatch]; - updateTimeline(changes.amount, changes.lockerCount, 0); - - delete sequencerStateChanges[currentBatch]; - - currentBatch = nextBatch; - } - - function _insertSigner(address newSigner) internal { - signers.push(newSigner); - - uint256 lastIndex = signers.length - 1; - uint256 i = lastIndex; - for (; i > 0; --i) { - address signer = signers[i - 1]; - if (signer < newSigner) { - break; - } - signers[i] = signer; - } - - if (i != lastIndex) { - signers[i] = newSigner; - } - } - - function _removeSigner(address signerToDelete) internal { - uint256 totalSigners = signers.length; - for (uint256 i = 0; i < totalSigners; i++) { - if (signers[i] == signerToDelete) { - signers[i] = signers[totalSigners - 1]; - signers.pop(); - break; - } - } - } - - function isContract(address _target) internal view returns (bool) { - return _target.code.length > 0; - } - - function _calculateReward( - uint256 totalRewards, - uint256 totalBlocks, - uint256 finishedBlocks - ) internal pure returns (uint256) { - // rewards are based on BlockInterval multiplied on `perSecondReward` - return totalRewards * (finishedBlocks / totalBlocks); - } - - /** - Private Methods - */ - - function _increaseReward(address sequencer, uint256 reward) private { - uint256 sequencerId = signerToSequencer[sequencer]; - // update reward - sequencers[sequencerId].reward += reward; - } - - function _liquidateRewards( - uint256 sequencerId, - address recipient, - uint32 l2Gas - ) private { - require(recipient != address(0), "invalid reward recipient"); - uint256 reward = sequencers[sequencerId].reward; - totalRewardsLiquidated = totalRewardsLiquidated + reward; - sequencers[sequencerId].reward = 0; - - // withdraw reward to L2 - IERC20(l1Token).safeIncreaseAllowance(bridge, reward); - IL1ERC20Bridge(bridge).depositERC20ToByChainId{value: msg.value}( - l2ChainId, - l1Token, - l2Token, - recipient, - reward, - l2Gas, - "" - ); - logger.logClaimRewards( - sequencerId, - recipient, - reward, - totalRewardsLiquidated - ); - } - - function _transferToken(address destination, uint256 amount) private { - IERC20(l1Token).safeTransfer(destination, amount); - } - - function _transferTokenFrom( - address from, - address destination, - uint256 amount - ) private { - IERC20(l1Token).safeTransferFrom(from, destination, amount); - } - - function _getAndAssertSigner( - bytes memory pub - ) private view returns (address) { - require(pub.length == 64, "not pub"); - address signer = address(uint160(uint256(keccak256(pub)))); - require( - signer != address(0) && signerToSequencer[signer] == 0, - "invalid signer" - ); - return signer; - } - - function _isSequencer( - Status status, - uint256 amount, - uint256 deactivationBatch, - uint256 _currentBatch - ) private pure returns (bool) { - return (amount > 0 && - (deactivationBatch == 0 || - deactivationBatch > _currentBatch || - status == Status.Active)); - } -} diff --git a/contracts/MetisSequencerSet.sol b/contracts/SequencerSet.sol similarity index 95% rename from contracts/MetisSequencerSet.sol rename to contracts/SequencerSet.sol index b4ee327..85036bd 100644 --- a/contracts/MetisSequencerSet.sol +++ b/contracts/SequencerSet.sol @@ -32,6 +32,7 @@ contract MetisSequencerSet is OwnableUpgradeable { event ReCommitEpoch( uint256 indexed oldEpochId, uint256 indexed newEpochId, + uint256 curEpochId, uint256 startBlock, uint256 endBlock, address newSigner @@ -102,14 +103,16 @@ contract MetisSequencerSet is OwnableUpgradeable { // get epoch number by block function getEpochByBlock(uint256 number) public view returns (uint256) { - for (uint256 i = epochNumbers.length; i > 0; ) { - Epoch memory epoch = epochs[epochNumbers[i - 1]]; + uint256 lastIndex = epochNumbers.length - 1; + for (uint256 i = lastIndex; i >= 0; i--) { + Epoch memory epoch = epochs[epochNumbers[i]]; if (epoch.startBlock <= number && number <= epoch.endBlock) { return epoch.number; } - unchecked { - --i; + // not in the last epoch + if (i == lastIndex && number > epoch.endBlock) { + return type(uint256).max; } } @@ -233,6 +236,7 @@ contract MetisSequencerSet is OwnableUpgradeable { emit ReCommitEpoch( oldEpochId, newEpochId, + currentEpochId, startBlock, endBlock, newSigner diff --git a/contracts/interfaces/ILockingBadge.sol b/contracts/interfaces/ILockingBadge.sol new file mode 100644 index 0000000..600f389 --- /dev/null +++ b/contracts/interfaces/ILockingBadge.sol @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.20; + +interface ILockingBadge { + error OwnedSequencer(); + error OwnedBadge(); + error ThresholdExceed(); + error NullAddress(); + error SeqNotActive(); + error NotSeqOwner(); + error NotSeq(); + error NoRewardRecipient(); + error NotWhitelisted(); + + // the sequencer status + enum Status { + Unavailabe, // placeholder for default value + Inactive, // the sequencer will be Inactive if its owner + Active, + Unlocked // Unlocked means sequencer exist + } + + struct Sequencer { + uint256 amount; // sequencer current locked + uint256 reward; // sequencer current reward + uint256 activationBatch; // sequencer activation batch id + uint256 updatingBatch; // batch id of the last updating + uint256 deactivationBatch; // sequencer deactivation batch id + uint256 deactivationTime; // sequencer deactivation timestamp + uint256 unlockClaimTime; // timestamp that sequencer can claim unlocked token, it's equal to deactivationTime + WITHDRAWAL_DELAY + uint256 nonce; // sequencer operations number, starts from 1, and used internally by the Metis consencus client + address owner; // the operator address, owns this sequencer ndoe, it controls lock/relock/unlock/cliam + address signer; // sequencer signer, an address for a sequencer node, it can change signer address + bytes pubkey; // sequencer signer pubkey + address rewardRecipient; // seqeuncer rewarder recipient address + Status status; // sequencer status + } + + /** + * @dev Emitted if owner call 'setThreshold' + * @param _threshold the new threshold + */ + event SetThreshold(uint256 _threshold); + + /** + * @dev Emitted if owner call 'setWhitelist' + * @param _user the address who can lock token + * @param _yes white address state + */ + event SetWhitelist(address _user, bool _yes); + + /** + * @dev Emitted when reward recipient address update in 'setSequencerRewardRecipient' + * @param _seqId the sequencerId + * @param _recipient the address receive reward token + */ + event SequencerRewardRecipientChanged(uint256 _seqId, address _recipient); + + /** + * @dev Emitted when sequencer owner is changed + * @param _seqId the sequencerId + * @param _owner the sequencer owner + */ + event SequencerOwnerChanged(uint256 _seqId, address _owner); + + /** + * @dev Emitted when the sequencer public key is updated in 'updateSigner()'. + * @param sequencerId unique integer to identify a sequencer. + * @param oldSigner oldSigner old address of the sequencer. + * @param newSigner newSigner new address of the sequencer. + * @param nonce to synchronize the events in themis. + * @param signerPubkey signerPubkey public key of the sequencer. + */ + event SignerChange( + uint256 indexed sequencerId, + address indexed oldSigner, + address indexed newSigner, + uint256 nonce, + bytes signerPubkey + ); + + function seqOwners(address owner) external returns (uint256 seqId); +} diff --git a/contracts/interfaces/ILockingEscrow.sol b/contracts/interfaces/ILockingEscrow.sol new file mode 100644 index 0000000..b5b2a0f --- /dev/null +++ b/contracts/interfaces/ILockingEscrow.sol @@ -0,0 +1,159 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.20; + +import {ILockingBadge} from "./ILockingBadge.sol"; + +interface ILockingEscrow { + /** + * @dev Emitted when min lock amount update in 'UpdateMinAmounts' + * @param _newMinLock new min lock. + */ + event SetMinLock(uint256 _newMinLock); + + /** + * @dev Emitted when min lock amount update in 'UpdateMaxAmounts' + * @param _newMaxLock new max lock. + */ + event SetMaxLock(uint256 _newMaxLock); + + /** + * @dev Emitted when the reward payer is changed + * @param _payer new reward payer + */ + event SetRewardPayer(address _payer); + + /** + * @dev Emitted when sequencer locks in '_lockFor()' in LockingPool. + * @param signer sequencer address. + * @param sequencerId unique integer to identify a sequencer. + * @param nonce to synchronize the events in themis. + * @param activationBatch sequencer's first epoch as proposer. + * @param amount locking amount. + * @param total total locking amount. + * @param signerPubkey public key of the sequencer + */ + event Locked( + address indexed signer, + uint256 indexed sequencerId, + uint256 indexed activationBatch, + uint256 nonce, + uint256 amount, + uint256 total, + bytes signerPubkey + ); + + /** + * @dev Emitted when the sequencer increase lock amoun in 'relock()'. + * @param _seqId unique integer to identify a sequencer. + * @param _amount locking new amount + * @param _total the total locking amount + */ + event Relocked(uint256 indexed _seqId, uint256 _amount, uint256 _total); + + /** + * @dev Emitted when sequencer relocking in 'relock()'. + * @param _seqId unique integer to identify a sequencer. + * @param _nonce to synchronize the events in themis. + * @param _amount the updated lock amount. + */ + event LockUpdate( + uint256 indexed _seqId, + uint256 indexed _nonce, + uint256 indexed _amount + ); + + /** + * @dev Emitted when sequencer withdraw rewards in 'withdrawRewards' or 'unlockClaim' + * @param _seqId unique integer to identify a sequencer. + * @param _recipient the address receive reward tokens + * @param _amount the reward amount. + * @param _totalAmount total rewards has liquidated + */ + event ClaimRewards( + uint256 indexed _seqId, + address indexed _recipient, + uint256 _amount, + uint256 _totalAmount + ); + + /** + * @dev Emitted when sequencer unlocks in '_unlock()'. + * @param user address of the sequencer. + * @param sequencerId unique integer to identify a sequencer. + * @param nonce to synchronize the events in themis. + * @param deactivationBatch last batch for sequencer. + * @param deactivationTime unlock block timestamp. + * @param unlockClaimTime when user can claim locked token. + * @param amount locking amount + */ + event UnlockInit( + address indexed user, + uint256 indexed sequencerId, + uint256 nonce, + uint256 deactivationBatch, + uint256 deactivationTime, + uint256 unlockClaimTime, + uint256 indexed amount + ); + + /** + * @dev Emitted when sequencer unlocks in 'unlockClaim()' + * @param user address of the sequencer. + * @param sequencerId unique integer to identify a sequencer. + * @param amount locking amount. + * @param total total locking amount. + */ + event Unlocked( + address indexed user, + uint256 indexed sequencerId, + uint256 amount, + uint256 total + ); + + /** + * @dev Emitted when batch update in 'batchSubmitRewards' + * @param _newBatchId new batchId. + */ + event BatchSubmitReward(uint256 _newBatchId); + + function newSequencer( + uint256 _id, + address _signer, + uint256 _amount, + uint256 _batchId, + bytes calldata _signerPubkey + ) external; + + function increaseLocked( + uint256 _seqId, + uint256 _nonce, + address _owner, + uint256 _locked, + uint256 _incoming, + uint256 _fromReward + ) external; + + function initializeUnlock( + uint256 _seqId, + uint32 _l2gas, + ILockingBadge.Sequencer calldata _seq + ) external payable; + + function finalizeUnlock( + address _owner, + uint256 _seqId, + uint256 _amount, + uint256 _reward, + address _recipient, + uint32 _l2gas + ) external payable; + + function liquidateReward( + uint256 _seqId, + uint256 _amount, + address _recipient, + uint32 _l2gas + ) external payable; + + function distributeReward(uint256 _batchId, uint256 _totalReward) external; +} diff --git a/contracts/interfaces/ILockingManager.sol b/contracts/interfaces/ILockingManager.sol new file mode 100644 index 0000000..fc0a579 --- /dev/null +++ b/contracts/interfaces/ILockingManager.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity 0.8.20; + +interface ILockingManager { + /** + * @dev Emitted when WITHDRAWAL_DELAY is updated. + * @param _cur current withdraw delay time + * @param _prev previours withdraw delay time + */ + event WithrawDelayTimeChange(uint256 _cur, uint256 _prev); + + /** + * @dev Emitted when the proxy update threshold in 'updateBlockReward()'. + * @param newReward new block reward + * @param oldReward old block reward + */ + event RewardUpdate(uint256 newReward, uint256 oldReward); + + /** + * @dev Emitted when mpc address update in 'UpdateMpc' + * @param _newMpc new min lock. + */ + event UpdateMpc(address _newMpc); +} diff --git a/contracts/interfaces/ILockingPool.sol b/contracts/interfaces/ILockingPool.sol deleted file mode 100644 index 2dd8bbc..0000000 --- a/contracts/interfaces/ILockingPool.sol +++ /dev/null @@ -1,104 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -pragma solidity 0.8.20; - -interface ILockingPool { - /** - * @dev lockFor is used to lock Metis and participate in the sequencer block node application - * - * @param signer sequencer signer address - * @param amount Amount of L1 metis token to lock for. - * @param signerPubkey sequencer signer pubkey - */ - function lockFor( - address signer, - uint256 amount, - bytes memory signerPubkey - ) external; - - /** - * @dev relock Allow sequencer to increase the amount of locked positions - * @param sequencerId sequencer id - * @param amount Amount of L1 metis token to relock for. - * @param lockRewards Whether to lock the current reward - */ - function relock( - uint256 sequencerId, - uint256 amount, - bool lockRewards - ) external; - - /** - * @dev withdrawRewards withdraw current reward - * - * @param sequencerId sequencer id - * @param l2Gas bridge reward to L2 gasLimit - */ - function withdrawRewards( - uint256 sequencerId, - uint32 l2Gas - ) external payable; - - /** - * @dev unlock is used to unlock Metis and exit the sequencer node - * - * @param sequencerId sequencer id - * @param l2Gas bridge reward to L2 gasLimit - */ - function unlock(uint256 sequencerId, uint32 l2Gas) external payable; - - /** - * @dev unlockClaim Because unlock has a waiting period, after the waiting period is over, you can claim locked tokens - * - * @param sequencerId sequencer id - * @param l2Gas bridge reward to L2 gasLimit - */ - function unlockClaim(uint256 sequencerId, uint32 l2Gas) external payable; - - /** - * @dev ownerOf query owner of the NFT - * - * @param tokenId NFT token id - */ - function ownerOf(uint256 tokenId) external view returns (address); - - /** - * @dev getSequencerId query sequencer id by signer address - * - * @param user sequencer signer address - */ - function getSequencerId(address user) external view returns (uint256); - - /** - * @dev sequencerReward query sequencer current reward - * - * @param sequencerId sequencerid - */ - function sequencerReward( - uint256 sequencerId - ) external view returns (uint256); - - /** - * @dev sequencerLock return the total lock amount of sequencer - * - * @param sequencerId sequencer id - */ - function sequencerLock(uint256 sequencerId) external view returns (uint256); - - /** - * @dev currentSequencerSetSize get all sequencer count - */ - function currentSequencerSetSize() external view returns (uint256); - - /** - * @dev currentSequencerSetTotalLock get total lock amount for all sequencers - */ - function currentSequencerSetTotalLock() external view returns (uint256); - - /** - * @dev fetchMpcAddress query mpc address by L1 block height, used by batch-submitter - * @param blockHeight L1 block height - */ - function fetchMpcAddress( - uint256 blockHeight - ) external view returns (address); -} diff --git a/ts-src/deploy/02_LockingPool.ts b/ts-src/deploy/01_LockingEscrow.ts similarity index 81% rename from ts-src/deploy/02_LockingPool.ts rename to ts-src/deploy/01_LockingEscrow.ts index bb17f2e..aed3224 100644 --- a/ts-src/deploy/02_LockingPool.ts +++ b/ts-src/deploy/01_LockingEscrow.ts @@ -1,6 +1,6 @@ import { DeployFunction } from "hardhat-deploy/types"; -const ctName = "LockingPool"; +const ctName = "LockingEscrow"; const func: DeployFunction = async function (hre) { if (!hre.network.tags["l1"]) { @@ -19,8 +19,6 @@ const func: DeployFunction = async function (hre) { throw new Error(`MEITS_L1_TOKEN env is not set or it's not an address`); } - const { address: LockingNFTAddress } = - await hre.deployments.get("LockingNFT"); const l2Chainid = parseInt(process.env.METIS_L2_CHAINID as string, 0); if (!l2Chainid) { throw new Error(`METIS_L2_CHAINID env should be valid chainId`); @@ -45,14 +43,7 @@ const func: DeployFunction = async function (hre) { execute: { init: { methodName: "initialize", - args: [ - bridge, - l1Metis, - l2Metis, - LockingNFTAddress, - deployer, - l2Chainid, - ], + args: [bridge, l1Metis, l2Metis, l2Chainid], }, }, }, diff --git a/ts-src/deploy/00_LockingNFT.ts b/ts-src/deploy/02_LockingManager.ts similarity index 57% rename from ts-src/deploy/00_LockingNFT.ts rename to ts-src/deploy/02_LockingManager.ts index a799594..8b7ccdc 100644 --- a/ts-src/deploy/00_LockingNFT.ts +++ b/ts-src/deploy/02_LockingManager.ts @@ -1,6 +1,6 @@ import { DeployFunction } from "hardhat-deploy/types"; -const ctName = "LockingNFT"; +const ctName = "LockingManager"; const func: DeployFunction = async function (hre) { if (!hre.network.tags["l1"]) { @@ -9,12 +9,20 @@ const func: DeployFunction = async function (hre) { const { deployer } = await hre.getNamedAccounts(); - const lockingNftName = "Metis Sequencer"; - const lockingNftSymbol = "MS"; + const { address: LockingEscrowAddress } = + await hre.deployments.get("LockingEscrow"); await hre.deployments.deploy(ctName, { from: deployer, - args: [lockingNftName, lockingNftSymbol], + proxy: { + proxyContract: "OpenZeppelinTransparentProxy", + execute: { + init: { + methodName: "initialize", + args: [LockingEscrowAddress], + }, + }, + }, waitConfirmations: 3, log: true, }); diff --git a/ts-src/deploy/03_LockingInfo.ts b/ts-src/deploy/03_LockingInfo.ts deleted file mode 100644 index 82100e5..0000000 --- a/ts-src/deploy/03_LockingInfo.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { DeployFunction } from "hardhat-deploy/types"; - -const ctName = "LockingInfo"; - -const func: DeployFunction = async function (hre) { - if (!hre.network.tags["l1"]) { - throw new Error(`current network ${hre.network.name} is not an L1`); - } - - const { deployer } = await hre.getNamedAccounts(); - - const { address: LockingPoolAddress } = - await hre.deployments.get("LockingPool"); - - await hre.deployments.deploy(ctName, { - from: deployer, - args: [LockingPoolAddress], - waitConfirmations: 3, - log: true, - }); -}; - -func.tags = [ctName, "l1", "proxy"]; - -export default func; diff --git a/ts-src/deploy/04_L1Config.ts b/ts-src/deploy/04_L1Config.ts index dd868e7..0c76d19 100644 --- a/ts-src/deploy/04_L1Config.ts +++ b/ts-src/deploy/04_L1Config.ts @@ -5,34 +5,21 @@ const func: DeployFunction = async function (hre) { throw new Error(`current network ${hre.network.name} is not an L1`); } - const { address: LockingPoolAddress } = - await hre.deployments.get("LockingPool"); - const { address: LockingNFTAddress } = - await hre.deployments.get("LockingNFT"); - const { address: LockingInfoAddress } = - await hre.deployments.get("LockingInfo"); + const { address: LockingEscrowAddress } = + await hre.deployments.get("LockingEscrow"); - const lockingNFT = await hre.ethers.getContractAt( - "LockingNFT", - LockingNFTAddress, - ); - - // update the owner - if ((await lockingNFT.owner()) != LockingPoolAddress) { - console.log("transfering owner of LockingNFT to LockingPool"); - const tx = await lockingNFT.transferOwnership(LockingPoolAddress); - await tx.wait(3); - } + const { address: LockingManagerAddress } = + await hre.deployments.get("LockingEscrow"); - const lockingPool = await hre.ethers.getContractAt( - "LockingPool", - LockingPoolAddress, + const lockingEscrow = await hre.ethers.getContractAt( + "LockingEscrow", + LockingEscrowAddress, ); - if ((await lockingPool.logger()) !== LockingInfoAddress) { - console.log("setting logger address"); - const tx = await lockingPool.updateLockingInfo(LockingInfoAddress); - await tx.wait(3); + console.log("updating manager address for LockingEscrow"); + if ((await lockingEscrow.manager()) != hre.ethers.ZeroAddress) { + const tx = await lockingEscrow.initManager(LockingManagerAddress); + console.log(`done block=${tx.blockNumber} tx=${tx.hash}`); } }; diff --git a/ts-src/deploy/05_SeqeuncerSet.ts b/ts-src/deploy/05_SeqeuncerSet.ts index f1f870f..674ff69 100644 --- a/ts-src/deploy/05_SeqeuncerSet.ts +++ b/ts-src/deploy/05_SeqeuncerSet.ts @@ -60,6 +60,7 @@ const func: DeployFunction = async function (hre) { ); await hre.deployments.deploy(ctName, { + contract: "MetisSequencerSet", from: deployer, proxy: { proxyContract: "OpenZeppelinTransparentProxy", diff --git a/ts-src/tasks/l1.ts b/ts-src/tasks/l1.ts index b99109c..7ebfe60 100644 --- a/ts-src/tasks/l1.ts +++ b/ts-src/tasks/l1.ts @@ -2,8 +2,10 @@ import { task, types } from "hardhat/config"; import fs from "fs"; import { parseDuration } from "../utils/params"; - -const lockingPoolName = "LockingPool"; +import { + LockingEscrowContractName, + LockingManagerContractName, +} from "../utils/constant"; task("l1:whitelist", "Whitelist an sequencer address") .addParam("addr", "the sequencer address", "", types.string) @@ -18,12 +20,13 @@ task("l1:whitelist", "Whitelist an sequencer address") throw new Error(`${hre.network.name} is not an l1`); } - const { address: lockingPoolAddress } = - await hre.deployments.get("LockingPool"); + const { address: LockingManagerAddress } = await hre.deployments.get( + LockingManagerContractName, + ); - const LockingPool = await hre.ethers.getContractAt( - lockingPoolName, - lockingPoolAddress, + const lockingManager = await hre.ethers.getContractAt( + LockingManagerContractName, + LockingManagerAddress, ); const addr = args["addr"]; @@ -38,18 +41,13 @@ task("l1:whitelist", "Whitelist an sequencer address") console.log(`Removing addr from whitelist`); } - const tx = await LockingPool.setWhiteListAddress(addr, enable); + const tx = await lockingManager.setWhitelist(addr, enable); console.log("Confrimed at", tx.hash); }); task("l1:lock", "Lock Metis to LockingPool contract") - .addParam( - "key", - "the private key file path for the sequencer", - "", - types.string, - ) - .addParam("amount", "lock amount in Metis", "", types.float) + .addParam("key", "the private key file path for the sequencer") + .addParam("amount", "lock amount in Metis", "", types.string) .setAction(async (args, hre) => { if (!hre.network.tags["l1"]) { throw new Error(`${hre.network.name} is not an l1`); @@ -62,8 +60,9 @@ task("l1:lock", "Lock Metis to LockingPool contract") const amountInWei = hre.ethers.parseEther(args["amount"]); - const { address: lockingPoolAddress } = - await hre.deployments.get("LockingPool"); + const { address: LockingManagerAddress } = await hre.deployments.get( + LockingManagerContractName, + ); const [signer] = await hre.ethers.getSigners(); @@ -74,13 +73,14 @@ task("l1:lock", "Lock Metis to LockingPool contract") const seqWallet = new hre.ethers.Wallet(seqKey, hre.ethers.provider); console.log("Locking Metis for", seqWallet.address); - const pool = await hre.ethers.getContractAt( - lockingPoolName, - lockingPoolAddress, + + const lockingManager = await hre.ethers.getContractAt( + LockingManagerContractName, + LockingManagerAddress, ); console.log("checking whitelist status"); - const isWhitelisted = await pool.whiteListAddresses(seqWallet.address); + const isWhitelisted = await lockingManager.whitelist(seqWallet.address); if (!isWhitelisted) { throw new Error(`Your address ${signer.address} is not whitelisted`); } @@ -97,18 +97,18 @@ task("l1:lock", "Lock Metis to LockingPool contract") console.log("checking the allowance"); const allowance = await metis.allowance( seqWallet.address, - lockingPoolAddress, + LockingManagerAddress, ); if (allowance < amountInWei) { console.log("approving Metis to LockingPool"); const tx = await metis .connect(seqWallet) - .approve(await pool.getAddress(), amountInWei); + .approve(LockingManagerAddress, amountInWei); await tx.wait(2); } console.log("locking..."); - const tx = await pool + const tx = await lockingManager .connect(seqWallet) .lockFor( seqWallet.address, @@ -119,31 +119,32 @@ task("l1:lock", "Lock Metis to LockingPool contract") }); task("l1:update-lock-amount", "Update locking amount condition") - .addOptionalParam("min", "Min amount in Metis", "", types.float) - .addOptionalParam("max", "Max amount in Metis", "", types.float) + .addOptionalParam("min", "Min amount in Metis", "", types.string) + .addOptionalParam("max", "Max amount in Metis", "", types.string) .setAction(async (args, hre) => { if (!hre.network.tags["l1"]) { throw new Error(`${hre.network.name} is not an l1`); } - const { address: lockingPoolAddress } = - await hre.deployments.get("LockingPool"); + const { address: LockingEscrowAddress } = await hre.deployments.get( + LockingEscrowContractName, + ); - const contract = await hre.ethers.getContractAt( - lockingPoolName, - lockingPoolAddress, + const lockingEscrow = await hre.ethers.getContractAt( + LockingEscrowContractName, + LockingEscrowAddress, ); let actions = 0; if (args["min"]) { actions++; const min = hre.ethers.parseEther(args["min"]); - const min2 = await contract.minLock(); + const min2 = await lockingEscrow.minLock(); if (min != min2) { console.log( `setting min lock to ${args["min"]}, the previous is ${hre.ethers.formatEther(min2)}`, ); - const tx = await contract.updateMinAmounts(min); + const tx = await lockingEscrow.setMinLock(min); await tx.wait(2); } } @@ -151,12 +152,12 @@ task("l1:update-lock-amount", "Update locking amount condition") if (args["max"]) { actions++; const max = hre.ethers.parseEther(args["max"]); - const max2 = await contract.maxLock(); + const max2 = await lockingEscrow.maxLock(); if (max != max2) { console.log( `setting min lock to ${args["max"]}, the previous is ${hre.ethers.formatEther(max2)}`, ); - const tx = await contract.updateMaxAmounts(max); + const tx = await lockingEscrow.setMaxLock(max); console.log("Confrimed at", tx.hash); } } @@ -168,12 +169,7 @@ task("l1:update-lock-amount", "Update locking amount condition") task("l1:update-mpc-address", "Update MPC address for LockingPool contract") .addParam("addr", "The new MPC address", "", types.string) - .addOptionalParam( - "fund", - "Send ETH gas to the MPC address at last", - "", - types.float, - ) + .addOptionalParam("fund", "Send ETH gas to the MPC address at last") .setAction(async (args, hre) => { if (!hre.network.tags["l1"]) { throw new Error(`${hre.network.name} is not an l1`); @@ -182,9 +178,13 @@ task("l1:update-mpc-address", "Update MPC address for LockingPool contract") const { address: lockingPoolAddress } = await hre.deployments.get("LockingPool"); - const lockingPool = await hre.ethers.getContractAt( - lockingPoolName, - lockingPoolAddress, + const { address: LockingManagerAddress } = await hre.deployments.get( + LockingManagerContractName, + ); + + const lockingManager = await hre.ethers.getContractAt( + LockingManagerContractName, + LockingManagerAddress, ); const newAddr = args["addr"]; @@ -193,7 +193,7 @@ task("l1:update-mpc-address", "Update MPC address for LockingPool contract") } console.log("Updating the MPC address to", newAddr); - const tx = await lockingPool.updateMpc(newAddr); + const tx = await lockingManager.updateMpc(newAddr); console.log("Confrimed at", tx.hash); if (args["fund"]) { @@ -224,16 +224,17 @@ task("l1:update-exit-delay", "update exit delay time duration") throw new Error(`${hre.network.name} is not an l1`); } - const { address: lockingPoolAddress } = - await hre.deployments.get("LockingPool"); + const { address: LockingManagerAddress } = await hre.deployments.get( + LockingManagerContractName, + ); - const lockingPool = await hre.ethers.getContractAt( - lockingPoolName, - lockingPoolAddress, + const lockingManager = await hre.ethers.getContractAt( + LockingManagerContractName, + LockingManagerAddress, ); const duration = parseDuration(args["duration"]); console.log(`update the delay to ${args["duration"]}(=${duration}s)`); - const tx = await lockingPool.updateWithdrawDelayTimeValue(duration); + const tx = await lockingManager.updateWithdrawDelayTimeValue(duration); console.log("Confrimed at", tx.hash); }); diff --git a/ts-src/tasks/l2.ts b/ts-src/tasks/l2.ts index 222db2f..1c96689 100644 --- a/ts-src/tasks/l2.ts +++ b/ts-src/tasks/l2.ts @@ -4,12 +4,7 @@ const conractName = "MetisSequencerSet"; task("l2:update-mpc-address", "Update MPC address for SequencerSet contract") .addParam("addr", "The new MPC address") - .addOptionalParam( - "fund", - "Send the Metis gas to the MPC address at last", - "", - types.float, - ) + .addOptionalParam("fund", "Send the Metis gas to the MPC address at last") .setAction(async (args, hre) => { if (!hre.network.tags["l2"]) { throw new Error(`${hre.network.name} is not an l2`); diff --git a/ts-src/test/LockinInfo.ts b/ts-src/test/LockinInfo.ts deleted file mode 100644 index e995b47..0000000 --- a/ts-src/test/LockinInfo.ts +++ /dev/null @@ -1,153 +0,0 @@ -import { ethers } from "hardhat"; -import { expect } from "chai"; -import { loadFixture } from "@nomicfoundation/hardhat-toolbox/network-helpers"; - -describe("LockingInfo", async () => { - async function fixture() { - const factory = await ethers.getContractFactory("LockingInfo"); - const [wallet, other, other1, other2] = await ethers.getSigners(); - const lockingInfo = await factory.deploy(wallet); - return { lockingInfo, wallet, other, other1, other2 }; - } - - it("update nonce", async () => { - const { lockingInfo, wallet } = await loadFixture(fixture); - - await expect(lockingInfo.updateNonce([1, 2], [3])).to.be.revertedWith( - "args length mismatch", - ); - await lockingInfo.updateNonce([1, 2], [3, 4]); - }); - - it("log locked", async () => { - const { lockingInfo, wallet, other } = await loadFixture(fixture); - - await lockingInfo.logLocked( - wallet.address, - Buffer.from([1, 2, 3]), - 1, - 2, - 2, - 5, - ); - - await expect( - lockingInfo - .connect(other) - .logLocked(wallet.address, Buffer.from([1, 2, 3]), 1, 2, 2, 5), - ).to.be.revertedWith("Invalid sender, not locking pool"); - }); - - it("log unlocked", async () => { - const { lockingInfo, wallet, other } = await loadFixture(fixture); - - await lockingInfo.logUnlocked(wallet.address, 1, 2, 5); - await expect( - lockingInfo.connect(other).logUnlocked(wallet.address, 1, 2, 5), - ).to.be.revertedWith("Invalid sender, not locking pool"); - }); - - it("log unlock init", async () => { - const { lockingInfo, wallet, other } = await loadFixture(fixture); - - await lockingInfo.logUnlockInit(wallet.address, 1, 2, 100, 200, 5); - await expect( - lockingInfo - .connect(other) - .logUnlockInit(wallet.address, 1, 2, 100, 200, 5), - ).to.be.revertedWith("Invalid sender, not locking pool"); - }); - - it("log signer change", async () => { - const { lockingInfo, wallet, other } = await loadFixture(fixture); - - await lockingInfo.logSignerChange( - 1, - wallet.address, - other.address, - Buffer.from([1, 2, 3]), - ); - await expect( - lockingInfo - .connect(other) - .logSignerChange( - 1, - wallet.address, - other.address, - Buffer.from([1, 2, 3]), - ), - ).to.be.revertedWith("Invalid sender, not locking pool"); - }); - - it("log relock", async () => { - const { lockingInfo, wallet, other } = await loadFixture(fixture); - - await lockingInfo.logRelockd(1, 2, 5); - await expect( - lockingInfo.connect(other).logRelockd(1, 2, 5), - ).to.be.revertedWith("Invalid sender, not locking pool"); - }); - - it("log ThresholdChange", async () => { - const { lockingInfo, wallet, other } = await loadFixture(fixture); - - await lockingInfo.logThresholdChange(1, 2); - await expect( - lockingInfo.connect(other).logThresholdChange(1, 2), - ).to.be.revertedWith("Invalid sender, not locking pool"); - }); - - it("log WithrawDelayTimeChange", async () => { - const { lockingInfo, wallet, other } = await loadFixture(fixture); - - await lockingInfo.logWithrawDelayTimeChange(1, 2); - await expect( - lockingInfo.connect(other).logWithrawDelayTimeChange(1, 2), - ).to.be.revertedWith("Invalid sender, not locking pool"); - }); - - it("log RewardUpdate", async () => { - const { lockingInfo, wallet, other } = await loadFixture(fixture); - - await lockingInfo.logRewardUpdate(1, 2); - await expect( - lockingInfo.connect(other).logRewardUpdate(1, 2), - ).to.be.revertedWith("Invalid sender, not locking pool"); - }); - - it("log LockUpdate", async () => { - const { lockingInfo, wallet, other } = await loadFixture(fixture); - - await lockingInfo.logLockUpdate(1, 100); - await expect( - lockingInfo.connect(other).logLockUpdate(1, 100), - ).to.be.revertedWith("Invalid sender, not locking pool"); - }); - - it("log ClaimRewards", async () => { - const { lockingInfo, wallet, other } = await loadFixture(fixture); - - await lockingInfo.logClaimRewards(1, wallet.address, 100, 200); - await expect( - lockingInfo.connect(other).logClaimRewards(1, wallet.address, 100, 200), - ).to.be.revertedWith("Invalid sender, not locking pool"); - }); - - it("log logBatchSubmitReward", async () => { - const { lockingInfo, wallet, other } = await loadFixture(fixture); - - await lockingInfo.logBatchSubmitReward(1); - await expect( - lockingInfo.connect(other).logBatchSubmitReward(1), - ).to.be.revertedWith("Invalid sender, not locking pool"); - }); - - it("log logUpdateEpochLength", async () => { - const { lockingInfo, wallet, other } = await loadFixture(fixture); - - await lockingInfo.logUpdateEpochLength(100, 1000, 5); - await expect( - lockingInfo.connect(other).logUpdateEpochLength(100, 1000, 5), - ).to.be.revertedWith("Invalid sender, not locking pool"); - }); -}); diff --git a/ts-src/test/LockingNFT.ts b/ts-src/test/LockingNFT.ts deleted file mode 100644 index a72ab6f..0000000 --- a/ts-src/test/LockingNFT.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { ethers } from "hardhat"; -import { expect } from "chai"; -import { loadFixture } from "@nomicfoundation/hardhat-toolbox/network-helpers"; - -describe("lockingNFT", async () => { - async function fixture() { - const factory = await ethers.getContractFactory("LockingNFT"); - const lockingNFT = await factory.deploy("Metis Sequencer", "MS"); - const [wallet, other, other1, other2] = await ethers.getSigners(); - return { lockingNFT, wallet, other, other1, other2 }; - } - - it("name,symbol", async () => { - const LockingNFTFactory = await ethers.getContractFactory("LockingNFT"); - await expect(LockingNFTFactory.deploy("", "SYMBOL")).to.be.revertedWith( - "invalid name", - ); - - await expect(LockingNFTFactory.deploy("NAME", "")).to.be.revertedWith( - "invalid symbol", - ); - - const name = "Metis Sequencer"; - const symbol = "MS"; - - const token = await LockingNFTFactory.deploy(name, symbol); - expect(await token.name(), name); - expect(await token.symbol(), symbol); - }); - - it("mint NFT", async () => { - const { lockingNFT, wallet } = await loadFixture(fixture); - - await lockingNFT.mint(wallet.address, 1); - let ownerOfTokenId1 = await lockingNFT.ownerOf(1); - expect(ownerOfTokenId1).to.eq(wallet.address); - - await expect(lockingNFT.mint(wallet.address, 2)).to.be.revertedWith( - "Sequencers MUST NOT own multiple lock position", - ); - }); - - it("burn NFT", async () => { - const { lockingNFT, wallet } = await loadFixture(fixture); - - await lockingNFT.mint(wallet.address, 2); - await lockingNFT.burn(2); - await expect(lockingNFT.ownerOf(2)).to.be.revertedWith( - "ERC721: invalid token ID", - ); - }); - - it("transfer NFT", async () => { - const { lockingNFT, wallet, other, other1, other2 } = - await loadFixture(fixture); - - await lockingNFT.mint(wallet.address, 3); - - await lockingNFT.approve(other.address, 3); - await lockingNFT.transferFrom(wallet.address, other.address, 3); - - let ownerOfTokenId3 = await lockingNFT.ownerOf(3); - expect(ownerOfTokenId3).to.eq(other.address); - - await lockingNFT.connect(other).approve(other1.address, 3); - await expect( - lockingNFT.connect(other).transferFrom(other.address, other1.address, 3), - ).to.be.revertedWith("Ownable: caller is not the owner"); - - await lockingNFT.mint(other2.address, 4); - await lockingNFT.transferOwnership(other.address); - await lockingNFT.connect(other).approve(other2.address, 3); - await expect( - lockingNFT.connect(other).transferFrom(other.address, other2.address, 3), - ).to.be.revertedWith("Sequencers MUST NOT own multiple lock position"); - }); -}); diff --git a/ts-src/test/LockingPool.ts b/ts-src/test/LockingPool.ts deleted file mode 100644 index 1efff2e..0000000 --- a/ts-src/test/LockingPool.ts +++ /dev/null @@ -1,513 +0,0 @@ -import { ethers, deployments } from "hardhat"; -import { expect } from "chai"; -import { loadFixture } from "@nomicfoundation/hardhat-toolbox/network-helpers"; - -const zeroAddress = ethers.ZeroAddress; -const l2MetisAddr = "0xDeadDeAddeAddEAddeadDEaDDEAdDeaDDeAD0000"; - -const trimPubKeyPrefix = (key: string) => { - if (key.startsWith("0x")) { - key = key.slice(2); - } - if (key.startsWith("04")) { - key = key.slice(2); - } - return Buffer.from(key, "hex"); -}; - -describe("LockingPool", async () => { - async function fixture() { - const wallets = new Array(5) - .fill(null) - .map(() => ethers.Wallet.createRandom(ethers.provider)); - - const [admin, mpc, ...others] = await ethers.getSigners(); - - // deploy test bridge - const TestBridge = await ethers.getContractFactory("TestBridge"); - const l1Bridge = await TestBridge.deploy(); - - // deploy test ERC20 - const TestERC20 = await ethers.getContractFactory("TestERC20"); - const metisToken = await TestERC20.deploy(0); - - const mintAmount = ethers.parseEther("1000"); - for (const wallet of wallets) { - await admin.sendTransaction({ - to: wallet.address, - value: ethers.parseEther("10"), - }); - await metisToken.mint(wallet, mintAmount); - } - - const LockingNFT = await ethers.getContractFactory("LockingNFT"); - - const lockingNFT = await LockingNFT.deploy("Metis Sequencer", "MS"); - - const lockingPoolProxy = await deployments.deploy("LockingPool", { - from: admin.address, - proxy: { - proxyContract: "OpenZeppelinTransparentProxy", - execute: { - init: { - methodName: "initialize", - args: [ - await l1Bridge.getAddress(), - await metisToken.getAddress(), - l2MetisAddr, - await lockingNFT.getAddress(), - mpc.address, - 0xdeadbeaf, - ], - }, - }, - }, - }); - - const lockingPool = await ethers.getContractAt( - "LockingPool", - lockingPoolProxy.address, - ); - - // approve the metis to the lockingPool - for (const wallet of wallets) { - await metisToken - .connect(wallet) - .approve(await lockingPool.getAddress(), ethers.MaxUint256); - } - - // first two addresses are whitelisted - await lockingPool.setWhiteListAddress(wallets[0].address, true); - await lockingPool.setWhiteListAddress(wallets[1].address, true); - - const LockingInfo = await ethers.getContractFactory("LockingInfo"); - const lockingInfo = await LockingInfo.deploy( - await lockingPool.getAddress(), - ); - - await lockingNFT.transferOwnership(lockingPoolProxy.address); - await lockingPool.updateLockingInfo(await lockingInfo.getAddress()); - - return { - wallets, - lockingNFT, - metisToken, - l1Bridge, - admin, - mpc, - others, - lockingPool, - lockingInfo, - }; - } - - it("initializer modifier", async () => { - const { lockingPool, mpc } = await loadFixture(fixture); - await expect( - lockingPool.initialize( - ethers.ZeroAddress, - ethers.ZeroAddress, - l2MetisAddr, - ethers.ZeroAddress, - mpc, - 0xdead, - ), - ).to.be.revertedWith("Initializable: contract is already initialized"); - }); - - it("bridge,l2ChainId,l2Token...", async () => { - const { lockingPool, l1Bridge, metisToken, lockingNFT, mpc, admin } = - await loadFixture(fixture); - expect(await lockingPool.bridge()).to.be.eq(await l1Bridge.getAddress()); - expect(await lockingPool.l1Token()).to.be.eq(await metisToken.getAddress()); - expect(await lockingPool.l2Token()).to.be.eq(l2MetisAddr); - expect(await lockingPool.NFTContract()).to.be.eq( - await lockingNFT.getAddress(), - ); - expect(await lockingPool.NFTContract()).to.be.eq( - await lockingNFT.getAddress(), - ); - expect(await lockingPool.mpcAddress()).to.be.eq(mpc.address); - expect(await lockingPool.l2ChainId()).to.be.eq(0xdeadbeaf); - expect(await lockingPool.owner()).to.be.eq(admin, "admin address"); - expect(await lockingPool.paused()).to.be.false; - - const { newMpcAddress } = await lockingPool.mpcHistory(0); - expect(newMpcAddress).to.be.eq(mpc.address); - }); - - it("updateNFTContract", async () => { - const { lockingPool, others } = await loadFixture(fixture); - - const LockingNFT = await ethers.getContractFactory("LockingNFT"); - const lockingNFT = await LockingNFT.deploy("New Metis Sequencer", "NMS"); - - await expect( - lockingPool - .connect(others[0]) - .updateNFTContract(await lockingNFT.getAddress()), - ).to.be.revertedWith("Ownable: caller is not the owner"); - - await expect(lockingPool.updateNFTContract(zeroAddress)).to.be.revertedWith( - "invalid _nftContract", - ); - - expect(await lockingPool.updateNFTContract(await lockingNFT.getAddress())) - .with.emit(lockingPool, "UpdateNFTContract") - .withArgs(await lockingNFT.getAddress()); - expect(await lockingPool.NFTContract()).to.eq( - await lockingNFT.getAddress(), - ); - }); - - it("updateLockingInfo", async () => { - const { lockingPool, others } = await loadFixture(fixture); - - const LockingInfo = await ethers.getContractFactory("LockingInfo"); - const lockingInfo = await LockingInfo.deploy( - await lockingPool.getAddress(), - ); - - await expect( - lockingPool - .connect(others[0]) - .updateLockingInfo(await lockingInfo.getAddress()), - ).to.be.revertedWith("Ownable: caller is not the owner"); - - await expect(lockingPool.updateLockingInfo(zeroAddress)).to.be.revertedWith( - "invalid _lockingInfo", - ); - - expect(await lockingPool.updateLockingInfo(await lockingInfo.getAddress())) - .with.emit(lockingPool, "UpdateNFTContract") - .withArgs(await lockingInfo.getAddress()); - expect(await lockingPool.logger()).to.eq(await lockingInfo.getAddress()); - }); - - it("updateSequencerThreshold", async () => { - const { lockingPool, lockingInfo, mpc, others } = - await loadFixture(fixture); - - const curThreadhold = await lockingPool.sequencerThreshold(); - const newThreshold = 10; - - await expect( - lockingPool.connect(mpc).updateSequencerThreshold(newThreshold), - ).to.be.revertedWith("Ownable: caller is not the owner"); - - await expect( - lockingPool.connect(others[0]).updateSequencerThreshold(newThreshold), - ).to.be.revertedWith("Ownable: caller is not the owner"); - - await expect(lockingPool.updateSequencerThreshold(0)).to.be.revertedWith( - "invalid newThreshold", - ); - - expect(await lockingPool.updateSequencerThreshold(newThreshold)) - .to.emit(lockingInfo, "ThresholdChange") - .withArgs(newThreshold, curThreadhold); - expect(await lockingPool.sequencerThreshold()).to.eq(newThreshold); - }); - - it("updatePerSecondReward", async () => { - const { lockingPool, mpc, others, lockingInfo } = - await loadFixture(fixture); - - const curReward = await lockingPool.perSecondReward(); - const newReward = 10n; - - await expect( - lockingPool.connect(mpc).updatePerSecondReward(newReward), - ).to.be.revertedWith("Ownable: caller is not the owner"); - - await expect( - lockingPool.connect(others[0]).updatePerSecondReward(newReward), - ).to.be.revertedWith("Ownable: caller is not the owner"); - - await expect(lockingPool.updatePerSecondReward(0)).to.be.revertedWith( - "invalid newReward", - ); - - expect(await lockingPool.updatePerSecondReward(newReward)) - .to.emit(lockingInfo, "RewardUpdate") - .withArgs(newReward, curReward); - expect(await lockingPool.perSecondReward()).to.be.eq( - newReward, - "newReward check", - ); - }); - - it("updateWithdrawDelayTimeValue", async () => { - const { lockingPool, mpc, others, lockingInfo } = - await loadFixture(fixture); - - const curDelayTime = await lockingPool.WITHDRAWAL_DELAY(); - const newDelayTime = 24 * 3600 * 1000; - - await expect( - lockingPool.connect(mpc).updateWithdrawDelayTimeValue(newDelayTime), - ).to.be.revertedWith("Ownable: caller is not the owner"); - - await expect( - lockingPool.connect(others[0]).updateWithdrawDelayTimeValue(newDelayTime), - ).to.be.revertedWith("Ownable: caller is not the owner"); - - await expect( - lockingPool.updateWithdrawDelayTimeValue(0), - ).to.be.revertedWith("invalid newWithdrawDelayTime"); - - expect(await lockingPool.updateWithdrawDelayTimeValue(newDelayTime)) - .to.emit(lockingInfo, "WithrawDelayTimeChange") - .withArgs(newDelayTime, curDelayTime); - expect(await lockingPool.WITHDRAWAL_DELAY()).to.eq(newDelayTime); - }); - - it("updateSignerUpdateLimit", async () => { - const { lockingPool, mpc, others } = await loadFixture(fixture); - - const newLimit = 10; - - await expect( - lockingPool.connect(mpc).updateSignerUpdateLimit(newLimit), - ).to.be.revertedWith("Ownable: caller is not the owner"); - - await expect( - lockingPool.connect(others[0]).updateSignerUpdateLimit(newLimit), - ).to.be.revertedWith("Ownable: caller is not the owner"); - - await expect(lockingPool.updateSignerUpdateLimit(0)).to.be.revertedWith( - "invalid _limit", - ); - - expect(await lockingPool.updateSignerUpdateLimit(newLimit)) - .to.emit(lockingPool, "UpdateSignerUpdateLimit") - .withArgs(newLimit); - expect(await lockingPool.signerUpdateLimit()).to.eq(newLimit); - }); - - it("updateMinAmounts", async () => { - const { lockingPool, mpc, others } = await loadFixture(fixture); - - const newLimit = 10; - - await expect( - lockingPool.connect(mpc).updateMinAmounts(newLimit), - ).to.be.revertedWith("Ownable: caller is not the owner"); - - await expect( - lockingPool.connect(others[0]).updateMinAmounts(newLimit), - ).to.be.revertedWith("Ownable: caller is not the owner"); - - await expect(lockingPool.updateMinAmounts(0)).to.be.revertedWith( - "invalid _minLock", - ); - - expect(await lockingPool.updateMinAmounts(newLimit)) - .to.emit(lockingPool, "UpdateMinAmounts") - .withArgs(newLimit); - expect(await lockingPool.minLock()).to.eq(newLimit); - }); - - it("updateMaxAmounts", async () => { - const { lockingPool, mpc, others } = await loadFixture(fixture); - - const newLimit = 10; - - await expect( - lockingPool.connect(mpc).updateMaxAmounts(newLimit), - ).to.be.revertedWith("Ownable: caller is not the owner"); - - await expect( - lockingPool.connect(others[0]).updateMaxAmounts(newLimit), - ).to.be.revertedWith("Ownable: caller is not the owner"); - - await expect(lockingPool.updateMaxAmounts(0)).to.be.revertedWith( - "invalid _maxLock", - ); - - expect(await lockingPool.updateMaxAmounts(newLimit)) - .to.emit(lockingPool, "UpdateMaxAmounts") - .withArgs(newLimit); - expect(await lockingPool.maxLock(), "macLock").to.eq(newLimit); - }); - - it("updateMpc", async () => { - const { lockingPool, l1Bridge, mpc, others } = await loadFixture(fixture); - - const newLimit = 10; - - await expect( - lockingPool.connect(mpc).updateMpc(others[0]), - ).to.be.revertedWith("Ownable: caller is not the owner"); - - await expect( - lockingPool.connect(others[0]).updateMpc(others[0]), - ).to.be.revertedWith("Ownable: caller is not the owner"); - - await expect(lockingPool.updateMpc(l1Bridge)).to.be.revertedWith( - "_newMpc is a contract", - ); - - await expect(lockingPool.updateMpc(ethers.ZeroAddress)).to.be.revertedWith( - "_newMpc is zero address", - ); - - expect(await lockingPool.updateMpc(others[0])) - .to.emit(lockingPool, "UpdateMaxAmounts") - .withArgs(newLimit); - - expect(await lockingPool.mpcAddress()).to.eq(others[0]); - const theHeight = await ethers.provider.getBlockNumber(); - const { startBlock, newMpcAddress } = await lockingPool.mpcHistory(1); - expect(theHeight, "mpcHistory.startBlock").to.be.eq(startBlock); - expect(newMpcAddress, "mpcHistory.newMpcAddress").to.be.eq(others[0]); - }); - - it("setWhiteListAddress", async () => { - const { lockingPool, mpc, others } = await loadFixture(fixture); - - expect( - await lockingPool.whiteListAddresses(others[0]), - "0/whitelist/false?", - ).to.be.false; - - await expect( - lockingPool.connect(mpc).setWhiteListAddress(others[0], true), - ).to.be.revertedWith("Ownable: caller is not the owner"); - - await expect( - lockingPool.connect(others[0]).setWhiteListAddress(others[0], true), - ).to.be.revertedWith("Ownable: caller is not the owner"); - - expect(await lockingPool.setWhiteListAddress(others[0], true)) - .to.emit(lockingPool, "WhiteListAdded") - .withArgs(others[0], true); - - expect(await lockingPool.whiteListAddresses(others[0]), "0/whitelist/true?") - .to.be.true; - - await expect( - lockingPool.setWhiteListAddress(others[0], true), - ).to.be.revertedWith("state not change"); - - expect(await lockingPool.setWhiteListAddress(others[0], false)) - .to.emit(lockingPool, "WhiteListAdded") - .withArgs(others[0], false); - - expect(await lockingPool.whiteListAddresses(others[0])).to.be.false; - }); - - it("setPause/setUnpause", async () => { - const { lockingPool, mpc, others } = await loadFixture(fixture); - - expect(await lockingPool.paused()).to.be.false; - - await expect(lockingPool.connect(mpc).setPause()).to.be.revertedWith( - "Ownable: caller is not the owner", - ); - - await expect(lockingPool.connect(others[0]).setPause()).to.be.revertedWith( - "Ownable: caller is not the owner", - ); - - await lockingPool.setPause(); - expect(await lockingPool.paused()).to.be.true; - - await expect(lockingPool.connect(mpc).setUnpause()).to.be.revertedWith( - "Ownable: caller is not the owner", - ); - - await expect( - lockingPool.connect(others[0]).setUnpause(), - ).to.be.revertedWith("Ownable: caller is not the owner"); - - await lockingPool.setUnpause(); - expect(await lockingPool.paused()).to.be.false; - }); - - it("lockFor", async () => { - const { lockingPool, mpc, others, metisToken, wallets } = - await loadFixture(fixture); - - const [wallet0, wallet1, wallet2] = wallets; - const minLock = 1n; - await lockingPool.updateMinAmounts(minLock); - const maxLock = 10n; - await lockingPool.updateMaxAmounts(maxLock); - - await lockingPool.setPause(); - await expect( - lockingPool - .connect(wallet0) - .lockFor( - wallet0, - minLock, - trimPubKeyPrefix(wallet0.signingKey.publicKey), - ), - ).to.be.revertedWith("Pausable: paused"); - await lockingPool.setUnpause(); - - await expect( - lockingPool - .connect(wallet2) - .lockFor( - wallet2, - minLock, - trimPubKeyPrefix(wallet2.signingKey.publicKey), - ), - ).to.be.revertedWith("msg sender should be in the white list"); - - await expect( - lockingPool - .connect(wallet0) - .lockFor(wallet0, 0, trimPubKeyPrefix(wallet0.signingKey.publicKey)), - ).to.be.revertedWith("amount less than minLock"); - - await expect( - lockingPool - .connect(wallet0) - .lockFor( - wallet0, - maxLock + 1n, - trimPubKeyPrefix(wallet0.signingKey.publicKey), - ), - ).to.be.revertedWith("amount large than maxLock"); - - await expect( - lockingPool - .connect(wallet0) - .lockFor(wallet0, minLock, Buffer.from([1, 2, 3])), - ).to.be.revertedWith("not pub"); - - await expect( - lockingPool - .connect(wallet0) - .lockFor( - wallet0, - minLock, - trimPubKeyPrefix(wallet1.signingKey.publicKey), - ), - ).to.be.revertedWith("user and signerPubkey mismatch"); - - { - await lockingPool - .connect(wallet0) - .lockFor( - wallet0, - minLock, - trimPubKeyPrefix(wallet0.signingKey.publicKey), - ); - - expect(await lockingPool.NFTCounter()).to.eq(2); - expect(await lockingPool.ownerOf(1)).to.eq(wallet0.address); - expect( - await metisToken.balanceOf(await lockingPool.getAddress()), - ).to.be.eq(minLock); - - expect(await lockingPool.sequencerLock(1)).to.eq(minLock); - expect(await lockingPool.getSequencerId(wallet0)).to.eq(1); - expect(await lockingPool.currentSequencerSetSize()).to.eq(1); - expect(await lockingPool.isSequencer(1)).to.be.true; - } - }); -}); diff --git a/ts-src/utils/constant.ts b/ts-src/utils/constant.ts new file mode 100644 index 0000000..fb712cd --- /dev/null +++ b/ts-src/utils/constant.ts @@ -0,0 +1,5 @@ +export const LockingEscrowContractName = "LockingEscrow"; + +export const LockingManagerContractName = "LockingManager"; + +export const SequencerSetContractName = "MetisSequencerSet"; diff --git a/ts-src/utils/params.ts b/ts-src/utils/params.ts index 793ed0b..5740e85 100644 --- a/ts-src/utils/params.ts +++ b/ts-src/utils/params.ts @@ -30,3 +30,13 @@ export const parseDuration = (durationString: string) => { return totalSeconds; }; + +export const trimPubKeyPrefix = (key: string) => { + if (key.startsWith("0x")) { + key = key.slice(2); + } + if (key.startsWith("04")) { + key = key.slice(2); + } + return Buffer.from(key, "hex"); +};