diff --git a/contracts/governance/locking/Locking.sol b/contracts/governance/locking/Locking.sol index 51e169b5..412b5f06 100644 --- a/contracts/governance/locking/Locking.sol +++ b/contracts/governance/locking/Locking.sol @@ -60,7 +60,7 @@ contract Locking is ILocking, LockingBase, LockingRelock, LockingVotes { counter++; uint32 currentBlock = getBlockNumber(); - uint32 time = roundTimestamp(currentBlock); + uint32 time = getWeekNumber(currentBlock); addLines(account, _delegate, amount, slopePeriod, cliff, time, currentBlock); accounts[account].amount = accounts[account].amount + (amount); @@ -92,7 +92,7 @@ contract Locking is ILocking, LockingBase, LockingRelock, LockingVotes { function getAvailableForWithdraw(address account) public view returns (uint96) { uint96 value = accounts[account].amount; uint32 currentBlock = getBlockNumber(); - uint32 time = roundTimestamp(currentBlock); + uint32 time = getWeekNumber(currentBlock); uint96 bias = accounts[account].locked.actualValue(time, currentBlock); value = value - (bias); return value; @@ -124,7 +124,7 @@ contract Locking is ILocking, LockingBase, LockingRelock, LockingVotes { * since the starting point week. The starting point is set during the contract initialization. */ function getWeek() external view returns (uint256) { - return roundTimestamp(getBlockNumber()); + return getWeekNumber(getBlockNumber()); } /** @@ -139,7 +139,7 @@ contract Locking is ILocking, LockingBase, LockingRelock, LockingVotes { address account = verifyLockOwner(id); address _delegate = locks[id].delegate; uint32 currentBlock = getBlockNumber(); - uint32 time = roundTimestamp(currentBlock); + uint32 time = getWeekNumber(currentBlock); accounts[_delegate].balance.update(time); (uint96 bias, uint96 slope, uint32 cliff) = accounts[_delegate].balance.remove(id, time, currentBlock); LibBrokenLine.Line memory line = LibBrokenLine.Line(time, bias, slope, cliff); @@ -158,7 +158,7 @@ contract Locking is ILocking, LockingBase, LockingRelock, LockingVotes { return 0; } uint32 currentBlock = getBlockNumber(); - uint32 time = roundTimestamp(currentBlock); + uint32 time = getWeekNumber(currentBlock); return totalSupplyLine.actualValue(time, currentBlock); } @@ -172,7 +172,7 @@ contract Locking is ILocking, LockingBase, LockingRelock, LockingVotes { return 0; } uint32 currentBlock = getBlockNumber(); - uint32 time = roundTimestamp(currentBlock); + uint32 time = getWeekNumber(currentBlock); return accounts[account].balance.actualValue(time, currentBlock); } diff --git a/contracts/governance/locking/LockingBase.sol b/contracts/governance/locking/LockingBase.sol index f6e8509b..6953b29c 100644 --- a/contracts/governance/locking/LockingBase.sol +++ b/contracts/governance/locking/LockingBase.sol @@ -17,9 +17,17 @@ import "./libs/LibBrokenLine.sol"; abstract contract LockingBase is OwnableUpgradeable, IVotesUpgradeable { using LibBrokenLine for LibBrokenLine.BrokenLine; /** - * @dev Duration of a week in blocks on the CELO blockchain assuming 5 seconds per block + * @dev Duration of a week in blocks on the CELO blockchain before the L2 transition (5 seconds per block) */ uint32 public constant WEEK = 120_960; + /** + * @dev Duration of a week in blocks on the CELO blockchain after the L2 transition (1 seconds per block) + */ + uint32 public constant L2_WEEK = 604_800; + /** + * @dev Epoch shift for L1 + */ + uint32 public constant L1_EPOCH_SHIFT = 89964; /** * @dev Maximum allowable cliff period for token locks in weeks */ @@ -84,6 +92,31 @@ abstract contract LockingBase is OwnableUpgradeable, IVotesUpgradeable { * @dev Total supply line of veMento */ LibBrokenLine.BrokenLine public totalSupplyLine; + + // *************** + // New variables for L2 transition upgrade (3 slots) + // *************** + /** + * @dev L2 transition block number + */ + uint256 public l2TransitionBlock; + /** + * @dev L2 starting point week number + */ + int256 public l2StartingPointWeek; + /** + * @dev Shift amount used after L2 transition to move the start of the epoch to 00-00 UTC Wednesday (approx) + */ + uint32 public l2EpochShift; + /** + * @dev Address of the Mento Labs multisig + */ + address public mentoLabsMultisig; + /** + * @dev Flag to pause locking and governance + */ + bool public paused; + /** * @dev Emitted when create Lock with parameters (account, delegate, amount, slopePeriod, cliff) */ @@ -125,6 +158,26 @@ abstract contract LockingBase is OwnableUpgradeable, IVotesUpgradeable { * @dev set newMinSlopePeriod */ event SetMinSlopePeriod(uint256 indexed newMinSlopePeriod); + /** + * @dev set new Mento Labs multisig address + */ + event SetMentoLabsMultisig(address indexed mentoLabsMultisig); + /** + * @dev set new L2 transition block number + */ + event SetL2TransitionBlock(uint256 indexed l2TransitionBlock); + /** + * @dev set new L2 shift amount + */ + event SetL2EpochShift(uint32 indexed l2EpochShift); + /** + * @dev set new L2 starting point week number + */ + event SetL2StartingPointWeek(int256 indexed l2StartingPointWeek); + /** + * @dev set new paused flag + */ + event SetPaused(bool indexed paused); /** * @dev Initializes the contract with token, starting point week, and minimum cliff and slope periods. @@ -149,6 +202,11 @@ abstract contract LockingBase is OwnableUpgradeable, IVotesUpgradeable { minSlopePeriod = _minSlopePeriod; } + modifier onlyMentoLabs() { + require(msg.sender == mentoLabsMultisig, "caller is not MentoLabs multisig"); + _; + } + /** * @notice Adds a new locking line for an account, initializing the lock with specified parameters. * @param account Address for which tokens are being locked. @@ -221,7 +279,7 @@ abstract contract LockingBase is OwnableUpgradeable, IVotesUpgradeable { * @param amount of tokens to lock * @param slopePeriod period over which the tokens will unlock * @param cliff initial period during which tokens remain locked and do not start unlocking - **/ + */ function getLock( uint96 amount, uint32 slopePeriod, @@ -256,23 +314,46 @@ abstract contract LockingBase is OwnableUpgradeable, IVotesUpgradeable { /** * @notice Calculates the week number for a given blocknumber - * @param ts block number + * @dev It takes L2 transition into account to calculate the week number consistently + * @param blockNumber block number to calculate the week number for * @return week number the block number belongs to */ - function roundTimestamp(uint32 ts) public view returns (uint32) { - if (ts < getEpochShift()) { + function getWeekNumber(uint32 blockNumber) public view returns (uint32) { + require(!paused, "locking is paused"); + + if (blockNumber < _getEpochShift(blockNumber)) { return 0; } - uint32 shifted = ts - (getEpochShift()); - return shifted / WEEK - uint32(startingPointWeek); + uint32 shifted = blockNumber - _getEpochShift(blockNumber); + + if (_isPreL2Transition(blockNumber)) { + return shifted / WEEK - uint32(startingPointWeek); + } else { + return uint32(uint256(int256(uint256(shifted / L2_WEEK)) - l2StartingPointWeek)); + } + } + + /** + * @notice Returns the epoch shift based on L2 transition status + * @dev Epoch shift is the amount of blocks to move the epoch start to 00-00 UTC Wednesday (approx). + * @dev l2EpochShift will be moved into a constant once L2 transition is complete and stable. + * @param blockNumber block number to calculate the shift for + * @return shift amount in blocks (L1_EPOCH_SHIFT for L1, l2EpochShift for L2) + */ + function _getEpochShift(uint32 blockNumber) internal view virtual returns (uint32) { + if (_isPreL2Transition(blockNumber)) { + return L1_EPOCH_SHIFT; + } + return l2EpochShift; } /** - * @notice method returns the amount of blocks to shift locking epoch to. - * we move it to 00-00 UTC Wednesday (approx) by shifting 89964 blocks (CELO) + * @notice Determines if a block is before the L2 transition point + * @param blockNumber block number to check + * @return true if before L2 transition, false if after */ - function getEpochShift() internal view virtual returns (uint32) { - return 89964; + function _isPreL2Transition(uint32 blockNumber) internal view returns (bool) { + return l2TransitionBlock == 0 || blockNumber < l2TransitionBlock; } /** @@ -339,7 +420,7 @@ abstract contract LockingBase is OwnableUpgradeable, IVotesUpgradeable { * @param blockNumber block number until which to update lines */ function updateAccountLinesBlockNumber(address account, uint32 blockNumber) external onlyOwner { - uint32 time = roundTimestamp(blockNumber); + uint32 time = getWeekNumber(blockNumber); updateAccountLines(account, time); } @@ -348,9 +429,59 @@ abstract contract LockingBase is OwnableUpgradeable, IVotesUpgradeable { * @param blockNumber block number until which to update line */ function updateTotalSupplyLineBlockNumber(uint32 blockNumber) external onlyOwner { - uint32 time = roundTimestamp(blockNumber); + uint32 time = getWeekNumber(blockNumber); updateTotalSupplyLine(time); } - uint256[50] private __gap; + /** + * @notice Sets the Mento Labs multisig address + * @param mentoLabsMultisig_ address of the Mento Labs multisig + */ + function setMentoLabsMultisig(address mentoLabsMultisig_) external onlyOwner { + mentoLabsMultisig = mentoLabsMultisig_; + emit SetMentoLabsMultisig(mentoLabsMultisig_); + } + + /** + * @notice Sets the L2 transition block number and pauses locking and governance + * @param l2TransitionBlock_ block number of the L2 transition + */ + function setL2TransitionBlock(uint256 l2TransitionBlock_) external onlyMentoLabs { + l2TransitionBlock = l2TransitionBlock_; + paused = true; + + emit SetL2TransitionBlock(l2TransitionBlock_); + } + + /** + * @notice Sets the L2 epoch shift amount + * @param l2EpochShift_ shift amount that will be used after L2 transition + */ + function setL2EpochShift(uint32 l2EpochShift_) external onlyMentoLabs { + l2EpochShift = l2EpochShift_; + + emit SetL2EpochShift(l2EpochShift_); + } + + /** + * @notice Sets the L2 starting point week number + * @param l2StartingPointWeek_ starting point week number that will be used after L2 transition + */ + function setL2StartingPointWeek(int256 l2StartingPointWeek_) external onlyMentoLabs { + l2StartingPointWeek = l2StartingPointWeek_; + + emit SetL2StartingPointWeek(l2StartingPointWeek_); + } + + /** + * @notice Sets the paused flag + * @param paused_ flag to pause locking and governance + */ + function setPaused(bool paused_) external onlyMentoLabs { + paused = paused_; + + emit SetPaused(paused_); + } + + uint256[47] private __gap; } diff --git a/contracts/governance/locking/LockingRelock.sol b/contracts/governance/locking/LockingRelock.sol index 089f07cb..f8fdaa36 100644 --- a/contracts/governance/locking/LockingRelock.sol +++ b/contracts/governance/locking/LockingRelock.sol @@ -30,7 +30,7 @@ abstract contract LockingRelock is LockingBase { address account = verifyLockOwner(id); uint32 currentBlock = getBlockNumber(); - uint32 time = roundTimestamp(currentBlock); + uint32 time = getWeekNumber(currentBlock); verification(account, id, newAmount, newSlopePeriod, newCliff, time); address _delegate = locks[id].delegate; diff --git a/contracts/governance/locking/LockingVotes.sol b/contracts/governance/locking/LockingVotes.sol index 6d84417a..710e4845 100644 --- a/contracts/governance/locking/LockingVotes.sol +++ b/contracts/governance/locking/LockingVotes.sol @@ -15,7 +15,7 @@ contract LockingVotes is LockingBase { */ function getVotes(address account) external view override returns (uint256) { uint32 currentBlock = getBlockNumber(); - uint32 currentWeek = roundTimestamp(currentBlock); + uint32 currentWeek = getWeekNumber(currentBlock); return accounts[account].balance.actualValue(currentWeek, currentBlock); } @@ -24,7 +24,7 @@ contract LockingVotes is LockingBase { * at the end of the last period */ function getPastVotes(address account, uint256 blockNumber) external view override returns (uint256) { - uint32 currentWeek = roundTimestamp(uint32(blockNumber)); + uint32 currentWeek = getWeekNumber(uint32(blockNumber)); require(blockNumber < getBlockNumber() && currentWeek > 0, "block not yet mined"); return accounts[account].balance.actualValue(currentWeek, uint32(blockNumber)); @@ -35,7 +35,7 @@ contract LockingVotes is LockingBase { * at the end of the last period */ function getPastTotalSupply(uint256 blockNumber) external view override returns (uint256) { - uint32 currentWeek = roundTimestamp(uint32(blockNumber)); + uint32 currentWeek = getWeekNumber(uint32(blockNumber)); require(blockNumber < getBlockNumber() && currentWeek > 0, "block not yet mined"); return totalSupplyLine.actualValue(currentWeek, uint32(blockNumber)); diff --git a/test/fork/ForkTests.t.sol b/test/fork/ForkTests.t.sol index c47de510..f9559dc4 100644 --- a/test/fork/ForkTests.t.sol +++ b/test/fork/ForkTests.t.sol @@ -44,6 +44,7 @@ import { BancorExchangeProviderForkTest } from "./BancorExchangeProviderForkTest import { GoodDollarTradingLimitsForkTest } from "./GoodDollar/TradingLimitsForkTest.sol"; import { GoodDollarSwapForkTest } from "./GoodDollar/SwapForkTest.sol"; import { GoodDollarExpansionForkTest } from "./GoodDollar/ExpansionForkTest.sol"; +import { LockingUpgradeForkTest } from "./upgrades/LockingUpgradeForkTest.sol"; contract Alfajores_ChainForkTest is ChainForkTest(ALFAJORES_ID, 1, uints(16)) {} @@ -120,3 +121,5 @@ contract Celo_GoodDollarTradingLimitsForkTest is GoodDollarTradingLimitsForkTest contract Celo_GoodDollarSwapForkTest is GoodDollarSwapForkTest(CELO_ID) {} contract Celo_GoodDollarExpansionForkTest is GoodDollarExpansionForkTest(CELO_ID) {} + +contract Celo_LockingUpgradeForkTest is LockingUpgradeForkTest(CELO_ID) {} diff --git a/test/fork/upgrades/LockingUpgradeForkTest.sol b/test/fork/upgrades/LockingUpgradeForkTest.sol new file mode 100644 index 00000000..15a9690d --- /dev/null +++ b/test/fork/upgrades/LockingUpgradeForkTest.sol @@ -0,0 +1,280 @@ +/* solhint-disable max-line-length */ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8; + +import { BaseForkTest } from "../BaseForkTest.sol"; +import { Locking } from "contracts/governance/locking/Locking.sol"; +import { GovernanceFactory } from "contracts/governance/GovernanceFactory.sol"; +import { MentoGovernor } from "contracts/governance/MentoGovernor.sol"; +import { MentoToken } from "contracts/governance/MentoToken.sol"; +import { ProxyAdmin } from "openzeppelin-contracts-next/contracts/proxy/transparent/ProxyAdmin.sol"; +import { ITransparentUpgradeableProxy } from "openzeppelin-contracts-next/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; + +// used to avoid stack too deep error +struct LockingSnapshot { + uint256 weekNo; + uint256 totalSupply; + uint256 pastTotalSupply; + uint256 balance1; + uint256 balance2; + uint256 votingPower1; + uint256 votingPower2; + uint256 pastVotingPower1; + uint256 pastVotingPower2; + uint256 lockedBalance1; + uint256 lockedBalance2; + uint256 withdrawable1; + uint256 withdrawable2; +} + +contract LockingUpgradeForkTest is BaseForkTest { + // airdrop claimers from the mainnet + address public constant AIRDROP_CLAIMER_1 = 0x3152eE4a18ee3209524F9071A6BcAdA098f19838; + address public constant AIRDROP_CLAIMER_2 = 0x44EB9Bf2D6B161499f1b706c331aa2Ba1d5069c7; + + uint256 public constant L1_WEEK = 7 days / 5; + uint256 public constant L2_WEEK = 7 days; + + GovernanceFactory public governanceFactory = GovernanceFactory(0xee6CE2dbe788dFC38b8F583Da86cB9caf2C8cF5A); + ProxyAdmin public proxyAdmin; + Locking public locking; + MentoGovernor public mentoGovernor; + MentoToken public mentoToken; + + address public timelockController; + address public newLockingImplementation; + + address public mentoLabsMultisig = makeAddr("mentoLabsMultisig"); + + constructor(uint256 _chainId) BaseForkTest(_chainId) {} + + function setUp() public virtual override { + super.setUp(); + proxyAdmin = governanceFactory.proxyAdmin(); + locking = governanceFactory.locking(); + timelockController = address(governanceFactory.governanceTimelock()); + mentoGovernor = governanceFactory.mentoGovernor(); + mentoToken = governanceFactory.mentoToken(); + + newLockingImplementation = address(new Locking()); + vm.prank(timelockController); + proxyAdmin.upgrade(ITransparentUpgradeableProxy(address(locking)), newLockingImplementation); + + vm.prank(timelockController); + locking.setMentoLabsMultisig(mentoLabsMultisig); + } + + function test_blockNoDependentCalculations_afterL2Transition_shouldWorkAsBefore() public { + LockingSnapshot memory beforeSnapshot; + LockingSnapshot memory afterSnapshot; + + // THU Nov-07-2024 00:00:23 +UTC + vm.roll(28653031); + vm.warp(1730937623); + + // move 3 weeks forward on L1 + _moveDays({ day: 3 * 7, forward: true, isL2: false }); + + // Take snapshot 3 weeks after Nov 07 + beforeSnapshot = _takeSnapshot(AIRDROP_CLAIMER_1, AIRDROP_CLAIMER_2); + + // move 5 weeks forward on L1 + _moveDays({ day: 5 * 7, forward: true, isL2: false }); + + // Take snapshot 8 weeks after Nov 07 + afterSnapshot = _takeSnapshot(AIRDROP_CLAIMER_1, AIRDROP_CLAIMER_2); + + // move 5 weeks backward on L1 + _moveDays({ day: 5 * 7, forward: false, isL2: false }); + + uint256 blocksTillNextWeekL1 = _calculateBlocksTillNextWeek({ isL2: false }); + + _simulateL2Upgrade(); + + uint256 blocksTillNextWeekL2 = _calculateBlocksTillNextWeek({ isL2: true }); + + // if the shift number is correct, the number of blocks till the next week should be 5 times the previous number + assertEq(blocksTillNextWeekL2, 5 * blocksTillNextWeekL1); + + assertEq(locking.getWeek(), beforeSnapshot.weekNo); + assertEq(locking.totalSupply(), beforeSnapshot.totalSupply); + // the past values should be calculated using the L1 week value + assertEq(locking.getPastTotalSupply(block.number - 3 * L1_WEEK), beforeSnapshot.pastTotalSupply); + assertEq(locking.balanceOf(AIRDROP_CLAIMER_1), beforeSnapshot.balance1); + assertEq(locking.balanceOf(AIRDROP_CLAIMER_2), beforeSnapshot.balance2); + assertEq(locking.getVotes(AIRDROP_CLAIMER_1), beforeSnapshot.votingPower1); + assertEq(locking.getVotes(AIRDROP_CLAIMER_2), beforeSnapshot.votingPower2); + assertEq(locking.getPastVotes(AIRDROP_CLAIMER_1, block.number - 3 * L1_WEEK), beforeSnapshot.pastVotingPower1); + assertEq(locking.getPastVotes(AIRDROP_CLAIMER_2, block.number - 3 * L1_WEEK), beforeSnapshot.pastVotingPower2); + assertEq(locking.locked(AIRDROP_CLAIMER_1), beforeSnapshot.lockedBalance1); + assertEq(locking.locked(AIRDROP_CLAIMER_2), beforeSnapshot.lockedBalance2); + assertEq(locking.getAvailableForWithdraw(AIRDROP_CLAIMER_1), beforeSnapshot.withdrawable1); + assertEq(locking.getAvailableForWithdraw(AIRDROP_CLAIMER_2), beforeSnapshot.withdrawable2); + + // move 5 weeks forward on L2 + _moveDays({ day: 5 * 7, forward: true, isL2: true }); + + blocksTillNextWeekL2 = _calculateBlocksTillNextWeek({ isL2: true }); + + assertEq(blocksTillNextWeekL2, 5 * blocksTillNextWeekL1); + assertEq(locking.getWeek(), afterSnapshot.weekNo); + assertEq(locking.totalSupply(), afterSnapshot.totalSupply); + assertEq(locking.getPastTotalSupply(block.number - 3 * L2_WEEK), afterSnapshot.pastTotalSupply); + assertEq(locking.balanceOf(AIRDROP_CLAIMER_1), afterSnapshot.balance1); + assertEq(locking.balanceOf(AIRDROP_CLAIMER_2), afterSnapshot.balance2); + assertEq(locking.getVotes(AIRDROP_CLAIMER_1), afterSnapshot.votingPower1); + assertEq(locking.getVotes(AIRDROP_CLAIMER_2), afterSnapshot.votingPower2); + assertEq(locking.getPastVotes(AIRDROP_CLAIMER_1, block.number - 3 * L2_WEEK), afterSnapshot.pastVotingPower1); + assertEq(locking.getPastVotes(AIRDROP_CLAIMER_2, block.number - 3 * L2_WEEK), afterSnapshot.pastVotingPower2); + assertEq(locking.locked(AIRDROP_CLAIMER_1), afterSnapshot.lockedBalance1); + assertEq(locking.locked(AIRDROP_CLAIMER_2), afterSnapshot.lockedBalance2); + assertEq(locking.getAvailableForWithdraw(AIRDROP_CLAIMER_1), afterSnapshot.withdrawable1); + assertEq(locking.getAvailableForWithdraw(AIRDROP_CLAIMER_2), afterSnapshot.withdrawable2); + + // move 5 days forward on L2 + _moveDays({ day: 5, forward: true, isL2: true }); + // we should be at the same week (TUE around 00:00) + assertEq(locking.getWeek(), afterSnapshot.weekNo); + // move 1 day forward on L2 + 90 mins as buffer + _moveDays({ day: 1, forward: true, isL2: true }); + vm.roll(block.number + 90 minutes); + // we should be at the next week (WED around 01:30) + assertEq(locking.getWeek(), afterSnapshot.weekNo + 1); + } + + function test_setPaused_shouldPauseGovernance() public { + _lockTokensForGovernance(AIRDROP_CLAIMER_1, 10_000_000e18); + + vm.prank(mentoLabsMultisig); + locking.setPaused(true); + + vm.prank(AIRDROP_CLAIMER_1); + vm.expectRevert("locking is paused"); + mentoGovernor.propose(new address[](1), new uint256[](1), new bytes[](1), "Test proposal"); + + vm.prank(mentoLabsMultisig); + locking.setPaused(false); + + vm.prank(AIRDROP_CLAIMER_1); + uint256 proposalId = mentoGovernor.propose(new address[](1), new uint256[](1), new bytes[](1), "Test proposal"); + + _moveDays(1, true, false); + + vm.prank(mentoLabsMultisig); + locking.setPaused(true); + + vm.prank(AIRDROP_CLAIMER_1); + vm.expectRevert("locking is paused"); + mentoGovernor.castVote(proposalId, 1); + } + + function test_governance_afterL2Transition_shouldWorkAsBefore() public { + _simulateL2Upgrade(); + + _moveDays(7, true, true); + + uint256 votingPower1 = locking.getVotes(AIRDROP_CLAIMER_1); + uint256 votingPower2 = locking.getVotes(AIRDROP_CLAIMER_2); + + uint256 lockId = _lockTokensForGovernance(AIRDROP_CLAIMER_1, 1_000_000e18); + + assertEq(locking.getVotes(AIRDROP_CLAIMER_1), votingPower1 + 1_000_000e18); + + vm.prank(AIRDROP_CLAIMER_1); + locking.delegateTo(lockId, AIRDROP_CLAIMER_2); + + assertEq(locking.getVotes(AIRDROP_CLAIMER_1), votingPower1); + assertEq(locking.getVotes(AIRDROP_CLAIMER_2), votingPower2 + 1_000_000e18); + + vm.prank(AIRDROP_CLAIMER_1); + locking.relock(lockId, AIRDROP_CLAIMER_1, 1_000_000e18, 1, 103); + + assertEq(locking.getVotes(AIRDROP_CLAIMER_1), votingPower1 + 1_000_000e18); + assertEq(locking.getVotes(AIRDROP_CLAIMER_2), votingPower2); + + vm.prank(AIRDROP_CLAIMER_1); + uint256 proposalId = mentoGovernor.propose(new address[](1), new uint256[](1), new bytes[](1), "Test proposal"); + + _moveDays(1, true, true); + + vm.prank(AIRDROP_CLAIMER_1); + mentoGovernor.castVote(proposalId, 1); + + vm.prank(AIRDROP_CLAIMER_2); + mentoGovernor.castVote(proposalId, 2); + + _moveDays(5, true, true); + + mentoGovernor.queue(proposalId); + + _moveDays(2, true, true); + + mentoGovernor.execute(proposalId); + } + + // used to give locker enough power to be able to propose + function _lockTokensForGovernance(address locker, uint96 amount) internal returns (uint256 lockId) { + deal(address(mentoToken), locker, amount); + + vm.prank(locker); + mentoToken.approve(address(locking), amount); + + vm.prank(locker); + lockId = locking.lock(locker, locker, amount, 104, 0); + + vm.roll(block.number + 1); + } + + // takes a snapshot of the locking contract at current block + function _takeSnapshot(address claimer1, address claimer2) internal view returns (LockingSnapshot memory snapshot) { + snapshot.weekNo = locking.getWeek(); + snapshot.totalSupply = locking.totalSupply(); + snapshot.pastTotalSupply = locking.getPastTotalSupply(block.number - 3 * L1_WEEK); + snapshot.balance1 = locking.balanceOf(claimer1); + snapshot.balance2 = locking.balanceOf(claimer2); + snapshot.votingPower1 = locking.getVotes(claimer1); + snapshot.votingPower2 = locking.getVotes(claimer2); + snapshot.pastVotingPower1 = locking.getPastVotes(claimer1, block.number - 3 * L1_WEEK); + snapshot.pastVotingPower2 = locking.getPastVotes(claimer2, block.number - 3 * L1_WEEK); + snapshot.lockedBalance1 = locking.locked(claimer1); + snapshot.lockedBalance2 = locking.locked(claimer2); + snapshot.withdrawable1 = locking.getAvailableForWithdraw(claimer1); + snapshot.withdrawable2 = locking.getAvailableForWithdraw(claimer2); + } + + // returns the number of blocks till the next week + // by calculating the first block of the next week and substracting the current block + function _calculateBlocksTillNextWeek(bool isL2) internal view returns (uint256) { + if (isL2) { + return L2_WEEK * uint256(int256(locking.getWeek()) + locking.l2StartingPointWeek() + 1) + 507776 - block.number; + } else { + return L1_WEEK * (locking.getWeek() + locking.startingPointWeek() + 1) + 89964 - block.number; + } + } + + // simulates the L2 upgrade by setting the necessary parameters + function _simulateL2Upgrade() internal { + vm.prank(mentoLabsMultisig); + locking.setL2TransitionBlock(block.number); + vm.prank(mentoLabsMultisig); + locking.setL2StartingPointWeek(20); + vm.prank(mentoLabsMultisig); + locking.setL2EpochShift(507776); + vm.prank(mentoLabsMultisig); + locking.setPaused(false); + } + + // move days forward or backward on L1 or L2 + function _moveDays(uint256 day, bool forward, bool isL2) internal { + uint256 ts = vm.getBlockTimestamp(); + uint256 height = vm.getBlockNumber(); + + uint256 newTs = forward ? ts + day * 1 days : ts - day * 1 days; + + uint256 blockChange = isL2 ? (day * 1 days) : ((day * 1 days) / 5); + uint256 newHeight = forward ? height + blockChange : height - blockChange; + + vm.warp(newTs); + vm.roll(newHeight); + } +} diff --git a/test/unit/governance/Locking/LockingTest.sol b/test/unit/governance/Locking/LockingTest.sol index 45f7b6b7..f55f0356 100644 --- a/test/unit/governance/Locking/LockingTest.sol +++ b/test/unit/governance/Locking/LockingTest.sol @@ -31,4 +31,8 @@ contract LockingTest is GovernanceTest { function _incrementBlock(uint32 _amount) internal { locking.incrementBlock(_amount); } + + function _reduceBlock(uint32 _amount) internal { + locking.reduceBlock(_amount); + } } diff --git a/test/unit/governance/Locking/locking.upgrade.t.sol b/test/unit/governance/Locking/locking.upgrade.t.sol new file mode 100644 index 00000000..30a09bac --- /dev/null +++ b/test/unit/governance/Locking/locking.upgrade.t.sol @@ -0,0 +1,362 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.18; +// solhint-disable func-name-mixedcase, contract-name-camelcase + +import { LockingTest } from "./LockingTest.sol"; + +contract Upgrade_LockingTest is LockingTest { + address public mentoLabs = makeAddr("MentoLabsMultisig"); + + uint32 public l1Day; + uint32 public l2Day; + uint32 public l1Week; + uint32 public l2Week; + + function setUp() public override { + super.setUp(); + l1Week = 7 days / 5; + l2Week = 7 days; + l1Day = l1Week / 7; + l2Day = l2Week / 7; + } + + function test_initialSetup_shouldHaveCorrectValues() public view { + assertEq(locking.L2_WEEK(), l2Week); + assertEq(locking.mentoLabsMultisig(), address(0)); + assertEq(locking.l2TransitionBlock(), 0); + assertEq(locking.l2StartingPointWeek(), 0); + assertEq(locking.l2EpochShift(), 0); + assert(!locking.paused()); + } + + function test_setMentoLabsMultisig_whenCalledByNonOwner_shouldRevert() public { + vm.prank(alice); + vm.expectRevert("Ownable: caller is not the owner"); + locking.setMentoLabsMultisig(mentoLabs); + } + + function test_setMentoLabsMultisig_whenCalledByOwner_shouldSetMultisigAddress() public { + vm.prank(owner); + locking.setMentoLabsMultisig(mentoLabs); + + assertEq(locking.mentoLabsMultisig(), mentoLabs); + } + + modifier setMultisig() { + vm.prank(owner); + locking.setMentoLabsMultisig(mentoLabs); + _; + } + + function test_setL2TransitionBlock_whenCalledByNonMentoMultisig_shouldRevert() public setMultisig { + vm.prank(alice); + vm.expectRevert("caller is not MentoLabs multisig"); + locking.setL2TransitionBlock(block.number); + } + + function test_setL2TransitionBlock_whenCalledByMentoMultisig_shouldSetL2BlockAndPause() public setMultisig { + uint32 blockNumber = uint32(block.number + 100); + + vm.prank(mentoLabs); + locking.setL2TransitionBlock(blockNumber); + + assertEq(locking.l2TransitionBlock(), blockNumber); + assert(locking.paused()); + } + + function test_setL2EpochShift_whenCalledByNonMentoMultisig_shouldRevert() public setMultisig { + vm.prank(alice); + vm.expectRevert("caller is not MentoLabs multisig"); + locking.setL2EpochShift(100); + } + + function test_setL2EpochShift_whenCalledByMentoMultisig_shouldSetL2BlockAndPause() public setMultisig { + vm.prank(mentoLabs); + locking.setL2EpochShift(100); + + assertEq(locking.l2EpochShift(), 100); + } + + function test_setL2StartingPointWeek_whenCalledByNonMentoMultisig_shouldRevert() public setMultisig { + vm.prank(alice); + vm.expectRevert("caller is not MentoLabs multisig"); + locking.setL2StartingPointWeek(100); + } + + function test_setL2StartingPointWeek_whenCalledByMentoMultisig_shouldSetL2BlockAndPause() public setMultisig { + vm.prank(mentoLabs); + locking.setL2StartingPointWeek(100); + + assertEq(locking.l2StartingPointWeek(), 100); + } + function test_setPaused_whenCalledByNonMentoMultisig_shouldRevert() public setMultisig { + vm.prank(alice); + vm.expectRevert("caller is not MentoLabs multisig"); + locking.setPaused(true); + } + + function test_setPaused_whenCalledByMentoMultisig_shouldPauseContracts() public setMultisig { + mentoToken.mint(alice, 1000000e18); + + vm.prank(mentoLabs); + locking.setPaused(true); + + assert(locking.paused()); + + vm.expectRevert("locking is paused"); + vm.prank(alice); + locking.lock(alice, bob, 1000e18, 5, 5); + + vm.expectRevert("locking is paused"); + vm.prank(alice); + locking.withdraw(); + + vm.prank(mentoLabs); + locking.setPaused(false); + + assert(!locking.paused()); + + vm.prank(alice); + locking.lock(alice, bob, 1000e18, 5, 5); + + vm.prank(alice); + locking.withdraw(); + } + + modifier l2LockingSetup(uint32 advanceWeeks, uint32 startingPointWeek, uint32 l1Shift) { + vm.prank(owner); + locking.setMentoLabsMultisig(mentoLabs); + + _incrementBlock(l1Week * advanceWeeks); + + locking.setStatingPointWeek(startingPointWeek); + locking.setEpochShift(l1Shift); + + vm.prank(mentoLabs); + locking.setL2TransitionBlock(block.number); + + vm.prank(mentoLabs); + locking.setPaused(false); + + _; + } + + function test_getWeek_whenShiftAndStartingPointIs0_shouldReturnCorrectWeekNo() public l2LockingSetup(8, 0, 0) { + // 2 + 8 weeks = 10 weeks on l1 = 2 weeks on l2 + assertEq(locking.getWeek(), 2); + assertEq(locking.blockTillNextPeriod(), l2Week); + + _incrementBlock(l2Day * 3); + + assertEq(locking.getWeek(), 2); + assertEq(locking.blockTillNextPeriod(), l2Day * 4); + + _incrementBlock(l2Day * 5); + + assertEq(locking.getWeek(), 3); + assertEq(locking.blockTillNextPeriod(), l2Day * 6); + + _incrementBlock(l2Day * 8); + + assertEq(locking.getWeek(), 4); + assertEq(locking.blockTillNextPeriod(), l2Day * 5); + } + + function test_getWeek_whenL2StartingPointIsPositive_shouldReturnCorrectWeekNo() public l2LockingSetup(198, 190, 0) { + // l1 week no = 198 + 2 - (190) = 10 + uint32 l1WeekNo = 10; + // l2 week no = (198 + 2) / 5 = 40 + assertEq(locking.getWeek(), 40); + assertEq(locking.blockTillNextPeriod(), l2Week); + + // l2WeekNo - l1WeekNo = 40 - 10 = 30 + vm.prank(mentoLabs); + locking.setL2StartingPointWeek(30); + + // after the L2 starting point week is set, the week should be equal to the l1 week no + assertEq(locking.getWeek(), l1WeekNo); + } + + function test_getWeek_whenL2StartingPointIsNegative_shouldReturnCorrectWeekNo() public l2LockingSetup(18, 0, 0) { + // l1 week no = 18 + 2 - 0 = 20 + uint32 l1WeekNo = 20; + // l2 week no = (18 + 2) / 5 = 4 + assertEq(locking.getWeek(), 4); + assertEq(locking.blockTillNextPeriod(), l2Week); + + // l2WeekNo - l1WeekNo = 4 - 20 = -16 + vm.prank(mentoLabs); + locking.setL2StartingPointWeek(-16); + + // after the L2 starting point week is set, the week should be equal to the l1 week no + assertEq(locking.getWeek(), l1WeekNo); + } + + function test_getWeek_whenShiftIsPositive_shouldReturnCorrectWeekNo() public l2LockingSetup(18, 5, l1Day * 3) { + // l1 week no = 18 + 2 - 5 - 1 = 19 + uint32 l1WeekNo = 14; + + // l2 week no = (18 + 2) / 5 = 4 + assertEq(locking.getWeek(), 4); + assertEq(locking.blockTillNextPeriod(), l2Week); + + // l2WeekNo - l1WeekNo = 4 - 14 - 1 = -11 + vm.prank(mentoLabs); + locking.setL2StartingPointWeek(-11); + + vm.prank(mentoLabs); + locking.setL2EpochShift(l2Day * 3); + + // after the L2 starting point week and l2EpochShift are set, the timing should be equal to the l1 timing + assertEq(locking.getWeek(), l1WeekNo); + assertEq(locking.blockTillNextPeriod(), l2Day * 3); + + _incrementBlock(l2Day); + + assertEq(locking.getWeek(), l1WeekNo); + assertEq(locking.blockTillNextPeriod(), l2Day * 2); + + _incrementBlock(l2Day); + + assertEq(locking.getWeek(), l1WeekNo); + assertEq(locking.blockTillNextPeriod(), l2Day); + + _incrementBlock(l2Day); + + assertEq(locking.getWeek(), l1WeekNo + 1); + assertEq(locking.blockTillNextPeriod(), l2Week); + } + + function test_totalSupply_whenCalledAfterL2Transition_shouldReturnCorrectValues() public setMultisig { + mentoToken.mint(alice, 1000000e18); + + // week no: 20 + _incrementBlock(l1Week * 18); + + vm.prank(alice); + locking.lock(alice, alice, 1000e18, 104, 0); + + // week no: 40 + _incrementBlock(l1Week * 20); + + uint256 totalSupplyL1W40 = locking.totalSupply(); + uint256 pastTotalSupplyL1W30 = locking.getPastTotalSupply(locking.blockNumberMocked() - l1Week * 10); + + // week no: 60 + _incrementBlock(l1Week * 20); + + uint256 totalSupplyL1W60 = locking.totalSupply(); + uint256 pastTotalSupplyL1W50 = locking.getPastTotalSupply(locking.blockNumberMocked() - l1Week * 10); + + // roll back to week 40 + _reduceBlock(l1Week * 20); + + vm.prank(mentoLabs); + locking.setL2TransitionBlock(l1Week * 40); + + vm.prank(mentoLabs); + locking.setPaused(false); + + // 8 - 40 = -32 + vm.prank(mentoLabs); + locking.setL2StartingPointWeek(-32); + + assertEq(locking.totalSupply(), totalSupplyL1W40); + assertEq(locking.getPastTotalSupply(locking.blockNumberMocked() - l1Week * 10), pastTotalSupplyL1W30); + + // week no: 60 + _incrementBlock(l2Week * 20); + assertEq(locking.totalSupply(), totalSupplyL1W60); + assertEq(locking.getPastTotalSupply(locking.blockNumberMocked() - l2Week * 10), pastTotalSupplyL1W50); + } + + function test_balanceOfAndGetVotes_whenCalledAfterL2Transition_shouldReturnCorrectValues() public setMultisig { + mentoToken.mint(alice, 1000000e18); + + // week no: 20 + _incrementBlock(l1Week * 18); + + vm.prank(alice); + locking.lock(alice, alice, 1000e18, 104, 0); + + // week no: 40 + _incrementBlock(l1Week * 20); + + uint256 balanceOfL1W40 = locking.balanceOf(alice); + uint256 votesL1W40 = locking.getVotes(alice); + uint256 pastVotesL1W30 = locking.getPastVotes(alice, locking.blockNumberMocked() - l1Week * 10); + + // week no: 60 + _incrementBlock(l1Week * 20); + + uint256 balanceOfL1W60 = locking.balanceOf(alice); + uint256 votesL1W60 = locking.getVotes(alice); + uint256 pastVotesL1W50 = locking.getPastVotes(alice, locking.blockNumberMocked() - l1Week * 10); + + // roll back to week 40 + _reduceBlock(l1Week * 20); + + vm.prank(mentoLabs); + locking.setL2TransitionBlock(l1Week * 40); + + vm.prank(mentoLabs); + locking.setPaused(false); + + // 8 - 40 = -32 + vm.prank(mentoLabs); + locking.setL2StartingPointWeek(-32); + + assertEq(locking.balanceOf(alice), balanceOfL1W40); + assertEq(locking.getVotes(alice), votesL1W40); + assertEq(locking.getPastVotes(alice, locking.blockNumberMocked() - l1Week * 10), pastVotesL1W30); + + // week no: 60 + _incrementBlock(l2Week * 20); + assertEq(locking.balanceOf(alice), balanceOfL1W60); + assertEq(locking.getVotes(alice), votesL1W60); + assertEq(locking.getPastVotes(alice, locking.blockNumberMocked() - l2Week * 10), pastVotesL1W50); + } + + function test_lockedAndWithdrawable_whenCalledAfterL2Transition_shouldReturnCorrectValues() public setMultisig { + mentoToken.mint(alice, 1000000e18); + + // week no: 20 + _incrementBlock(l1Week * 18); + + vm.prank(alice); + locking.lock(alice, alice, 1000e18, 104, 0); + + // week no: 40 + _incrementBlock(l1Week * 20); + + uint256 lockedL1W40 = locking.locked(alice); + uint256 withdrawableL1W40 = locking.getAvailableForWithdraw(alice); + + // week no: 60 + _incrementBlock(l1Week * 20); + + uint256 lockedL1W60 = locking.locked(alice); + uint256 withdrawableL1W60 = locking.getAvailableForWithdraw(alice); + + // roll back to week 40 + _reduceBlock(l1Week * 20); + + vm.prank(mentoLabs); + locking.setL2TransitionBlock(l1Week * 40); + + vm.prank(mentoLabs); + locking.setPaused(false); + + // 8 - 40 = -32 + vm.prank(mentoLabs); + locking.setL2StartingPointWeek(-32); + + assertEq(locking.locked(alice), lockedL1W40); + assertEq(locking.getAvailableForWithdraw(alice), withdrawableL1W40); + + // week no: 60 + _incrementBlock(l2Week * 20); + assertEq(locking.locked(alice), lockedL1W60); + assertEq(locking.getAvailableForWithdraw(alice), withdrawableL1W60); + } +} diff --git a/test/utils/harnesses/LockingHarness.sol b/test/utils/harnesses/LockingHarness.sol index 440708f7..6b956ec7 100644 --- a/test/utils/harnesses/LockingHarness.sol +++ b/test/utils/harnesses/LockingHarness.sol @@ -11,6 +11,10 @@ contract LockingHarness is Locking { blockNumberMocked = blockNumberMocked + _amount; } + function reduceBlock(uint32 _amount) external { + blockNumberMocked = blockNumberMocked - _amount; + } + function getBlockNumber() internal view override returns (uint32) { return blockNumberMocked; } @@ -31,8 +35,11 @@ contract LockingHarness is Locking { (lockAmount, lockSlope) = getLock(amount, slope, cliff); } - function getEpochShift() internal view override returns (uint32) { - return epochShift; + function _getEpochShift(uint32) internal view override returns (uint32) { + if (_isPreL2Transition(getBlockNumber())) { + return epochShift; + } + return l2EpochShift; } function setEpochShift(uint32 _epochShift) external { @@ -45,6 +52,16 @@ contract LockingHarness is Locking { function blockTillNextPeriod() external view returns (uint256) { uint256 currentWeek = this.getWeek(); - return (WEEK * (currentWeek + 1)) + getEpochShift() - getBlockNumber(); + if (_isPreL2Transition(getBlockNumber())) { + return (WEEK * (currentWeek + startingPointWeek + 1)) + _getEpochShift(getBlockNumber()) - getBlockNumber(); + } + return + (L2_WEEK * uint256(int256(currentWeek) + l2StartingPointWeek + 1)) + + _getEpochShift(getBlockNumber()) - + getBlockNumber(); + } + + function setStatingPointWeek(uint32 _week) external { + startingPointWeek = _week; } }